2022-09-20 02:32:41 +00:00
package irr
2022-09-19 06:45:13 +00:00
import (
"bytes"
"context"
"errors"
"fmt"
"os"
"sync"
"time"
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/datatype/floats"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/indicator"
"github.com/c9s/bbgo/pkg/interact"
"github.com/c9s/bbgo/pkg/types"
"github.com/sirupsen/logrus"
"github.com/wcharczuk/go-chart/v2"
)
2022-09-28 09:07:13 +00:00
const ID = "irr"
2022-09-19 06:45:13 +00:00
var one = fixedpoint . One
var zero = fixedpoint . Zero
var Fee = 0.0008 // taker fee % * 2, for upper bound
var log = logrus . WithField ( "strategy" , ID )
func init ( ) {
bbgo . RegisterStrategy ( ID , & Strategy { } )
}
type Strategy struct {
Environment * bbgo . Environment
Symbol string ` json:"symbol" `
Market types . Market
types . IntervalWindow
// persistence fields
Position * types . Position ` persistence:"position" `
ProfitStats * types . ProfitStats ` persistence:"profit_stats" `
TradeStats * types . TradeStats ` persistence:"trade_stats" `
activeOrders * bbgo . ActiveOrderBook
ExitMethods bbgo . ExitMethodSet ` json:"exits" `
session * bbgo . ExchangeSession
orderExecutor * bbgo . GeneralOrderExecutor
bbgo . QuantityOrAmount
nrr * NRR
// StrategyController
bbgo . StrategyController
// plotting
bbgo . SourceSelector
alpha * NRR
priceLines * types . Queue
trendLine types . UpdatableSeriesExtend
ma types . UpdatableSeriesExtend
stdevHigh * indicator . StdDev
stdevLow * indicator . StdDev
atr * indicator . ATR
midPrice fixedpoint . Value
lock sync . RWMutex ` ignore:"true" `
positionLock sync . RWMutex ` ignore:"true" `
startTime time . Time
minutesCounter int
orderPendingCounter map [ uint64 ] int
frameKLine * types . KLine
kline1m * types . KLine
beta float64
StopLoss fixedpoint . Value ` json:"stoploss" `
CanvasPath string ` json:"canvasPath" `
PredictOffset int ` json:"predictOffset" `
HighLowVarianceMultiplier float64 ` json:"hlVarianceMultiplier" `
NoTrailingStopLoss bool ` json:"noTrailingStopLoss" `
TrailingStopLossType string ` json:"trailingStopLossType" ` // trailing stop sources. Possible options are `kline` for 1m kline and `realtime` from order updates
HLRangeWindow int ` json:"hlRangeWindow" `
Window1m int ` json:"window1m" `
FisherTransformWindow1m int ` json:"fisherTransformWindow1m" `
SmootherWindow1m int ` json:"smootherWindow1m" `
SmootherWindow int ` json:"smootherWindow" `
FisherTransformWindow int ` json:"fisherTransformWindow" `
ATRWindow int ` json:"atrWindow" `
PendingMinutes int ` json:"pendingMinutes" ` // if order not be traded for pendingMinutes of time, cancel it.
NoRebalance bool ` json:"noRebalance" ` // disable rebalance
TrendWindow int ` json:"trendWindow" ` // trendLine is used for rebalancing the position. When trendLine goes up, hold base, otherwise hold quote
RebalanceFilter float64 ` json:"rebalanceFilter" ` // beta filter on the Linear Regression of trendLine
TrailingCallbackRate [ ] float64 ` json:"trailingCallbackRate" `
TrailingActivationRatio [ ] float64 ` json:"trailingActivationRatio" `
// This is not related to trade but for statistics graph generation
// Will deduct fee in percentage from every trade
GraphPNLDeductFee bool ` json:"graphPNLDeductFee" `
GraphPNLPath string ` json:"graphPNLPath" `
GraphCumPNLPath string ` json:"graphCumPNLPath" `
// Whether to generate graph when shutdown
GenerateGraph bool ` json:"generateGraph" `
}
func ( s * Strategy ) Subscribe ( session * bbgo . ExchangeSession ) {
session . Subscribe ( types . KLineChannel , s . Symbol , types . SubscribeOptions { Interval : s . Interval } )
if ! bbgo . IsBackTesting {
session . Subscribe ( types . MarketTradeChannel , s . Symbol , types . SubscribeOptions { } )
}
s . ExitMethods . SetAndSubscribe ( session , s )
}
func ( s * Strategy ) ID ( ) string {
return ID
}
func ( s * Strategy ) InstanceID ( ) string {
return fmt . Sprintf ( "%s:%s" , ID , s . Symbol )
}
func ( s * Strategy ) Run ( ctx context . Context , orderExecutor bbgo . OrderExecutor , session * bbgo . ExchangeSession ) error {
var instanceID = s . InstanceID ( )
if s . Position == nil {
s . Position = types . NewPositionFromMarket ( s . Market )
}
if s . ProfitStats == nil {
s . ProfitStats = types . NewProfitStats ( s . Market )
}
if s . TradeStats == nil {
s . TradeStats = types . NewTradeStats ( s . Symbol )
}
// StrategyController
s . Status = types . StrategyStatusRunning
s . OnSuspend ( func ( ) {
// Cancel active orders
_ = s . orderExecutor . GracefulCancel ( ctx )
} )
s . OnEmergencyStop ( func ( ) {
// Cancel active orders
_ = s . orderExecutor . GracefulCancel ( ctx )
// Close 100% position
//_ = s.ClosePosition(ctx, fixedpoint.One)
} )
// initial required information
s . session = session
s . orderExecutor = bbgo . NewGeneralOrderExecutor ( session , s . Symbol , ID , instanceID , s . Position )
s . orderExecutor . BindEnvironment ( s . Environment )
s . orderExecutor . BindProfitStats ( s . ProfitStats )
s . orderExecutor . BindTradeStats ( s . TradeStats )
//modify := func(p float64) float64 {
// return p
//}
//if s.GraphPNLDeductFee {
// modify = func(p float64) float64 {
// return p * (1. - Fee)
// }
//}
profit := floats . Slice { 1. , 1. }
price , _ := s . session . LastPrice ( s . Symbol )
initAsset := s . CalcAssetValue ( price ) . Float64 ( )
cumProfit := floats . Slice { initAsset , initAsset }
s . orderExecutor . TradeCollector ( ) . OnTrade ( func ( trade types . Trade , _profit , netProfit fixedpoint . Value ) {
profit . Update ( netProfit . Float64 ( ) )
cumProfit . Update ( s . CalcAssetValue ( trade . Price ) . Float64 ( ) )
} )
s . orderExecutor . TradeCollector ( ) . OnPositionUpdate ( func ( position * types . Position ) {
bbgo . Sync ( s )
} )
s . orderExecutor . Bind ( )
s . activeOrders = bbgo . NewActiveOrderBook ( s . Symbol )
for _ , method := range s . ExitMethods {
method . Bind ( session , s . orderExecutor )
}
2022-09-20 02:32:41 +00:00
kLineStore , _ := s . session . MarketDataStore ( s . Symbol )
2022-09-19 06:45:13 +00:00
s . nrr = & NRR { IntervalWindow : types . IntervalWindow { Window : 2 , Interval : s . Interval } , RankingWindow : s . Window }
2022-09-20 02:32:41 +00:00
s . nrr . BindK ( s . session . MarketDataStream , s . Symbol , s . Interval )
if klines , ok := kLineStore . KLinesOfInterval ( s . nrr . Interval ) ; ok {
s . nrr . LoadK ( ( * klines ) [ 0 : ] )
}
2022-09-19 06:45:13 +00:00
//startTime := s.Environment.StartTime()
//s.TradeStats.SetIntervalProfitCollector(types.NewIntervalProfitCollector(types.Interval1h, startTime))
s . session . MarketDataStream . OnKLineClosed ( types . KLineWith ( s . Symbol , s . Interval , func ( kline types . KLine ) {
2022-09-20 02:32:41 +00:00
// ts_rank(): transformed to [0~1] which divided equally
// queued first signal as its initial process
// important: delayed signal in order to submit order at current kline close (a.k.a. next open while in production)
// instead of right in current kline open
2022-09-19 06:45:13 +00:00
// alpha-weighted assets (inventory and capital)
targetBase := s . QuantityOrAmount . CalculateQuantity ( kline . Close ) . Mul ( fixedpoint . NewFromFloat ( s . nrr . RankedValues . Index ( 1 ) ) )
diffQty := targetBase . Sub ( s . Position . Base )
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 ( ) )
// use kline direction to prevent reversing position too soon
if diffQty . Sign ( ) > 0 { // && kline.Direction() >= 0
s . orderExecutor . OpenPosition ( context . Background ( ) , bbgo . OpenPositionOptions { Quantity : diffQty . Abs ( ) , Long : true , MarketOrder : true } )
} else if diffQty . Sign ( ) < 0 { // && kline.Direction() <= 0
s . orderExecutor . OpenPosition ( context . Background ( ) , bbgo . OpenPositionOptions { Quantity : diffQty . Abs ( ) , Short : true , MarketOrder : true } )
}
} ) )
bbgo . RegisterCommand ( "/draw" , "Draw Indicators" , func ( reply interact . Reply ) {
canvas := s . DrawIndicators ( s . frameKLine . StartTime )
var buffer bytes . Buffer
if err := canvas . Render ( chart . PNG , & buffer ) ; err != nil {
log . WithError ( err ) . Errorf ( "cannot render indicators in oneliner" )
reply . Message ( fmt . Sprintf ( "[error] cannot render indicators in drift: %v" , err ) )
return
}
bbgo . SendPhoto ( & buffer )
} )
bbgo . RegisterCommand ( "/pnl" , "Draw PNL(%) per trade" , func ( reply interact . Reply ) {
canvas := s . DrawPNL ( & profit )
var buffer bytes . Buffer
if err := canvas . Render ( chart . PNG , & buffer ) ; err != nil {
log . WithError ( err ) . Errorf ( "cannot render pnl in oneliner" )
reply . Message ( fmt . Sprintf ( "[error] cannot render pnl in drift: %v" , err ) )
return
}
bbgo . SendPhoto ( & buffer )
} )
bbgo . RegisterCommand ( "/cumpnl" , "Draw Cumulative PNL(Quote)" , func ( reply interact . Reply ) {
canvas := s . DrawCumPNL ( & cumProfit )
var buffer bytes . Buffer
if err := canvas . Render ( chart . PNG , & buffer ) ; err != nil {
log . WithError ( err ) . Errorf ( "cannot render cumpnl in oneliner" )
reply . Message ( fmt . Sprintf ( "[error] canot render cumpnl in drift: %v" , err ) )
return
}
bbgo . SendPhoto ( & buffer )
} )
bbgo . OnShutdown ( func ( ctx context . Context , wg * sync . WaitGroup ) {
defer wg . Done ( )
_ , _ = fmt . Fprintln ( os . Stderr , s . TradeStats . String ( ) )
_ = s . orderExecutor . GracefulCancel ( ctx )
} )
return nil
}
func ( s * Strategy ) CalcAssetValue ( price fixedpoint . Value ) fixedpoint . Value {
balances := s . session . GetAccount ( ) . Balances ( )
return balances [ s . Market . BaseCurrency ] . Total ( ) . Mul ( price ) . Add ( balances [ s . Market . QuoteCurrency ] . Total ( ) )
}
func ( s * Strategy ) DrawPNL ( profit types . Series ) * types . Canvas {
canvas := types . NewCanvas ( s . InstanceID ( ) )
//log.Errorf("pnl Highest: %f, Lowest: %f", types.Highest(profit, profit.Length()), types.Lowest(profit, profit.Length()))
length := profit . Length ( )
if s . GraphPNLDeductFee {
canvas . PlotRaw ( "pnl (with Fee Deducted)" , profit , length )
} else {
canvas . PlotRaw ( "pnl" , profit , length )
}
canvas . YAxis = chart . YAxis {
ValueFormatter : func ( v interface { } ) string {
if vf , isFloat := v . ( float64 ) ; isFloat {
return fmt . Sprintf ( "%.4f" , vf )
}
return ""
} ,
}
canvas . PlotRaw ( "1" , types . NumberSeries ( 1 ) , length )
return canvas
}
func ( s * Strategy ) DrawCumPNL ( cumProfit types . Series ) * types . Canvas {
canvas := types . NewCanvas ( s . InstanceID ( ) )
canvas . PlotRaw ( "cumulative pnl" , cumProfit , cumProfit . Length ( ) )
canvas . YAxis = chart . YAxis {
ValueFormatter : func ( v interface { } ) string {
if vf , isFloat := v . ( float64 ) ; isFloat {
return fmt . Sprintf ( "%.4f" , vf )
}
return ""
} ,
}
return canvas
}
func ( s * Strategy ) initIndicators ( store * bbgo . SerialMarketDataStore ) error {
s . ma = & indicator . SMA { IntervalWindow : types . IntervalWindow { Interval : s . Interval , Window : s . HLRangeWindow } }
s . stdevHigh = & indicator . StdDev { IntervalWindow : types . IntervalWindow { Interval : s . Interval , Window : s . HLRangeWindow } }
s . stdevLow = & indicator . StdDev { IntervalWindow : types . IntervalWindow { Interval : s . Interval , Window : s . HLRangeWindow } }
s . alpha = & NRR {
IntervalWindow : types . IntervalWindow { Window : 2 , Interval : s . Interval } ,
}
s . alpha . SeriesBase . Series = s . alpha
s . atr = & indicator . ATR { IntervalWindow : types . IntervalWindow { Interval : s . Interval , Window : s . ATRWindow } }
s . trendLine = & indicator . EWMA { IntervalWindow : types . IntervalWindow { Interval : s . Interval , Window : s . TrendWindow } }
klines , ok := store . KLinesOfInterval ( s . Interval )
klinesLength := len ( * klines )
if ! ok || klinesLength == 0 {
return errors . New ( "klines not exists" )
}
for _ , kline := range * klines {
source := s . GetSource ( & kline ) . Float64 ( )
high := kline . High . Float64 ( )
low := kline . Low . Float64 ( )
s . ma . Update ( source )
s . stdevHigh . Update ( high - s . ma . Last ( ) )
s . stdevLow . Update ( s . ma . Last ( ) - low )
s . alpha . Update ( kline . Close . Float64 ( ) )
s . trendLine . Update ( source )
s . atr . PushK ( kline )
s . priceLines . Update ( source )
}
if s . frameKLine != nil && klines != nil {
s . frameKLine . Set ( & ( * klines ) [ len ( * klines ) - 1 ] )
}
klines , ok = store . KLinesOfInterval ( types . Interval1m )
klinesLength = len ( * klines )
if ! ok || klinesLength == 0 {
return errors . New ( "klines not exists" )
}
if s . kline1m != nil && klines != nil {
s . kline1m . Set ( & ( * klines ) [ len ( * klines ) - 1 ] )
}
s . startTime = s . kline1m . StartTime . Time ( ) . Add ( s . kline1m . Interval . Duration ( ) )
return nil
}
func ( s * Strategy ) DrawIndicators ( time types . Time ) * types . Canvas {
canvas := types . NewCanvas ( s . InstanceID ( ) , s . Interval )
Length := s . priceLines . Length ( )
if Length > 300 {
Length = 300
}
log . Infof ( "draw indicators with %d data" , Length )
mean := s . priceLines . Mean ( Length )
highestPrice := s . priceLines . Minus ( mean ) . Abs ( ) . Highest ( Length )
highestDrift := s . alpha . Abs ( ) . Highest ( Length )
hi := s . alpha . Abs ( ) . Highest ( Length )
ratio := highestPrice / highestDrift
2022-09-20 02:32:41 +00:00
canvas . Plot ( "alpha" , s . alpha . Mul ( ratio ) . Add ( mean ) , time , Length )
2022-09-19 06:45:13 +00:00
canvas . Plot ( "driftOrig" , s . alpha . Mul ( highestPrice / hi ) . Add ( mean ) , time , Length )
canvas . Plot ( "zero" , types . NumberSeries ( mean ) , time , Length )
canvas . Plot ( "price" , s . priceLines , time , Length )
return canvas
}
func ( s * Strategy ) Draw ( time types . Time , profit types . Series , cumProfit types . Series ) {
canvas := s . DrawIndicators ( time )
f , err := os . Create ( s . CanvasPath )
if err != nil {
log . WithError ( err ) . Errorf ( "cannot create on %s" , s . CanvasPath )
return
}
defer f . Close ( )
if err := canvas . Render ( chart . PNG , f ) ; err != nil {
log . WithError ( err ) . Errorf ( "cannot render in drift" )
}
canvas = s . DrawPNL ( profit )
f , err = os . Create ( s . GraphPNLPath )
if err != nil {
log . WithError ( err ) . Errorf ( "open pnl" )
return
}
defer f . Close ( )
if err := canvas . Render ( chart . PNG , f ) ; err != nil {
log . WithError ( err ) . Errorf ( "render pnl" )
}
canvas = s . DrawCumPNL ( cumProfit )
f , err = os . Create ( s . GraphCumPNLPath )
if err != nil {
log . WithError ( err ) . Errorf ( "open cumpnl" )
return
}
defer f . Close ( )
if err := canvas . Render ( chart . PNG , f ) ; err != nil {
log . WithError ( err ) . Errorf ( "render cumpnl" )
}
}