mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-22 14:55:16 +00:00
Merge pull request #977 from austin362667/austin362667/irr
strategy: fix irr
This commit is contained in:
commit
06675d0ac8
|
@ -13,22 +13,25 @@ sessions:
|
|||
|
||||
exchangeStrategies:
|
||||
- on: binance
|
||||
oneliner:
|
||||
irr:
|
||||
symbol: BTCBUSD
|
||||
interval: 1m
|
||||
window: 120
|
||||
amount: 5_000.0
|
||||
# Draw pnl
|
||||
drawGraph: true
|
||||
graphPNLPath: "./pnl.png"
|
||||
graphCumPNLPath: "./cumpnl.png"
|
||||
|
||||
backtest:
|
||||
sessions:
|
||||
- binance
|
||||
startTime: "2021-01-01"
|
||||
endTime: "2022-09-30"
|
||||
startTime: "2022-09-01"
|
||||
endTime: "2022-10-04"
|
||||
symbols:
|
||||
- BTCBUSD
|
||||
accounts:
|
||||
binance:
|
||||
takerFeeRate: 0.0
|
||||
balances:
|
||||
# ETH: 1
|
||||
BUSD: 5_000.0
|
||||
|
|
90
pkg/strategy/irr/draw.go
Normal file
90
pkg/strategy/irr/draw.go
Normal file
|
@ -0,0 +1,90 @@
|
|||
package irr
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/bbgo"
|
||||
"github.com/c9s/bbgo/pkg/interact"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
"github.com/wcharczuk/go-chart/v2"
|
||||
)
|
||||
|
||||
func (s *Strategy) InitDrawCommands(profit, cumProfit types.Series) {
|
||||
bbgo.RegisterCommand("/pnl", "Draw PNL(%) per trade", func(reply interact.Reply) {
|
||||
canvas := DrawPNL(s.InstanceID(), profit)
|
||||
var buffer bytes.Buffer
|
||||
if err := canvas.Render(chart.PNG, &buffer); err != nil {
|
||||
log.WithError(err).Errorf("cannot render pnl in drift")
|
||||
reply.Message(fmt.Sprintf("[error] cannot render pnl in ewo: %v", err))
|
||||
return
|
||||
}
|
||||
bbgo.SendPhoto(&buffer)
|
||||
})
|
||||
bbgo.RegisterCommand("/cumpnl", "Draw Cummulative PNL(Quote)", func(reply interact.Reply) {
|
||||
canvas := DrawCumPNL(s.InstanceID(), cumProfit)
|
||||
var buffer bytes.Buffer
|
||||
if err := canvas.Render(chart.PNG, &buffer); err != nil {
|
||||
log.WithError(err).Errorf("cannot render cumpnl in drift")
|
||||
reply.Message(fmt.Sprintf("[error] canot render cumpnl in drift: %v", err))
|
||||
return
|
||||
}
|
||||
bbgo.SendPhoto(&buffer)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Strategy) Draw(profit, cumProfit types.Series) error {
|
||||
|
||||
canvas := DrawPNL(s.InstanceID(), profit)
|
||||
fPnL, err := os.Create(s.GraphPNLPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create on path " + s.GraphPNLPath)
|
||||
}
|
||||
defer fPnL.Close()
|
||||
if err = canvas.Render(chart.PNG, fPnL); err != nil {
|
||||
return fmt.Errorf("cannot render pnl")
|
||||
}
|
||||
canvas = DrawCumPNL(s.InstanceID(), cumProfit)
|
||||
fCumPnL, err := os.Create(s.GraphCumPNLPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create on path " + s.GraphCumPNLPath)
|
||||
}
|
||||
defer fCumPnL.Close()
|
||||
if err = canvas.Render(chart.PNG, fCumPnL); err != nil {
|
||||
return fmt.Errorf("cannot render cumpnl")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func DrawPNL(instanceID string, profit types.Series) *types.Canvas {
|
||||
canvas := types.NewCanvas(instanceID)
|
||||
length := profit.Length()
|
||||
log.Infof("pnl Highest: %f, Lowest: %f", types.Highest(profit, length), types.Lowest(profit, length))
|
||||
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 DrawCumPNL(instanceID string, cumProfit types.Series) *types.Canvas {
|
||||
canvas := types.NewCanvas(instanceID)
|
||||
canvas.PlotRaw("cummulative 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
|
||||
}
|
|
@ -1,23 +1,18 @@
|
|||
package irr
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/bbgo"
|
||||
"github.com/c9s/bbgo/pkg/data/tsv"
|
||||
"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"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/wcharczuk/go-chart/v2"
|
||||
)
|
||||
|
||||
const ID = "irr"
|
||||
|
@ -57,53 +52,145 @@ type Strategy struct {
|
|||
// 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
|
||||
AccountValueCalculator *bbgo.AccountValueCalculator
|
||||
|
||||
beta float64
|
||||
// whether to draw graph or not by the end of backtest
|
||||
DrawGraph bool `json:"drawGraph"`
|
||||
GraphPNLPath string `json:"graphPNLPath"`
|
||||
GraphCumPNLPath string `json:"graphCumPNLPath"`
|
||||
|
||||
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"`
|
||||
// for position
|
||||
buyPrice float64 `persistence:"buy_price"`
|
||||
sellPrice float64 `persistence:"sell_price"`
|
||||
highestPrice float64 `persistence:"highest_price"`
|
||||
lowestPrice float64 `persistence:"lowest_price"`
|
||||
|
||||
// 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"`
|
||||
// Accumulated profit report
|
||||
AccumulatedProfitReport *AccumulatedProfitReport `json:"accumulatedProfitReport"`
|
||||
}
|
||||
|
||||
// AccumulatedProfitReport For accumulated profit report output
|
||||
type AccumulatedProfitReport struct {
|
||||
// AccumulatedProfitMAWindow Accumulated profit SMA window, in number of trades
|
||||
AccumulatedProfitMAWindow int `json:"accumulatedProfitMAWindow"`
|
||||
|
||||
// IntervalWindow interval window, in days
|
||||
IntervalWindow int `json:"intervalWindow"`
|
||||
|
||||
// NumberOfInterval How many intervals to output to TSV
|
||||
NumberOfInterval int `json:"NumberOfInterval"`
|
||||
|
||||
// TsvReportPath The path to output report to
|
||||
TsvReportPath string `json:"tsvReportPath"`
|
||||
|
||||
// AccumulatedDailyProfitWindow The window to sum up the daily profit, in days
|
||||
AccumulatedDailyProfitWindow int `json:"accumulatedDailyProfitWindow"`
|
||||
|
||||
// Accumulated profit
|
||||
accumulatedProfit fixedpoint.Value
|
||||
accumulatedProfitPerDay floats.Slice
|
||||
previousAccumulatedProfit fixedpoint.Value
|
||||
|
||||
// Accumulated profit MA
|
||||
accumulatedProfitMA *indicator.SMA
|
||||
accumulatedProfitMAPerDay floats.Slice
|
||||
|
||||
// Daily profit
|
||||
dailyProfit floats.Slice
|
||||
|
||||
// Accumulated fee
|
||||
accumulatedFee fixedpoint.Value
|
||||
accumulatedFeePerDay floats.Slice
|
||||
|
||||
// Win ratio
|
||||
winRatioPerDay floats.Slice
|
||||
|
||||
// Profit factor
|
||||
profitFactorPerDay floats.Slice
|
||||
|
||||
// Trade number
|
||||
dailyTrades floats.Slice
|
||||
accumulatedTrades int
|
||||
previousAccumulatedTrades int
|
||||
}
|
||||
|
||||
func (r *AccumulatedProfitReport) Initialize() {
|
||||
if r.AccumulatedProfitMAWindow <= 0 {
|
||||
r.AccumulatedProfitMAWindow = 60
|
||||
}
|
||||
if r.IntervalWindow <= 0 {
|
||||
r.IntervalWindow = 7
|
||||
}
|
||||
if r.AccumulatedDailyProfitWindow <= 0 {
|
||||
r.AccumulatedDailyProfitWindow = 7
|
||||
}
|
||||
if r.NumberOfInterval <= 0 {
|
||||
r.NumberOfInterval = 1
|
||||
}
|
||||
r.accumulatedProfitMA = &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: types.Interval1d, Window: r.AccumulatedProfitMAWindow}}
|
||||
}
|
||||
|
||||
func (r *AccumulatedProfitReport) RecordProfit(profit fixedpoint.Value) {
|
||||
r.accumulatedProfit = r.accumulatedProfit.Add(profit)
|
||||
}
|
||||
|
||||
func (r *AccumulatedProfitReport) RecordTrade(fee fixedpoint.Value) {
|
||||
r.accumulatedFee = r.accumulatedFee.Add(fee)
|
||||
r.accumulatedTrades += 1
|
||||
}
|
||||
|
||||
func (r *AccumulatedProfitReport) DailyUpdate(tradeStats *types.TradeStats) {
|
||||
// Daily profit
|
||||
r.dailyProfit.Update(r.accumulatedProfit.Sub(r.previousAccumulatedProfit).Float64())
|
||||
r.previousAccumulatedProfit = r.accumulatedProfit
|
||||
|
||||
// Accumulated profit
|
||||
r.accumulatedProfitPerDay.Update(r.accumulatedProfit.Float64())
|
||||
|
||||
// Accumulated profit MA
|
||||
r.accumulatedProfitMA.Update(r.accumulatedProfit.Float64())
|
||||
r.accumulatedProfitMAPerDay.Update(r.accumulatedProfitMA.Last())
|
||||
|
||||
// Accumulated Fee
|
||||
r.accumulatedFeePerDay.Update(r.accumulatedFee.Float64())
|
||||
|
||||
// Win ratio
|
||||
r.winRatioPerDay.Update(tradeStats.WinningRatio.Float64())
|
||||
|
||||
// Profit factor
|
||||
r.profitFactorPerDay.Update(tradeStats.ProfitFactor.Float64())
|
||||
|
||||
// Daily trades
|
||||
r.dailyTrades.Update(float64(r.accumulatedTrades - r.previousAccumulatedTrades))
|
||||
r.previousAccumulatedTrades = r.accumulatedTrades
|
||||
}
|
||||
|
||||
// Output Accumulated profit report to a TSV file
|
||||
func (r *AccumulatedProfitReport) Output(symbol string) {
|
||||
if r.TsvReportPath != "" {
|
||||
tsvwiter, err := tsv.AppendWriterFile(r.TsvReportPath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer tsvwiter.Close()
|
||||
// Output symbol, total acc. profit, acc. profit 60MA, interval acc. profit, fee, win rate, profit factor
|
||||
_ = tsvwiter.Write([]string{"#", "Symbol", "accumulatedProfit", "accumulatedProfitMA", fmt.Sprintf("%dd profit", r.AccumulatedDailyProfitWindow), "accumulatedFee", "winRatio", "profitFactor", "60D trades"})
|
||||
for i := 0; i <= r.NumberOfInterval-1; i++ {
|
||||
accumulatedProfit := r.accumulatedProfitPerDay.Index(r.IntervalWindow * i)
|
||||
accumulatedProfitStr := fmt.Sprintf("%f", accumulatedProfit)
|
||||
accumulatedProfitMA := r.accumulatedProfitMAPerDay.Index(r.IntervalWindow * i)
|
||||
accumulatedProfitMAStr := fmt.Sprintf("%f", accumulatedProfitMA)
|
||||
intervalAccumulatedProfit := r.dailyProfit.Tail(r.AccumulatedDailyProfitWindow+r.IntervalWindow*i).Sum() - r.dailyProfit.Tail(r.IntervalWindow*i).Sum()
|
||||
intervalAccumulatedProfitStr := fmt.Sprintf("%f", intervalAccumulatedProfit)
|
||||
accumulatedFee := fmt.Sprintf("%f", r.accumulatedFeePerDay.Index(r.IntervalWindow*i))
|
||||
winRatio := fmt.Sprintf("%f", r.winRatioPerDay.Index(r.IntervalWindow*i))
|
||||
profitFactor := fmt.Sprintf("%f", r.profitFactorPerDay.Index(r.IntervalWindow*i))
|
||||
trades := r.dailyTrades.Tail(60+r.IntervalWindow*i).Sum() - r.dailyTrades.Tail(r.IntervalWindow*i).Sum()
|
||||
tradesStr := fmt.Sprintf("%f", trades)
|
||||
|
||||
_ = tsvwiter.Write([]string{fmt.Sprintf("%d", i+1), symbol, accumulatedProfitStr, accumulatedProfitMAStr, intervalAccumulatedProfitStr, accumulatedFee, winRatio, profitFactor, tradesStr})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
|
||||
|
@ -157,27 +244,81 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
|||
// initial required information
|
||||
s.session = session
|
||||
|
||||
// Set fee rate
|
||||
if s.session.MakerFeeRate.Sign() > 0 || s.session.TakerFeeRate.Sign() > 0 {
|
||||
s.Position.SetExchangeFeeRate(s.session.ExchangeName, types.ExchangeFee{
|
||||
MakerFeeRate: s.session.MakerFeeRate,
|
||||
TakerFeeRate: s.session.TakerFeeRate,
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
// AccountValueCalculator
|
||||
s.AccountValueCalculator = bbgo.NewAccountValueCalculator(s.session, s.Market.QuoteCurrency)
|
||||
|
||||
// Accumulated profit report
|
||||
if bbgo.IsBackTesting {
|
||||
if s.AccumulatedProfitReport == nil {
|
||||
s.AccumulatedProfitReport = &AccumulatedProfitReport{}
|
||||
}
|
||||
s.AccumulatedProfitReport.Initialize()
|
||||
s.orderExecutor.TradeCollector().OnProfit(func(trade types.Trade, profit *types.Profit) {
|
||||
if profit == nil {
|
||||
return
|
||||
}
|
||||
|
||||
s.AccumulatedProfitReport.RecordProfit(profit.Profit)
|
||||
})
|
||||
// s.orderExecutor.TradeCollector().OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) {
|
||||
// s.AccumulatedProfitReport.RecordTrade(trade.Fee)
|
||||
// })
|
||||
session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1d, func(kline types.KLine) {
|
||||
s.AccumulatedProfitReport.DailyUpdate(s.TradeStats)
|
||||
}))
|
||||
}
|
||||
|
||||
// For drawing
|
||||
profitSlice := floats.Slice{1., 1.}
|
||||
price, _ := 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())
|
||||
cumProfitSlice := floats.Slice{initAsset, initAsset}
|
||||
|
||||
s.orderExecutor.TradeCollector().OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) {
|
||||
if bbgo.IsBackTesting {
|
||||
s.AccumulatedProfitReport.RecordTrade(trade.Fee)
|
||||
}
|
||||
|
||||
// For drawing/charting
|
||||
price := trade.Price.Float64()
|
||||
if s.buyPrice > 0 {
|
||||
profitSlice.Update(price / s.buyPrice)
|
||||
cumProfitSlice.Update(s.CalcAssetValue(trade.Price).Float64())
|
||||
} else if s.sellPrice > 0 {
|
||||
profitSlice.Update(s.sellPrice / price)
|
||||
cumProfitSlice.Update(s.CalcAssetValue(trade.Price).Float64())
|
||||
}
|
||||
if s.Position.IsDust(trade.Price) {
|
||||
s.buyPrice = 0
|
||||
s.sellPrice = 0
|
||||
s.highestPrice = 0
|
||||
s.lowestPrice = 0
|
||||
} else if s.Position.IsLong() {
|
||||
s.buyPrice = price
|
||||
s.sellPrice = 0
|
||||
s.highestPrice = s.buyPrice
|
||||
s.lowestPrice = 0
|
||||
} else {
|
||||
s.sellPrice = price
|
||||
s.buyPrice = 0
|
||||
s.highestPrice = 0
|
||||
s.lowestPrice = s.sellPrice
|
||||
}
|
||||
})
|
||||
|
||||
s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) {
|
||||
bbgo.Sync(ctx, s)
|
||||
})
|
||||
|
@ -213,49 +354,39 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
|||
|
||||
// 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, LimitOrder: false})
|
||||
_, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
|
||||
Symbol: s.Symbol,
|
||||
Side: types.SideTypeBuy,
|
||||
Quantity: diffQty.Abs(),
|
||||
Type: types.OrderTypeMarket,
|
||||
Tag: "irr buy more",
|
||||
})
|
||||
} else if diffQty.Sign() < 0 { // && kline.Direction() <= 0
|
||||
s.orderExecutor.OpenPosition(context.Background(), bbgo.OpenPositionOptions{Quantity: diffQty.Abs(), Short: true, LimitOrder: false})
|
||||
_, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
|
||||
Symbol: s.Symbol,
|
||||
Side: types.SideTypeSell,
|
||||
Quantity: diffQty.Abs(),
|
||||
Type: types.OrderTypeMarket,
|
||||
Tag: "irr sell more",
|
||||
})
|
||||
}
|
||||
|
||||
}))
|
||||
|
||||
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(ctx, func(ctx context.Context, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
|
||||
// Output accumulated profit report
|
||||
if bbgo.IsBackTesting {
|
||||
defer s.AccumulatedProfitReport.Output(s.Symbol)
|
||||
|
||||
if s.DrawGraph {
|
||||
if err := s.Draw(&profitSlice, &cumProfitSlice); err != nil {
|
||||
log.WithError(err).Errorf("cannot draw graph")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintln(os.Stderr, s.TradeStats.String())
|
||||
_ = s.orderExecutor.GracefulCancel(ctx)
|
||||
})
|
||||
|
@ -267,136 +398,3 @@ 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
|
||||
|
||||
canvas.Plot("alpha", s.alpha.Mul(ratio).Add(mean), time, Length)
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user