Skip to content

Commit 7a56ec9

Browse files
authored
feat(backtesting): add volume indication to trading performance
1 parent 8a44016 commit 7a56ec9

File tree

4 files changed

+66
-19
lines changed

4 files changed

+66
-19
lines changed

ninjabot.go

+11-6
Original file line numberDiff line numberDiff line change
@@ -130,12 +130,13 @@ func (n *NinjaBot) SubscribeOrder(subscriptions ...OrderSubscriber) {
130130

131131
func (n *NinjaBot) Summary() {
132132
var (
133-
total float64
134-
wins int
135-
loses int
133+
total float64
134+
wins int
135+
loses int
136+
volume float64
136137
)
137138
table := tablewriter.NewWriter(os.Stdout)
138-
table.SetHeader([]string{"Pair", "Trades", "Win", "Loss", "% Win", "Payoff", "Profit"})
139+
table.SetHeader([]string{"Pair", "Trades", "Win", "Loss", "% Win", "Payoff", "Profit", "Volume"})
139140
table.SetFooterAlignment(tablewriter.ALIGN_RIGHT)
140141
avgPayoff := 0.0
141142
for _, summary := range n.orderController.Results {
@@ -147,20 +148,24 @@ func (n *NinjaBot) Summary() {
147148
strconv.Itoa(len(summary.Lose)),
148149
fmt.Sprintf("%.1f %%", float64(len(summary.Win))/float64(len(summary.Win)+len(summary.Lose))*100),
149150
fmt.Sprintf("%.3f", summary.Payoff()),
150-
fmt.Sprintf("%.4f", summary.Profit()),
151+
fmt.Sprintf("%.2f", summary.Profit()),
152+
fmt.Sprintf("%.2f", summary.Volume),
151153
})
152154
total += summary.Profit()
153155
wins += len(summary.Win)
154156
loses += len(summary.Lose)
157+
volume += summary.Volume
155158
}
159+
156160
table.SetFooter([]string{
157161
"TOTAL",
158162
strconv.Itoa(wins + loses),
159163
strconv.Itoa(wins),
160164
strconv.Itoa(loses),
161165
fmt.Sprintf("%.1f %%", float64(wins)/float64(wins+loses)*100),
162166
fmt.Sprintf("%.3f", avgPayoff/float64(wins+loses)),
163-
fmt.Sprintf("%.4f", total),
167+
fmt.Sprintf("%.2f", total),
168+
fmt.Sprintf("%.2f", volume),
164169
})
165170
table.Render()
166171
}

pkg/exchange/paperwallet.go

+32-4
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ type PaperWallet struct {
2929
orders []model.Order
3030
assets map[string]*assetInfo
3131
avgPrice map[string]float64
32+
volume map[string]float64
3233
lastCandle map[string]model.Candle
3334
fistCandle map[string]model.Candle
3435
}
@@ -66,6 +67,7 @@ func NewPaperWallet(ctx context.Context, baseCoin string, options ...PaperWallet
6667
fistCandle: make(map[string]model.Candle),
6768
lastCandle: make(map[string]model.Candle),
6869
avgPrice: make(map[string]float64),
70+
volume: make(map[string]float64),
6971
}
7072

7173
for _, option := range options {
@@ -88,18 +90,29 @@ func (p *PaperWallet) Summary() {
8890
var (
8991
total float64
9092
marketChange float64
93+
volume float64
9194
)
9295

9396
fmt.Println("--------------")
9497
fmt.Println("WALLET SUMMARY")
9598
fmt.Println("--------------")
99+
96100
for pair, price := range p.avgPrice {
97101
asset, _ := SplitAssetQuote(pair)
98102
quantity := p.assets[asset].Free + p.assets[asset].Lock
99103
total += quantity * price
100104
marketChange += (p.lastCandle[pair].Close - p.fistCandle[pair].Close) / p.fistCandle[pair].Close
101105
fmt.Printf("%f %s\n", quantity, asset)
102106
}
107+
108+
fmt.Println()
109+
fmt.Println("TRADING VOLUME")
110+
for symbol, vol := range p.volume {
111+
volume += vol
112+
fmt.Printf("%s = %.2f %s\n", symbol, vol, p.baseCoin)
113+
}
114+
fmt.Println()
115+
103116
avgMarketChange := marketChange / float64(len(p.avgPrice))
104117
baseCoinValue := p.assets[p.baseCoin].Free + p.assets[p.baseCoin].Lock
105118
profit := total + baseCoinValue - p.initialValue
@@ -109,6 +122,8 @@ func (p *PaperWallet) Summary() {
109122
fmt.Println("FINAL PORTFOLIO = ", total+baseCoinValue, p.baseCoin)
110123
fmt.Printf("GROSS PROFIT = %f %s (%.2f%%)\n", profit, p.baseCoin, profit/p.initialValue*100)
111124
fmt.Printf("MARKET CHANGE = %.2f%%\n", avgMarketChange*100)
125+
fmt.Printf("VOLUME = %.2f %s\n", volume, p.baseCoin)
126+
fmt.Printf("COSTS (0.001*V) = %.2f %s (ESTIMATION) \n", volume*0.001, p.baseCoin)
112127
fmt.Println("--------------")
113128
}
114129

@@ -136,20 +151,25 @@ func (p *PaperWallet) OnCandle(candle model.Candle) {
136151
continue
137152
}
138153

154+
if _, ok := p.volume[candle.Symbol]; !ok {
155+
p.volume[candle.Symbol] = 0
156+
}
157+
139158
asset, quote := SplitAssetQuote(order.Symbol)
140159
if order.Side == model.SideTypeBuy && order.Price <= candle.Close {
141160
if _, ok := p.assets[asset]; !ok {
142161
p.assets[asset] = &assetInfo{}
143162
}
144163

145164
actualQty := p.assets[asset].Free + p.assets[asset].Lock
146-
orderValue := order.Price * order.Quantity
165+
orderVolume := order.Price * order.Quantity
147166
walletValue := p.avgPrice[candle.Symbol] * actualQty
148167

168+
p.volume[candle.Symbol] += orderVolume
149169
p.orders[i].Status = model.OrderStatusTypeFilled
150-
p.avgPrice[candle.Symbol] = (walletValue + orderValue) / (actualQty + order.Quantity)
170+
p.avgPrice[candle.Symbol] = (walletValue + orderVolume) / (actualQty + order.Quantity)
151171
p.assets[asset].Free = p.assets[asset].Free + order.Quantity
152-
p.assets[quote].Lock = p.assets[quote].Lock - orderValue
172+
p.assets[quote].Lock = p.assets[quote].Lock - orderVolume
153173
}
154174

155175
if order.Side == model.SideTypeSell {
@@ -168,7 +188,7 @@ func (p *PaperWallet) OnCandle(candle model.Candle) {
168188
continue
169189
}
170190

171-
// cancel other others from same group
191+
// Cancel other orders from same group
172192
if order.GroupID != nil {
173193
for j, groupOrder := range p.orders {
174194
if groupOrder.GroupID != nil && *groupOrder.GroupID == *order.GroupID &&
@@ -183,10 +203,12 @@ func (p *PaperWallet) OnCandle(candle model.Candle) {
183203
p.assets[quote] = &assetInfo{}
184204
}
185205

206+
orderVolume := order.Quantity * orderPrice
186207
profitValue := order.Quantity*orderPrice - order.Quantity*p.avgPrice[candle.Symbol]
187208
percentage := profitValue / (order.Quantity * p.avgPrice[candle.Symbol])
188209
log.Infof("PROFIT = %.4f %s (%.2f %%)", profitValue, quote, percentage*100)
189210

211+
p.volume[candle.Symbol] += orderVolume
190212
p.orders[i].UpdatedAt = candle.Time
191213
p.orders[i].Status = model.OrderStatusTypeFilled
192214
p.assets[asset].Lock = p.assets[asset].Lock - order.Quantity
@@ -324,6 +346,12 @@ func (p *PaperWallet) OrderMarket(side model.SideType, symbol string, size float
324346
p.assets[asset].Free = p.assets[asset].Free + size
325347
}
326348

349+
if _, ok := p.volume[symbol]; !ok {
350+
p.volume[symbol] = 0
351+
}
352+
353+
p.volume[symbol] += p.lastCandle[symbol].Close * size
354+
327355
order := model.Order{
328356
ExchangeID: p.ID(),
329357
CreatedAt: p.lastCandle[symbol].Time,

pkg/order/controller.go

+13-4
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ type summary struct {
2323
Symbol string
2424
Win []float64
2525
Lose []float64
26+
Volume float64
2627
}
2728

2829
func (s summary) Profit() float64 {
@@ -63,6 +64,7 @@ func (s summary) String() string {
6364
{"% Win", fmt.Sprintf("%.1f", float64(len(s.Win))/float64(len(s.Win)+len(s.Lose))*100)},
6465
{"Payoff", fmt.Sprintf("%.1f", s.Payoff()*100)},
6566
{"Profit", fmt.Sprintf("%.4f %s", s.Profit(), quote)},
67+
{"Volume", fmt.Sprintf("%.4f %s", s.Volume, quote)},
6668
}
6769
table.AppendBulk(data)
6870
table.SetColumnAlignment([]int{tablewriter.ALIGN_LEFT, tablewriter.ALIGN_RIGHT})
@@ -97,19 +99,21 @@ func NewController(ctx context.Context, exchange exchange.Exchange, storage *ent
9799
}
98100
}
99101

100-
func (c *Controller) calculateProfit(o *model.Order) (value, percent float64, err error) {
102+
func (c *Controller) calculateProfit(o *model.Order) (value, percent, volume float64, err error) {
101103
orders, err := c.storage.Order.Query().Where(
102104
order.UpdatedAtLTE(o.UpdatedAt),
103105
order.Status(string(model.OrderStatusTypeFilled)),
104106
order.Symbol(o.Symbol),
105107
order.IDNEQ(o.ID),
106108
).Order(ent.Asc(order.FieldUpdatedAt)).All(c.ctx)
107109
if err != nil {
108-
return 0, 0, err
110+
return 0, 0, 0, err
109111
}
110112

111113
quantity := 0.0
112114
avgPrice := 0.0
115+
tradeVolume := 0.0
116+
113117
for _, order := range orders {
114118
if order.Side == string(model.SideTypeBuy) {
115119
price := order.Price
@@ -121,6 +125,9 @@ func (c *Controller) calculateProfit(o *model.Order) (value, percent float64, er
121125
} else {
122126
quantity = math.Max(quantity-order.Quantity, 0)
123127
}
128+
129+
// We keep track of volume to have an indication of costs. (0.001%) binance.
130+
tradeVolume += order.Quantity * order.Price
124131
}
125132

126133
cost := o.Quantity * avgPrice
@@ -129,7 +136,7 @@ func (c *Controller) calculateProfit(o *model.Order) (value, percent float64, er
129136
price = *o.Stop
130137
}
131138
profitValue := o.Quantity*price - cost
132-
return profitValue, profitValue / cost, nil
139+
return profitValue, profitValue / cost, tradeVolume, nil
133140
}
134141

135142
func (c *Controller) notify(message string) {
@@ -140,7 +147,7 @@ func (c *Controller) notify(message string) {
140147
}
141148

142149
func (c *Controller) processTrade(order *model.Order) {
143-
profitValue, profit, err := c.calculateProfit(order)
150+
profitValue, profit, volume, err := c.calculateProfit(order)
144151
if err != nil {
145152
log.Errorf("order/controller storage: %s", err)
146153
}
@@ -155,6 +162,8 @@ func (c *Controller) processTrade(order *model.Order) {
155162
c.Results[order.Symbol].Lose = append(c.Results[order.Symbol].Lose, profitValue)
156163
}
157164

165+
c.Results[order.Symbol].Volume = volume
166+
158167
_, quote := exchange.SplitAssetQuote(order.Symbol)
159168
c.notify(fmt.Sprintf("[PROFIT] %f %s (%f %%)\n%s", profitValue, quote, profit*100, c.Results[order.Symbol].String()))
160169
}

pkg/order/controller_test.go

+10-5
Original file line numberDiff line numberDiff line change
@@ -33,19 +33,21 @@ func TestController_calculateProfit(t *testing.T) {
3333
sellOrder, err := controller.OrderMarket(model.SideTypeSell, "BTCUSDT", 1)
3434
require.NoError(t, err)
3535

36-
value, profit, err := controller.calculateProfit(&sellOrder)
36+
value, profit, volume, err := controller.calculateProfit(&sellOrder)
3737
require.NoError(t, err)
3838
assert.Equal(t, 1500.0, value)
3939
assert.Equal(t, 1.0, profit)
40+
assert.Equal(t, 3000.0, volume)
4041

4142
// sell remaining BTC, 50% of loss
4243
wallet.OnCandle(model.Candle{Symbol: "BTCUSDT", Close: 750})
4344
sellOrder, err = controller.OrderMarket(model.SideTypeSell, "BTCUSDT", 1)
4445
require.NoError(t, err)
45-
value, profit, err = controller.calculateProfit(&sellOrder)
46+
value, profit, volume, err = controller.calculateProfit(&sellOrder)
4647
require.NoError(t, err)
4748
assert.Equal(t, -750.0, value)
4849
assert.Equal(t, -0.5, profit)
50+
assert.Equal(t, 6000.0, volume)
4951
})
5052

5153
t.Run("limit order", func(t *testing.T) {
@@ -69,10 +71,11 @@ func TestController_calculateProfit(t *testing.T) {
6971
wallet.OnCandle(model.Candle{Time: time.Now(), Symbol: "BTCUSDT", High: 2000, Close: 2000})
7072
controller.updateOrders()
7173

72-
value, profit, err := controller.calculateProfit(&sellOrder)
74+
value, profit, volume, err := controller.calculateProfit(&sellOrder)
7375
require.NoError(t, err)
7476
assert.Equal(t, 1000.0, value)
7577
assert.Equal(t, 1.0, profit)
78+
assert.Equal(t, 7750.0, volume)
7679
})
7780

7881
t.Run("oco order limit maker", func(t *testing.T) {
@@ -96,10 +99,11 @@ func TestController_calculateProfit(t *testing.T) {
9699
wallet.OnCandle(model.Candle{Time: time.Now(), Symbol: "BTCUSDT", High: 2000, Close: 2000})
97100
controller.updateOrders()
98101

99-
value, profit, err := controller.calculateProfit(&sellOrder[0])
102+
value, profit, volume, err := controller.calculateProfit(&sellOrder[0])
100103
require.NoError(t, err)
101104
assert.Equal(t, 1000.0, value)
102105
assert.Equal(t, 1.0, profit)
106+
assert.Equal(t, 10750.0, volume)
103107
})
104108

105109
t.Run("oco stop sell", func(t *testing.T) {
@@ -129,9 +133,10 @@ func TestController_calculateProfit(t *testing.T) {
129133
wallet.OnCandle(model.Candle{Time: time.Now(), Symbol: "BTCUSDT", Close: 400, Low: 400})
130134
controller.updateOrders()
131135

132-
value, profit, err := controller.calculateProfit(&sellOrder[1])
136+
value, profit, volume, err := controller.calculateProfit(&sellOrder[1])
133137
require.NoError(t, err)
134138
assert.Equal(t, -500.0, value)
135139
assert.Equal(t, -0.5, profit)
140+
assert.Equal(t, 15750.0, volume)
136141
})
137142
}

0 commit comments

Comments
 (0)