Merge pull request #911 from c9s/feature/backtest-report

feature: add fee mode to back-test matching engine
This commit is contained in:
Yo-An Lin 2022-09-02 15:09:04 +08:00 committed by GitHub
commit fcb7a27f70
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 484 additions and 153 deletions

View File

@ -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
View File

@ -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
View File

@ -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=

View File

@ -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,

View File

@ -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 {

View File

@ -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
View 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
View 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)
})
}

View File

@ -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

View File

@ -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))

View File

@ -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"`

View 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
}

View File

@ -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"`

View File

@ -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(`

View File

@ -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)
}

View File

@ -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