Skip to content

Commit

Permalink
feat(backtesting): add volume indication to trading performance
Browse files Browse the repository at this point in the history
  • Loading branch information
spreeker authored Jul 26, 2021
1 parent 8a44016 commit 7a56ec9
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 19 deletions.
17 changes: 11 additions & 6 deletions ninjabot.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,12 +130,13 @@ func (n *NinjaBot) SubscribeOrder(subscriptions ...OrderSubscriber) {

func (n *NinjaBot) Summary() {
var (
total float64
wins int
loses int
total float64
wins int
loses int
volume float64
)
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"Pair", "Trades", "Win", "Loss", "% Win", "Payoff", "Profit"})
table.SetHeader([]string{"Pair", "Trades", "Win", "Loss", "% Win", "Payoff", "Profit", "Volume"})
table.SetFooterAlignment(tablewriter.ALIGN_RIGHT)
avgPayoff := 0.0
for _, summary := range n.orderController.Results {
Expand All @@ -147,20 +148,24 @@ func (n *NinjaBot) Summary() {
strconv.Itoa(len(summary.Lose)),
fmt.Sprintf("%.1f %%", float64(len(summary.Win))/float64(len(summary.Win)+len(summary.Lose))*100),
fmt.Sprintf("%.3f", summary.Payoff()),
fmt.Sprintf("%.4f", summary.Profit()),
fmt.Sprintf("%.2f", summary.Profit()),
fmt.Sprintf("%.2f", summary.Volume),
})
total += summary.Profit()
wins += len(summary.Win)
loses += len(summary.Lose)
volume += summary.Volume
}

table.SetFooter([]string{
"TOTAL",
strconv.Itoa(wins + loses),
strconv.Itoa(wins),
strconv.Itoa(loses),
fmt.Sprintf("%.1f %%", float64(wins)/float64(wins+loses)*100),
fmt.Sprintf("%.3f", avgPayoff/float64(wins+loses)),
fmt.Sprintf("%.4f", total),
fmt.Sprintf("%.2f", total),
fmt.Sprintf("%.2f", volume),
})
table.Render()
}
Expand Down
36 changes: 32 additions & 4 deletions pkg/exchange/paperwallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type PaperWallet struct {
orders []model.Order
assets map[string]*assetInfo
avgPrice map[string]float64
volume map[string]float64
lastCandle map[string]model.Candle
fistCandle map[string]model.Candle
}
Expand Down Expand Up @@ -66,6 +67,7 @@ func NewPaperWallet(ctx context.Context, baseCoin string, options ...PaperWallet
fistCandle: make(map[string]model.Candle),
lastCandle: make(map[string]model.Candle),
avgPrice: make(map[string]float64),
volume: make(map[string]float64),
}

for _, option := range options {
Expand All @@ -88,18 +90,29 @@ func (p *PaperWallet) Summary() {
var (
total float64
marketChange float64
volume float64
)

fmt.Println("--------------")
fmt.Println("WALLET SUMMARY")
fmt.Println("--------------")

for pair, price := range p.avgPrice {
asset, _ := SplitAssetQuote(pair)
quantity := p.assets[asset].Free + p.assets[asset].Lock
total += quantity * price
marketChange += (p.lastCandle[pair].Close - p.fistCandle[pair].Close) / p.fistCandle[pair].Close
fmt.Printf("%f %s\n", quantity, asset)
}

fmt.Println()
fmt.Println("TRADING VOLUME")
for symbol, vol := range p.volume {
volume += vol
fmt.Printf("%s = %.2f %s\n", symbol, vol, p.baseCoin)
}
fmt.Println()

avgMarketChange := marketChange / float64(len(p.avgPrice))
baseCoinValue := p.assets[p.baseCoin].Free + p.assets[p.baseCoin].Lock
profit := total + baseCoinValue - p.initialValue
Expand All @@ -109,6 +122,8 @@ func (p *PaperWallet) Summary() {
fmt.Println("FINAL PORTFOLIO = ", total+baseCoinValue, p.baseCoin)
fmt.Printf("GROSS PROFIT = %f %s (%.2f%%)\n", profit, p.baseCoin, profit/p.initialValue*100)
fmt.Printf("MARKET CHANGE = %.2f%%\n", avgMarketChange*100)
fmt.Printf("VOLUME = %.2f %s\n", volume, p.baseCoin)
fmt.Printf("COSTS (0.001*V) = %.2f %s (ESTIMATION) \n", volume*0.001, p.baseCoin)
fmt.Println("--------------")
}

Expand Down Expand Up @@ -136,20 +151,25 @@ func (p *PaperWallet) OnCandle(candle model.Candle) {
continue
}

if _, ok := p.volume[candle.Symbol]; !ok {
p.volume[candle.Symbol] = 0
}

asset, quote := SplitAssetQuote(order.Symbol)
if order.Side == model.SideTypeBuy && order.Price <= candle.Close {
if _, ok := p.assets[asset]; !ok {
p.assets[asset] = &assetInfo{}
}

actualQty := p.assets[asset].Free + p.assets[asset].Lock
orderValue := order.Price * order.Quantity
orderVolume := order.Price * order.Quantity
walletValue := p.avgPrice[candle.Symbol] * actualQty

p.volume[candle.Symbol] += orderVolume
p.orders[i].Status = model.OrderStatusTypeFilled
p.avgPrice[candle.Symbol] = (walletValue + orderValue) / (actualQty + order.Quantity)
p.avgPrice[candle.Symbol] = (walletValue + orderVolume) / (actualQty + order.Quantity)
p.assets[asset].Free = p.assets[asset].Free + order.Quantity
p.assets[quote].Lock = p.assets[quote].Lock - orderValue
p.assets[quote].Lock = p.assets[quote].Lock - orderVolume
}

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

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

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

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

if _, ok := p.volume[symbol]; !ok {
p.volume[symbol] = 0
}

p.volume[symbol] += p.lastCandle[symbol].Close * size

order := model.Order{
ExchangeID: p.ID(),
CreatedAt: p.lastCandle[symbol].Time,
Expand Down
17 changes: 13 additions & 4 deletions pkg/order/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type summary struct {
Symbol string
Win []float64
Lose []float64
Volume float64
}

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

func (c *Controller) calculateProfit(o *model.Order) (value, percent float64, err error) {
func (c *Controller) calculateProfit(o *model.Order) (value, percent, volume float64, err error) {
orders, err := c.storage.Order.Query().Where(
order.UpdatedAtLTE(o.UpdatedAt),
order.Status(string(model.OrderStatusTypeFilled)),
order.Symbol(o.Symbol),
order.IDNEQ(o.ID),
).Order(ent.Asc(order.FieldUpdatedAt)).All(c.ctx)
if err != nil {
return 0, 0, err
return 0, 0, 0, err
}

quantity := 0.0
avgPrice := 0.0
tradeVolume := 0.0

for _, order := range orders {
if order.Side == string(model.SideTypeBuy) {
price := order.Price
Expand All @@ -121,6 +125,9 @@ func (c *Controller) calculateProfit(o *model.Order) (value, percent float64, er
} else {
quantity = math.Max(quantity-order.Quantity, 0)
}

// We keep track of volume to have an indication of costs. (0.001%) binance.
tradeVolume += order.Quantity * order.Price
}

cost := o.Quantity * avgPrice
Expand All @@ -129,7 +136,7 @@ func (c *Controller) calculateProfit(o *model.Order) (value, percent float64, er
price = *o.Stop
}
profitValue := o.Quantity*price - cost
return profitValue, profitValue / cost, nil
return profitValue, profitValue / cost, tradeVolume, nil
}

func (c *Controller) notify(message string) {
Expand All @@ -140,7 +147,7 @@ func (c *Controller) notify(message string) {
}

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

c.Results[order.Symbol].Volume = volume

_, quote := exchange.SplitAssetQuote(order.Symbol)
c.notify(fmt.Sprintf("[PROFIT] %f %s (%f %%)\n%s", profitValue, quote, profit*100, c.Results[order.Symbol].String()))
}
Expand Down
15 changes: 10 additions & 5 deletions pkg/order/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,21 @@ func TestController_calculateProfit(t *testing.T) {
sellOrder, err := controller.OrderMarket(model.SideTypeSell, "BTCUSDT", 1)
require.NoError(t, err)

value, profit, err := controller.calculateProfit(&sellOrder)
value, profit, volume, err := controller.calculateProfit(&sellOrder)
require.NoError(t, err)
assert.Equal(t, 1500.0, value)
assert.Equal(t, 1.0, profit)
assert.Equal(t, 3000.0, volume)

// sell remaining BTC, 50% of loss
wallet.OnCandle(model.Candle{Symbol: "BTCUSDT", Close: 750})
sellOrder, err = controller.OrderMarket(model.SideTypeSell, "BTCUSDT", 1)
require.NoError(t, err)
value, profit, err = controller.calculateProfit(&sellOrder)
value, profit, volume, err = controller.calculateProfit(&sellOrder)
require.NoError(t, err)
assert.Equal(t, -750.0, value)
assert.Equal(t, -0.5, profit)
assert.Equal(t, 6000.0, volume)
})

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

value, profit, err := controller.calculateProfit(&sellOrder)
value, profit, volume, err := controller.calculateProfit(&sellOrder)
require.NoError(t, err)
assert.Equal(t, 1000.0, value)
assert.Equal(t, 1.0, profit)
assert.Equal(t, 7750.0, volume)
})

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

value, profit, err := controller.calculateProfit(&sellOrder[0])
value, profit, volume, err := controller.calculateProfit(&sellOrder[0])
require.NoError(t, err)
assert.Equal(t, 1000.0, value)
assert.Equal(t, 1.0, profit)
assert.Equal(t, 10750.0, volume)
})

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

value, profit, err := controller.calculateProfit(&sellOrder[1])
value, profit, volume, err := controller.calculateProfit(&sellOrder[1])
require.NoError(t, err)
assert.Equal(t, -500.0, value)
assert.Equal(t, -0.5, profit)
assert.Equal(t, 15750.0, volume)
})
}

0 comments on commit 7a56ec9

Please sign in to comment.