mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-22 14:55:16 +00:00
Merge pull request #911 from c9s/feature/backtest-report
feature: add fee mode to back-test matching engine
This commit is contained in:
commit
fcb7a27f70
|
@ -21,6 +21,13 @@ backtest:
|
|||
sessions:
|
||||
- binance
|
||||
|
||||
# feeMode is optional
|
||||
# valid values are: quote, native, token
|
||||
# quote: always deduct fee from the quote balance
|
||||
# native: the crypto exchange fee deduction, base fee for buy order, quote fee for sell order.
|
||||
# token: count fee as crypto exchange fee token
|
||||
# feeMode: quote
|
||||
|
||||
accounts:
|
||||
# the initial account balance you want to start with
|
||||
binance: # exchange name
|
||||
|
|
5
go.mod
5
go.mod
|
@ -71,6 +71,7 @@ require (
|
|||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/denisenkom/go-mssqldb v0.12.2 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/dmarkham/enumer v1.5.6 // indirect
|
||||
github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239 // indirect
|
||||
github.com/fsnotify/fsnotify v1.4.9 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
|
@ -101,6 +102,7 @@ require (
|
|||
github.com/mitchellh/mapstructure v1.4.1 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.1 // indirect
|
||||
github.com/pascaldekloe/name v1.0.0 // indirect
|
||||
github.com/pelletier/go-toml v1.8.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
|
@ -123,11 +125,12 @@ require (
|
|||
go.uber.org/atomic v1.9.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect
|
||||
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 // indirect
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
|
||||
golang.org/x/net v0.0.0-20220403103023-749bd193bc2b // indirect
|
||||
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c // indirect
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/tools v0.1.9 // indirect
|
||||
golang.org/x/tools v0.1.11 // indirect
|
||||
google.golang.org/genproto v0.0.0-20220405205423-9d709892a2bf // indirect
|
||||
gopkg.in/ini.v1 v1.62.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
|
|
8
go.sum
8
go.sum
|
@ -130,6 +130,8 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm
|
|||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/dmarkham/enumer v1.5.6 h1:afhpzVOu8PoBL/+4J07PxVBf9cNnSawS/jAZK1snyLw=
|
||||
github.com/dmarkham/enumer v1.5.6/go.mod h1:eAawajOQnFBxf0NndBKgbqJImkHytg3eFEngUovqgo8=
|
||||
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
|
@ -495,6 +497,8 @@ github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAl
|
|||
github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw=
|
||||
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pascaldekloe/name v1.0.0 h1:n7LKFgHixETzxpRv2R77YgPUFo85QHGZKrdaYm7eY5U=
|
||||
github.com/pascaldekloe/name v1.0.0/go.mod h1:Z//MfYJnH4jVpQ9wkclwu2I2MkHmXTlT9wR5UZScttM=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM=
|
||||
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
|
||||
|
@ -728,6 +732,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
|
@ -922,6 +928,8 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f
|
|||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.9 h1:j9KsMiaP1c3B0OTQGth0/k+miLGTgLsAFUCrF2vLcF8=
|
||||
golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
||||
golang.org/x/tools v0.1.11 h1:loJ25fNOEhSXfHrpoGj91eCUThwdNX6u24rO1xnNteY=
|
||||
golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4=
|
||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
|
|
@ -14,7 +14,7 @@ type AverageCostCalculator struct {
|
|||
Market types.Market
|
||||
}
|
||||
|
||||
func (c *AverageCostCalculator) Calculate(symbol string, trades []types.Trade, currentPrice fixedpoint.Value) *AverageCostPnlReport {
|
||||
func (c *AverageCostCalculator) Calculate(symbol string, trades []types.Trade, currentPrice fixedpoint.Value) *AverageCostPnLReport {
|
||||
// copy trades, so that we can truncate it.
|
||||
var bidVolume = fixedpoint.Zero
|
||||
var askVolume = fixedpoint.Zero
|
||||
|
@ -23,7 +23,7 @@ func (c *AverageCostCalculator) Calculate(symbol string, trades []types.Trade, c
|
|||
var grossLoss = fixedpoint.Zero
|
||||
|
||||
if len(trades) == 0 {
|
||||
return &AverageCostPnlReport{
|
||||
return &AverageCostPnLReport{
|
||||
Symbol: symbol,
|
||||
Market: c.Market,
|
||||
LastPrice: currentPrice,
|
||||
|
@ -90,12 +90,13 @@ func (c *AverageCostCalculator) Calculate(symbol string, trades []types.Trade, c
|
|||
unrealizedProfit := currentPrice.Sub(position.AverageCost).
|
||||
Mul(position.GetBase())
|
||||
|
||||
return &AverageCostPnlReport{
|
||||
return &AverageCostPnLReport{
|
||||
Symbol: symbol,
|
||||
Market: c.Market,
|
||||
LastPrice: currentPrice,
|
||||
NumTrades: len(trades),
|
||||
StartTime: time.Time(trades[0].Time),
|
||||
Position: position,
|
||||
|
||||
BuyVolume: bidVolume,
|
||||
SellVolume: askVolume,
|
||||
|
|
|
@ -14,7 +14,7 @@ import (
|
|||
"github.com/c9s/bbgo/pkg/types"
|
||||
)
|
||||
|
||||
type AverageCostPnlReport struct {
|
||||
type AverageCostPnLReport struct {
|
||||
LastPrice fixedpoint.Value `json:"lastPrice"`
|
||||
StartTime time.Time `json:"startTime"`
|
||||
Symbol string `json:"symbol"`
|
||||
|
@ -27,6 +27,7 @@ type AverageCostPnlReport struct {
|
|||
NetProfit fixedpoint.Value `json:"netProfit"`
|
||||
GrossProfit fixedpoint.Value `json:"grossProfit"`
|
||||
GrossLoss fixedpoint.Value `json:"grossLoss"`
|
||||
Position *types.Position `json:"position,omitempty"`
|
||||
|
||||
AverageCost fixedpoint.Value `json:"averageCost"`
|
||||
BuyVolume fixedpoint.Value `json:"buyVolume,omitempty"`
|
||||
|
@ -36,14 +37,14 @@ type AverageCostPnlReport struct {
|
|||
CurrencyFees map[string]fixedpoint.Value `json:"currencyFees"`
|
||||
}
|
||||
|
||||
func (report *AverageCostPnlReport) JSON() ([]byte, error) {
|
||||
func (report *AverageCostPnLReport) JSON() ([]byte, error) {
|
||||
return json.MarshalIndent(report, "", " ")
|
||||
}
|
||||
|
||||
func (report AverageCostPnlReport) Print() {
|
||||
func (report AverageCostPnLReport) Print() {
|
||||
color.Green("TRADES SINCE: %v", report.StartTime)
|
||||
color.Green("NUMBER OF TRADES: %d", report.NumTrades)
|
||||
|
||||
color.Green(report.Position.String())
|
||||
color.Green("AVERAGE COST: %s", types.USD.FormatMoney(report.AverageCost))
|
||||
color.Green("BASE ASSET POSITION: %s", report.BaseAssetPosition.String())
|
||||
|
||||
|
@ -69,7 +70,7 @@ func (report AverageCostPnlReport) Print() {
|
|||
}
|
||||
}
|
||||
|
||||
func (report AverageCostPnlReport) SlackAttachment() slack.Attachment {
|
||||
func (report AverageCostPnLReport) SlackAttachment() slack.Attachment {
|
||||
var color = slackstyle.Red
|
||||
|
||||
if report.UnrealizedProfit.Sign() > 0 {
|
||||
|
|
|
@ -36,10 +36,10 @@ import (
|
|||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/cache"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/cache"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/bbgo"
|
||||
"github.com/c9s/bbgo/pkg/service"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
|
@ -138,12 +138,15 @@ func (e *Exchange) addMatchingBook(symbol string, market types.Market) {
|
|||
}
|
||||
|
||||
func (e *Exchange) _addMatchingBook(symbol string, market types.Market) {
|
||||
e.matchingBooks[symbol] = &SimplePriceMatching{
|
||||
CurrentTime: e.currentTime,
|
||||
Account: e.account,
|
||||
Market: market,
|
||||
closedOrders: make(map[uint64]types.Order),
|
||||
matching := &SimplePriceMatching{
|
||||
currentTime: e.currentTime,
|
||||
account: e.account,
|
||||
Market: market,
|
||||
closedOrders: make(map[uint64]types.Order),
|
||||
feeModeFunction: getFeeModeFunction(e.config.FeeMode),
|
||||
}
|
||||
|
||||
e.matchingBooks[symbol] = matching
|
||||
}
|
||||
|
||||
func (e *Exchange) NewStream() types.Stream {
|
||||
|
@ -257,7 +260,7 @@ func (e *Exchange) QueryTicker(ctx context.Context, symbol string) (*types.Ticke
|
|||
return nil, fmt.Errorf("matching engine is not initialized for symbol %s", symbol)
|
||||
}
|
||||
|
||||
kline := matching.LastKLine
|
||||
kline := matching.lastKLine
|
||||
return &types.Ticker{
|
||||
Time: kline.EndTime.Time(),
|
||||
Volume: kline.Volume,
|
||||
|
@ -382,7 +385,7 @@ func (e *Exchange) ConsumeKLine(k types.KLine) {
|
|||
e.currentTime = kline1m.EndTime.Time()
|
||||
// here we generate trades and order updates
|
||||
matching.processKLine(kline1m)
|
||||
matching.NextKLine = &k
|
||||
matching.nextKLine = &k
|
||||
for _, kline := range matching.klineCache {
|
||||
e.MarketDataStream.EmitKLineClosed(kline)
|
||||
for _, h := range e.Src.Callbacks {
|
||||
|
|
57
pkg/backtest/fee.go
Normal file
57
pkg/backtest/fee.go
Normal file
|
@ -0,0 +1,57 @@
|
|||
package backtest
|
||||
|
||||
import (
|
||||
"github.com/c9s/bbgo/pkg/bbgo"
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
)
|
||||
|
||||
type FeeModeFunction func(order *types.Order, market *types.Market, feeRate fixedpoint.Value) (fee fixedpoint.Value, feeCurrency string)
|
||||
|
||||
func feeModeFunctionToken(order *types.Order, _ *types.Market, feeRate fixedpoint.Value) (fee fixedpoint.Value, feeCurrency string) {
|
||||
quoteQuantity := order.Quantity.Mul(order.Price)
|
||||
feeCurrency = FeeToken
|
||||
fee = quoteQuantity.Mul(feeRate)
|
||||
return fee, feeCurrency
|
||||
}
|
||||
|
||||
func feeModeFunctionNative(order *types.Order, market *types.Market, feeRate fixedpoint.Value) (fee fixedpoint.Value, feeCurrency string) {
|
||||
switch order.Side {
|
||||
|
||||
case types.SideTypeBuy:
|
||||
fee = order.Quantity.Mul(feeRate)
|
||||
feeCurrency = market.BaseCurrency
|
||||
|
||||
case types.SideTypeSell:
|
||||
quoteQuantity := order.Quantity.Mul(order.Price)
|
||||
fee = quoteQuantity.Mul(feeRate)
|
||||
feeCurrency = market.QuoteCurrency
|
||||
|
||||
}
|
||||
|
||||
return fee, feeCurrency
|
||||
}
|
||||
|
||||
func feeModeFunctionQuote(order *types.Order, market *types.Market, feeRate fixedpoint.Value) (fee fixedpoint.Value, feeCurrency string) {
|
||||
feeCurrency = market.QuoteCurrency
|
||||
quoteQuantity := order.Quantity.Mul(order.Price)
|
||||
fee = quoteQuantity.Mul(feeRate)
|
||||
return fee, feeCurrency
|
||||
}
|
||||
|
||||
func getFeeModeFunction(feeMode bbgo.BacktestFeeMode) FeeModeFunction {
|
||||
switch feeMode {
|
||||
|
||||
case bbgo.BacktestFeeModeNative:
|
||||
return feeModeFunctionNative
|
||||
|
||||
case bbgo.BacktestFeeModeQuote:
|
||||
return feeModeFunctionQuote
|
||||
|
||||
case bbgo.BacktestFeeModeToken:
|
||||
return feeModeFunctionToken
|
||||
|
||||
default:
|
||||
return feeModeFunctionQuote
|
||||
}
|
||||
}
|
124
pkg/backtest/fee_test.go
Normal file
124
pkg/backtest/fee_test.go
Normal file
|
@ -0,0 +1,124 @@
|
|||
package backtest
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
)
|
||||
|
||||
func Test_feeModeFunctionToken(t *testing.T) {
|
||||
market := getTestMarket()
|
||||
t.Run("sellOrder", func(t *testing.T) {
|
||||
order := types.Order{
|
||||
SubmitOrder: types.SubmitOrder{
|
||||
Symbol: market.Symbol,
|
||||
Side: types.SideTypeSell,
|
||||
Type: types.OrderTypeLimit,
|
||||
Quantity: fixedpoint.NewFromFloat(0.1),
|
||||
Price: fixedpoint.NewFromFloat(20000.0),
|
||||
TimeInForce: types.TimeInForceGTC,
|
||||
},
|
||||
}
|
||||
feeRate := fixedpoint.MustNewFromString("0.075%")
|
||||
fee, feeCurrency := feeModeFunctionToken(&order, &market, feeRate)
|
||||
assert.Equal(t, "1.5", fee.String())
|
||||
assert.Equal(t, "FEE", feeCurrency)
|
||||
})
|
||||
|
||||
t.Run("buyOrder", func(t *testing.T) {
|
||||
order := types.Order{
|
||||
SubmitOrder: types.SubmitOrder{
|
||||
Symbol: market.Symbol,
|
||||
Side: types.SideTypeBuy,
|
||||
Type: types.OrderTypeLimit,
|
||||
Quantity: fixedpoint.NewFromFloat(0.1),
|
||||
Price: fixedpoint.NewFromFloat(20000.0),
|
||||
TimeInForce: types.TimeInForceGTC,
|
||||
},
|
||||
}
|
||||
|
||||
feeRate := fixedpoint.MustNewFromString("0.075%")
|
||||
fee, feeCurrency := feeModeFunctionToken(&order, &market, feeRate)
|
||||
assert.Equal(t, "1.5", fee.String())
|
||||
assert.Equal(t, "FEE", feeCurrency)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_feeModeFunctionQuote(t *testing.T) {
|
||||
market := getTestMarket()
|
||||
t.Run("sellOrder", func(t *testing.T) {
|
||||
order := types.Order{
|
||||
SubmitOrder: types.SubmitOrder{
|
||||
Symbol: market.Symbol,
|
||||
Side: types.SideTypeSell,
|
||||
Type: types.OrderTypeLimit,
|
||||
Quantity: fixedpoint.NewFromFloat(0.1),
|
||||
Price: fixedpoint.NewFromFloat(20000.0),
|
||||
TimeInForce: types.TimeInForceGTC,
|
||||
},
|
||||
}
|
||||
feeRate := fixedpoint.MustNewFromString("0.075%")
|
||||
fee, feeCurrency := feeModeFunctionQuote(&order, &market, feeRate)
|
||||
assert.Equal(t, "1.5", fee.String())
|
||||
assert.Equal(t, "USDT", feeCurrency)
|
||||
})
|
||||
|
||||
t.Run("buyOrder", func(t *testing.T) {
|
||||
order := types.Order{
|
||||
SubmitOrder: types.SubmitOrder{
|
||||
Symbol: market.Symbol,
|
||||
Side: types.SideTypeBuy,
|
||||
Type: types.OrderTypeLimit,
|
||||
Quantity: fixedpoint.NewFromFloat(0.1),
|
||||
Price: fixedpoint.NewFromFloat(20000.0),
|
||||
TimeInForce: types.TimeInForceGTC,
|
||||
},
|
||||
}
|
||||
|
||||
feeRate := fixedpoint.MustNewFromString("0.075%")
|
||||
fee, feeCurrency := feeModeFunctionQuote(&order, &market, feeRate)
|
||||
assert.Equal(t, "1.5", fee.String())
|
||||
assert.Equal(t, "USDT", feeCurrency)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_feeModeFunctionNative(t *testing.T) {
|
||||
market := getTestMarket()
|
||||
t.Run("sellOrder", func(t *testing.T) {
|
||||
order := types.Order{
|
||||
SubmitOrder: types.SubmitOrder{
|
||||
Symbol: market.Symbol,
|
||||
Side: types.SideTypeSell,
|
||||
Type: types.OrderTypeLimit,
|
||||
Quantity: fixedpoint.NewFromFloat(0.1),
|
||||
Price: fixedpoint.NewFromFloat(20000.0),
|
||||
TimeInForce: types.TimeInForceGTC,
|
||||
},
|
||||
}
|
||||
feeRate := fixedpoint.MustNewFromString("0.075%")
|
||||
fee, feeCurrency := feeModeFunctionNative(&order, &market, feeRate)
|
||||
assert.Equal(t, "1.5", fee.String())
|
||||
assert.Equal(t, "USDT", feeCurrency)
|
||||
})
|
||||
|
||||
t.Run("buyOrder", func(t *testing.T) {
|
||||
order := types.Order{
|
||||
SubmitOrder: types.SubmitOrder{
|
||||
Symbol: market.Symbol,
|
||||
Side: types.SideTypeBuy,
|
||||
Type: types.OrderTypeLimit,
|
||||
Quantity: fixedpoint.NewFromFloat(0.1),
|
||||
Price: fixedpoint.NewFromFloat(20000.0),
|
||||
TimeInForce: types.TimeInForceGTC,
|
||||
},
|
||||
}
|
||||
|
||||
feeRate := fixedpoint.MustNewFromString("0.075%")
|
||||
fee, feeCurrency := feeModeFunctionNative(&order, &market, feeRate)
|
||||
assert.Equal(t, "0.000075", fee.String())
|
||||
assert.Equal(t, "BTC", feeCurrency)
|
||||
})
|
||||
}
|
|
@ -59,12 +59,14 @@ type SimplePriceMatching struct {
|
|||
closedOrders map[uint64]types.Order
|
||||
|
||||
klineCache map[types.Interval]types.KLine
|
||||
LastPrice fixedpoint.Value
|
||||
LastKLine types.KLine
|
||||
NextKLine *types.KLine
|
||||
CurrentTime time.Time
|
||||
lastPrice fixedpoint.Value
|
||||
lastKLine types.KLine
|
||||
nextKLine *types.KLine
|
||||
currentTime time.Time
|
||||
|
||||
Account *types.Account
|
||||
feeModeFunction FeeModeFunction
|
||||
|
||||
account *types.Account
|
||||
|
||||
tradeUpdateCallbacks []func(trade types.Trade)
|
||||
orderUpdateCallbacks []func(order types.Order)
|
||||
|
@ -109,38 +111,38 @@ func (m *SimplePriceMatching) CancelOrder(o types.Order) (types.Order, error) {
|
|||
|
||||
switch o.Side {
|
||||
case types.SideTypeBuy:
|
||||
if err := m.Account.UnlockBalance(m.Market.QuoteCurrency, o.Price.Mul(o.Quantity)); err != nil {
|
||||
if err := m.account.UnlockBalance(m.Market.QuoteCurrency, o.Price.Mul(o.Quantity)); err != nil {
|
||||
return o, err
|
||||
}
|
||||
|
||||
case types.SideTypeSell:
|
||||
if err := m.Account.UnlockBalance(m.Market.BaseCurrency, o.Quantity); err != nil {
|
||||
if err := m.account.UnlockBalance(m.Market.BaseCurrency, o.Quantity); err != nil {
|
||||
return o, err
|
||||
}
|
||||
}
|
||||
|
||||
o.Status = types.OrderStatusCanceled
|
||||
m.EmitOrderUpdate(o)
|
||||
m.EmitBalanceUpdate(m.Account.Balances())
|
||||
m.EmitBalanceUpdate(m.account.Balances())
|
||||
return o, nil
|
||||
}
|
||||
|
||||
// PlaceOrder returns the created order object, executed trade (if any) and error
|
||||
func (m *SimplePriceMatching) PlaceOrder(o types.SubmitOrder) (*types.Order, *types.Trade, error) {
|
||||
if o.Type == types.OrderTypeMarket {
|
||||
if m.LastPrice.IsZero() {
|
||||
if m.lastPrice.IsZero() {
|
||||
panic("unexpected error: for market order, the last price can not be zero")
|
||||
}
|
||||
}
|
||||
|
||||
isTaker := o.Type == types.OrderTypeMarket || isLimitTakerOrder(o, m.LastPrice)
|
||||
isTaker := o.Type == types.OrderTypeMarket || isLimitTakerOrder(o, m.lastPrice)
|
||||
|
||||
// price for checking account balance, default price
|
||||
price := o.Price
|
||||
|
||||
switch o.Type {
|
||||
case types.OrderTypeMarket:
|
||||
price = m.Market.TruncatePrice(m.LastPrice)
|
||||
price = m.Market.TruncatePrice(m.lastPrice)
|
||||
|
||||
case types.OrderTypeStopMarket:
|
||||
// the actual price might be different.
|
||||
|
@ -165,17 +167,17 @@ func (m *SimplePriceMatching) PlaceOrder(o types.SubmitOrder) (*types.Order, *ty
|
|||
|
||||
switch o.Side {
|
||||
case types.SideTypeBuy:
|
||||
if err := m.Account.LockBalance(m.Market.QuoteCurrency, quoteQuantity); err != nil {
|
||||
if err := m.account.LockBalance(m.Market.QuoteCurrency, quoteQuantity); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
case types.SideTypeSell:
|
||||
if err := m.Account.LockBalance(m.Market.BaseCurrency, o.Quantity); err != nil {
|
||||
if err := m.account.LockBalance(m.Market.BaseCurrency, o.Quantity); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
m.EmitBalanceUpdate(m.Account.Balances())
|
||||
m.EmitBalanceUpdate(m.account.Balances())
|
||||
|
||||
// start from one
|
||||
orderID := incOrderID()
|
||||
|
@ -184,19 +186,19 @@ func (m *SimplePriceMatching) PlaceOrder(o types.SubmitOrder) (*types.Order, *ty
|
|||
if isTaker {
|
||||
var price fixedpoint.Value
|
||||
if order.Type == types.OrderTypeMarket {
|
||||
order.Price = m.Market.TruncatePrice(m.LastPrice)
|
||||
order.Price = m.Market.TruncatePrice(m.lastPrice)
|
||||
price = order.Price
|
||||
} else if order.Type == types.OrderTypeLimit {
|
||||
// if limit order's price is with the range of next kline
|
||||
// we assume it will be traded as a maker trade, and is traded at its original price
|
||||
// TODO: if it is treated as a maker trade, fee should be specially handled
|
||||
// otherwise, set NextKLine.Close(i.e., m.LastPrice) to be the taker traded price
|
||||
if m.NextKLine != nil && m.NextKLine.High.Compare(order.Price) > 0 && order.Side == types.SideTypeBuy {
|
||||
if m.nextKLine != nil && m.nextKLine.High.Compare(order.Price) > 0 && order.Side == types.SideTypeBuy {
|
||||
order.AveragePrice = order.Price
|
||||
} else if m.NextKLine != nil && m.NextKLine.Low.Compare(order.Price) < 0 && order.Side == types.SideTypeSell {
|
||||
} else if m.nextKLine != nil && m.nextKLine.Low.Compare(order.Price) < 0 && order.Side == types.SideTypeSell {
|
||||
order.AveragePrice = order.Price
|
||||
} else {
|
||||
order.AveragePrice = m.Market.TruncatePrice(m.LastPrice)
|
||||
order.AveragePrice = m.Market.TruncatePrice(m.lastPrice)
|
||||
}
|
||||
price = order.AveragePrice
|
||||
}
|
||||
|
@ -223,10 +225,10 @@ func (m *SimplePriceMatching) PlaceOrder(o types.SubmitOrder) (*types.Order, *ty
|
|||
// the executed price is lower than the given price, so we will use less quote currency to buy the base asset.
|
||||
amount := order.Price.Sub(order.AveragePrice).Mul(order.Quantity)
|
||||
if amount.Sign() > 0 {
|
||||
if err := m.Account.UnlockBalance(m.Market.QuoteCurrency, amount); err != nil {
|
||||
if err := m.account.UnlockBalance(m.Market.QuoteCurrency, amount); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
m.EmitBalanceUpdate(m.Account.Balances())
|
||||
m.EmitBalanceUpdate(m.account.Balances())
|
||||
}
|
||||
|
||||
case types.SideTypeSell:
|
||||
|
@ -234,8 +236,8 @@ func (m *SimplePriceMatching) PlaceOrder(o types.SubmitOrder) (*types.Order, *ty
|
|||
// the executed price is higher than the given price, so we will get more quote currency back
|
||||
amount := order.AveragePrice.Sub(order.Price).Mul(order.Quantity)
|
||||
if amount.Sign() > 0 {
|
||||
m.Account.AddBalance(m.Market.QuoteCurrency, amount)
|
||||
m.EmitBalanceUpdate(m.Account.Balances())
|
||||
m.account.AddBalance(m.Market.QuoteCurrency, amount)
|
||||
m.EmitBalanceUpdate(m.account.Balances())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -273,24 +275,27 @@ func (m *SimplePriceMatching) executeTrade(trade types.Trade) {
|
|||
var err error
|
||||
// execute trade, update account balances
|
||||
if trade.IsBuyer {
|
||||
err = m.Account.UseLockedBalance(m.Market.QuoteCurrency, trade.QuoteQuantity)
|
||||
err = m.account.UseLockedBalance(m.Market.QuoteCurrency, trade.QuoteQuantity)
|
||||
|
||||
// here the fee currency is the base currency
|
||||
q := trade.Quantity
|
||||
if trade.FeeCurrency == m.Market.BaseCurrency {
|
||||
q = q.Sub(trade.Fee)
|
||||
// all-in buy trade, we can only deduct the fee from the quote quantity and re-calculate the base quantity
|
||||
switch trade.FeeCurrency {
|
||||
case m.Market.QuoteCurrency:
|
||||
m.account.AddBalance(m.Market.QuoteCurrency, trade.Fee.Neg())
|
||||
m.account.AddBalance(m.Market.BaseCurrency, trade.Quantity)
|
||||
case m.Market.BaseCurrency:
|
||||
m.account.AddBalance(m.Market.BaseCurrency, trade.Quantity.Sub(trade.Fee))
|
||||
}
|
||||
|
||||
m.Account.AddBalance(m.Market.BaseCurrency, q)
|
||||
} else {
|
||||
err = m.Account.UseLockedBalance(m.Market.BaseCurrency, trade.Quantity)
|
||||
} else { // sell trade
|
||||
err = m.account.UseLockedBalance(m.Market.BaseCurrency, trade.Quantity)
|
||||
|
||||
// here the fee currency is the quote currency
|
||||
qq := trade.QuoteQuantity
|
||||
if trade.FeeCurrency == m.Market.QuoteCurrency {
|
||||
qq = qq.Sub(trade.Fee)
|
||||
switch trade.FeeCurrency {
|
||||
case m.Market.QuoteCurrency:
|
||||
m.account.AddBalance(m.Market.QuoteCurrency, trade.QuoteQuantity.Sub(trade.Fee))
|
||||
case m.Market.BaseCurrency:
|
||||
m.account.AddBalance(m.Market.BaseCurrency, trade.Fee.Neg())
|
||||
m.account.AddBalance(m.Market.QuoteCurrency, trade.QuoteQuantity)
|
||||
}
|
||||
m.Account.AddBalance(m.Market.QuoteCurrency, qq)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
@ -298,16 +303,16 @@ func (m *SimplePriceMatching) executeTrade(trade types.Trade) {
|
|||
}
|
||||
|
||||
m.EmitTradeUpdate(trade)
|
||||
m.EmitBalanceUpdate(m.Account.Balances())
|
||||
m.EmitBalanceUpdate(m.account.Balances())
|
||||
}
|
||||
|
||||
func (m *SimplePriceMatching) getFeeRate(isMaker bool) (feeRate fixedpoint.Value) {
|
||||
// BINANCE uses 0.1% for both maker and taker
|
||||
// MAX uses 0.050% for maker and 0.15% for taker
|
||||
if isMaker {
|
||||
feeRate = m.Account.MakerFeeRate
|
||||
feeRate = m.account.MakerFeeRate
|
||||
} else {
|
||||
feeRate = m.Account.TakerFeeRate
|
||||
feeRate = m.account.TakerFeeRate
|
||||
}
|
||||
return feeRate
|
||||
}
|
||||
|
@ -320,15 +325,14 @@ func (m *SimplePriceMatching) newTradeFromOrder(order *types.Order, isMaker bool
|
|||
var fee fixedpoint.Value
|
||||
var feeCurrency string
|
||||
|
||||
if useFeeToken {
|
||||
feeCurrency = FeeToken
|
||||
fee = quoteQuantity.Mul(feeRate)
|
||||
if m.feeModeFunction != nil {
|
||||
fee, feeCurrency = m.feeModeFunction(order, &m.Market, feeRate)
|
||||
} else {
|
||||
fee, feeCurrency = calculateNativeOrderFee(order, m.Market, feeRate)
|
||||
fee, feeCurrency = feeModeFunctionQuote(order, &m.Market, feeRate)
|
||||
}
|
||||
|
||||
// update order time
|
||||
order.UpdateTime = types.Time(m.CurrentTime)
|
||||
order.UpdateTime = types.Time(m.currentTime)
|
||||
|
||||
var id = incTradeID()
|
||||
return types.Trade{
|
||||
|
@ -342,7 +346,7 @@ func (m *SimplePriceMatching) newTradeFromOrder(order *types.Order, isMaker bool
|
|||
Side: order.Side,
|
||||
IsBuyer: order.Side == types.SideTypeBuy,
|
||||
IsMaker: isMaker,
|
||||
Time: types.Time(m.CurrentTime),
|
||||
Time: types.Time(m.currentTime),
|
||||
Fee: fee,
|
||||
FeeCurrency: feeCurrency,
|
||||
}
|
||||
|
@ -458,7 +462,7 @@ func (m *SimplePriceMatching) buyToPrice(price fixedpoint.Value) (closedOrders [
|
|||
}
|
||||
|
||||
m.askOrders = askOrders
|
||||
m.LastPrice = price
|
||||
m.lastPrice = price
|
||||
|
||||
for i := range closedOrders {
|
||||
o := closedOrders[i]
|
||||
|
@ -587,7 +591,7 @@ func (m *SimplePriceMatching) sellToPrice(price fixedpoint.Value) (closedOrders
|
|||
}
|
||||
|
||||
m.bidOrders = bidOrders
|
||||
m.LastPrice = price
|
||||
m.lastPrice = price
|
||||
|
||||
for i := range closedOrders {
|
||||
o := closedOrders[i]
|
||||
|
@ -631,12 +635,12 @@ func (m *SimplePriceMatching) getOrder(orderID uint64) (types.Order, bool) {
|
|||
}
|
||||
|
||||
func (m *SimplePriceMatching) processKLine(kline types.KLine) {
|
||||
m.CurrentTime = kline.EndTime.Time()
|
||||
m.currentTime = kline.EndTime.Time()
|
||||
|
||||
if m.LastPrice.IsZero() {
|
||||
m.LastPrice = kline.Open
|
||||
if m.lastPrice.IsZero() {
|
||||
m.lastPrice = kline.Open
|
||||
} else {
|
||||
if m.LastPrice.Compare(kline.Open) > 0 {
|
||||
if m.lastPrice.Compare(kline.Open) > 0 {
|
||||
m.sellToPrice(kline.Open)
|
||||
} else {
|
||||
m.buyToPrice(kline.Open)
|
||||
|
@ -669,12 +673,12 @@ func (m *SimplePriceMatching) processKLine(kline types.KLine) {
|
|||
m.buyToPrice(kline.Close)
|
||||
}
|
||||
default: // no trade up or down
|
||||
if m.LastPrice.IsZero() {
|
||||
if m.lastPrice.IsZero() {
|
||||
m.buyToPrice(kline.Close)
|
||||
}
|
||||
}
|
||||
|
||||
m.LastKLine = kline
|
||||
m.lastKLine = kline
|
||||
}
|
||||
|
||||
func (m *SimplePriceMatching) newOrder(o types.SubmitOrder, orderID uint64) types.Order {
|
||||
|
@ -685,27 +689,11 @@ func (m *SimplePriceMatching) newOrder(o types.SubmitOrder, orderID uint64) type
|
|||
Status: types.OrderStatusNew,
|
||||
ExecutedQuantity: fixedpoint.Zero,
|
||||
IsWorking: true,
|
||||
CreationTime: types.Time(m.CurrentTime),
|
||||
UpdateTime: types.Time(m.CurrentTime),
|
||||
CreationTime: types.Time(m.currentTime),
|
||||
UpdateTime: types.Time(m.currentTime),
|
||||
}
|
||||
}
|
||||
|
||||
func calculateNativeOrderFee(order *types.Order, market types.Market, feeRate fixedpoint.Value) (fee fixedpoint.Value, feeCurrency string) {
|
||||
switch order.Side {
|
||||
|
||||
case types.SideTypeBuy:
|
||||
fee = order.Quantity.Mul(feeRate)
|
||||
feeCurrency = market.BaseCurrency
|
||||
|
||||
case types.SideTypeSell:
|
||||
quoteQuantity := order.Quantity.Mul(order.Price)
|
||||
fee = quoteQuantity.Mul(feeRate)
|
||||
feeCurrency = market.QuoteCurrency
|
||||
|
||||
}
|
||||
return fee, feeCurrency
|
||||
}
|
||||
|
||||
func isTakerOrder(o types.Order) bool {
|
||||
if o.AveragePrice.IsZero() {
|
||||
return false
|
||||
|
|
|
@ -44,11 +44,11 @@ func TestSimplePriceMatching_orderUpdate(t *testing.T) {
|
|||
|
||||
t1 := time.Date(2021, 7, 1, 0, 0, 0, 0, time.UTC)
|
||||
engine := &SimplePriceMatching{
|
||||
Account: account,
|
||||
account: account,
|
||||
Market: market,
|
||||
CurrentTime: t1,
|
||||
currentTime: t1,
|
||||
closedOrders: make(map[uint64]types.Order),
|
||||
LastPrice: fixedpoint.NewFromFloat(25000),
|
||||
lastPrice: fixedpoint.NewFromFloat(25000),
|
||||
}
|
||||
|
||||
orderUpdateCnt := 0
|
||||
|
@ -97,11 +97,11 @@ func TestSimplePriceMatching_CancelOrder(t *testing.T) {
|
|||
market := getTestMarket()
|
||||
t1 := time.Date(2021, 7, 1, 0, 0, 0, 0, time.UTC)
|
||||
engine := &SimplePriceMatching{
|
||||
Account: account,
|
||||
account: account,
|
||||
Market: market,
|
||||
CurrentTime: t1,
|
||||
currentTime: t1,
|
||||
closedOrders: make(map[uint64]types.Order),
|
||||
LastPrice: fixedpoint.NewFromFloat(30000.0),
|
||||
lastPrice: fixedpoint.NewFromFloat(30000.0),
|
||||
}
|
||||
|
||||
createdOrder1, trade1, err := engine.PlaceOrder(newLimitOrder("BTCUSDT", types.SideTypeBuy, 20000.0, 0.1))
|
||||
|
@ -139,11 +139,11 @@ func TestSimplePriceMatching_processKLine(t *testing.T) {
|
|||
|
||||
t1 := time.Date(2021, 7, 1, 0, 0, 0, 0, time.UTC)
|
||||
engine := &SimplePriceMatching{
|
||||
Account: account,
|
||||
account: account,
|
||||
Market: market,
|
||||
CurrentTime: t1,
|
||||
currentTime: t1,
|
||||
closedOrders: make(map[uint64]types.Order),
|
||||
LastPrice: fixedpoint.NewFromFloat(30000.0),
|
||||
lastPrice: fixedpoint.NewFromFloat(30000.0),
|
||||
}
|
||||
|
||||
for i := 0; i <= 5; i++ {
|
||||
|
@ -216,10 +216,10 @@ func TestSimplePriceMatching_LimitBuyTakerOrder(t *testing.T) {
|
|||
account := getTestAccount()
|
||||
market := getTestMarket()
|
||||
engine := &SimplePriceMatching{
|
||||
Account: account,
|
||||
account: account,
|
||||
Market: market,
|
||||
closedOrders: make(map[uint64]types.Order),
|
||||
LastPrice: fixedpoint.NewFromFloat(19000.0),
|
||||
lastPrice: fixedpoint.NewFromFloat(19000.0),
|
||||
}
|
||||
|
||||
takerOrder := types.SubmitOrder{
|
||||
|
@ -249,17 +249,18 @@ func TestSimplePriceMatching_LimitBuyTakerOrder(t *testing.T) {
|
|||
assert.Equal(t, fixedpoint.NewFromFloat(100.0).Add(createdOrder.Quantity).String(), btc.Available.String())
|
||||
|
||||
usedQuoteAmount := createdOrder.AveragePrice.Mul(createdOrder.Quantity)
|
||||
assert.Equal(t, usdt.Available.String(), fixedpoint.NewFromFloat(1000000.0).Sub(usedQuoteAmount).String())
|
||||
assert.Equal(t, "USDT", trade.FeeCurrency)
|
||||
assert.Equal(t, usdt.Available.String(), fixedpoint.NewFromFloat(1000000.0).Sub(usedQuoteAmount).Sub(trade.Fee).String())
|
||||
}
|
||||
|
||||
func TestSimplePriceMatching_StopLimitOrderBuy(t *testing.T) {
|
||||
account := getTestAccount()
|
||||
market := getTestMarket()
|
||||
engine := &SimplePriceMatching{
|
||||
Account: account,
|
||||
account: account,
|
||||
Market: market,
|
||||
closedOrders: make(map[uint64]types.Order),
|
||||
LastPrice: fixedpoint.NewFromFloat(19000.0),
|
||||
lastPrice: fixedpoint.NewFromFloat(19000.0),
|
||||
}
|
||||
|
||||
stopBuyOrder := types.SubmitOrder{
|
||||
|
@ -299,7 +300,7 @@ func TestSimplePriceMatching_StopLimitOrderBuy(t *testing.T) {
|
|||
assert.Equal(t, "21001", trades[0].Price.String())
|
||||
assert.Equal(t, "22000", closedOrders[0].Price.String(), "order.Price should not be adjusted")
|
||||
|
||||
assert.Equal(t, fixedpoint.NewFromFloat(21001.0).String(), engine.LastPrice.String())
|
||||
assert.Equal(t, fixedpoint.NewFromFloat(21001.0).String(), engine.lastPrice.String())
|
||||
|
||||
stopOrder2 := types.SubmitOrder{
|
||||
Symbol: market.Symbol,
|
||||
|
@ -326,10 +327,10 @@ func TestSimplePriceMatching_StopLimitOrderSell(t *testing.T) {
|
|||
account := getTestAccount()
|
||||
market := getTestMarket()
|
||||
engine := &SimplePriceMatching{
|
||||
Account: account,
|
||||
account: account,
|
||||
Market: market,
|
||||
closedOrders: make(map[uint64]types.Order),
|
||||
LastPrice: fixedpoint.NewFromFloat(22000.0),
|
||||
lastPrice: fixedpoint.NewFromFloat(22000.0),
|
||||
}
|
||||
|
||||
stopSellOrder := types.SubmitOrder{
|
||||
|
@ -370,7 +371,7 @@ func TestSimplePriceMatching_StopLimitOrderSell(t *testing.T) {
|
|||
assert.Equal(t, types.OrderTypeLimit, closedOrders[0].Type)
|
||||
assert.Equal(t, "20000", closedOrders[0].Price.String(), "limit order price should not be changed")
|
||||
assert.Equal(t, "20990", trades[0].Price.String())
|
||||
assert.Equal(t, "20990", engine.LastPrice.String())
|
||||
assert.Equal(t, "20990", engine.lastPrice.String())
|
||||
|
||||
// place a stop limit sell order with a higher price than the current price
|
||||
stopOrder2 := types.SubmitOrder{
|
||||
|
@ -395,7 +396,7 @@ func TestSimplePriceMatching_StopLimitOrderSell(t *testing.T) {
|
|||
assert.Equal(t, types.OrderStatusFilled, closedOrders[0].Status)
|
||||
assert.Equal(t, types.OrderTypeLimit, closedOrders[0].Type)
|
||||
assert.Equal(t, "21000", trades[0].Price.String(), "trade price should be the kline price not the order price")
|
||||
assert.Equal(t, "21000", engine.LastPrice.String(), "engine last price should be updated correctly")
|
||||
assert.Equal(t, "21000", engine.lastPrice.String(), "engine last price should be updated correctly")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -403,10 +404,10 @@ func TestSimplePriceMatching_StopMarketOrderSell(t *testing.T) {
|
|||
account := getTestAccount()
|
||||
market := getTestMarket()
|
||||
engine := &SimplePriceMatching{
|
||||
Account: account,
|
||||
account: account,
|
||||
Market: market,
|
||||
closedOrders: make(map[uint64]types.Order),
|
||||
LastPrice: fixedpoint.NewFromFloat(22000.0),
|
||||
lastPrice: fixedpoint.NewFromFloat(22000.0),
|
||||
}
|
||||
|
||||
stopOrder := types.SubmitOrder{
|
||||
|
@ -440,7 +441,7 @@ func TestSimplePriceMatching_PlaceLimitOrder(t *testing.T) {
|
|||
account := getTestAccount()
|
||||
market := getTestMarket()
|
||||
engine := &SimplePriceMatching{
|
||||
Account: account,
|
||||
account: account,
|
||||
Market: market,
|
||||
closedOrders: make(map[uint64]types.Order),
|
||||
}
|
||||
|
@ -497,53 +498,14 @@ func TestSimplePriceMatching_PlaceLimitOrder(t *testing.T) {
|
|||
assert.Len(t, trades, 4)
|
||||
}
|
||||
|
||||
func Test_calculateNativeOrderFee(t *testing.T) {
|
||||
market := getTestMarket()
|
||||
|
||||
t.Run("sellOrder", func(t *testing.T) {
|
||||
order := types.Order{
|
||||
SubmitOrder: types.SubmitOrder{
|
||||
Symbol: market.Symbol,
|
||||
Side: types.SideTypeSell,
|
||||
Type: types.OrderTypeLimit,
|
||||
Quantity: fixedpoint.NewFromFloat(0.1),
|
||||
Price: fixedpoint.NewFromFloat(20000.0),
|
||||
TimeInForce: types.TimeInForceGTC,
|
||||
},
|
||||
}
|
||||
feeRate := fixedpoint.MustNewFromString("0.075%")
|
||||
fee, feeCurrency := calculateNativeOrderFee(&order, market, feeRate)
|
||||
assert.Equal(t, "1.5", fee.String())
|
||||
assert.Equal(t, "USDT", feeCurrency)
|
||||
})
|
||||
|
||||
t.Run("buyOrder", func(t *testing.T) {
|
||||
order := types.Order{
|
||||
SubmitOrder: types.SubmitOrder{
|
||||
Symbol: market.Symbol,
|
||||
Side: types.SideTypeBuy,
|
||||
Type: types.OrderTypeLimit,
|
||||
Quantity: fixedpoint.NewFromFloat(0.1),
|
||||
Price: fixedpoint.NewFromFloat(20000.0),
|
||||
TimeInForce: types.TimeInForceGTC,
|
||||
},
|
||||
}
|
||||
|
||||
feeRate := fixedpoint.MustNewFromString("0.075%")
|
||||
fee, feeCurrency := calculateNativeOrderFee(&order, market, feeRate)
|
||||
assert.Equal(t, "0.000075", fee.String())
|
||||
assert.Equal(t, "BTC", feeCurrency)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSimplePriceMatching_LimitTakerOrder(t *testing.T) {
|
||||
account := getTestAccount()
|
||||
market := getTestMarket()
|
||||
engine := &SimplePriceMatching{
|
||||
Account: account,
|
||||
account: account,
|
||||
Market: market,
|
||||
closedOrders: make(map[uint64]types.Order),
|
||||
LastPrice: fixedpoint.NewFromFloat(20000.0),
|
||||
lastPrice: fixedpoint.NewFromFloat(20000.0),
|
||||
}
|
||||
|
||||
closedOrder, trade, err := engine.PlaceOrder(newLimitOrder("BTCUSDT", types.SideTypeBuy, 21000.0, 1.0))
|
||||
|
|
|
@ -75,7 +75,7 @@ type SessionSymbolReport struct {
|
|||
Market types.Market `json:"market"`
|
||||
LastPrice fixedpoint.Value `json:"lastPrice,omitempty"`
|
||||
StartPrice fixedpoint.Value `json:"startPrice,omitempty"`
|
||||
PnL *pnl.AverageCostPnlReport `json:"pnl,omitempty"`
|
||||
PnL *pnl.AverageCostPnLReport `json:"pnl,omitempty"`
|
||||
InitialBalances types.BalanceMap `json:"initialBalances,omitempty"`
|
||||
FinalBalances types.BalanceMap `json:"finalBalances,omitempty"`
|
||||
Manifests Manifests `json:"manifests,omitempty"`
|
||||
|
|
117
pkg/bbgo/backtestfeemode_enumer.go
Normal file
117
pkg/bbgo/backtestfeemode_enumer.go
Normal file
|
@ -0,0 +1,117 @@
|
|||
// Code generated by "enumer -type=BacktestFeeMode -transform=snake -trimprefix BacktestFeeMode -yaml -json"; DO NOT EDIT.
|
||||
|
||||
package bbgo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const _BacktestFeeModeName = "quotenativetoken"
|
||||
|
||||
var _BacktestFeeModeIndex = [...]uint8{0, 5, 11, 16}
|
||||
|
||||
const _BacktestFeeModeLowerName = "quotenativetoken"
|
||||
|
||||
func (i BacktestFeeMode) String() string {
|
||||
if i < 0 || i >= BacktestFeeMode(len(_BacktestFeeModeIndex)-1) {
|
||||
return fmt.Sprintf("BacktestFeeMode(%d)", i)
|
||||
}
|
||||
return _BacktestFeeModeName[_BacktestFeeModeIndex[i]:_BacktestFeeModeIndex[i+1]]
|
||||
}
|
||||
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
func _BacktestFeeModeNoOp() {
|
||||
var x [1]struct{}
|
||||
_ = x[BacktestFeeModeQuote-(0)]
|
||||
_ = x[BacktestFeeModeNative-(1)]
|
||||
_ = x[BacktestFeeModeToken-(2)]
|
||||
}
|
||||
|
||||
var _BacktestFeeModeValues = []BacktestFeeMode{BacktestFeeModeQuote, BacktestFeeModeNative, BacktestFeeModeToken}
|
||||
|
||||
var _BacktestFeeModeNameToValueMap = map[string]BacktestFeeMode{
|
||||
_BacktestFeeModeName[0:5]: BacktestFeeModeQuote,
|
||||
_BacktestFeeModeLowerName[0:5]: BacktestFeeModeQuote,
|
||||
_BacktestFeeModeName[5:11]: BacktestFeeModeNative,
|
||||
_BacktestFeeModeLowerName[5:11]: BacktestFeeModeNative,
|
||||
_BacktestFeeModeName[11:16]: BacktestFeeModeToken,
|
||||
_BacktestFeeModeLowerName[11:16]: BacktestFeeModeToken,
|
||||
}
|
||||
|
||||
var _BacktestFeeModeNames = []string{
|
||||
_BacktestFeeModeName[0:5],
|
||||
_BacktestFeeModeName[5:11],
|
||||
_BacktestFeeModeName[11:16],
|
||||
}
|
||||
|
||||
// BacktestFeeModeString retrieves an enum value from the enum constants string name.
|
||||
// Throws an error if the param is not part of the enum.
|
||||
func BacktestFeeModeString(s string) (BacktestFeeMode, error) {
|
||||
if val, ok := _BacktestFeeModeNameToValueMap[s]; ok {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
if val, ok := _BacktestFeeModeNameToValueMap[strings.ToLower(s)]; ok {
|
||||
return val, nil
|
||||
}
|
||||
return 0, fmt.Errorf("%s does not belong to BacktestFeeMode values", s)
|
||||
}
|
||||
|
||||
// BacktestFeeModeValues returns all values of the enum
|
||||
func BacktestFeeModeValues() []BacktestFeeMode {
|
||||
return _BacktestFeeModeValues
|
||||
}
|
||||
|
||||
// BacktestFeeModeStrings returns a slice of all String values of the enum
|
||||
func BacktestFeeModeStrings() []string {
|
||||
strs := make([]string, len(_BacktestFeeModeNames))
|
||||
copy(strs, _BacktestFeeModeNames)
|
||||
return strs
|
||||
}
|
||||
|
||||
// IsABacktestFeeMode returns "true" if the value is listed in the enum definition. "false" otherwise
|
||||
func (i BacktestFeeMode) IsABacktestFeeMode() bool {
|
||||
for _, v := range _BacktestFeeModeValues {
|
||||
if i == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface for BacktestFeeMode
|
||||
func (i BacktestFeeMode) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(i.String())
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface for BacktestFeeMode
|
||||
func (i *BacktestFeeMode) UnmarshalJSON(data []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
return fmt.Errorf("BacktestFeeMode should be a string, got %s", data)
|
||||
}
|
||||
|
||||
var err error
|
||||
*i, err = BacktestFeeModeString(s)
|
||||
return err
|
||||
}
|
||||
|
||||
// MarshalYAML implements a YAML Marshaler for BacktestFeeMode
|
||||
func (i BacktestFeeMode) MarshalYAML() (interface{}, error) {
|
||||
return i.String(), nil
|
||||
}
|
||||
|
||||
// UnmarshalYAML implements a YAML Unmarshaler for BacktestFeeMode
|
||||
func (i *BacktestFeeMode) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var s string
|
||||
if err := unmarshal(&s); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var err error
|
||||
*i, err = BacktestFeeModeString(s)
|
||||
return err
|
||||
}
|
|
@ -101,6 +101,25 @@ type Session struct {
|
|||
IsolatedMarginSymbol string `json:"isolatedMarginSymbol,omitempty" yaml:"isolatedMarginSymbol,omitempty"`
|
||||
}
|
||||
|
||||
//go:generate go run github.com/dmarkham/enumer -type=BacktestFeeMode -transform=snake -trimprefix BacktestFeeMode -yaml -json
|
||||
type BacktestFeeMode int
|
||||
|
||||
const (
|
||||
// BackTestFeeModeQuoteFee is designed for clean position but which also counts the fee in the quote balance.
|
||||
// buy order = quote currency fee
|
||||
// sell order = quote currency fee
|
||||
BacktestFeeModeQuote BacktestFeeMode = iota // quote
|
||||
|
||||
// BackTestFeeModeNativeFee is the default crypto exchange fee mode.
|
||||
// buy order = base currency fee
|
||||
// sell order = quote currency fee
|
||||
BacktestFeeModeNative // BackTestFeeMode = "native"
|
||||
|
||||
// BackTestFeeModeFeeToken is the mode which calculates fee from the outside of the balances.
|
||||
// the fee will not be included in the balances nor the profit.
|
||||
BacktestFeeModeToken // BackTestFeeMode = "token"
|
||||
)
|
||||
|
||||
type Backtest struct {
|
||||
StartTime types.LooseFormatTime `json:"startTime,omitempty" yaml:"startTime,omitempty"`
|
||||
EndTime *types.LooseFormatTime `json:"endTime,omitempty" yaml:"endTime,omitempty"`
|
||||
|
@ -112,6 +131,8 @@ type Backtest struct {
|
|||
// Account is deprecated, use Accounts instead
|
||||
Account map[string]BacktestAccount `json:"account" yaml:"account"`
|
||||
|
||||
FeeMode BacktestFeeMode `json:"feeMode" yaml:"feeMode"`
|
||||
|
||||
Accounts map[string]BacktestAccount `json:"accounts" yaml:"accounts"`
|
||||
Symbols []string `json:"symbols" yaml:"symbols"`
|
||||
Sessions []string `json:"sessions" yaml:"sessions"`
|
||||
|
|
|
@ -254,6 +254,13 @@ func TestSyncSymbol(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestBackTestFeeMode(t *testing.T) {
|
||||
var mode BacktestFeeMode
|
||||
var err = yaml.Unmarshal([]byte(`quote`), &mode)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, BacktestFeeModeQuote, mode)
|
||||
}
|
||||
|
||||
func Test_categorizeSyncSymbol(t *testing.T) {
|
||||
var ss []SyncSymbol
|
||||
var err = yaml.Unmarshal([]byte(`
|
||||
|
|
|
@ -583,7 +583,6 @@ var BacktestCmd = &cobra.Command{
|
|||
color.Green("END TIME: %s\n", endTime.Format(time.RFC1123))
|
||||
color.Green("INITIAL TOTAL BALANCE: %v\n", initTotalBalances)
|
||||
color.Green("FINAL TOTAL BALANCE: %v\n", finalTotalBalances)
|
||||
|
||||
for _, symbolReport := range summaryReport.SymbolReports {
|
||||
symbolReport.Print(wantBaseAssetBaseline)
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package types
|
|||
import (
|
||||
"encoding/json"
|
||||
"math"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
@ -203,6 +204,38 @@ func (s *TradeStats) SetIntervalProfitCollector(c *IntervalProfitCollector) {
|
|||
s.IntervalProfits[c.Interval] = c
|
||||
}
|
||||
|
||||
func (s *TradeStats) CsvHeader() []string {
|
||||
return []string{
|
||||
"winningRatio",
|
||||
"numOfProfitTrade",
|
||||
"numOfLossTrade",
|
||||
"grossProfit",
|
||||
"grossLoss",
|
||||
"profitFactor",
|
||||
"largestProfitTrade",
|
||||
"largestLossTrade",
|
||||
"maximumConsecutiveWins",
|
||||
"maximumConsecutiveLosses",
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TradeStats) CsvRecords() [][]string {
|
||||
return [][]string{
|
||||
{
|
||||
s.WinningRatio.String(),
|
||||
strconv.Itoa(s.NumOfProfitTrade),
|
||||
strconv.Itoa(s.NumOfLossTrade),
|
||||
s.GrossProfit.String(),
|
||||
s.GrossLoss.String(),
|
||||
s.ProfitFactor.String(),
|
||||
s.LargestProfitTrade.String(),
|
||||
s.LargestLossTrade.String(),
|
||||
strconv.Itoa(s.MaximumConsecutiveWins),
|
||||
strconv.Itoa(s.MaximumConsecutiveLosses),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TradeStats) Add(profit *Profit) {
|
||||
if s.Symbol != "" && profit.Symbol != s.Symbol {
|
||||
return
|
||||
|
|
Loading…
Reference in New Issue
Block a user