암호통화거래소 만들기 둘째

in #cex6 years ago


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와 관련한 부분입니다.
# 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 &gt; 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는 아래와 같습니다.

# 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로 개발한 프로젝트입니다.(^^)

ViaBTC Exchange Server

트위터를 보니까 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을 관리하는 사례를 보실 수 있습니다.
/*
 * Description: 
 *     History: [email protected], 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

비동기식 데이타를 처리하기 위해 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도 중요합니다.


Posted from my blog with SteemPress : http://smallake.kr/?p=26877

Coin Marketplace

STEEM 0.16
TRX 0.15
JST 0.027
BTC 60244.17
ETH 2333.72
USDT 1.00
SBD 2.47