Skip to content

Commit b0bf1ac

Browse files
committed
strategy:irr: add backtest/realtime ability
1 parent 7204e25 commit b0bf1ac

File tree

2 files changed

+138
-59
lines changed

2 files changed

+138
-59
lines changed

config/irr.yaml

+6-4
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ exchangeStrategies:
1515
- on: binance
1616
irr:
1717
symbol: BTCBUSD
18-
interval: 1m
18+
interval: 1s
1919
window: 120
20-
amount: 5_000.0
20+
amount: 500.0
21+
humpThreshold: 0.000025
2122
# Draw pnl
2223
drawGraph: true
2324
graphPNLPath: "./pnl.png"
@@ -26,8 +27,9 @@ exchangeStrategies:
2627
backtest:
2728
sessions:
2829
- binance
29-
startTime: "2022-09-01"
30-
endTime: "2022-10-04"
30+
startTime: "2022-10-09"
31+
endTime: "2022-10-11"
32+
# syncSecKLines: true
3133
symbols:
3234
- BTCBUSD
3335
accounts:

pkg/strategy/irr/strategy.go

+132-55
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,19 @@ import (
99
"github.com/c9s/bbgo/pkg/fixedpoint"
1010
"github.com/c9s/bbgo/pkg/indicator"
1111
"github.com/c9s/bbgo/pkg/types"
12+
"github.com/c9s/bbgo/pkg/util"
13+
"github.com/sirupsen/logrus"
14+
"math"
1215
"os"
1316
"sync"
14-
15-
"github.com/sirupsen/logrus"
17+
"time"
1618
)
1719

1820
const ID = "irr"
1921

2022
var one = fixedpoint.One
2123
var zero = fixedpoint.Zero
22-
var Fee = 0.0008 // taker fee % * 2, for upper bound
24+
var Fee = 0.000 // taker fee % * 2, for upper bound
2325

2426
var log = logrus.WithField("strategy", ID)
2527

@@ -47,7 +49,16 @@ type Strategy struct {
4749
orderExecutor *bbgo.GeneralOrderExecutor
4850

4951
bbgo.QuantityOrAmount
50-
nrr *NRR
52+
53+
HumpThreshold float64 `json:"humpThreshold"`
54+
55+
lastTwoPrices *types.Queue
56+
// for back-test
57+
Nrr *NRR
58+
// for realtime book ticker
59+
lastPrice fixedpoint.Value
60+
rtNrr *types.Queue
61+
stopC chan struct{}
5162

5263
// StrategyController
5364
bbgo.StrategyController
@@ -194,10 +205,10 @@ func (r *AccumulatedProfitReport) Output(symbol string) {
194205
}
195206

196207
func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
197-
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval})
198-
199208
if !bbgo.IsBackTesting {
200-
session.Subscribe(types.MarketTradeChannel, s.Symbol, types.SubscribeOptions{})
209+
session.Subscribe(types.BookTickerChannel, s.Symbol, types.SubscribeOptions{})
210+
} else {
211+
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval})
201212
}
202213

203214
s.ExitMethods.SetAndSubscribe(session, s)
@@ -273,9 +284,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
273284

274285
s.AccumulatedProfitReport.RecordProfit(profit.Profit)
275286
})
276-
// s.orderExecutor.TradeCollector().OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) {
277-
// s.AccumulatedProfitReport.RecordTrade(trade.Fee)
278-
// })
279287
session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1d, func(kline types.KLine) {
280288
s.AccumulatedProfitReport.DailyUpdate(s.TradeStats)
281289
}))
@@ -319,63 +327,59 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
319327
}
320328
})
321329

330+
s.InitDrawCommands(&profitSlice, &cumProfitSlice)
331+
322332
s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) {
323333
bbgo.Sync(ctx, s)
324334
})
325335
s.orderExecutor.Bind()
326336
s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol)
327337

328-
for _, method := range s.ExitMethods {
329-
method.Bind(session, s.orderExecutor)
330-
}
331-
338+
//back-test only, because 1s delayed a lot
332339
kLineStore, _ := s.session.MarketDataStore(s.Symbol)
333-
s.nrr = &NRR{IntervalWindow: types.IntervalWindow{Window: 2, Interval: s.Interval}, RankingWindow: s.Window}
334-
s.nrr.BindK(s.session.MarketDataStream, s.Symbol, s.Interval)
335-
if klines, ok := kLineStore.KLinesOfInterval(s.nrr.Interval); ok {
336-
s.nrr.LoadK((*klines)[0:])
340+
s.Nrr = &NRR{IntervalWindow: types.IntervalWindow{Window: 2, Interval: s.Interval}, RankingWindow: s.Window}
341+
s.Nrr.BindK(s.session.MarketDataStream, s.Symbol, s.Interval)
342+
if klines, ok := kLineStore.KLinesOfInterval(s.Nrr.Interval); ok {
343+
s.Nrr.LoadK((*klines)[0:])
337344
}
338345

339-
// startTime := s.Environment.StartTime()
340-
// s.TradeStats.SetIntervalProfitCollector(types.NewIntervalProfitCollector(types.Interval1h, startTime))
341-
342-
s.session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) {
343-
344-
// ts_rank(): transformed to [0~1] which divided equally
345-
// queued first signal as its initial process
346-
// important: delayed signal in order to submit order at current kline close (a.k.a. next open while in production)
347-
// instead of right in current kline open
348-
349-
// alpha-weighted assets (inventory and capital)
350-
targetBase := s.QuantityOrAmount.CalculateQuantity(kline.Close).Mul(fixedpoint.NewFromFloat(s.nrr.RankedValues.Index(1)))
351-
diffQty := targetBase.Sub(s.Position.Base)
352-
353-
log.Infof("decision alpah: %f, ranked negative return: %f, current position: %f, target position diff: %f", s.nrr.RankedValues.Index(1), s.nrr.RankedValues.Last(), s.Position.Base.Float64(), diffQty.Float64())
354-
355-
// use kline direction to prevent reversing position too soon
356-
if diffQty.Sign() > 0 { // && kline.Direction() >= 0
357-
_, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
358-
Symbol: s.Symbol,
359-
Side: types.SideTypeBuy,
360-
Quantity: diffQty.Abs(),
361-
Type: types.OrderTypeMarket,
362-
Tag: "irr buy more",
363-
})
364-
} else if diffQty.Sign() < 0 { // && kline.Direction() <= 0
365-
_, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
366-
Symbol: s.Symbol,
367-
Side: types.SideTypeSell,
368-
Quantity: diffQty.Abs(),
369-
Type: types.OrderTypeMarket,
370-
Tag: "irr sell more",
371-
})
372-
}
346+
s.lastTwoPrices = types.NewQueue(2) // current price & previous price
347+
s.rtNrr = types.NewQueue(s.Window)
348+
if !bbgo.IsBackTesting {
373349

374-
}))
350+
s.stopC = make(chan struct{})
351+
352+
go func() {
353+
secondTicker := time.NewTicker(util.MillisecondsJitter(s.Interval.Duration(), 200))
354+
defer secondTicker.Stop()
355+
356+
for {
357+
select {
358+
case <-secondTicker.C:
359+
s.rebalancePosition(true)
360+
case <-s.stopC:
361+
log.Warnf("%s goroutine stopped, due to the stop signal", s.Symbol)
362+
return
363+
364+
case <-ctx.Done():
365+
log.Warnf("%s goroutine stopped, due to the cancelled context", s.Symbol)
366+
return
367+
}
368+
}
369+
}()
370+
371+
s.session.MarketDataStream.OnBookTickerUpdate(func(bt types.BookTicker) {
372+
s.lastPrice = bt.Buy.Add(bt.Sell).Div(fixedpoint.Two)
373+
})
374+
} else {
375+
s.session.MarketDataStream.OnKLineClosed(func(kline types.KLine) {
376+
s.lastPrice = kline.Close
377+
s.rebalancePosition(false)
378+
})
379+
}
375380

376381
bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) {
377382
defer wg.Done()
378-
379383
// Output accumulated profit report
380384
if bbgo.IsBackTesting {
381385
defer s.AccumulatedProfitReport.Output(s.Symbol)
@@ -385,8 +389,9 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
385389
log.WithError(err).Errorf("cannot draw graph")
386390
}
387391
}
392+
} else {
393+
close(s.stopC)
388394
}
389-
390395
_, _ = fmt.Fprintln(os.Stderr, s.TradeStats.String())
391396
_ = s.orderExecutor.GracefulCancel(ctx)
392397
})
@@ -398,3 +403,75 @@ func (s *Strategy) CalcAssetValue(price fixedpoint.Value) fixedpoint.Value {
398403
balances := s.session.GetAccount().Balances()
399404
return balances[s.Market.BaseCurrency].Total().Mul(price).Add(balances[s.Market.QuoteCurrency].Total())
400405
}
406+
407+
func (s *Strategy) rebalancePosition(rt bool) {
408+
409+
s.lastTwoPrices.Update(s.lastPrice.Float64())
410+
if s.lastTwoPrices.Length() >= 2 {
411+
log.Infof("Interval Closed Price: %f", s.lastTwoPrices.Last())
412+
// main idea: negative return
413+
nr := -1 * (s.lastTwoPrices.Last()/s.lastTwoPrices.Index(1) - 1)
414+
if rt {
415+
// hump operation to reduce noise
416+
// update nirr indicator when above threshold
417+
if math.Abs(s.rtNrr.Last()-nr) < s.HumpThreshold {
418+
s.rtNrr.Update(s.rtNrr.Last())
419+
} else {
420+
s.rtNrr.Update(nr)
421+
return
422+
}
423+
} else {
424+
if math.Abs(s.Nrr.Last()-s.Nrr.Index(1)) < s.HumpThreshold {
425+
return
426+
}
427+
}
428+
429+
// when have enough Nrr to do ts_rank()
430+
if (s.rtNrr.Length() >= s.Window && rt) || (s.Nrr.Length() >= s.Window && !rt) {
431+
432+
// alpha-weighted assets (inventory and capital)
433+
position := s.orderExecutor.Position()
434+
// weight: 0~1, since it's a long only strategy
435+
weight := fixedpoint.NewFromFloat(s.rtNrr.Rank(s.Window).Last() / float64(s.Window))
436+
if !rt {
437+
weight = fixedpoint.NewFromFloat(s.Nrr.Rank(s.Window).Last() / float64(s.Window))
438+
}
439+
targetBase := s.QuantityOrAmount.CalculateQuantity(fixedpoint.NewFromFloat(s.lastTwoPrices.Mean(2))).Mul(weight)
440+
441+
// to buy/sell quantity
442+
diffQty := targetBase.Sub(position.Base)
443+
log.Infof("Alpha: %f/1.0, Target Position Diff: %f", weight.Float64(), diffQty.Float64())
444+
445+
// ignore small changes
446+
if diffQty.Abs().Float64() < 0.001 {
447+
return
448+
}
449+
// re-balance position
450+
if diffQty.Sign() > 0 {
451+
_, err := s.orderExecutor.SubmitOrders(context.Background(), types.SubmitOrder{
452+
Symbol: s.Symbol,
453+
Side: types.SideTypeBuy,
454+
Quantity: diffQty.Abs(),
455+
Type: types.OrderTypeMarket,
456+
//Price: bt.Sell,
457+
Tag: "irr re-balance: buy",
458+
})
459+
if err != nil {
460+
log.WithError(err)
461+
}
462+
} else if diffQty.Sign() < 0 {
463+
_, err := s.orderExecutor.SubmitOrders(context.Background(), types.SubmitOrder{
464+
Symbol: s.Symbol,
465+
Side: types.SideTypeSell,
466+
Quantity: diffQty.Abs(),
467+
Type: types.OrderTypeMarket,
468+
//Price: bt.Buy,
469+
Tag: "irr re-balance: sell",
470+
})
471+
if err != nil {
472+
log.WithError(err)
473+
}
474+
}
475+
}
476+
}
477+
}

0 commit comments

Comments
 (0)