Skip to content

Commit 1c7b40a

Browse files
authored
Merge pull request #922 from c9s/fix/trade-stats-for-live
fix: types/tradeStats: use last order id to identity consecutive win and loss
2 parents 3e07f1f + 3aebb58 commit 1c7b40a

File tree

2 files changed

+125
-58
lines changed

2 files changed

+125
-58
lines changed

pkg/types/trade_stats.go

+80-58
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ type TradeStats struct {
193193
// MaximumConsecutiveLoss - ($) the longest series of losing trades and their total loss;
194194
MaximumConsecutiveLoss fixedpoint.Value `json:"maximumConsecutiveLoss" yaml:"maximumConsecutiveLoss"`
195195

196+
lastOrderID uint64
196197
consecutiveSide int
197198
consecutiveCounter int
198199
consecutiveAmount fixedpoint.Value
@@ -252,7 +253,7 @@ func (s *TradeStats) Add(profit *Profit) {
252253
s.orderProfits[profit.OrderID] = append(s.orderProfits[profit.OrderID], profit)
253254
}
254255

255-
s.add(profit.Profit)
256+
s.add(profit)
256257

257258
for _, v := range s.IntervalProfits {
258259
v.Update(profit)
@@ -275,8 +276,13 @@ func grossProfitReducer(prev, curr fixedpoint.Value) fixedpoint.Value {
275276
return prev
276277
}
277278

278-
// update the trade stats fields from the orderProfits
279-
func (s *TradeStats) update() {
279+
// Recalculate the trade stats fields from the orderProfits
280+
// this is for live-trading, one order may have many trades, and we need to merge them.
281+
func (s *TradeStats) Recalculate() {
282+
if len(s.orderProfits) == 0 {
283+
return
284+
}
285+
280286
var profitsByOrder []fixedpoint.Value
281287
var netProfitsByOrder []fixedpoint.Value
282288
for _, profits := range s.orderProfits {
@@ -291,92 +297,107 @@ func (s *TradeStats) update() {
291297
netProfitsByOrder = append(netProfitsByOrder, sumNetProfit)
292298
}
293299

294-
s.NumOfProfitTrade = fixedpoint.Count(profitsByOrder, func(a fixedpoint.Value) bool {
295-
return a.Sign() > 0
296-
})
297-
s.NumOfLossTrade = fixedpoint.Count(profitsByOrder, func(a fixedpoint.Value) bool {
298-
return a.Sign() < 0
299-
})
300+
s.NumOfProfitTrade = fixedpoint.Count(profitsByOrder, fixedpoint.PositiveTester)
301+
s.NumOfLossTrade = fixedpoint.Count(profitsByOrder, fixedpoint.NegativeTester)
300302
s.TotalNetProfit = fixedpoint.Reduce(profitsByOrder, fixedpoint.SumReducer)
301303
s.GrossProfit = fixedpoint.Reduce(profitsByOrder, grossProfitReducer)
302304
s.GrossLoss = fixedpoint.Reduce(profitsByOrder, grossLossReducer)
303305

304306
sort.Sort(fixedpoint.Descending(profitsByOrder))
305307
sort.Sort(fixedpoint.Descending(netProfitsByOrder))
306308

307-
s.Losses = fixedpoint.Filter(profitsByOrder, fixedpoint.NegativeTester)
308309
s.Profits = fixedpoint.Filter(profitsByOrder, fixedpoint.PositiveTester)
310+
s.Losses = fixedpoint.Filter(profitsByOrder, fixedpoint.NegativeTester)
309311
s.LargestProfitTrade = profitsByOrder[0]
310312
s.LargestLossTrade = profitsByOrder[len(profitsByOrder)-1]
311313
if s.LargestLossTrade.Sign() > 0 {
312314
s.LargestLossTrade = fixedpoint.Zero
313315
}
316+
317+
s.ProfitFactor = s.GrossProfit.Div(s.GrossLoss.Abs())
318+
if len(s.Profits) > 0 {
319+
s.AverageProfitTrade = fixedpoint.Avg(s.Profits)
320+
}
321+
if len(s.Losses) > 0 {
322+
s.AverageLossTrade = fixedpoint.Avg(s.Losses)
323+
}
324+
325+
s.updateWinningRatio()
314326
}
315327

316-
func (s *TradeStats) add(pnl fixedpoint.Value) {
317-
if pnl.Sign() > 0 {
318-
s.NumOfProfitTrade++
319-
s.Profits = append(s.Profits, pnl)
320-
s.GrossProfit = s.GrossProfit.Add(pnl)
321-
s.LargestProfitTrade = fixedpoint.Max(s.LargestProfitTrade, pnl)
322-
323-
// consecutive same side (made profit last time)
324-
if s.consecutiveSide == 0 || s.consecutiveSide == 1 {
325-
s.consecutiveSide = 1
326-
s.consecutiveCounter++
327-
s.consecutiveAmount = s.consecutiveAmount.Add(pnl)
328-
} else { // was loss, now profit, store the last loss and the loss amount
329-
s.MaximumConsecutiveLosses = int(math.Max(float64(s.MaximumConsecutiveLosses), float64(s.consecutiveCounter)))
330-
s.MaximumConsecutiveLoss = fixedpoint.Min(s.MaximumConsecutiveLoss, s.consecutiveAmount)
331-
332-
s.consecutiveSide = 1
333-
s.consecutiveCounter = 0
334-
s.consecutiveAmount = pnl
328+
func (s *TradeStats) add(profit *Profit) {
329+
pnl := profit.Profit
330+
331+
// order id changed
332+
if s.lastOrderID != profit.OrderID {
333+
if pnl.Sign() > 0 {
334+
s.NumOfProfitTrade++
335+
s.GrossProfit = s.GrossProfit.Add(pnl)
336+
337+
if s.consecutiveSide == 0 {
338+
s.consecutiveSide = 1
339+
s.consecutiveCounter = 1
340+
s.consecutiveAmount = pnl
341+
} else if s.consecutiveSide == 1 {
342+
s.consecutiveCounter++
343+
s.consecutiveAmount = s.consecutiveAmount.Add(pnl)
344+
s.MaximumConsecutiveWins = int(math.Max(float64(s.MaximumConsecutiveWins), float64(s.consecutiveCounter)))
345+
s.MaximumConsecutiveProfit = fixedpoint.Max(s.MaximumConsecutiveProfit, s.consecutiveAmount)
346+
} else {
347+
s.MaximumConsecutiveLosses = int(math.Max(float64(s.MaximumConsecutiveLosses), float64(s.consecutiveCounter)))
348+
s.MaximumConsecutiveLoss = fixedpoint.Min(s.MaximumConsecutiveLoss, s.consecutiveAmount)
349+
s.consecutiveSide = 1
350+
s.consecutiveCounter = 1
351+
s.consecutiveAmount = pnl
352+
}
353+
} else {
354+
s.NumOfLossTrade++
355+
s.GrossLoss = s.GrossLoss.Add(pnl)
356+
357+
if s.consecutiveSide == 0 {
358+
s.consecutiveSide = -1
359+
s.consecutiveCounter = 1
360+
s.consecutiveAmount = pnl
361+
} else if s.consecutiveSide == -1 {
362+
s.consecutiveCounter++
363+
s.consecutiveAmount = s.consecutiveAmount.Add(pnl)
364+
s.MaximumConsecutiveLosses = int(math.Max(float64(s.MaximumConsecutiveLosses), float64(s.consecutiveCounter)))
365+
s.MaximumConsecutiveLoss = fixedpoint.Min(s.MaximumConsecutiveLoss, s.consecutiveAmount)
366+
} else { // was profit, now loss, store the last win and profit
367+
s.MaximumConsecutiveWins = int(math.Max(float64(s.MaximumConsecutiveWins), float64(s.consecutiveCounter)))
368+
s.MaximumConsecutiveProfit = fixedpoint.Max(s.MaximumConsecutiveProfit, s.consecutiveAmount)
369+
s.consecutiveSide = -1
370+
s.consecutiveCounter = 1
371+
s.consecutiveAmount = pnl
372+
}
335373
}
336-
337374
} else {
338-
s.NumOfLossTrade++
339-
s.Losses = append(s.Losses, pnl)
340-
s.GrossLoss = s.GrossLoss.Add(pnl)
341-
s.LargestLossTrade = fixedpoint.Min(s.LargestLossTrade, pnl)
342-
343-
// consecutive same side (made loss last time)
344-
if s.consecutiveSide == 0 || s.consecutiveSide == -1 {
345-
s.consecutiveSide = -1
346-
s.consecutiveCounter++
347-
s.consecutiveAmount = s.consecutiveAmount.Add(pnl)
348-
} else { // was profit, now loss, store the last win and profit
349-
s.MaximumConsecutiveWins = int(math.Max(float64(s.MaximumConsecutiveWins), float64(s.consecutiveCounter)))
350-
s.MaximumConsecutiveProfit = fixedpoint.Max(s.MaximumConsecutiveProfit, s.consecutiveAmount)
351-
352-
s.consecutiveSide = -1
353-
s.consecutiveCounter = 0
354-
s.consecutiveAmount = pnl
355-
}
356-
375+
s.consecutiveAmount = s.consecutiveAmount.Add(pnl)
357376
}
377+
378+
s.lastOrderID = profit.OrderID
358379
s.TotalNetProfit = s.TotalNetProfit.Add(pnl)
380+
s.ProfitFactor = s.GrossProfit.Div(s.GrossLoss.Abs())
381+
382+
s.updateWinningRatio()
383+
}
359384

385+
func (s *TradeStats) updateWinningRatio() {
360386
// The win/loss ratio is your wins divided by your losses.
361387
// In the example, suppose for the sake of simplicity that 60 trades were winners, and 40 were losers.
362388
// Your win/loss ratio would be 60/40 = 1.5. That would mean that you are winning 50% more often than you are losing.
363-
if s.NumOfLossTrade == 0 && s.NumOfProfitTrade > 0 {
389+
if s.NumOfLossTrade == 0 && s.NumOfProfitTrade == 0 {
390+
s.WinningRatio = fixedpoint.Zero
391+
} else if s.NumOfLossTrade == 0 && s.NumOfProfitTrade > 0 {
364392
s.WinningRatio = fixedpoint.One
365393
} else {
366394
s.WinningRatio = fixedpoint.NewFromFloat(float64(s.NumOfProfitTrade) / float64(s.NumOfLossTrade))
367395
}
368-
369-
s.ProfitFactor = s.GrossProfit.Div(s.GrossLoss.Abs())
370-
if len(s.Profits) > 0 {
371-
s.AverageProfitTrade = fixedpoint.Avg(s.Profits)
372-
}
373-
if len(s.Losses) > 0 {
374-
s.AverageLossTrade = fixedpoint.Avg(s.Losses)
375-
}
376396
}
377397

378398
// Output TradeStats without Profits and Losses
379399
func (s *TradeStats) BriefString() string {
400+
s.Recalculate()
380401
out, _ := yaml.Marshal(&TradeStats{
381402
Symbol: s.Symbol,
382403
WinningRatio: s.WinningRatio,
@@ -400,6 +421,7 @@ func (s *TradeStats) BriefString() string {
400421
}
401422

402423
func (s *TradeStats) String() string {
424+
s.Recalculate()
403425
out, _ := yaml.Marshal(s)
404426
return string(out)
405427
}

pkg/types/trade_stats_test.go

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package types
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
8+
"github.com/c9s/bbgo/pkg/fixedpoint"
9+
)
10+
11+
func number(v float64) fixedpoint.Value {
12+
return fixedpoint.NewFromFloat(v)
13+
}
14+
15+
func TestTradeStats_consecutiveCounterAndAmount(t *testing.T) {
16+
stats := NewTradeStats("BTCUSDT")
17+
stats.add(&Profit{OrderID: 1, Profit: number(20.0)})
18+
stats.add(&Profit{OrderID: 1, Profit: number(30.0)})
19+
20+
assert.Equal(t, 1, stats.consecutiveSide)
21+
assert.Equal(t, 1, stats.consecutiveCounter)
22+
assert.Equal(t, "50", stats.consecutiveAmount.String())
23+
24+
stats.add(&Profit{OrderID: 2, Profit: number(50.0)})
25+
stats.add(&Profit{OrderID: 2, Profit: number(50.0)})
26+
assert.Equal(t, 1, stats.consecutiveSide)
27+
assert.Equal(t, 2, stats.consecutiveCounter)
28+
assert.Equal(t, "150", stats.consecutiveAmount.String())
29+
assert.Equal(t, 2, stats.MaximumConsecutiveWins)
30+
31+
stats.add(&Profit{OrderID: 3, Profit: number(-50.0)})
32+
stats.add(&Profit{OrderID: 3, Profit: number(-50.0)})
33+
assert.Equal(t, -1, stats.consecutiveSide)
34+
assert.Equal(t, 1, stats.consecutiveCounter)
35+
assert.Equal(t, "-100", stats.consecutiveAmount.String())
36+
37+
assert.Equal(t, "150", stats.MaximumConsecutiveProfit.String())
38+
assert.Equal(t, "0", stats.MaximumConsecutiveLoss.String())
39+
40+
stats.add(&Profit{OrderID: 4, Profit: number(-100.0)})
41+
assert.Equal(t, -1, stats.consecutiveSide)
42+
assert.Equal(t, 2, stats.consecutiveCounter)
43+
assert.Equal(t, "-200", stats.MaximumConsecutiveLoss.String())
44+
assert.Equal(t, 2, stats.MaximumConsecutiveLosses)
45+
}

0 commit comments

Comments
 (0)