Commit 60e54b41 authored by 贾浩@五瓣科技's avatar 贾浩@五瓣科技

init

parents
Pipeline #830 canceled with stages
.vscode
.idea
build
\ No newline at end of file
.PHONY: default all clean dev api
GOBIN = $(shell pwd)/build
default: api
api:
go build $(BUILD_FLAGS) -v -o=${GOBIN}/$@ ./cmd/api
\ No newline at end of file
package handlers
import (
"net/http"
"github.com/ethereum/go-ethereum/common"
"github.com/holiman/uint256"
"github.com/gin-gonic/gin"
apiTypes "github.com/exchain/orderbook/api/types"
"github.com/exchain/orderbook/engine"
"github.com/exchain/orderbook/orderbook"
"github.com/exchain/orderbook/types"
"github.com/exchain/orderbook/utils"
)
type OrderHandler struct {
eg *engine.Engine
}
func NewOrderHandler(eg *engine.Engine) *OrderHandler {
return &OrderHandler{
eg: eg,
}
}
// PlaceLimitOrder 处理限价单请求
func (h *OrderHandler) PlaceLimitOrder(c *gin.Context) {
var req apiTypes.PlaceLimitOrderRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
ok, agent := req.Recover()
if !ok {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid signature"})
return
}
side := orderbook.Buy
if req.Side == "sell" {
side = orderbook.Sell
}
quantity, err := uint256.FromDecimal(req.Quantity)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid quantity"})
return
}
price, err := uint256.FromDecimal(req.Price)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid price"})
return
}
orderID := utils.GenOrderID()
_, resp, err := h.eg.ProcessLimitOrder(orderID, types.Coin(req.BaseToken), types.Coin(req.QuoteToken), agent, side, price, quantity, req.Nonce)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, resp)
}
// PlaceMarketOrder 处理市价单请求
func (h *OrderHandler) PlaceMarketOrder(c *gin.Context) {
var req apiTypes.PlaceMarketOrderRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
ok, agent := req.Recover()
if !ok {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid signature"})
return
}
side := orderbook.Sell
if req.Side == "buy" {
side = orderbook.Buy
}
quantity, err := uint256.FromDecimal(req.Quantity)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid quantity"})
return
}
orderID := utils.GenOrderID()
_, resp, err := h.eg.ProcessMarketOrder(orderID,
types.Coin(req.BaseToken),
types.Coin(req.QuoteToken),
agent,
side, quantity, req.Nonce)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, resp)
}
// GetOrderBook 获取订单簿深度
func (h *OrderHandler) GetOrderBookDepth(c *gin.Context) {
var req apiTypes.DepthRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
asks, bids, err := h.eg.Depth(types.Coin(req.BaseToken), types.Coin(req.QuoteToken))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"asks": asks,
"bids": bids,
})
}
// CancelOrder 取消订单
func (h *OrderHandler) CancelOrder(c *gin.Context) {
var req apiTypes.CancelOrderRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
ok, agent := req.Recover()
if !ok {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid signature"})
return
}
err := h.eg.ProcessCancelOrder(req.OrderId, types.Coin(req.BaseToken), types.Coin(req.QuoteToken), agent, req.Nonce)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "success",
})
}
func (h *OrderHandler) SignProxy(c *gin.Context) {
var req apiTypes.SignProxyRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
ok, user := req.Recover()
if !ok {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid signature"})
return
}
err := h.eg.PorcessSignProxy(user, common.HexToAddress(req.Agent), req.Nonce)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "success",
})
}
package router
import (
"github.com/exchain/orderbook/api/handlers"
"github.com/gin-gonic/gin"
)
// SetupRouter 配置API路由
func SetupRouter(orderHandler *handlers.OrderHandler) *gin.Engine {
r := gin.Default()
// 订单相关路由
orderGroup := r.Group("/api/v1/orders")
{
orderGroup.POST("/limit", orderHandler.PlaceLimitOrder) // 限价单
orderGroup.POST("/market", orderHandler.PlaceMarketOrder) // 市价单
orderGroup.POST("/cancel", orderHandler.CancelOrder) // 取消订单
}
// 订单簿相关路由
r.GET("/api/v1/orderbook", orderHandler.GetOrderBookDepth) // 获取订单簿深度
return r
}
package types
import (
"fmt"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
"github.com/holiman/uint256"
)
// PlaceLimitOrderRequest 限价单请求
type PlaceLimitOrderRequest struct {
BaseToken string `json:"baseToken" binding:"required"`
QuoteToken string `json:"quoteToken" binding:"required"`
Side string `json:"side" binding:"required,oneof=buy sell"`
Quantity string `json:"quantity" binding:"required"`
Price string `json:"price" binding:"required"`
Nonce uint64 `json:"nonce" binding:"required"`
V int32 `json:"v" binding:"required"`
R hexutil.Bytes `json:"r" binding:"required"`
S hexutil.Bytes `json:"s" binding:"required"`
}
func (r *PlaceLimitOrderRequest) Recover() (ok bool, address common.Address) {
data := fmt.Sprintf("baseToken=%s&quoteToken=%s&side=%s&quantity=%s&price=%s&nonce=%d", r.BaseToken, r.QuoteToken, r.Side, r.Quantity, r.Price, r.Nonce)
hashsum := crypto.Keccak256Hash([]byte(data))
// combile sig
sig := make([]byte, 65)
copy(sig, r.R)
copy(sig[32:], r.S)
sig[64] = byte(r.V)
pubkey, err := crypto.SigToPub(hashsum.Bytes(), sig)
if err != nil {
return false, common.Address{}
}
return true, crypto.PubkeyToAddress(*pubkey)
}
// PlaceMarketOrderRequest 市价单请求
type PlaceMarketOrderRequest struct {
BaseToken string `json:"baseToken" binding:"required"`
QuoteToken string `json:"quoteToken" binding:"required"`
Side string `json:"side" binding:"required,oneof=buy sell"`
Quantity string `json:"quantity" binding:"required"`
Nonce uint64 `json:"nonce" binding:"required"`
V int32 `json:"v" binding:"required"`
R hexutil.Bytes `json:"r" binding:"required"`
S hexutil.Bytes `json:"s" binding:"required"`
}
func (r *PlaceMarketOrderRequest) Recover() (ok bool, address common.Address) {
data := fmt.Sprintf("baseToken=%s&quoteToken=%s&side=%s&quantity=%s&nonce=%d", r.BaseToken, r.QuoteToken, r.Side, r.Quantity, r.Nonce)
hashsum := crypto.Keccak256Hash([]byte(data))
// combile sig
sig := make([]byte, 65)
copy(sig, r.R)
copy(sig[32:], r.S)
sig[64] = byte(r.V)
pubkey, err := crypto.SigToPub(hashsum.Bytes(), sig)
if err != nil {
return false, common.Address{}
}
return true, crypto.PubkeyToAddress(*pubkey)
}
type DepthRequest struct {
BaseToken string `json:"baseToken" binding:"required"`
QuoteToken string `json:"quoteToken" binding:"required"`
}
type CancelOrderRequest struct {
BaseToken string `json:"baseToken" binding:"required"`
QuoteToken string `json:"quoteToken" binding:"required"`
OrderId string `json:"orderId" binding:"required"`
Nonce uint64 `json:"nonce" binding:"required"`
V int32 `json:"v" binding:"required"`
R hexutil.Bytes `json:"r" binding:"required"`
S hexutil.Bytes `json:"s" binding:"required"`
}
func (r *CancelOrderRequest) Recover() (ok bool, address common.Address) {
data := fmt.Sprintf("baseToken=%s&quoteToken=%s&orderId=%s&nonce=%d", r.BaseToken, r.QuoteToken, r.OrderId, r.Nonce)
hashsum := crypto.Keccak256Hash([]byte(data))
// combile sig
sig := make([]byte, 65)
copy(sig, r.R)
copy(sig[32:], r.S)
sig[64] = byte(r.V)
pubkey, err := crypto.SigToPub(hashsum.Bytes(), sig)
if err != nil {
return false, common.Address{}
}
return true, crypto.PubkeyToAddress(*pubkey)
}
type SignProxyRequest struct {
Agent string `json:"agent" binding:"required"`
Nonce uint64 `json:"nonce" binding:"required"`
V int32 `json:"v" binding:"required"`
R hexutil.Bytes `json:"r" binding:"required"`
S hexutil.Bytes `json:"s" binding:"required"`
}
func (r *SignProxyRequest) Recover() (ok bool, address common.Address) {
data := fmt.Sprintf("s&agent=%s&nonce=%d", r.Agent, r.Nonce)
hashsum := crypto.Keccak256Hash([]byte(data))
// combile sig
sig := make([]byte, 65)
copy(sig, r.R)
copy(sig[32:], r.S)
sig[64] = byte(r.V)
pubkey, err := crypto.SigToPub(hashsum.Bytes(), sig)
if err != nil {
return false, common.Address{}
}
return true, crypto.PubkeyToAddress(*pubkey)
}
// OrderResponse 订单响应
type OrderResponse struct {
OrderID string `json:"orderId"`
Pair string `json:"pair"`
Side string `json:"side"`
Quantity *uint256.Int `json:"quantity"`
Price *uint256.Int `json:"price"`
Status string `json:"status"`
CreatedAt uint64 `json:"createdAt"`
}
package main
import (
"log"
"github.com/exchain/orderbook/api/handlers"
"github.com/exchain/orderbook/api/router"
"github.com/exchain/orderbook/engine"
)
func main() {
eg := engine.GetEngine()
// 创建订单处理器
orderHandler := handlers.NewOrderHandler(eg)
// 设置路由
r := router.SetupRouter(orderHandler)
// 启动服务器
if err := r.Run(":8080"); err != nil {
log.Fatalf("Failed to start server: %v", err)
}
}
package database
import (
"bytes"
"encoding/gob"
"sync"
"github.com/exchain/orderbook/leveldb"
"github.com/exchain/orderbook/types"
"github.com/ethereum/go-ethereum/common"
"github.com/holiman/uint256"
)
type AccountObject struct {
db *leveldb.Database
Address common.Address
Balances map[types.Coin]*uint256.Int
Proxy *common.Address
sync.RWMutex
}
func NewAccountObject(db *leveldb.Database, address common.Address) *AccountObject {
return &AccountObject{
db: db,
Address: address,
Balances: make(map[types.Coin]*uint256.Int),
}
}
func (a *AccountObject) SetProxy(proxy common.Address) {
a.Lock()
defer a.Unlock()
a.Proxy = &proxy
}
func (a *AccountObject) GetProxy() *common.Address {
a.RLock()
defer a.RUnlock()
return a.Proxy
}
func (a *AccountObject) Deposit(coin types.Coin, val *uint256.Int) {
a.AddBalance(coin, val, false)
}
func (a *AccountObject) GetBalance(coin types.Coin, freeze bool) *uint256.Int {
a.RLock()
defer a.RUnlock()
k := coin
if freeze {
k = coin + "_freeze"
}
return a.Balances[k]
}
func (a *AccountObject) Freeze(coin types.Coin, val *uint256.Int) (overflow bool) {
a.Lock()
defer a.Unlock()
of := a.SubBalance(coin, val, false)
if of {
return true
}
a.AddBalance(coin, val, true)
return false
}
func (a *AccountObject) SetBalance(coin types.Coin, balance *uint256.Int, freeze bool) {
a.Lock()
defer a.Unlock()
k := coin
if freeze {
k = coin + "_freeze"
}
a.Balances[k] = balance
}
func (a *AccountObject) SubBalance(coin types.Coin, balance *uint256.Int, freeze bool) (overflow bool) {
a.Lock()
defer a.Unlock()
k := coin
if freeze {
k = coin + "_freeze"
}
newBalance, of := new(uint256.Int).SubOverflow(a.Balances[k], balance)
if of {
return true
}
a.Balances[k] = newBalance
return false
}
func (a *AccountObject) AddBalance(coin types.Coin, balance *uint256.Int, freeze bool) {
a.Lock()
defer a.Unlock()
k := coin
if freeze {
k = coin + "_freeze"
}
newBalance := new(uint256.Int).Add(a.Balances[k], balance)
a.Balances[k] = newBalance
}
func (a *AccountObject) Load(data []byte) error {
decoder := gob.NewDecoder(bytes.NewBuffer(data))
return decoder.Decode(a)
}
func (a *AccountObject) Dump() ([]byte, error) {
var buffer bytes.Buffer
encoder := gob.NewEncoder(&buffer)
if err := encoder.Encode(a); err != nil {
return nil, err
}
return buffer.Bytes(), nil
}
package database
import (
"encoding/json"
"errors"
"fmt"
"github.com/exchain/orderbook/leveldb"
"github.com/exchain/orderbook/orderbook"
"github.com/ethereum/go-ethereum/common"
goleveldb "github.com/syndtr/goleveldb/leveldb"
)
type DexDB struct {
db *leveldb.Database
accountObjects map[common.Address]*AccountObject
}
// New creates a new state from a given trie.
func New(db *leveldb.Database) (*DexDB, error) {
ddb := &DexDB{
db: db,
accountObjects: make(map[common.Address]*AccountObject),
}
return ddb, nil
}
func (ddb *DexDB) GetPairs() (pairs [][]string, err error) {
data, err := ddb.db.Get([]byte("_pairs"))
if err != nil {
return
}
pairs = make([][]string, 0)
err = json.Unmarshal(data, &pairs)
if err != nil {
return
}
return
}
func (ddb *DexDB) GetOrNewAcountObjectByAgent(agent common.Address) (acc *AccountObject, err error) {
data, err := ddb.db.Get([]byte(fmt.Sprintf("proxy_%s", agent.Hex())))
if err != nil && errors.Is(err, goleveldb.ErrNotFound) {
return nil, nil
}
user := common.Address(data)
return ddb.GetOrNewAccountObject(user)
}
func (ddb *DexDB) GetOrNewAccountObject(address common.Address) (acc *AccountObject, err error) {
if acc, ok := ddb.accountObjects[address]; ok {
return acc, nil
}
// get from db
data, err := ddb.db.Get(address.Bytes())
if err != nil && errors.Is(err, goleveldb.ErrNotFound) {
return nil, err
}
if data != nil {
acc = NewAccountObject(ddb.db, address)
if err != nil {
return nil, err
}
err = acc.Load(data)
if err != nil {
return nil, err
}
ddb.accountObjects[address] = acc
return acc, nil
}
acc = NewAccountObject(ddb.db, address)
ddb.accountObjects[address] = acc
return acc, nil
}
func (ddb *DexDB) SaveOrder(order *orderbook.Order) error {
if order == nil {
return nil
}
data, err := order.Dump()
if err != nil {
return err
}
return ddb.db.Put([]byte(order.Id), data)
}
func (ddb *DexDB) SaveAccountObject(acc *AccountObject) error {
if acc == nil {
return nil
}
if acc == nil {
return nil
}
data, err := acc.Dump()
if err != nil {
return err
}
if acc.Proxy != nil {
err = ddb.db.Put([]byte(fmt.Sprintf("proxy_%s", acc.Proxy.Hex())), acc.Address.Bytes())
if err != nil {
return err
}
}
return ddb.db.Put(acc.Address.Bytes(), data)
}
package database
import (
"github.com/exchain/orderbook/leveldb"
"github.com/exchain/orderbook/orderbook"
)
type OrderObject struct {
db *leveldb.Database
Order *orderbook.Order
}
func NewOrderObject(db *leveldb.Database, order *orderbook.Order) *OrderObject {
return &OrderObject{
db: db,
Order: order,
}
}
func (o *OrderObject) Save() error {
data, err := o.Order.Dump()
if err != nil {
return err
}
return o.db.Put([]byte(o.Order.Id), data)
}
func (o *OrderObject) Load() error {
b, err := o.db.Get([]byte(o.Order.Id))
if err != nil {
return err
}
o.Order.Load(b)
return nil
}
package engine
import (
"errors"
"github.com/exchain/orderbook/types"
"github.com/ethereum/go-ethereum/common"
"github.com/holiman/uint256"
)
func (e *Engine) PorcessSignProxy(user common.Address, proxy common.Address, nonce uint64) (err error) {
userObject, err := e.db.GetOrNewAccountObject(user)
if err != nil {
return
}
if userObject == nil {
return errors.New("invalid user")
}
userObject.SetProxy(proxy)
err = e.db.SaveAccountObject(userObject)
if err != nil {
return
}
tx := &types.SignProxyTx{
Tx: types.Tx{
Time: nonce,
User: user,
Action: types.OrderActionSignProxy,
Nonce: nonce,
},
ProxyAddress: proxy,
}
e.AddToQueue(tx)
return
}
func (e *Engine) ProcessDepositBalance(user common.Address, coin types.Coin, amount *uint256.Int) (err error) {
userObject, err := e.db.GetOrNewAccountObject(user)
if err != nil {
return
}
if userObject == nil {
return errors.New("invalid user")
}
userObject.AddBalance(coin, amount, false)
err = e.db.SaveAccountObject(userObject)
if err != nil {
return
}
return
}
package engine
import (
"github.com/exchain/orderbook/database"
"github.com/exchain/orderbook/leveldb"
"github.com/exchain/orderbook/orderbook"
"github.com/exchain/orderbook/types"
"sync"
"github.com/exchain/go-exchain/exchain"
"github.com/exchain/go-exchain/exchain/chaindb"
nebulav1 "github.com/exchain/go-exchain/exchain/protocol/gen/go/nebula/v1"
"github.com/exchain/go-exchain/exchain/wrapper"
)
// Engine 实现订单撮合引擎
type Engine struct {
db *database.DexDB
orderbooks map[string]*orderbook.OrderBook
chainDB chaindb.ChainDB
txQueue chan *nebulav1.Transaction
sync.Mutex
}
var eg *Engine
func GetEngine() *Engine {
if eg == nil {
panic("no engine")
}
return eg
}
// NewEngine 创建一个新的订单撮合引擎
func NewEngine(chainDB chaindb.ChainDB) *Engine {
if eg != nil {
return eg
}
lvdb, err := leveldb.New("data/dexdb", 1024, 1024, "dexdata", false)
if err != nil {
panic(err)
}
db, err := database.New(lvdb)
if err != nil {
panic(err)
}
eg = &Engine{
db: db,
orderbooks: make(map[string]*orderbook.OrderBook),
chainDB: chainDB,
txQueue: make(chan *nebulav1.Transaction, 10240),
}
return eg
}
func (e *Engine) InitPairs() {
pairs, err := e.db.GetPairs()
if err != nil {
panic(err)
}
_defaultPairs := [][]string{
{"BTC", "USDT"},
}
for _, pair := range pairs {
e.orderbooks[string(pair[0]+pair[1])] = orderbook.NewOrderBook(pair[0], pair[1])
}
for _, pair := range _defaultPairs {
e.orderbooks[string(pair[0]+pair[1])] = orderbook.NewOrderBook(pair[0], pair[1])
}
}
func (e *Engine) Start() {
e.InitPairs()
}
func (e *Engine) NewPayload(params exchain.PayloadParams) (*exchain.ExecutionResult, error) {
parent, err := e.chainDB.GetBlockByLabel(chaindb.ExChainBlockLatest)
if err != nil {
return nil, err
}
wParent := wrapper.NewBlkWrapper(parent)
header := &nebulav1.BlockHeader{
Height: parent.Header.Height + 1,
ParentHash: wParent.Hash().Bytes(),
Timestamp: params.Timestamp,
Proposer: params.Proposer.Bytes(),
L1Hash: params.L1Info.BlockHash.Bytes(),
L1Height: params.L1Info.Number,
AppRoot: make([]byte, 0),
}
receipts, err := e.ProcessTx(header, params.Transactions)
if err != nil {
return nil, err
}
orderTxs, orderReceipts, err := e.ProcessOrders(header)
if err != nil {
return nil, err
}
params.Transactions.Txs = append(params.Transactions.Txs, orderTxs.Txs...)
receipts.Receipts = append(receipts.Receipts, orderReceipts.Receipts...)
result := &exchain.ExecutionResult{
Payload: &nebulav1.Block{
Header: header,
Transactions: params.Transactions,
},
Receipts: receipts,
}
return result, nil
}
func (e *Engine) ProcessPayload(block *nebulav1.Block) (exchain.ExecutionResult, error) {
panic("not implemented")
}
func (e *Engine) AddToQueue(tx types.Transaction) {
switch t := tx.(type) {
case *types.PlaceOrderTx:
txType := nebulav1.TxType_MarketTx
if t.LimitPrice != nil {
txType = nebulav1.TxType_LimitTx
}
side := nebulav1.OrderSide_BUY
if t.Action == types.OrderActionSell {
side = nebulav1.OrderSide_SELL
}
ptx := &nebulav1.Transaction{
TxType: txType,
User: t.User.Hex(),
Nonce: t.GetNonce(),
Tx: &nebulav1.Transaction_LimitTx{
LimitTx: &nebulav1.LimitOrderTransaction{
Pair: t.GetPair(),
Side: side,
Quantity: t.GetQuality().Bytes(),
Price: t.GetLimitPrice().Bytes(),
},
},
}
e.txQueue <- ptx
case *types.CancelOrderTx:
ptx := &nebulav1.Transaction{
TxType: nebulav1.TxType_CancelTx,
User: t.User.Hex(),
Nonce: t.GetNonce(),
Tx: &nebulav1.Transaction_CancelTx{
CancelTx: &nebulav1.CancelOrderTransaction{
OrderId: t.GetOrderID(),
},
},
}
e.txQueue <- ptx
case *types.SignProxyTx:
pTx := &nebulav1.Transaction{
TxType: nebulav1.TxType_SignProxyTx,
User: t.User.Hex(),
Nonce: t.GetNonce(),
Tx: &nebulav1.Transaction_SignProxyTx{
SignProxyTx: &nebulav1.SignProxyTransaction{
SignerProxy: t.GetProxyAddress().Bytes(),
},
},
}
e.txQueue <- pTx
default:
panic("not implemented")
}
}
package engine
import (
"errors"
apiTypes "github.com/exchain/orderbook/api/types"
"github.com/exchain/orderbook/orderbook"
"github.com/exchain/orderbook/types"
"github.com/ethereum/go-ethereum/common"
"github.com/holiman/uint256"
)
func (e *Engine) ProcessMarketOrder(orderId string, baseToken, quoteToken types.Coin, agent common.Address, side orderbook.Side, quantity *uint256.Int, nonce uint64) (tx *types.PlaceOrderTx, response *apiTypes.OrderResponse, err error) {
response = new(apiTypes.OrderResponse)
e.Lock()
ob, ok := e.orderbooks[string(baseToken+quoteToken)]
e.Unlock()
if !ok {
err = errors.New("invalid pair")
return
}
makerObj, err := e.db.GetOrNewAcountObjectByAgent(agent)
if err != nil {
return
}
if makerObj == nil {
err = errors.New("invalid agent")
return
}
tx = new(types.PlaceOrderTx)
tx.OrderID = orderId
tx.Nonce = nonce
tx.Time = nonce
tx.User = makerObj.Address
tx.BaseCoin = baseToken
tx.QuoteCoin = quoteToken
tx.Type = types.PlaceOrder
if side == orderbook.Sell {
tx.Action = types.OrderActionSell
} else {
tx.Action = types.OrderActionBuy
}
tx.LimitPrice = nil
tx.Quantity = quantity
makerOrder := orderbook.NewOrder(orderId, makerObj.Address, side, quantity, uint256.NewInt(0), nonce)
err = e.db.SaveOrder(makerOrder)
if err != nil {
return
}
done, partial, partialQty, quantityLeft, err := ob.ProcessMarketOrder(side, quantity)
if err != nil {
return
}
totalQuantity := uint256.NewInt(0)
totalValue := uint256.NewInt(0)
for _, order := range done {
totalQuantity = new(uint256.Int).Add(totalQuantity, order.Quantity)
value := new(uint256.Int).Mul(order.Price, order.Quantity)
totalValue = new(uint256.Int).Add(totalValue, value)
err = e.db.SaveOrder(order)
if err != nil {
return nil, nil, err
}
makerOrder.Fill(order.Quantity)
takerObj, err := e.db.GetOrNewAccountObject(order.Creator)
if err != nil {
return nil, nil, err
}
if order.Side == orderbook.Sell {
takerObj.SubBalance(baseToken, order.Quantity, true) // 市价单对手只能是限价单,减去对手的冻结金额
takerObj.AddBalance(quoteToken, value, false) // 对手加quoteToken
makerObj.SubBalance(quoteToken, value, false) // 直接减去流动金额
makerObj.AddBalance(baseToken, order.Quantity, false) // 对手加baseToken
}
if order.Side == orderbook.Buy {
takerObj.SubBalance(quoteToken, order.Quantity, true)
takerObj.AddBalance(baseToken, value, false)
makerObj.SubBalance(baseToken, order.Quantity, false)
makerObj.AddBalance(quoteToken, value, false)
}
}
if partial != nil {
totalQuantity = new(uint256.Int).Add(totalQuantity, partialQty)
value := new(uint256.Int).Mul(partial.Price, partialQty)
totalValue = new(uint256.Int).Add(totalValue, value)
err = e.db.SaveOrder(partial)
if err != nil {
return nil, nil, err
}
makerOrder.Fill(partialQty)
takerObj, err := e.db.GetOrNewAccountObject(partial.Creator)
if err != nil {
return nil, nil, err
}
if partial.Side == orderbook.Sell {
takerObj.SubBalance(baseToken, partialQty, true)
takerObj.AddBalance(quoteToken, value, false)
makerObj.SubBalance(quoteToken, value, false)
makerObj.AddBalance(baseToken, partialQty, false)
}
if partial.Side == orderbook.Buy {
takerObj.SubBalance(quoteToken, partialQty, true)
takerObj.AddBalance(baseToken, value, false)
makerObj.SubBalance(baseToken, value, false)
makerObj.AddBalance(quoteToken, partialQty, false)
}
}
avgPrice := uint256.NewInt(0)
if totalQuantity.Cmp(uint256.NewInt(0)) > 0 {
avgPrice = new(uint256.Int).Div(totalValue, totalQuantity)
}
makerOrder.AvgPrice = avgPrice
err = e.db.SaveOrder(makerOrder)
if err != nil {
return nil, nil, err
}
e.AddToQueue(tx)
response = &apiTypes.OrderResponse{
OrderID: makerOrder.Id,
Side: string(tx.Action),
Quantity: new(uint256.Int).Sub(quantity, quantityLeft),
Price: avgPrice,
CreatedAt: nonce,
}
if quantityLeft.Cmp(uint256.NewInt(0)) > 0 {
response.Status = "partial"
} else {
response.Status = "filled"
}
return tx, response, nil
}
func (e *Engine) ProcessLimitOrder(orderId string, baseToken, quoteToken types.Coin, agent common.Address, side orderbook.Side, price, quantity *uint256.Int, nonce uint64) (tx *types.PlaceOrderTx, response *apiTypes.OrderResponse, err error) {
response = new(apiTypes.OrderResponse)
e.Lock()
ob, ok := e.orderbooks[string(baseToken+quoteToken)]
e.Unlock()
if !ok {
err = errors.New("invalid pair")
return
}
makerObj, err := e.db.GetOrNewAcountObjectByAgent(agent)
if err != nil {
return
}
if makerObj == nil {
err = errors.New("invalid agent")
return
}
tx = new(types.PlaceOrderTx)
tx.OrderID = orderId
tx.Nonce = nonce
tx.Time = nonce
tx.User = makerObj.Address
tx.BaseCoin = baseToken
tx.QuoteCoin = quoteToken
tx.Type = types.PlaceOrder
if side == orderbook.Sell {
tx.Action = types.OrderActionSell
} else {
tx.Action = types.OrderActionBuy
}
tx.LimitPrice = price
tx.Quantity = quantity
currentBaseBalance := makerObj.GetBalance(baseToken, false)
currentQuoteBalance := makerObj.GetBalance(quoteToken, false)
if side == orderbook.Sell {
if currentBaseBalance.Cmp(quantity) < 0 {
err = errors.New("insufficient balance")
return
}
// 冻结金额
makerObj.SubBalance(baseToken, quantity, false)
makerObj.AddBalance(baseToken, quantity, true)
} else {
if currentQuoteBalance.Cmp(quantity) < 0 {
err = errors.New("insufficient balance")
return
}
// 计算需要消耗多少quoteToken,再冻结
value := new(uint256.Int).Mul(price, quantity)
makerObj.SubBalance(quoteToken, value, false)
makerObj.AddBalance(quoteToken, value, true)
}
done, partial, _, partialQty, quantityLeft, err := ob.ProcessLimitOrder(side, orderId, makerObj.Address, quantity, price, nonce)
if err != nil {
return
}
for _, order := range done {
value := new(uint256.Int).Mul(order.Price, order.Quantity)
err = e.db.SaveOrder(order)
if err != nil {
return nil, nil, err
}
takerObj, err := e.db.GetOrNewAccountObject(order.Creator)
if err != nil {
return nil, nil, err
}
if order.Side == orderbook.Sell {
takerObj.SubBalance(baseToken, order.Quantity, true)
takerObj.AddBalance(quoteToken, value, false)
makerObj.SubBalance(quoteToken, value, true)
makerObj.AddBalance(baseToken, order.Quantity, false)
}
if order.Side == orderbook.Buy {
takerObj.SubBalance(quoteToken, order.Quantity, true)
takerObj.AddBalance(baseToken, value, false)
makerObj.SubBalance(baseToken, order.Quantity, true)
makerObj.AddBalance(quoteToken, value, false)
}
}
// != 说明完全成交了,但要更新对手最后半个订单的状态
if partial != nil && partial.Creator.Hex() != makerObj.Address.Hex() {
value := new(uint256.Int).Mul(partial.Price, partialQty)
err = e.db.SaveOrder(partial)
if err != nil {
return nil, nil, err
}
takerObj, err := e.db.GetOrNewAccountObject(partial.Creator)
if err != nil {
return nil, nil, err
}
if partial.Side == orderbook.Sell {
takerObj.SubBalance(baseToken, partialQty, true)
takerObj.AddBalance(quoteToken, value, false)
makerObj.SubBalance(quoteToken, value, true)
makerObj.AddBalance(baseToken, partialQty, false)
}
if partial.Side == orderbook.Buy {
takerObj.SubBalance(quoteToken, partialQty, true)
takerObj.AddBalance(baseToken, value, false)
makerObj.SubBalance(baseToken, partialQty, true)
makerObj.AddBalance(quoteToken, value, false)
}
}
response.Status = "filled"
// == 说明吃了所有的单也没有完全成交,假设一点也没成交,不需要关心余额变化
if partial != nil && partial.Creator.Hex() == makerObj.Address.Hex() {
err = e.db.SaveOrder(partial)
if err != nil {
return nil, nil, err
}
response.Status = "partial"
}
avgPrice := price
e.AddToQueue(tx)
response = &apiTypes.OrderResponse{
OrderID: orderId,
Side: string(tx.Action),
Quantity: new(uint256.Int).Sub(quantity, quantityLeft),
Price: avgPrice,
CreatedAt: nonce,
}
return tx, response, nil
}
func (e *Engine) ProcessCancelOrder(orderId string, baseToken, quoteToken types.Coin, agent common.Address, nonce uint64) (err error) {
e.Lock()
ob, ok := e.orderbooks[string(baseToken+quoteToken)]
e.Unlock()
if !ok {
return
}
makerObj, err := e.db.GetOrNewAcountObjectByAgent(agent)
if err != nil {
return
}
if makerObj == nil {
return errors.New("invalid agent")
}
order := ob.Order(orderId)
if order == nil {
return errors.New("invalid order id")
}
if order.Creator.Hex() != makerObj.Address.Hex() {
return errors.New("not owner")
}
_, err = ob.CancelOrder(orderId)
if err != nil {
return
}
tx := &types.CancelOrderTx{
Tx: types.Tx{
OrderID: orderId,
Time: nonce,
User: makerObj.Address,
Action: types.OrderActionCancel,
Nonce: nonce,
},
}
e.AddToQueue(tx)
return nil
}
func (e *Engine) Depth(baseToken, quoteToken types.Coin) (asks, bids []*orderbook.PriceLevel, err error) {
e.Lock()
ob, ok := e.orderbooks[string(baseToken+quoteToken)]
e.Unlock()
if !ok {
err = errors.New("invalid pair")
return
}
a, b := ob.Depth()
return a, b, nil
}
package engine
import (
"errors"
"github.com/exchain/orderbook/types"
"github.com/ethereum/go-ethereum/common"
nebulav1 "github.com/exchain/go-exchain/exchain/protocol/gen/go/nebula/v1"
"github.com/exchain/go-exchain/exchain/wrapper"
"github.com/holiman/uint256"
)
func (e *Engine) ProcessOrders(header *nebulav1.BlockHeader) (txs *nebulav1.TransactionList, receipts *nebulav1.TransactionReceiptList, err error) {
txs = &nebulav1.TransactionList{}
txs.Txs = make([]*nebulav1.Transaction, 0)
receipts = &nebulav1.TransactionReceiptList{}
receipts.Receipts = make([]*nebulav1.TransactionReceipt, 0)
outer:
for i := 0; i < 100 && len(txs.Txs) < 100; i++ {
select {
case tx := <-e.txQueue:
if tx == nil {
break
}
txs.Txs = append(txs.Txs, tx)
default:
break outer
}
}
for _, tx := range txs.Txs {
wtx := wrapper.NewTxWrapper(tx)
switch tx.TxType {
case nebulav1.TxType_LimitTx:
receipts.Receipts = append(receipts.Receipts, &nebulav1.TransactionReceipt{
Hash: wtx.Hash().Bytes(),
TxType: tx.TxType,
Success: true,
BlockHeight: header.Height,
Timestamp: header.Timestamp,
Content: &nebulav1.TransactionReceipt_LimitR{
LimitR: &nebulav1.LimitOrderReceipt{},
},
})
case nebulav1.TxType_MarketTx:
receipts.Receipts = append(receipts.Receipts, &nebulav1.TransactionReceipt{
Hash: wtx.Hash().Bytes(),
TxType: tx.TxType,
Success: true,
BlockHeight: header.Height,
Timestamp: header.Timestamp,
Content: &nebulav1.TransactionReceipt_MarketR{
MarketR: &nebulav1.MarketOrderReceipt{},
},
})
case nebulav1.TxType_CancelTx:
receipts.Receipts = append(receipts.Receipts, &nebulav1.TransactionReceipt{
Hash: wtx.Hash().Bytes(),
TxType: tx.TxType,
Success: true,
BlockHeight: header.Height,
Timestamp: header.Timestamp,
Content: &nebulav1.TransactionReceipt_CancelR{
CancelR: &nebulav1.CancelOrderReceipt{},
},
})
case nebulav1.TxType_SignProxyTx:
receipts.Receipts = append(receipts.Receipts, &nebulav1.TransactionReceipt{
Hash: wtx.Hash().Bytes(),
TxType: tx.TxType,
Success: true,
BlockHeight: header.Height,
Timestamp: header.Timestamp,
Content: &nebulav1.TransactionReceipt_SignProxyR{
SignProxyR: &nebulav1.SignProxyReceipt{},
},
})
default:
panic("not implemented")
}
}
return txs, receipts, nil
}
func (e *Engine) ProcessTx(header *nebulav1.BlockHeader, txs *nebulav1.TransactionList) (*nebulav1.TransactionReceiptList, error) {
receipts := &nebulav1.TransactionReceiptList{
Receipts: make([]*nebulav1.TransactionReceipt, 0),
}
for _, tx := range txs.Txs {
wtx := wrapper.NewTxWrapper(tx)
receipt := &nebulav1.TransactionReceipt{
Hash: wtx.Hash().Bytes(),
TxType: tx.TxType,
Success: true,
BlockHeight: header.Height,
Timestamp: header.Timestamp,
}
switch tx.TxType {
case nebulav1.TxType_DepositTx:
address := common.Address(tx.Tx.(*nebulav1.Transaction_DepositTx).DepositTx.User)
coin := types.Coin(tx.Tx.(*nebulav1.Transaction_DepositTx).DepositTx.Coin)
amount := tx.Tx.(*nebulav1.Transaction_DepositTx).DepositTx.Amount
amt := new(uint256.Int).SetBytes(amount)
err := e.ProcessDepositBalance(address, coin, amt)
if err != nil {
receipt.Success = false
}
receipt.Content = &nebulav1.TransactionReceipt_DepositR{
DepositR: &nebulav1.DepositReceipt{},
}
case nebulav1.TxType_LimitTx:
receipt.Content = &nebulav1.TransactionReceipt_LimitR{
LimitR: &nebulav1.LimitOrderReceipt{},
}
case nebulav1.TxType_WithdrawTx:
receipt.Content = &nebulav1.TransactionReceipt_WithdrawR{
WithdrawR: &nebulav1.WithdrawReceipt{},
}
case nebulav1.TxType_CancelTx:
receipt.Content = &nebulav1.TransactionReceipt_CancelR{
CancelR: &nebulav1.CancelOrderReceipt{},
}
case nebulav1.TxType_MarketTx:
receipt.Content = &nebulav1.TransactionReceipt_MarketR{
MarketR: &nebulav1.MarketOrderReceipt{},
}
case nebulav1.TxType_CreatePairTx:
err := e.ProcessCreatePair(tx)
if err != nil {
receipt.Success = false
}
receipt.Content = &nebulav1.TransactionReceipt_CreatePairR{
CreatePairR: &nebulav1.CreatePairReceipt{},
}
case nebulav1.TxType_DisablePairTx:
err := e.ProcessDisablePair(tx)
if err != nil {
receipt.Success = false
}
receipt.Content = &nebulav1.TransactionReceipt_DisablePairR{
DisablePairR: &nebulav1.DisablePairReceipt{},
}
case nebulav1.TxType_ProtocolTx:
receipt.Content = &nebulav1.TransactionReceipt_ProtocolR{
ProtocolR: &nebulav1.ProtocolTransactionReceipt{},
}
case nebulav1.TxType_SignProxyTx:
receipt.Content = &nebulav1.TransactionReceipt_SignProxyR{
SignProxyR: &nebulav1.SignProxyReceipt{},
}
default:
// TODO: return error
return receipts, errors.New("not implemented")
}
receipts.Receipts = append(receipts.Receipts, receipt)
}
return receipts, nil
}
package engine
import nebulav1 "github.com/exchain/go-exchain/exchain/protocol/gen/go/nebula/v1"
func (e *Engine) ProcessCreatePair(tx *nebulav1.Transaction) error {
panic("not implemented")
}
func (e *Engine) ProcessDisablePair(tx *nebulav1.Transaction) error {
panic("not implemented")
}
module github.com/exchain/orderbook
go 1.23.3
require (
github.com/bwmarrin/snowflake v0.3.0
github.com/emirpasic/gods v1.18.1
github.com/ethereum/go-ethereum v1.15.0
github.com/exchain/go-exchain v0.0.1
github.com/gin-gonic/gin v1.10.0
github.com/holiman/uint256 v1.3.2
github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a
)
require (
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/golang-lru v0.5.0 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/oklog/ulid/v2 v2.1.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect
)
require (
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/shirou/gopsutil v3.21.11+incompatible // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.32.0 // indirect
golang.org/x/net v0.34.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/text v0.21.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace github.com/exchain/go-exchain v0.0.1 => ../nebula/
This diff is collapsed.
This diff is collapsed.
// Copyright 2019 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package leveldb
import (
"testing"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/ethdb/dbtest"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/storage"
)
func TestLevelDB(t *testing.T) {
t.Run("DatabaseSuite", func(t *testing.T) {
dbtest.TestDatabaseSuite(t, func() ethdb.KeyValueStore {
db, err := leveldb.Open(storage.NewMemStorage(), nil)
if err != nil {
t.Fatal(err)
}
return &Database{
db: db,
}
})
})
}
func BenchmarkLevelDB(b *testing.B) {
dbtest.BenchDatabaseSuite(b, func() ethdb.KeyValueStore {
db, err := leveldb.Open(storage.NewMemStorage(), nil)
if err != nil {
b.Fatal(err)
}
return &Database{
db: db,
}
})
}
# Go orderbook
Improved matching engine written in Go (Golang)
[![Go Report Card](https://goreportcard.com/badge/github.com/i25959341/orderbook)](https://goreportcard.com/report/github.com/i25959341/orderbook)
[![GoDoc](https://godoc.org/github.com/i25959341/orderbook?status.svg)](https://godoc.org/github.com/i25959341/orderbook)
[![gocover.run](https://gocover.run/github.com/i25959341/orderbook.svg?style=flat&tag=1.10)](https://gocover.run?tag=1.10&repo=github.com%2Fi25959341%2Forderbook)
[![Stability: Active](https://masterminds.github.io/stability/active.svg)](https://masterminds.github.io/stability/active.html)
[![Build Status](https://travis-ci.org/i25959341/orderbook.svg?branch=master)](https://travis-ci.org/i25959341/orderbook)
## Features
- Standard price-time priority
- Supports both market and limit orders
- Supports order cancelling
- High performance (above 300k trades per second)
- Optimal memory usage
- JSON Marshalling and Unmarsalling
- Calculating market price for definite quantity
## Usage
To start using order book you need to create object:
```go
import (
"fmt"
ob "github.com/muzykantov/orderbook"
)
func main() {
orderBook := ob.NewOrderBook()
fmt.Println(orderBook)
}
```
Then you be able to use next primary functions:
```go
func (ob *OrderBook) ProcessLimitOrder(side Side, orderID string, quantity, price decimal.Decimal) (done []*Order, partial *Order, err error) { ... }
func (ob *OrderBook) ProcessMarketOrder(side Side, quantity decimal.Decimal) (done []*Order, partial *Order, quantityLeft decimal.Decimal, err error) { .. }
func (ob *OrderBook) CancelOrder(orderID string) *Order { ... }
```
## About primary functions
### ProcessLimitOrder
```go
// ProcessLimitOrder places new order to the OrderBook
// Arguments:
// side - what do you want to do (ob.Sell or ob.Buy)
// orderID - unique order ID in depth
// quantity - how much quantity you want to sell or buy
// price - no more expensive (or cheaper) this price
// * to create new decimal number you should use decimal.New() func
// read more at https://github.com/shopspring/decimal
// Return:
// error - not nil if quantity (or price) is less or equal 0. Or if order with given ID is exists
// done - not nil if your order produces ends of anoter order, this order will add to
// the "done" slice. If your order have done too, it will be places to this array too
// partial - not nil if your order has done but top order is not fully done. Or if your order is
// partial done and placed to the orderbook without full quantity - partial will contain
// your order with quantity to left
// partialQuantityProcessed - if partial order is not nil this result contains processed quatity from partial order
func (ob *OrderBook) ProcessLimitOrder(side Side, orderID string, quantity, price decimal.Decimal) (done []*Order, partial *Order, err error) { ... }
```
For example:
```
ProcessLimitOrder(ob.Sell, "uinqueID", decimal.New(55, 0), decimal.New(100, 0))
asks: 110 -> 5 110 -> 5
100 -> 1 100 -> 56
-------------- -> --------------
bids: 90 -> 5 90 -> 5
80 -> 1 80 -> 1
done - nil
partial - nil
```
```
ProcessLimitOrder(ob.Buy, "uinqueID", decimal.New(7, 0), decimal.New(120, 0))
asks: 110 -> 5
100 -> 1
-------------- -> --------------
bids: 90 -> 5 120 -> 1
80 -> 1 90 -> 5
80 -> 1
done - 2 (or more orders)
partial - uinqueID order
```
```
ProcessLimitOrder(ob.Buy, "uinqueID", decimal.New(3, 0), decimal.New(120, 0))
asks: 110 -> 5
100 -> 1 110 -> 3
-------------- -> --------------
bids: 90 -> 5 90 -> 5
80 -> 1 90 -> 5
done - 1 order with 100 price, (may be also few orders with 110 price) + uinqueID order
partial - 1 order with price 110
```
### ProcessMarketOrder
```go
// ProcessMarketOrder immediately gets definite quantity from the order book with market price
// Arguments:
// side - what do you want to do (ob.Sell or ob.Buy)
// quantity - how much quantity you want to sell or buy
// * to create new decimal number you should use decimal.New() func
// read more at https://github.com/shopspring/decimal
// Return:
// error - not nil if price is less or equal 0
// done - not nil if your market order produces ends of anoter orders, this order will add to
// the "done" slice
// partial - not nil if your order has done but top order is not fully done
// partialQuantityProcessed - if partial order is not nil this result contains processed quatity from partial order
// quantityLeft - more than zero if it is not enought orders to process all quantity
func (ob *OrderBook) ProcessMarketOrder(side Side, quantity decimal.Decimal) (done []*Order, partial *Order, quantityLeft decimal.Decimal, err error) { .. }
```
For example:
```
ProcessMarketOrder(ob.Sell, decimal.New(6, 0))
asks: 110 -> 5 110 -> 5
100 -> 1 100 -> 1
-------------- -> --------------
bids: 90 -> 5 80 -> 1
80 -> 2
done - 2 (or more orders)
partial - 1 order with price 80
quantityLeft - 0
```
```
ProcessMarketOrder(ob.Buy, decimal.New(10, 0))
asks: 110 -> 5
100 -> 1
-------------- -> --------------
bids: 90 -> 5 90 -> 5
80 -> 1 80 -> 1
done - 2 (or more orders)
partial - nil
quantityLeft - 4
```
### CancelOrder
```go
// CancelOrder removes order with given ID from the order book
func (ob *OrderBook) CancelOrder(orderID string) *Order { ... }
```
```
CancelOrder("myUinqueID-Sell-1-with-100")
asks: 110 -> 5
100 -> 1 110 -> 5
-------------- -> --------------
bids: 90 -> 5 90 -> 5
80 -> 1 80 -> 1
done - 2 (or more orders)
partial - nil
quantityLeft - 4
```
## License
The MIT License (MIT)
See LICENSE and AUTHORS files
package orderbook
import "errors"
// OrderBook 错误定义
var (
ErrInvalidQuantity = errors.New("orderbook: 订单数量无效")
ErrInvalidPrice = errors.New("orderbook: 订单价格无效")
ErrOrderExists = errors.New("orderbook: 订单已存在")
ErrOrderNotExists = errors.New("orderbook: 订单不存在")
ErrInsufficientQuantity = errors.New("orderbook: 计算价格的数量不足")
ErrOverflow = errors.New("orderbook: 计算价格的数量溢出")
)
package orderbook
import (
"bytes"
"encoding/gob"
"encoding/json"
"fmt"
"github.com/ethereum/go-ethereum/common"
"github.com/holiman/uint256"
)
// Order 存储订单请求的信息
type Order struct {
Creator common.Address
Side Side
Id string
Timestamp uint64
Quantity *uint256.Int
Price *uint256.Int
Filled *uint256.Int
AvgPrice *uint256.Int
FillAll bool
}
// MarketView 表示订单簿的快照视图
type MarketView struct {
Asks map[string]*uint256.Int `json:"asks"`
Bids map[string]*uint256.Int `json:"bids"`
}
// NewOrder 创建新的不可变Order对象
func NewOrder(orderID string, creator common.Address, side Side, quantity, price *uint256.Int, timestamp uint64) *Order {
return &Order{
Creator: creator,
Side: side,
Id: orderID,
Quantity: quantity,
Price: price,
Timestamp: timestamp,
Filled: uint256.NewInt(0),
}
}
func (o *Order) Fill(v *uint256.Int) {
o.Filled = new(uint256.Int).Add(o.Filled, v)
if o.Filled.Cmp(o.Quantity) == 0 {
o.FillAll = true
}
}
func (o *Order) IsFilled() bool {
return o.FillAll
}
func (o *Order) Remaining() *uint256.Int {
return new(uint256.Int).Sub(o.Quantity, o.Filled)
}
// String 实现Stringer接口
func (o *Order) String() string {
return fmt.Sprintf("\n\"%s\":\n\t交易方向: %s\n\t数量: %s\n\t价格: %s\n\t时间: %d\n", o.Id, o.Side, o.Quantity, o.Price, o.Timestamp)
}
// MarshalJSON 实现json.Marshaler接口
func (o *Order) MarshalJSON() ([]byte, error) {
return json.Marshal(
&struct {
S Side `json:"side"`
ID string `json:"id"`
Timestamp uint64 `json:"timestamp"`
Quantity *uint256.Int `json:"quantity"`
Price *uint256.Int `json:"price"`
}{
S: o.Side,
ID: o.Id,
Timestamp: o.Timestamp,
Quantity: o.Quantity,
Price: o.Price,
},
)
}
// UnmarshalJSON 实现json.Unmarshaler接口
func (o *Order) UnmarshalJSON(data []byte) error {
obj := struct {
S Side `json:"side"`
ID string `json:"id"`
Timestamp uint64 `json:"timestamp"`
Quantity *uint256.Int `json:"quantity"`
Price *uint256.Int `json:"price"`
}{}
if err := json.Unmarshal(data, &obj); err != nil {
return err
}
o.Side = obj.S
o.Id = obj.ID
o.Timestamp = obj.Timestamp
o.Quantity = obj.Quantity
o.Price = obj.Price
return nil
}
func (o *Order) Dump() ([]byte, error) {
var buf bytes.Buffer
enc := gob.NewEncoder(&buf)
if err := enc.Encode(o); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func (o *Order) Load(data []byte) error {
decoder := gob.NewDecoder(bytes.NewBuffer(data))
if err := decoder.Decode(o); err != nil {
return err
}
return nil
}
// GetOrderSide 获取市场一方的订单方向及其订单
func (ob *OrderBook) GetOrderSide(side Side) *OrderSide {
switch side {
case Buy:
return ob.bids
default:
return ob.asks
}
}
// MarketOverview 提供市场概览,包括市场每一方的数量和价格
// asks: qty price bids: qty price
//
// 0.2 14 0.9 13
// 0.1 14.5 5 14
// 0.8 16 2 16
func (ob *OrderBook) MarketOverview() *MarketView {
return &MarketView{
Asks: compileOrders(ob.asks),
Bids: compileOrders(ob.bids),
}
}
// compileOrders 按以下格式编译订单
func compileOrders(orders *OrderSide) map[string]*uint256.Int {
// 显示队列
queue := make(map[string]*uint256.Int)
if orders != nil {
level := orders.MaxPriceQueue()
for level != nil {
if q, exists := queue[level.Price().String()]; exists {
queue[level.Price().String()] = new(uint256.Int).Add(q, level.Volume())
} else {
queue[level.Price().String()] = level.Volume()
}
level = orders.LessThan(level.Price())
}
}
return queue
}
func (ob *OrderBook) Dump() ([]byte, error) {
var buffer bytes.Buffer
encoder := gob.NewEncoder(&buffer)
if err := encoder.Encode(ob); err != nil {
return nil, err
}
return buffer.Bytes(), nil
}
func (ob *OrderBook) Load(data []byte) error {
decoder := gob.NewDecoder(bytes.NewBuffer(data))
if err := decoder.Decode(ob); err != nil {
return err
}
return nil
}
package orderbook
import (
"encoding/json"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/holiman/uint256"
)
func TestNewOrder(t *testing.T) {
t.Log(NewOrder("order-1", common.HexToAddress("0x0000000000000000000000000000000000000000"), Sell, uint256.NewInt(100), uint256.NewInt(100), 1234567890))
}
func TestOrderJSON(t *testing.T) {
data := []*Order{
NewOrder("one", common.HexToAddress("0x0000000000000000000000000000000000000000"), Buy, uint256.NewInt(11), uint256.NewInt(110), 1234567890),
NewOrder("two", common.HexToAddress("0x0000000000000000000000000000000000000000"), Buy, uint256.NewInt(22), uint256.NewInt(220), 1234567890),
NewOrder("three", common.HexToAddress("0x0000000000000000000000000000000000000000"), Sell, uint256.NewInt(33), uint256.NewInt(330), 1234567890),
NewOrder("four", common.HexToAddress("0x0000000000000000000000000000000000000000"), Sell, uint256.NewInt(44), uint256.NewInt(440), 1234567890),
}
result, _ := json.Marshal(data)
t.Log(string(result))
data = []*Order{}
_ = json.Unmarshal(result, &data)
t.Log(data)
err := json.Unmarshal([]byte(`[{"side":"fake"}]`), &data)
if err == nil {
t.Fatal("can unmarshal unsupported value")
}
}
This diff is collapsed.
package orderbook
import (
"fmt"
"testing"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/holiman/uint256"
)
func addDepth(ob *OrderBook, prefix string, quantity *uint256.Int) {
creator := common.Address{}
for i := 50; i < 100; i = i + 10 {
ob.ProcessLimitOrder(Buy, fmt.Sprintf("%sbuy-%d", prefix, i), creator, quantity, uint256.NewInt(uint64(i)), 1234567890)
}
for i := 100; i < 150; i = i + 10 {
ob.ProcessLimitOrder(Sell, fmt.Sprintf("%ssell-%d", prefix, i), creator, quantity, uint256.NewInt(uint64(i)), 1234567890)
}
}
func TestLimitPlace(t *testing.T) {
ob := NewOrderBook("USDT", "BTC")
creator := common.Address{}
quantity := uint256.NewInt(2)
for i := 50; i < 100; i = i + 10 {
done, partial, _, partialQty, _, err := ob.ProcessLimitOrder(Buy, fmt.Sprintf("buy-%d", i), creator, quantity, uint256.NewInt(uint64(i)), 1234567890)
if len(done) != 0 {
t.Fatal("OrderBook failed to process limit order (done is not empty)")
}
if partial != nil {
t.Fatal("OrderBook failed to process limit order (partial is not empty)")
}
if partialQty.Sign() != 0 {
t.Fatal("OrderBook failed to process limit order (partialQty is not zero)")
}
if err != nil {
t.Fatal(err)
}
}
for i := 100; i < 150; i = i + 10 {
done, partial, _, partialQty, _, err := ob.ProcessLimitOrder(Sell, fmt.Sprintf("sell-%d", i), creator, quantity, uint256.NewInt(uint64(i)), 1234567890)
if len(done) != 0 {
t.Fatal("OrderBook failed to process limit order (done is not empty)")
}
if partial != nil {
t.Fatal("OrderBook failed to process limit order (partial is not empty)")
}
if partialQty.Sign() != 0 {
t.Fatal("OrderBook failed to process limit order (partialQty is not zero)")
}
if err != nil {
t.Fatal(err)
}
}
t.Log(ob)
if ob.Order("fake") != nil {
t.Fatal("can get fake order")
}
if ob.Order("sell-100") == nil {
t.Fatal("can't get real order")
}
t.Log(ob.Depth())
return
}
func TestLimitProcess(t *testing.T) {
ob := NewOrderBook("USDT", "BTC")
addDepth(ob, "", uint256.NewInt(2))
creator := common.Address{}
done, partial, _, partialQty, _, err := ob.ProcessLimitOrder(Buy, "order-b100", creator, uint256.NewInt(1), uint256.NewInt(100), 1234567890)
if err != nil {
t.Fatal(err)
}
t.Log("Done:", done)
if done[0].Id != "order-b100" {
t.Fatal("Wrong done id")
}
t.Log("Partial:", partial)
if partial.Id != "sell-100" {
t.Fatal("Wrong partial id")
}
if partialQty.Cmp(uint256.NewInt(1)) != 0 {
t.Fatal("Wrong partial quantity processed")
}
t.Log(ob)
done, partial, _, partialQty, _, err = ob.ProcessLimitOrder(Buy, "order-b150", creator, uint256.NewInt(10), uint256.NewInt(150), 1234567890)
if err != nil {
t.Fatal(err)
}
t.Log("Done:", done)
if len(done) != 5 {
t.Fatal("Wrong done quantity")
}
t.Log("Partial:", partial)
if partial.Id != "order-b150" {
t.Fatal("Wrong partial id")
}
if partialQty.Cmp(uint256.NewInt(9)) != 0 {
t.Fatal("Wrong partial quantity processed", partialQty)
}
t.Log(ob)
if _, _, _, _, _, err := ob.ProcessLimitOrder(Sell, "buy-70", creator, uint256.NewInt(11), uint256.NewInt(40), 1234567890); err == nil {
t.Fatal("Can add existing order")
}
if _, _, _, _, _, err := ob.ProcessLimitOrder(Sell, "fake-70", creator, uint256.NewInt(0), uint256.NewInt(40), 1234567890); err == nil {
t.Fatal("Can add empty quantity order")
}
if _, _, _, _, _, err := ob.ProcessLimitOrder(Sell, "fake-70", creator, uint256.NewInt(10), uint256.NewInt(0), 1234567890); err == nil {
t.Fatal("Can add zero price")
}
if o, err := ob.CancelOrder("order-b100"); o != nil || err != nil {
t.Fatal("Can cancel done order")
}
done, partial, _, partialQty, _, err = ob.ProcessLimitOrder(Sell, "order-s40", creator, uint256.NewInt(11), uint256.NewInt(40), 1234567890)
if err != nil {
t.Fatal(err)
}
t.Log("Done:", done)
if len(done) != 7 {
t.Fatal("Wrong done quantity")
}
if partial != nil {
t.Fatal("Wrong partial")
}
if partialQty.Sign() != 0 {
t.Fatal("Wrong partialQty")
}
t.Log(ob)
}
func TestMarketProcess(t *testing.T) {
ob := NewOrderBook("USDT", "BTC")
addDepth(ob, "", uint256.NewInt(2))
done, partial, partialQty, left, err := ob.ProcessMarketOrder(Buy, uint256.NewInt(3))
if err != nil {
t.Fatal(err)
}
if left.Sign() > 0 {
t.Fatal("Wrong quantity left")
}
if partialQty.Cmp(uint256.NewInt(1)) != 0 {
t.Fatal("Wrong partial quantity left")
}
t.Log("Done", done)
t.Log("Partial", partial)
t.Log(ob)
if _, _, _, _, err := ob.ProcessMarketOrder(Buy, uint256.NewInt(0)); err == nil {
t.Fatal("Can add zero quantity order")
}
done, partial, partialQty, left, err = ob.ProcessMarketOrder(Sell, uint256.NewInt(12))
if err != nil {
t.Fatal(err)
}
if partial != nil {
t.Fatal("Partial is not nil")
}
if partialQty.Sign() != 0 {
t.Fatal("PartialQty is not nil")
}
if len(done) != 5 {
t.Fatal("Invalid done amount")
}
if left.Cmp(uint256.NewInt(2)) != 0 {
t.Fatal("Invalid left amount", left)
}
t.Log("Done", done)
t.Log(ob)
}
// func TestOrderBookJSON(t *testing.T) {
// data := NewOrderBook("USDT", "BTC")
// result, _ := json.Marshal(data)
// t.Log(string(result))
// if err := json.Unmarshal(result, data); err != nil {
// t.Fatal(err)
// }
// addDepth(data, "01-", uint256.NewInt(10))
// addDepth(data, "02-", uint256.NewInt(1))
// addDepth(data, "03-", uint256.NewInt(2))
// result, _ = json.Marshal(data)
// t.Log(string(result))
// data = NewOrderBook("USDT", "BTC")
// if err := json.Unmarshal(result, data); err != nil {
// t.Fatal(err)
// }
// t.Log(data)
// err := json.Unmarshal([]byte(`[{"side":"fake"}]`), &data)
// if err == nil {
// t.Fatal("can unmarshal unsupported value")
// }
// }
func TestPriceCalculation(t *testing.T) {
ob := NewOrderBook("USDT", "BTC")
addDepth(ob, "05-", uint256.NewInt(10))
addDepth(ob, "10-", uint256.NewInt(10))
addDepth(ob, "15-", uint256.NewInt(10))
t.Log(ob)
price, _, err := ob.CalculateMarketPrice(Buy, uint256.NewInt(115))
if err != nil {
t.Fatal(err)
}
if price.Cmp(uint256.NewInt(13150)) != 0 {
t.Fatal("invalid price", price)
}
price, _, err = ob.CalculateMarketPrice(Buy, uint256.NewInt(200))
if err == nil {
t.Fatal("invalid quantity count")
}
if price.Cmp(uint256.NewInt(18000)) != 0 {
t.Fatal("invalid price", price)
}
// -------
price, _, err = ob.CalculateMarketPrice(Sell, uint256.NewInt(115))
if err != nil {
t.Fatal(err)
}
if price.Cmp(uint256.NewInt(8700)) != 0 {
t.Fatal("invalid price", price)
}
price, _, err = ob.CalculateMarketPrice(Sell, uint256.NewInt(200))
if err == nil {
t.Fatal("invalid quantity count")
}
if price.Cmp(uint256.NewInt(10500)) != 0 {
t.Fatal("invalid price", price)
}
}
func BenchmarkLimitOrder(b *testing.B) {
ob := NewOrderBook("USDT", "BTC")
stopwatch := time.Now()
creator := common.Address{}
for i := 0; i < b.N; i++ {
addDepth(ob, "05-", uint256.NewInt(10)) // 10 ts
addDepth(ob, "10-", uint256.NewInt(10)) // 10 ts
addDepth(ob, "15-", uint256.NewInt(10)) // 10 ts
ob.ProcessLimitOrder(Buy, "order-b150", creator, uint256.NewInt(160), uint256.NewInt(150), 1234567890) // 1 ts
ob.ProcessMarketOrder(Sell, uint256.NewInt(200)) // 1 ts = total 32
}
elapsed := time.Since(stopwatch)
fmt.Printf("\n\nElapsed: %s\nTransactions per second (avg): %f\n", elapsed, float64(b.N*32)/elapsed.Seconds())
}
package orderbook
import (
"container/list"
"encoding/json"
"fmt"
"strings"
"github.com/holiman/uint256"
)
// OrderQueue 存储和管理订单链
type OrderQueue struct {
volume *uint256.Int
price *uint256.Int
orders *list.List
}
// NewOrderQueue 创建并初始化OrderQueue对象
func NewOrderQueue(price *uint256.Int) *OrderQueue {
return &OrderQueue{
price: price,
volume: uint256.NewInt(0),
orders: list.New(),
}
}
// Len 返回队列中的订单数量
func (oq *OrderQueue) Len() int {
return oq.orders.Len()
}
// Price 返回队列的价格等级
func (oq *OrderQueue) Price() *uint256.Int {
return oq.price
}
// Volume 返回总订单量
func (oq *OrderQueue) Volume() *uint256.Int {
return oq.volume
}
// Head 返回队列中的顶部订单
func (oq *OrderQueue) Head() *list.Element {
return oq.orders.Front()
}
// Tail 返回队列中的底部订单
func (oq *OrderQueue) Tail() *list.Element {
return oq.orders.Back()
}
// Append 将订单添加到队列尾部
func (oq *OrderQueue) Append(o *Order) (*list.Element, error) {
overflow := false
oq.volume, overflow = new(uint256.Int).AddOverflow(oq.volume, o.Quantity)
if overflow {
return nil, ErrOverflow
}
return oq.orders.PushBack(o), nil
}
// Update 在列表值中设置新订单
func (oq *OrderQueue) Update(e *list.Element, o *Order) (*list.Element, error) {
overflow := false
oq.volume, overflow = new(uint256.Int).SubOverflow(oq.volume, e.Value.(*Order).Quantity)
if overflow {
return nil, ErrOverflow
}
oq.volume, overflow = new(uint256.Int).AddOverflow(oq.volume, o.Quantity)
if overflow {
return nil, ErrOverflow
}
e.Value = o
return e, nil
}
// Remove 从队列中移除订单并链接订单链
func (oq *OrderQueue) Remove(e *list.Element) (*Order, error) {
overflow := false
oq.volume, overflow = new(uint256.Int).SubOverflow(oq.volume, e.Value.(*Order).Quantity)
if overflow {
return nil, ErrOverflow
}
return oq.orders.Remove(e).(*Order), nil
}
// String 实现fmt.Stringer接口
func (oq *OrderQueue) String() string {
sb := strings.Builder{}
iter := oq.orders.Front()
sb.WriteString(fmt.Sprintf("\n队列长度: %d, 价格: %s, 数量: %s, 订单:", oq.Len(), oq.Price(), oq.Volume()))
for iter != nil {
order := iter.Value.(*Order)
str := fmt.Sprintf("\n\t订单ID: %s, 数量: %s, 价格:%s, 时间: %d", order.Id, order.Quantity, order.Price, order.Timestamp)
sb.WriteString(str)
iter = iter.Next()
}
return sb.String()
}
// MarshalJSON 实现json.Marshaler接口
func (oq *OrderQueue) MarshalJSON() ([]byte, error) {
iter := oq.Head()
var orders []*Order
for iter != nil {
orders = append(orders, iter.Value.(*Order))
iter = iter.Next()
}
return json.Marshal(
&struct {
Volume *uint256.Int `json:"volume"`
Price *uint256.Int `json:"price"`
Orders []*Order `json:"orders"`
}{
Volume: oq.Volume(),
Price: oq.Price(),
Orders: orders,
},
)
}
// UnmarshalJSON 实现json.Unmarshaler接口
func (oq *OrderQueue) UnmarshalJSON(data []byte) error {
obj := struct {
Volume *uint256.Int `json:"volume"`
Price *uint256.Int `json:"price"`
Orders []*Order `json:"orders"`
}{}
if err := json.Unmarshal(data, &obj); err != nil {
return err
}
oq.volume = obj.Volume
oq.price = obj.Price
oq.orders = list.New()
for _, order := range obj.Orders {
oq.orders.PushBack(order)
}
return nil
}
package orderbook
import (
"encoding/json"
"fmt"
"testing"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/holiman/uint256"
)
func TestOrderQueue(t *testing.T) {
price := uint256.NewInt(100)
oq := NewOrderQueue(price)
o1 := NewOrder(
"order-1",
common.HexToAddress("0x0000000000000000000000000000000000000000"),
Buy,
uint256.NewInt(100),
uint256.NewInt(100),
1234567890,
)
o2 := NewOrder(
"order-2",
common.HexToAddress("0x0000000000000000000000000000000000000000"),
Buy,
uint256.NewInt(100),
uint256.NewInt(100),
1234567890,
)
head, _ := oq.Append(o1)
tail, _ := oq.Append(o2)
if head == nil || tail == nil {
t.Fatal("Could not append order to the OrderQueue")
}
if oq.Volume().Cmp(uint256.NewInt(200)) != 0 {
t.Fatalf("Invalid order volume (have: %s, want: 200", oq.Volume())
}
if head.Value.(*Order) != o1 || tail.Value.(*Order) != o2 {
t.Fatal("Invalid element value")
}
if oq.Head() != head || oq.Tail() != tail {
t.Fatal("Invalid element position")
}
if oq.Head().Next() != oq.Tail() || oq.Tail().Prev() != head ||
oq.Head().Prev() != nil || oq.Tail().Next() != nil {
t.Fatal("Invalid element link")
}
o1 = NewOrder(
"order-3",
common.HexToAddress("0x0000000000000000000000000000000000000000"),
Buy,
uint256.NewInt(200),
uint256.NewInt(200),
1234567890,
)
oq.Update(head, o1)
if oq.Volume().Cmp(uint256.NewInt(300)) != 0 {
t.Fatalf("Invalid order volume (have: %s, want: 300", oq.Volume())
}
if o, err := oq.Remove(head); o != o1 || err != nil {
t.Fatal("Invalid element value")
}
if oq.Volume().Cmp(uint256.NewInt(100)) != 0 {
t.Fatalf("Invalid order volume (have: %s, want: 100", oq.Volume())
}
t.Log(oq)
}
func TestOrderQueueJSON(t *testing.T) {
data := NewOrderQueue(uint256.NewInt(111))
data.Append(NewOrder("one", common.HexToAddress("0x0000000000000000000000000000000000000000"), Buy, uint256.NewInt(11), uint256.NewInt(110), 1234567890))
data.Append(NewOrder("two", common.HexToAddress("0x0000000000000000000000000000000000000000"), Buy, uint256.NewInt(22), uint256.NewInt(220), 1234567890))
data.Append(NewOrder("three", common.HexToAddress("0x0000000000000000000000000000000000000000"), Sell, uint256.NewInt(33), uint256.NewInt(330), 1234567890))
data.Append(NewOrder("four", common.HexToAddress("0x0000000000000000000000000000000000000000"), Sell, uint256.NewInt(44), uint256.NewInt(440), 1234567890))
result, _ := json.Marshal(data)
t.Log(string(result))
data = NewOrderQueue(uint256.NewInt(0))
if err := json.Unmarshal(result, data); err != nil {
t.Fatal(err)
}
t.Log(data)
err := json.Unmarshal([]byte(`[{"side":"fake"}]`), &data)
if err == nil {
t.Fatal("can unmarshal unsupported value")
}
}
func BenchmarkOrderQueue(b *testing.B) {
price := uint256.NewInt(100)
orderQueue := NewOrderQueue(price)
stopwatch := time.Now()
var o *Order
for i := 0; i < b.N; i++ {
o = NewOrder(
fmt.Sprintf("order-%d", i),
common.HexToAddress("0x0000000000000000000000000000000000000000"),
Buy,
uint256.NewInt(100),
uint256.NewInt(uint64(i)),
uint64(stopwatch.UnixMilli()),
)
orderQueue.Append(o)
}
elapsed := time.Since(stopwatch)
fmt.Printf("\n\nElapsed: %s\nTransactions per second: %f\n", elapsed, float64(b.N)/elapsed.Seconds())
}
package orderbook
import (
"container/list"
"encoding/json"
"fmt"
"strings"
rbtx "github.com/emirpasic/gods/examples/redblacktreeextended"
rbt "github.com/emirpasic/gods/trees/redblacktree"
"github.com/holiman/uint256"
)
// OrderSide 实现订单队列操作的外观模式
type OrderSide struct {
priceTree *rbtx.RedBlackTreeExtended
prices map[string]*OrderQueue
volume *uint256.Int
numOrders int
depth int
}
func rbtComparator(a, b interface{}) int {
return a.(*uint256.Int).Cmp(b.(*uint256.Int))
}
// NewOrderSide 创建新的OrderSide管理器
func NewOrderSide() *OrderSide {
return &OrderSide{
priceTree: &rbtx.RedBlackTreeExtended{
Tree: rbt.NewWith(rbtComparator),
},
prices: map[string]*OrderQueue{},
volume: uint256.NewInt(0),
}
}
// Len 返回订单数量
func (os *OrderSide) Len() int {
return os.numOrders
}
// Depth 返回市场深度
func (os *OrderSide) Depth() int {
return os.depth
}
// Volume 返回该方向的总数量
func (os *OrderSide) Volume() *uint256.Int {
return os.volume
}
// Append 将订单添加到指定价格等级
func (os *OrderSide) Append(o *Order) (*list.Element, error) {
price := o.Price
strPrice := price.String()
priceQueue, ok := os.prices[strPrice]
if !ok {
priceQueue = NewOrderQueue(price)
os.prices[strPrice] = priceQueue
os.priceTree.Put(price, priceQueue)
os.depth++
}
os.numOrders++
overflow := false
fmt.Println(os.volume, o.Quantity)
os.volume, overflow = new(uint256.Int).AddOverflow(os.volume, o.Quantity)
if overflow {
return nil, ErrOverflow
}
return priceQueue.Append(o)
}
// Remove 从指定价格等级移除订单
func (os *OrderSide) Remove(e *list.Element) (*Order, error) {
price := e.Value.(*Order).Price
strPrice := price.String()
priceQueue := os.prices[strPrice]
o, err := priceQueue.Remove(e)
if err != nil {
return nil, err
}
if priceQueue.Len() == 0 {
delete(os.prices, strPrice)
os.priceTree.Remove(price)
os.depth--
}
os.numOrders--
overflow := false
os.volume, overflow = new(uint256.Int).SubOverflow(os.volume, o.Quantity)
if overflow {
return nil, ErrOverflow
}
return o, nil
}
// MaxPriceQueue 返回最高价格等级
func (os *OrderSide) MaxPriceQueue() *OrderQueue {
if os.depth > 0 {
if value, found := os.priceTree.GetMax(); found {
return value.(*OrderQueue)
}
}
return nil
}
// MinPriceQueue 返回最低价格等级
func (os *OrderSide) MinPriceQueue() *OrderQueue {
if os.depth > 0 {
if value, found := os.priceTree.GetMin(); found {
return value.(*OrderQueue)
}
}
return nil
}
// LessThan 返回价格小于给定价格的最近OrderQueue
func (os *OrderSide) LessThan(price *uint256.Int) *OrderQueue {
tree := os.priceTree.Tree
node := tree.Root
var floor *rbt.Node
for node != nil {
if tree.Comparator(price, node.Key) > 0 {
floor = node
node = node.Right
} else {
node = node.Left
}
}
if floor != nil {
return floor.Value.(*OrderQueue)
}
return nil
}
// GreaterThan 返回价格大于给定价格的最近OrderQueue
func (os *OrderSide) GreaterThan(price *uint256.Int) *OrderQueue {
tree := os.priceTree.Tree
node := tree.Root
var ceiling *rbt.Node
for node != nil {
if tree.Comparator(price, node.Key) < 0 {
ceiling = node
node = node.Left
} else {
node = node.Right
}
}
if ceiling != nil {
return ceiling.Value.(*OrderQueue)
}
return nil
}
// Orders 返回所有*list.Element订单
func (os *OrderSide) Orders() (orders []*list.Element) {
for _, price := range os.prices {
iter := price.Head()
for iter != nil {
orders = append(orders, iter)
iter = iter.Next()
}
}
return
}
// String 实现fmt.Stringer接口
func (os *OrderSide) String() string {
sb := strings.Builder{}
level := os.MaxPriceQueue()
for level != nil {
sb.WriteString(fmt.Sprintf("\n%s -> %s", level.Price(), level.Volume()))
level = os.LessThan(level.Price())
}
return sb.String()
}
// MarshalJSON 实现json.Marshaler接口
func (os *OrderSide) MarshalJSON() ([]byte, error) {
return json.Marshal(
&struct {
NumOrders int `json:"numOrders"`
Depth int `json:"depth"`
Prices map[string]*OrderQueue `json:"prices"`
}{
NumOrders: os.numOrders,
Depth: os.depth,
Prices: os.prices,
},
)
}
// UnmarshalJSON 实现json.Unmarshaler接口
func (os *OrderSide) UnmarshalJSON(data []byte) error {
obj := struct {
NumOrders int `json:"numOrders"`
Depth int `json:"depth"`
Prices map[string]*OrderQueue `json:"prices"`
}{}
if err := json.Unmarshal(data, &obj); err != nil {
return err
}
os.numOrders = obj.NumOrders
os.depth = obj.Depth
os.prices = obj.Prices
os.priceTree = &rbtx.RedBlackTreeExtended{
Tree: rbt.NewWith(rbtComparator),
}
for price, queue := range os.prices {
k, err := uint256.FromDecimal(price)
if err != nil {
return err
}
os.priceTree.Put(k, queue)
}
return nil
}
package orderbook
import (
"encoding/json"
"fmt"
"testing"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/holiman/uint256"
)
func TestOrderSide(t *testing.T) {
ot := NewOrderSide()
o1 := NewOrder(
"order-1",
common.HexToAddress("0x0000000000000000000000000000000000000000"),
Buy,
uint256.NewInt(10),
uint256.NewInt(10),
1234567890,
)
o2 := NewOrder(
"order-2",
common.HexToAddress("0x0000000000000000000000000000000000000000"),
Buy,
uint256.NewInt(10),
uint256.NewInt(20),
1234567890,
)
if ot.MinPriceQueue() != nil || ot.MaxPriceQueue() != nil {
t.Fatal("invalid price levels")
}
el1, _ := ot.Append(o1)
if ot.MinPriceQueue() != ot.MaxPriceQueue() {
t.Fatal("invalid price levels")
}
el2, _ := ot.Append(o2)
if ot.Depth() != 2 {
t.Fatal("invalid depth")
}
if ot.Len() != 2 {
t.Fatal("invalid orders count")
}
t.Log(ot)
if ot.MinPriceQueue().Head() != el1 || ot.MinPriceQueue().Tail() != el1 ||
ot.MaxPriceQueue().Head() != el2 || ot.MaxPriceQueue().Tail() != el2 {
t.Fatal("invalid price levels")
}
if o, err := ot.Remove(el1); o != o1 || err != nil {
t.Fatal("invalid order")
}
if ot.MinPriceQueue() != ot.MaxPriceQueue() {
t.Fatal("invalid price levels")
}
t.Log(ot)
}
func TestOrderSideJSON(t *testing.T) {
data := NewOrderSide()
data.Append(NewOrder("one", common.HexToAddress("0x0000000000000000000000000000000000000000"), Buy, uint256.NewInt(11), uint256.NewInt(110), 1234567890))
data.Append(NewOrder("two", common.HexToAddress("0x0000000000000000000000000000000000000000"), Buy, uint256.NewInt(22), uint256.NewInt(220), 1234567890))
data.Append(NewOrder("three", common.HexToAddress("0x0000000000000000000000000000000000000000"), Sell, uint256.NewInt(33), uint256.NewInt(330), 1234567890))
data.Append(NewOrder("four", common.HexToAddress("0x0000000000000000000000000000000000000000"), Sell, uint256.NewInt(44), uint256.NewInt(440), 1234567890))
data.Append(NewOrder("five", common.HexToAddress("0x0000000000000000000000000000000000000000"), Buy, uint256.NewInt(11), uint256.NewInt(110), 1234567890))
data.Append(NewOrder("six", common.HexToAddress("0x0000000000000000000000000000000000000000"), Buy, uint256.NewInt(22), uint256.NewInt(220), 1234567890))
data.Append(NewOrder("seven", common.HexToAddress("0x0000000000000000000000000000000000000000"), Sell, uint256.NewInt(33), uint256.NewInt(330), 1234567890))
data.Append(NewOrder("eight", common.HexToAddress("0x0000000000000000000000000000000000000000"), Sell, uint256.NewInt(44), uint256.NewInt(440), 1234567890))
result, _ := json.Marshal(data)
t.Log(string(result))
data = NewOrderSide()
if err := json.Unmarshal(result, data); err != nil {
t.Fatal(err)
}
t.Log(data)
err := json.Unmarshal([]byte(`[{"side":"fake"}]`), &data)
if err == nil {
t.Fatal("can unmarshal unsupported value")
}
}
func TestPriceFinding(t *testing.T) {
os := NewOrderSide()
os.Append(NewOrder("five", common.HexToAddress("0x0000000000000000000000000000000000000000"), Sell, uint256.NewInt(5), uint256.NewInt(130), 1234567890))
os.Append(NewOrder("one", common.HexToAddress("0x0000000000000000000000000000000000000000"), Sell, uint256.NewInt(5), uint256.NewInt(170), 1234567890))
os.Append(NewOrder("eight", common.HexToAddress("0x0000000000000000000000000000000000000000"), Sell, uint256.NewInt(5), uint256.NewInt(100), 1234567890))
os.Append(NewOrder("two", common.HexToAddress("0x0000000000000000000000000000000000000000"), Sell, uint256.NewInt(5), uint256.NewInt(160), 1234567890))
os.Append(NewOrder("four", common.HexToAddress("0x0000000000000000000000000000000000000000"), Sell, uint256.NewInt(5), uint256.NewInt(140), 1234567890))
os.Append(NewOrder("six", common.HexToAddress("0x0000000000000000000000000000000000000000"), Sell, uint256.NewInt(5), uint256.NewInt(120), 1234567890))
os.Append(NewOrder("three", common.HexToAddress("0x0000000000000000000000000000000000000000"), Sell, uint256.NewInt(5), uint256.NewInt(150), 1234567890))
os.Append(NewOrder("seven", common.HexToAddress("0x0000000000000000000000000000000000000000"), Sell, uint256.NewInt(5), uint256.NewInt(110), 1234567890))
if os.Volume().Cmp(uint256.NewInt(40)) != 0 {
t.Fatal("invalid volume")
}
if os.LessThan(uint256.NewInt(101)).Price().Cmp(uint256.NewInt(100)) != 0 ||
os.LessThan(uint256.NewInt(150)).Price().Cmp(uint256.NewInt(140)) != 0 ||
os.LessThan(uint256.NewInt(100)) != nil {
t.Fatal("LessThan return invalid price")
}
if os.GreaterThan(uint256.NewInt(169)).Price().Cmp(uint256.NewInt(170)) != 0 ||
os.GreaterThan(uint256.NewInt(150)).Price().Cmp(uint256.NewInt(160)) != 0 ||
os.GreaterThan(uint256.NewInt(170)) != nil {
t.Fatal("GreaterThan return invalid price")
}
t.Log(os.LessThan(uint256.NewInt(101)))
t.Log(os.GreaterThan(uint256.NewInt(169)))
}
func BenchmarkOrderSide(b *testing.B) {
ot := NewOrderSide()
stopwatch := time.Now()
var o *Order
for i := 0; i < b.N; i++ {
o = NewOrder(
fmt.Sprintf("order-%d", i),
common.HexToAddress("0x0000000000000000000000000000000000000000"),
Buy,
uint256.NewInt(10),
uint256.NewInt(uint64(i)),
uint64(stopwatch.UnixMilli()),
)
ot.Append(o)
}
elapsed := time.Since(stopwatch)
fmt.Printf("\n\nElapsed: %s\nTransactions per second: %f\n", elapsed, float64(b.N)/elapsed.Seconds())
}
package orderbook
import (
"encoding/json"
"reflect"
)
// Side 订单方向
type Side int
// Sell (卖出/卖单) 或 Buy (买入/买单)
const (
Sell Side = iota
Buy
)
// String 实现fmt.Stringer接口
func (s Side) String() string {
if s == Buy {
return "buy"
}
return "sell"
}
// MarshalJSON 实现json.Marshaler接口
func (s Side) MarshalJSON() ([]byte, error) {
return []byte(`"` + s.String() + `"`), nil
}
// UnmarshalJSON 实现json.Unmarshaler接口
func (s *Side) UnmarshalJSON(data []byte) error {
switch string(data) {
case `"buy"`:
*s = Buy
case `"sell"`:
*s = Sell
default:
return &json.UnsupportedValueError{
Value: reflect.New(reflect.TypeOf(data)),
Str: string(data),
}
}
return nil
}
package orderbook
import (
"encoding/json"
"testing"
)
func TestSideJSON(t *testing.T) {
data := struct {
S Side `json:"side"`
}{}
data.S = Buy
resultBuy, _ := json.Marshal(data)
t.Log(string(resultBuy))
data.S = Sell
resultSell, _ := json.Marshal(&data)
t.Log(string(resultSell))
_ = json.Unmarshal(resultBuy, &data)
t.Log(data)
_ = json.Unmarshal(resultSell, &data)
t.Log(data)
err := json.Unmarshal([]byte(`{"side":"fake"}`), &data)
if err == nil {
t.Fatal("can unmarshal unsupported value")
}
}
package types
type Coin string
const (
USDT Coin = "USDT"
BTC Coin = "BTC"
)
package types
import (
"bytes"
"encoding/gob"
"github.com/ethereum/go-ethereum/common"
"github.com/holiman/uint256"
)
type OrderType string
const (
CancelOrder OrderType = "cancel order"
PlaceOrder OrderType = "place order"
)
type OrderAction string
const (
OrderActionBuy OrderAction = "buy"
OrderActionSell OrderAction = "sell"
OrderActionCancel OrderAction = "cancel"
OrderActionSignProxy OrderAction = "approve agent"
)
type Transaction interface {
Dump() ([]byte, error)
Load(data []byte) error
GetUser() common.Address
GetLimitPrice() *uint256.Int
GetPair() string
GetAction() OrderAction
GetQuality() *uint256.Int
GetOrderID() string
GetNonce() []byte
GetProxyAddress() common.Address
}
type Tx struct {
OrderID string
Time uint64
User common.Address
BaseCoin Coin
QuoteCoin Coin
Type OrderType
Action OrderAction
Nonce uint64
}
type CancelOrderTx struct {
Tx
}
func (tx *CancelOrderTx) Dump() ([]byte, error) {
var buffer bytes.Buffer
encoder := gob.NewEncoder(&buffer)
err := encoder.Encode(tx)
if err != nil {
return nil, err
}
return buffer.Bytes(), nil
}
func (tx *CancelOrderTx) Load(data []byte) error {
decoder := gob.NewDecoder(bytes.NewBuffer(data))
return decoder.Decode(tx)
}
func (tx *CancelOrderTx) GetUser() common.Address {
return tx.User
}
func (tx *CancelOrderTx) GetLimitPrice() *uint256.Int {
return nil
}
func (tx *CancelOrderTx) GetPair() string {
return string(tx.BaseCoin) + string(tx.QuoteCoin)
}
func (tx *CancelOrderTx) GetAction() OrderAction {
return OrderActionCancel
}
func (tx *CancelOrderTx) GetQuality() *uint256.Int {
return nil
}
func (tx *CancelOrderTx) GetOrderID() string {
return tx.OrderID
}
func (tx *CancelOrderTx) GetNonce() []byte {
return uint256.NewInt(tx.Nonce).Bytes()
}
func (tx *CancelOrderTx) GetProxyAddress() common.Address {
return common.Address{}
}
type PlaceOrderTx struct {
Tx
LimitPrice *uint256.Int
Quantity *uint256.Int
}
func (tx *PlaceOrderTx) Dump() ([]byte, error) {
var buffer bytes.Buffer
encoder := gob.NewEncoder(&buffer)
err := encoder.Encode(tx)
if err != nil {
return nil, err
}
return buffer.Bytes(), nil
}
func (tx *PlaceOrderTx) Load(data []byte) error {
decoder := gob.NewDecoder(bytes.NewBuffer(data))
return decoder.Decode(tx)
}
func (tx *PlaceOrderTx) GetUser() common.Address {
return tx.User
}
func (tx *PlaceOrderTx) GetLimitPrice() *uint256.Int {
return tx.LimitPrice
}
func (tx *PlaceOrderTx) GetPair() string {
return string(tx.BaseCoin) + string(tx.QuoteCoin)
}
func (tx *PlaceOrderTx) GetAction() OrderAction {
return tx.Action
}
func (tx *PlaceOrderTx) GetQuality() *uint256.Int {
return tx.Quantity
}
func (tx *PlaceOrderTx) GetOrderID() string {
return tx.OrderID
}
func (tx *PlaceOrderTx) GetNonce() []byte {
return uint256.NewInt(tx.Nonce).Bytes()
}
func (tx *PlaceOrderTx) GetProxyAddress() common.Address {
return common.Address{}
}
type SignProxyTx struct {
Tx
ProxyAddress common.Address
}
func (tx *SignProxyTx) Dump() ([]byte, error) {
var buffer bytes.Buffer
encoder := gob.NewEncoder(&buffer)
err := encoder.Encode(tx)
if err != nil {
return nil, err
}
return buffer.Bytes(), nil
}
func (tx *SignProxyTx) Load(data []byte) error {
decoder := gob.NewDecoder(bytes.NewBuffer(data))
return decoder.Decode(tx)
}
func (tx *SignProxyTx) GetUser() common.Address {
return tx.User
}
func (tx *SignProxyTx) GetLimitPrice() *uint256.Int {
return nil
}
func (tx *SignProxyTx) GetPair() string {
return ""
}
func (tx *SignProxyTx) GetAction() OrderAction {
return OrderActionSignProxy
}
func (tx *SignProxyTx) GetQuality() *uint256.Int {
return nil
}
func (tx *SignProxyTx) GetOrderID() string {
return ""
}
func (tx *SignProxyTx) GetNonce() []byte {
return uint256.NewInt(tx.Nonce).Bytes()
}
func (tx *SignProxyTx) GetProxyAddress() common.Address {
return tx.ProxyAddress
}
package utils_test
import (
"bytes"
"encoding/gob"
"testing"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/holiman/uint256"
)
func TestGob(t *testing.T) {
type Order struct {
Creator common.Address
Id string
Timestamp time.Time
Quantity *uint256.Int
Price *uint256.Int
Filled *uint256.Int
}
var buf bytes.Buffer
o := &Order{
Creator: common.HexToAddress("0x0000000000000000000000000000000000000001"),
Id: "order-1",
Timestamp: time.Now(),
Quantity: uint256.NewInt(10),
Price: uint256.NewInt(1),
Filled: uint256.NewInt(0),
}
enc := gob.NewEncoder(&buf)
err := enc.Encode(o)
if err != nil {
t.Fatal(err)
}
dec := gob.NewDecoder(&buf)
b := &Order{
}
err = dec.Decode(&b)
if err != nil {
t.Fatal(err)
}
t.Log("111", b.Quantity)
}
package utils
import (
"time"
"github.com/bwmarrin/snowflake"
)
var node *snowflake.Node
func init() {
node, _ = snowflake.NewNode(1)
}
func GenOrderID() string {
return node.Generate().String()
}
func NonceToTime(nonce uint64) time.Time {
return time.UnixMilli(int64(nonce))
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment