@@ -9,17 +9,19 @@ import (
9
9
"github.com/c9s/bbgo/pkg/fixedpoint"
10
10
"github.com/c9s/bbgo/pkg/indicator"
11
11
"github.com/c9s/bbgo/pkg/types"
12
+ "github.com/c9s/bbgo/pkg/util"
13
+ "github.com/sirupsen/logrus"
14
+ "math"
12
15
"os"
13
16
"sync"
14
-
15
- "github.com/sirupsen/logrus"
17
+ "time"
16
18
)
17
19
18
20
const ID = "irr"
19
21
20
22
var one = fixedpoint .One
21
23
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
23
25
24
26
var log = logrus .WithField ("strategy" , ID )
25
27
@@ -47,7 +49,16 @@ type Strategy struct {
47
49
orderExecutor * bbgo.GeneralOrderExecutor
48
50
49
51
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 {}
51
62
52
63
// StrategyController
53
64
bbgo.StrategyController
@@ -194,10 +205,10 @@ func (r *AccumulatedProfitReport) Output(symbol string) {
194
205
}
195
206
196
207
func (s * Strategy ) Subscribe (session * bbgo.ExchangeSession ) {
197
- session .Subscribe (types .KLineChannel , s .Symbol , types.SubscribeOptions {Interval : s .Interval })
198
-
199
208
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 })
201
212
}
202
213
203
214
s .ExitMethods .SetAndSubscribe (session , s )
@@ -273,9 +284,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
273
284
274
285
s .AccumulatedProfitReport .RecordProfit (profit .Profit )
275
286
})
276
- // s.orderExecutor.TradeCollector().OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) {
277
- // s.AccumulatedProfitReport.RecordTrade(trade.Fee)
278
- // })
279
287
session .MarketDataStream .OnKLineClosed (types .KLineWith (s .Symbol , types .Interval1d , func (kline types.KLine ) {
280
288
s .AccumulatedProfitReport .DailyUpdate (s .TradeStats )
281
289
}))
@@ -319,63 +327,59 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
319
327
}
320
328
})
321
329
330
+ s .InitDrawCommands (& profitSlice , & cumProfitSlice )
331
+
322
332
s .orderExecutor .TradeCollector ().OnPositionUpdate (func (position * types.Position ) {
323
333
bbgo .Sync (ctx , s )
324
334
})
325
335
s .orderExecutor .Bind ()
326
336
s .activeOrders = bbgo .NewActiveOrderBook (s .Symbol )
327
337
328
- for _ , method := range s .ExitMethods {
329
- method .Bind (session , s .orderExecutor )
330
- }
331
-
338
+ //back-test only, because 1s delayed a lot
332
339
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 :])
337
344
}
338
345
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 {
373
349
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
+ }
375
380
376
381
bbgo .OnShutdown (ctx , func (ctx context.Context , wg * sync.WaitGroup ) {
377
382
defer wg .Done ()
378
-
379
383
// Output accumulated profit report
380
384
if bbgo .IsBackTesting {
381
385
defer s .AccumulatedProfitReport .Output (s .Symbol )
@@ -385,8 +389,9 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
385
389
log .WithError (err ).Errorf ("cannot draw graph" )
386
390
}
387
391
}
392
+ } else {
393
+ close (s .stopC )
388
394
}
389
-
390
395
_ , _ = fmt .Fprintln (os .Stderr , s .TradeStats .String ())
391
396
_ = s .orderExecutor .GracefulCancel (ctx )
392
397
})
@@ -398,3 +403,75 @@ func (s *Strategy) CalcAssetValue(price fixedpoint.Value) fixedpoint.Value {
398
403
balances := s .session .GetAccount ().Balances ()
399
404
return balances [s .Market .BaseCurrency ].Total ().Mul (price ).Add (balances [s .Market .QuoteCurrency ].Total ())
400
405
}
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