1.
암호통화거래소 만들기 첫째에 이어지는 글입니다. 앞서 글은 DB Schema 및 구조 더하여 개발시 유의할 점을 소개했습니다. 이번에는 직접 개발할 때 참고할 수 있는 오픈소스 프로젝트를 소개할까 합니다. 초창기에 나왔던 암호화폐 거래소 시스템에 비하여 최근에 공개된 프로젝트를 보면 완성도가 남드릅니다. 물론 직접 운용을 하려면 여러가지 변경을 하여야 하지만 충분히 참고할 가치가 있습니다. 더우기 오픈소스를 기반으로 시스템을 구축하고자 할 때 오픈소스화한 프로젝트는 더 유용하지 않을까 생각합니다.
처음 블록체인을 기반으로 암호통화거래소를 만들었던 프로젝트를 조사할 때 살펴보았던 프로젝트들입니다.
Blink Trade
Wlox
Peatio
CoreCX
이 중에서 현재까지 유지되고 있는 프로젝트는 Peatio입니다. 물론 Github의 주소가 https://github.com/rubykube/peatio으로 바뀌었습니다.
Peatio is a free and open-source crypto currency exchange implementation with the Rails framework. Peatio.tech is a fork of Peatio designed for micro-services architecture. We have simplified the code in order to use only Peatio API with external frontend and server components.
Ruby로 개발된 프로젝트이면서 기업용 서비스까지 제공합니다. Peatio의 핵심은 Peatio-Core입니다. DBMS 및 RabitMQ 및 인증과 관련한 공통프레임워크입니다. 그리고 DB Schema와 관련한 정보는 MVC모델로 개발한 프로젝트라 Models에 있습니다. 그 중 Order와 관련한 부분입니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 |
# encoding: UTF-8 # frozen_string_literal: true class Order < ActiveRecord::Base include BelongsToMarket include BelongsToMember extend Enumerize enumerize :state, in: { wait: 100, done: 200, cancel: 0 }, scope: true TYPES = %w[ market limit ] enumerize :ord_type, in: TYPES, scope: true after_commit(on: :create) { trigger_pusher_event } before_validation :fix_number_precision, on: :create validates :ord_type, :volume, :origin_volume, :locked, :origin_locked, presence: true validates :origin_volume, numericality: { greater_than: 0.to_d } validates :price, numericality: { greater_than: 0 }, if: ->(order) { order.ord_type == 'limit' } validate :market_order_validations, if: ->(order) { order.ord_type == 'market' } WAIT = 'wait' DONE = 'done' CANCEL = 'cancel' scope :done, -> { with_state(:done) } scope :active, -> { with_state(:wait) } before_validation(on: :create) { self.fee = config.public_send("#{kind}_fee") } after_commit on: :create do next unless ord_type == 'limit' EventAPI.notify ['market', market_id, 'order_created'].join('.'), \ Serializers::EventAPI::OrderCreated.call(self) end after_commit on: :update do next unless ord_type == 'limit' event = case previous_changes.dig('state', 1) when 'cancel' then 'order_canceled' when 'done' then 'order_completed' else 'order_updated' end EventAPI.notify ['market', market_id, event].join('.'), \ Serializers::EventAPI.const_get(event.camelize).call(self) end def funds_used origin_locked - locked end def config market end def trigger_pusher_event Member.trigger_pusher_event member_id, :order, \ id: id, at: at, market: market_id, kind: kind, price: price&.to_s('F'), state: state, volume: volume.to_s('F'), origin_volume: origin_volume.to_s('F') end def kind self.class.name.underscore[-3, 3] end def at created_at.to_i end def to_matching_attributes { id: id, market: market_id, type: type[-3, 3].downcase.to_sym, ord_type: ord_type, volume: volume, price: price, locked: locked, timestamp: created_at.to_i } end def fix_number_precision self.price = config.fix_number_precision(:bid, price.to_d) if price if volume self.volume = config.fix_number_precision(:ask, volume.to_d) self.origin_volume = origin_volume.present? ? config.fix_number_precision(:ask, origin_volume.to_d) : volume end end def record_submit_operations! transaction do # Debit main fiat/crypto Liability account. # Credit locked fiat/crypto Liability account. Operations::Liability.transfer!( reference: self, amount: locked, from_kind: :main, to_kind: :locked ) end end def record_cancel_operations! transaction do # Debit locked fiat/crypto Liability account. # Credit main fiat/crypto Liability account. Operations::Liability.transfer!( reference: self, amount: locked, from_kind: :locked, to_kind: :main ) end end private def is_limit_order? ord_type == 'limit' end def market_order_validations errors.add(:price, 'must not be present') if price.present? end FUSE = '0.9'.to_d def estimate_required_funds(price_levels) required_funds = Account::ZERO expected_volume = volume start_from, _ = price_levels.first filled_at = start_from until expected_volume.zero? || price_levels.empty? level_price, level_volume = price_levels.shift filled_at = level_price v = [expected_volume, level_volume].min required_funds += yield level_price, v expected_volume -= v end raise "Market is not deep enough" unless expected_volume.zero? raise "Volume too large" if (filled_at-start_from).abs/start_from > FUSE required_funds end end # == Schema Information # Schema version: 20180813105100 # # Table name: orders # # id :integer not null, primary key # bid :string(10) not null # ask :string(10) not null # market_id :string(20) not null # price :decimal(32, 16) # volume :decimal(32, 16) not null # origin_volume :decimal(32, 16) not null # fee :decimal(32, 16) default(0.0), not null # state :integer not null # type :string(8) not null # member_id :integer not null # ord_type :string not null # locked :decimal(32, 16) default(0.0), not null # origin_locked :decimal(32, 16) default(0.0), not null # funds_received :decimal(32, 16) default(0.0) # trades_count :integer default(0), not null # created_at :datetime not null # updated_at :datetime not null # # Indexes # # index_orders_on_member_id (member_id) # index_orders_on_state (state) # index_orders_on_type_and_market_id (type,market_id) # index_orders_on_type_and_member_id (type,member_id) # index_orders_on_type_and_state_and_market_id (type,state,market_id) # index_orders_on_type_and_state_and_member_id (type,state,member_id) # |
DB Schema는 아래와 같습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 |
# encoding: UTF-8 # This file is auto-generated from the current state of the database. Instead # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition. # # Note that this schema.rb definition is the authoritative source for your # database schema. If you need to create the application database on another # system, you should be using db:schema:load, not running all the migrations # from scratch. The latter is a flawed and unsustainable approach (the more migrations # you'll amass, the slower it'll run and the greater likelihood for issues). # # It's strongly recommended that you check this file into your version control system. ActiveRecord::Schema.define(version: 20181126101312) do create_table "accounts", force: :cascade do |t| t.integer "member_id", limit: 4, null: false t.string "currency_id", limit: 10, null: false t.decimal "balance", precision: 32, scale: 16, default: 0.0, null: false t.decimal "locked", precision: 32, scale: 16, default: 0.0, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false end add_index "accounts", ["currency_id", "member_id"], name: "index_accounts_on_currency_id_and_member_id", unique: true, using: :btree add_index "accounts", ["member_id"], name: "index_accounts_on_member_id", using: :btree create_table "assets", force: :cascade do |t| t.integer "code", limit: 4, null: false t.string "currency_id", limit: 255, null: false t.integer "reference_id", limit: 4, null: false t.string "reference_type", limit: 255, null: false t.decimal "debit", precision: 32, scale: 16, default: 0.0, null: false t.decimal "credit", precision: 32, scale: 16, default: 0.0, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false end add_index "assets", ["currency_id"], name: "index_assets_on_currency_id", using: :btree add_index "assets", ["reference_type", "reference_id"], name: "index_assets_on_reference_type_and_reference_id", using: :btree create_table "authentications", force: :cascade do |t| t.string "provider", limit: 30, null: false t.string "uid", limit: 255, null: false t.string "token", limit: 1024 t.integer "member_id", limit: 4, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false end add_index "authentications", ["member_id"], name: "index_authentications_on_member_id", using: :btree add_index "authentications", ["provider", "member_id", "uid"], name: "index_authentications_on_provider_and_member_id_and_uid", unique: true, using: :btree add_index "authentications", ["provider", "member_id"], name: "index_authentications_on_provider_and_member_id", unique: true, using: :btree add_index "authentications", ["provider", "uid"], name: "index_authentications_on_provider_and_uid", unique: true, using: :btree create_table "blockchains", force: :cascade do |t| t.string "key", limit: 255, null: false t.string "name", limit: 255 t.string "client", limit: 255, null: false t.string "server", limit: 255 t.integer "height", limit: 4, null: false t.string "explorer_address", limit: 255 t.string "explorer_transaction", limit: 255 t.integer "min_confirmations", limit: 4, default: 6, null: false t.string "status", limit: 255, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false end add_index "blockchains", ["key"], name: "index_blockchains_on_key", unique: true, using: :btree add_index "blockchains", ["status"], name: "index_blockchains_on_status", using: :btree create_table "currencies", force: :cascade do |t| t.string "blockchain_key", limit: 32 t.string "symbol", limit: 1, null: false t.string "type", limit: 30, default: "coin", null: false t.decimal "deposit_fee", precision: 32, scale: 16, default: 0.0, null: false t.decimal "withdraw_limit_24h", precision: 32, scale: 16, default: 0.0, null: false t.decimal "withdraw_limit_72h", precision: 32, scale: 16, default: 0.0, null: false t.decimal "min_deposit_amount", precision: 32, scale: 16, default: 0.0, null: false t.decimal "min_collection_amount", precision: 32, scale: 16, default: 0.0, null: false t.decimal "withdraw_fee", precision: 32, scale: 16, default: 0.0, null: false t.string "options", limit: 1000, default: "{}", null: false t.boolean "enabled", default: true, null: false t.integer "base_factor", limit: 8, default: 1, null: false t.integer "precision", limit: 1, default: 8, null: false t.string "icon_url", limit: 255 t.datetime "created_at", null: false t.datetime "updated_at", null: false end add_index "currencies", ["enabled"], name: "index_currencies_on_enabled", using: :btree create_table "deposits", force: :cascade do |t| t.integer "member_id", limit: 4, null: false t.string "currency_id", limit: 10, null: false t.decimal "amount", precision: 32, scale: 16, null: false t.decimal "fee", precision: 32, scale: 16, null: false t.string "address", limit: 95 t.string "txid", limit: 128 t.integer "txout", limit: 4 t.string "aasm_state", limit: 30, null: false t.integer "block_number", limit: 4 t.string "type", limit: 30, null: false t.string "tid", limit: 64, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.datetime "completed_at" end add_index "deposits", ["aasm_state", "member_id", "currency_id"], name: "index_deposits_on_aasm_state_and_member_id_and_currency_id", using: :btree add_index "deposits", ["currency_id", "txid", "txout"], name: "index_deposits_on_currency_id_and_txid_and_txout", unique: true, using: :btree add_index "deposits", ["currency_id"], name: "index_deposits_on_currency_id", using: :btree add_index "deposits", ["member_id", "txid"], name: "index_deposits_on_member_id_and_txid", using: :btree add_index "deposits", ["tid"], name: "index_deposits_on_tid", using: :btree add_index "deposits", ["type"], name: "index_deposits_on_type", using: :btree create_table "expenses", force: :cascade do |t| t.integer "code", limit: 4, null: false t.string "currency_id", limit: 255, null: false t.integer "reference_id", limit: 4, null: false t.string "reference_type", limit: 255, null: false t.decimal "debit", precision: 32, scale: 16, default: 0.0, null: false t.decimal "credit", precision: 32, scale: 16, default: 0.0, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false end add_index "expenses", ["currency_id"], name: "index_expenses_on_currency_id", using: :btree add_index "expenses", ["reference_type", "reference_id"], name: "index_expenses_on_reference_type_and_reference_id", using: :btree create_table "liabilities", force: :cascade do |t| t.integer "code", limit: 4, null: false t.string "currency_id", limit: 255, null: false t.integer "member_id", limit: 4, null: false t.integer "reference_id", limit: 4, null: false t.string "reference_type", limit: 255, null: false t.decimal "debit", precision: 32, scale: 16, default: 0.0, null: false t.decimal "credit", precision: 32, scale: 16, default: 0.0, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false end add_index "liabilities", ["currency_id"], name: "index_liabilities_on_currency_id", using: :btree add_index "liabilities", ["member_id"], name: "index_liabilities_on_member_id", using: :btree add_index "liabilities", ["reference_type", "reference_id"], name: "index_liabilities_on_reference_type_and_reference_id", using: :btree create_table "markets", force: :cascade do |t| t.string "ask_unit", limit: 10, null: false t.string "bid_unit", limit: 10, null: false t.decimal "ask_fee", precision: 17, scale: 16, default: 0.0, null: false t.decimal "bid_fee", precision: 17, scale: 16, default: 0.0, null: false t.decimal "max_bid", precision: 17, scale: 16 t.decimal "min_ask", precision: 17, scale: 16, default: 0.0, null: false t.integer "ask_precision", limit: 1, default: 8, null: false t.integer "bid_precision", limit: 1, default: 8, null: false t.integer "position", limit: 4, default: 0, null: false t.boolean "enabled", default: true, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false end add_index "markets", ["ask_unit", "bid_unit"], name: "index_markets_on_ask_unit_and_bid_unit", unique: true, using: :btree add_index "markets", ["ask_unit"], name: "index_markets_on_ask_unit", using: :btree add_index "markets", ["bid_unit"], name: "index_markets_on_bid_unit", using: :btree add_index "markets", ["enabled"], name: "index_markets_on_enabled", using: :btree add_index "markets", ["position"], name: "index_markets_on_position", using: :btree create_table "members", force: :cascade do |t| t.integer "level", limit: 1, default: 0, null: false t.string "sn", limit: 12, null: false t.string "email", limit: 255, null: false t.boolean "disabled", default: false, null: false t.boolean "api_disabled", default: false, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false end add_index "members", ["disabled"], name: "index_members_on_disabled", using: :btree add_index "members", ["email"], name: "index_members_on_email", unique: true, using: :btree add_index "members", ["sn"], name: "index_members_on_sn", unique: true, using: :btree create_table "orders", force: :cascade do |t| t.string "bid", limit: 10, null: false t.string "ask", limit: 10, null: false t.string "market_id", limit: 20, null: false t.decimal "price", precision: 32, scale: 16 t.decimal "volume", precision: 32, scale: 16, null: false t.decimal "origin_volume", precision: 32, scale: 16, null: false t.decimal "fee", precision: 32, scale: 16, default: 0.0, null: false t.integer "state", limit: 4, null: false t.string "type", limit: 8, null: false t.integer "member_id", limit: 4, null: false t.string "ord_type", limit: 30, null: false t.decimal "locked", precision: 32, scale: 16, default: 0.0, null: false t.decimal "origin_locked", precision: 32, scale: 16, default: 0.0, null: false t.decimal "funds_received", precision: 32, scale: 16, default: 0.0 t.integer "trades_count", limit: 4, default: 0, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false end add_index "orders", ["member_id"], name: "index_orders_on_member_id", using: :btree add_index "orders", ["state"], name: "index_orders_on_state", using: :btree add_index "orders", ["type", "market_id"], name: "index_orders_on_type_and_market_id", using: :btree add_index "orders", ["type", "member_id"], name: "index_orders_on_type_and_member_id", using: :btree add_index "orders", ["type", "state", "market_id"], name: "index_orders_on_type_and_state_and_market_id", using: :btree add_index "orders", ["type", "state", "member_id"], name: "index_orders_on_type_and_state_and_member_id", using: :btree create_table "payment_addresses", force: :cascade do |t| t.string "currency_id", limit: 10, null: false t.integer "account_id", limit: 4, null: false t.string "address", limit: 95 t.string "secret", limit: 128 t.string "details", limit: 1024, default: "{}", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false end add_index "payment_addresses", ["currency_id", "address"], name: "index_payment_addresses_on_currency_id_and_address", unique: true, using: :btree create_table "revenues", force: :cascade do |t| t.integer "code", limit: 4, null: false t.string "currency_id", limit: 255, null: false t.integer "reference_id", limit: 4, null: false t.string "reference_type", limit: 255, null: false t.decimal "debit", precision: 32, scale: 16, default: 0.0, null: false t.decimal "credit", precision: 32, scale: 16, default: 0.0, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false end add_index "revenues", ["currency_id"], name: "index_revenues_on_currency_id", using: :btree add_index "revenues", ["reference_type", "reference_id"], name: "index_revenues_on_reference_type_and_reference_id", using: :btree create_table "trades", force: :cascade do |t| t.decimal "price", precision: 32, scale: 16, null: false t.decimal "volume", precision: 32, scale: 16, null: false t.integer "ask_id", limit: 4, null: false t.integer "bid_id", limit: 4, null: false t.integer "trend", limit: 4, null: false t.string "market_id", limit: 20, null: false t.integer "ask_member_id", limit: 4, null: false t.integer "bid_member_id", limit: 4, null: false t.decimal "funds", precision: 32, scale: 16, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false end add_index "trades", ["ask_id"], name: "index_trades_on_ask_id", using: :btree add_index "trades", ["ask_member_id", "bid_member_id"], name: "index_trades_on_ask_member_id_and_bid_member_id", using: :btree add_index "trades", ["bid_id"], name: "index_trades_on_bid_id", using: :btree add_index "trades", ["market_id", "created_at"], name: "index_trades_on_market_id_and_created_at", using: :btree create_table "wallets", force: :cascade do |t| t.string "blockchain_key", limit: 32 t.string "currency_id", limit: 10 t.string "name", limit: 64 t.string "address", limit: 255, null: false t.integer "kind", limit: 4, null: false t.integer "nsig", limit: 4 t.string "gateway", limit: 20, default: "", null: false t.string "settings", limit: 1000, default: "{}", null: false t.decimal "max_balance", precision: 32, scale: 16, default: 0.0, null: false t.integer "parent", limit: 4 t.string "status", limit: 32 t.datetime "created_at", null: false t.datetime "updated_at", null: false end add_index "wallets", ["currency_id"], name: "index_wallets_on_currency_id", using: :btree add_index "wallets", ["kind", "currency_id", "status"], name: "index_wallets_on_kind_and_currency_id_and_status", using: :btree add_index "wallets", ["kind"], name: "index_wallets_on_kind", using: :btree add_index "wallets", ["status"], name: "index_wallets_on_status", using: :btree create_table "withdraws", force: :cascade do |t| t.integer "account_id", limit: 4, null: false t.integer "member_id", limit: 4, null: false t.string "currency_id", limit: 10, null: false t.decimal "amount", precision: 32, scale: 16, null: false t.decimal "fee", precision: 32, scale: 16, null: false t.string "txid", limit: 128 t.string "aasm_state", limit: 30, null: false t.integer "block_number", limit: 4 t.decimal "sum", precision: 32, scale: 16, null: false t.string "type", limit: 30, null: false t.string "tid", limit: 64, null: false t.string "rid", limit: 95, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.datetime "completed_at" end add_index "withdraws", ["aasm_state"], name: "index_withdraws_on_aasm_state", using: :btree add_index "withdraws", ["account_id"], name: "index_withdraws_on_account_id", using: :btree add_index "withdraws", ["currency_id", "txid"], name: "index_withdraws_on_currency_id_and_txid", unique: true, using: :btree add_index "withdraws", ["currency_id"], name: "index_withdraws_on_currency_id", using: :btree add_index "withdraws", ["member_id"], name: "index_withdraws_on_member_id", using: :btree add_index "withdraws", ["tid"], name: "index_withdraws_on_tid", using: :btree add_index "withdraws", ["type"], name: "index_withdraws_on_type", using: :btree |
2.
다음으로 소개할 프로젝트는 ViaBTC Exchange Server입니다. 중국 viaBTC가 공개한 오픈소스 프로젝트입니다. 제가 본 소스중에서 유일한 C로 개발한 프로젝트입니다.(^^)
트위터를 보니까 2017년에 공개하였던군요.
We just released the open source for ViaBTC trading engine. Fork it on https://t.co/dUrnowdavv
— ViaBTC (@ViaBTC) September 27, 2017
Typical Bitcoin exchanges use relational database for their trade matching machine. The advantage is that it can rapidly realize business logic and guarantee data accuracy and reliability using data-dependent index, transaction etc. mechanisms. But it would also invite problems to the database and result in poor performance. With the development of quantitative trading, an increasing number of orders are now processed systematically and most of them are high-frequency trading with large order quantity which requires high standard for transaction interface delay. When faced with these technical issues, mainstream exchanges are now aware that these traditional data-dependent database can no longer meet the growing demand of trading. In order to break through the bottlenecks of database performance, we have chosen single process to avoid spending on database transactions and locks, and memory calculation to avoid spending on data persistence in return of significant performance improvement.
Fundamentally, the mechanism of a matching machine is simple: Submit orders by time, and match trading based on preferences of price and time efficiency. User balance change indicates the trading result. Since user deposit and withdrawal will also affect account balance, therefore, the final result should be identical to operational log. This is very similar to AOF mode of Redis, which essentially is an in-memory database that relies on operational logs for data recovery. Besides, by generating data state slices periodically, the matching engine can upload slice data and then log data to complete data recovery, hence reducing time of loading historical data to restart services.
Based on calculation of Benchmark of Redis, a single write server is fully capable of supporting up to 10,000+ orders. For the common benefit of achieving high availability, matching service requires one-master multi-slave cluster and in our case, we use Zookeeper to ensure its management. In addition, database is required for asynchronous persistent storage of order history, transaction history, asset change history etc.
Peatio와 비교하면 Redis와 Kafka를 사용하고 있습니다. 또한 MySQL 을 적용하는 방식도 다릅니다. 운영로그를 기록하고 데이타를 복구할 때 사용합니다.
matchengine: This is the most important part for it records user balance and executes user order. It is in memory database, saves operation log in MySQL and redoes the operation log when start. It also writes user history into MySQL, push balance, orders and deals message to kafka.
기본적으로 모든 데이타는 Shared Memory를 이용하고 Linked List방식의 자료구조를 가지고 있습니다. 매매체결엔진 퀀트컵을 보시면 비슷한 방식으로 Limit OrderBook을 관리하는 사례를 보실 수 있습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
/* * Description: * History: yang@haipo.me, 2017/03/21, create */ # ifndef _UT_LIST_H_ # define _UT_LIST_H_ typedef struct list_node { struct list_node *prev; struct list_node *next; void *value; } list_node; typedef struct list_iter { list_node *next; int direction; } list_iter; typedef struct list_type { void *(*dup)(void *value); void (*free)(void *value); int (*compare)(const void *value1, const void *value2); } list_type; typedef struct list_t { list_node *head; list_node *tail; list_type type; unsigned long len; } list_t; # define list_len(l) ((l)->len) # define list_head(l) ((l)->head) # define list_tail(l) ((l)->tail) # define list_prev_node(n) ((n)->prev) # define list_next_node(n) ((n)->next) # define list_node_value(n) ((n)->value) list_t *list_create(list_type *type); list_t *list_add_node_head(list_t *list, void *value); list_t *list_add_node_tail(list_t *list, void *value); list_t *list_insert_node(list_t *list, list_node *pos, void *value, int before); void list_del(list_t *list, list_node *node); void list_clear(list_t *list); void list_rotate(list_t *list); void list_release(list_t *list); # define LIST_START_HEAD 0 # define LIST_START_TAIL 1 list_iter *list_get_iterator(list_t *list, int direction); list_node *list_next(list_iter *iter); void list_rewind_head(list_t *list, list_iter *iter); void list_rewind_tail(list_t *list, list_iter *iter); void list_release_iterator(list_iter *iter); list_t *list_dup(list_t *orig); list_node *list_find(list_t *list, void *value); list_node *list_index(list_t *list, long index); # endif |
3.
비동기식 데이타를 처리하기 위해 AMQP나 Kafka를 사용하는 것은 무척 좋은 선택입니다. 이상의 프로젝트를 보면서 두가지 생각이 들었습니다.
첫째는 매칭엔진의 기능 정의입니다. Peatio와 viaBTC의 매칭엔진는 Limit Orderbook기능외에 금융시장에서의 위험관리, 계좌관리, 잔고관리 및 포지션 관리에 더한 개념입니다. 여의도의 경험에 익숙한 분이라면 증권회사와 거래소의 기능을 분리합니다. 저도 예전에 프로젝트를 했을 때 같은 방식으로 설계구현했습니다. 분리하지 않고 통합을 하니까 Central Limit Orderbook이 일반적인 거래소와 다른 방식을 가집니다. 지금 되돌아보면 하나의 거래소에 여럿 중개회사가 아닌 구조라고 하면 굳이 분리할 필요가 있을까 하는 의문을 가집니다. 분리할 경우 관리비용이 증가합니다. 프로세스를 나누어야 하고 장애에 따른 복구도 복잡해지기 때문입니다.
둘째 Scalibility입니다. 두경우 모두 MySQL을 중요히 사용합니다. 후자는 운영로그관리라고 하지만 프로세스를 시작할 때 기준은 MySQL의 데이타입니다. 만약에 동시에 다량의 거래가 발생하여 MySQL부하가 늘어날 때 어떤 방식이 가능할까요? Read Only일 경우에는 간단하지만 Write일 경우 쉽지 않습니다. Scaling a cryptocurrency exchange using Go and microservices와 같은 글을 읽어보면 Microservice가 중요한 해결지점이라고 하지만 어떤 구조를 택하더라도 MySQL에서 병목이 발생하면 다른 프로세스의 지연으로 이어집니다.
거래소사업을 한다고 할 때 어떤 목적을 가질지, 하나로 정의할 수 없습니다. 다만 거래소를 지속적으로 운영하여 다양한 비즈니스기회를 보고자 한다면 최소한 개발팀을 직접 만들고 운영능력을 키워야 합니다. 유지보수비용도 고려해야 합니다. 맨땅에서 시작하고자 할 경우 오픈소스를 분석하면서 시작하는 것도 좋은 방법이라고 생각합니다. 물론 UI와 API도 중요합니다.