mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-26 00:35:15 +00:00
feature: add pnl / cummulative pnl graph, add continuous graph
This commit is contained in:
parent
62aac8ecc4
commit
ac5c7f5773
|
@ -20,6 +20,11 @@ exchangeStrategies:
|
||||||
predictOffset: 14
|
predictOffset: 14
|
||||||
noStopPrice: true
|
noStopPrice: true
|
||||||
noTrailingStopLoss: false
|
noTrailingStopLoss: false
|
||||||
|
|
||||||
|
generateGraph: true
|
||||||
|
graphPNLDeductFee: false
|
||||||
|
graphPNLPath: "./pnl.png"
|
||||||
|
graphCumPNLPath: "./cumpnl.png"
|
||||||
#exits:
|
#exits:
|
||||||
#- roiStopLoss:
|
#- roiStopLoss:
|
||||||
# percentage: 0.8%
|
# percentage: 0.8%
|
||||||
|
|
|
@ -60,6 +60,14 @@ type Strategy struct {
|
||||||
NoStopPrice bool `json:"noStopPrice"`
|
NoStopPrice bool `json:"noStopPrice"`
|
||||||
NoTrailingStopLoss bool `json:"noTrailingStopLoss"`
|
NoTrailingStopLoss bool `json:"noTrailingStopLoss"`
|
||||||
|
|
||||||
|
// 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"`
|
||||||
|
|
||||||
StopOrders map[uint64]types.SubmitOrder
|
StopOrders map[uint64]types.SubmitOrder
|
||||||
|
|
||||||
ExitMethods bbgo.ExitMethodSet `json:"exits"`
|
ExitMethods bbgo.ExitMethodSet `json:"exits"`
|
||||||
|
@ -116,6 +124,7 @@ func (s *Strategy) ClosePosition(ctx context.Context) (*types.Order, bool) {
|
||||||
if order == nil {
|
if order == nil {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
order.Tag = "close"
|
||||||
order.TimeInForce = ""
|
order.TimeInForce = ""
|
||||||
balances := s.Session.GetAccount().Balances()
|
balances := s.Session.GetAccount().Balances()
|
||||||
baseBalance := balances[s.Market.BaseCurrency].Available
|
baseBalance := balances[s.Market.BaseCurrency].Available
|
||||||
|
@ -305,7 +314,7 @@ func (s *Strategy) InitTickerFunctions(ctx context.Context) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Strategy) Draw(time types.Time, priceLine types.SeriesExtend) {
|
func (s *Strategy) Draw(time types.Time, priceLine types.SeriesExtend, profit types.Series, cumProfit types.Series) {
|
||||||
canvas := types.NewCanvas(s.InstanceID(), s.Interval)
|
canvas := types.NewCanvas(s.InstanceID(), s.Interval)
|
||||||
Length := priceLine.Length()
|
Length := priceLine.Length()
|
||||||
if Length > 100 {
|
if Length > 100 {
|
||||||
|
@ -319,7 +328,7 @@ func (s *Strategy) Draw(time types.Time, priceLine types.SeriesExtend) {
|
||||||
canvas.Plot("drift", s.drift, time, Length)
|
canvas.Plot("drift", s.drift, time, Length)
|
||||||
canvas.Plot("zero", types.NumberSeries(0), time, Length)
|
canvas.Plot("zero", types.NumberSeries(0), time, Length)
|
||||||
canvas.Plot("price", priceLine.Minus(mean).Mul(ratio), time, Length)
|
canvas.Plot("price", priceLine.Minus(mean).Mul(ratio), time, Length)
|
||||||
canvas.Plot("driftMean", types.NumberSeries(meanDrift), time, Length)
|
canvas.Plot("0", types.NumberSeries(meanDrift), time, Length)
|
||||||
f, err := os.Create(s.CanvasPath)
|
f, err := os.Create(s.CanvasPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Errorf("cannot create on %s", s.CanvasPath)
|
log.WithError(err).Errorf("cannot create on %s", s.CanvasPath)
|
||||||
|
@ -329,6 +338,36 @@ func (s *Strategy) Draw(time types.Time, priceLine types.SeriesExtend) {
|
||||||
if err := canvas.Render(chart.PNG, f); err != nil {
|
if err := canvas.Render(chart.PNG, f); err != nil {
|
||||||
log.WithError(err).Errorf("cannot render in drift")
|
log.WithError(err).Errorf("cannot render in drift")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
canvas = types.NewCanvas(s.InstanceID())
|
||||||
|
if s.GraphPNLDeductFee {
|
||||||
|
canvas.PlotRaw("pnl % (with Fee Deducted)", profit, profit.Length())
|
||||||
|
} else {
|
||||||
|
canvas.PlotRaw("pnl %", profit, profit.Length())
|
||||||
|
}
|
||||||
|
f, err = os.Create(s.GraphPNLPath)
|
||||||
|
if err != nil {
|
||||||
|
panic("open pnl")
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
if err := canvas.Render(chart.PNG, f); err != nil {
|
||||||
|
panic("render pnl")
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas = types.NewCanvas(s.InstanceID())
|
||||||
|
if s.GraphPNLDeductFee {
|
||||||
|
canvas.PlotRaw("cummulative pnl % (with Fee Deducted)", cumProfit, cumProfit.Length())
|
||||||
|
} else {
|
||||||
|
canvas.PlotRaw("cummulative pnl %", cumProfit, cumProfit.Length())
|
||||||
|
}
|
||||||
|
f, err = os.Create(s.GraphCumPNLPath)
|
||||||
|
if err != nil {
|
||||||
|
panic("open cumpnl")
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
if err := canvas.Render(chart.PNG, f); err != nil {
|
||||||
|
panic("render cumpnl")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
|
func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
|
||||||
|
@ -371,6 +410,86 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
||||||
for _, method := range s.ExitMethods {
|
for _, method := range s.ExitMethods {
|
||||||
method.Bind(session, s.GeneralOrderExecutor)
|
method.Bind(session, s.GeneralOrderExecutor)
|
||||||
}
|
}
|
||||||
|
buyPrice := fixedpoint.Zero
|
||||||
|
sellPrice := fixedpoint.Zero
|
||||||
|
profit := types.Float64Slice{}
|
||||||
|
cumProfit := types.Float64Slice{1.}
|
||||||
|
orderTagHistory := make(map[uint64]string)
|
||||||
|
if s.GenerateGraph {
|
||||||
|
s.Session.UserDataStream.OnOrderUpdate(func(order types.Order) {
|
||||||
|
orderTagHistory[order.OrderID] = order.Tag
|
||||||
|
})
|
||||||
|
modify := func(p fixedpoint.Value) fixedpoint.Value {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
if s.GraphPNLDeductFee {
|
||||||
|
fee := fixedpoint.NewFromFloat(0.0004) // taker fee % * 2, for upper bound
|
||||||
|
modify = func(p fixedpoint.Value) fixedpoint.Value {
|
||||||
|
return p.Mul(fixedpoint.One.Sub(fee))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.Session.UserDataStream.OnTradeUpdate(func(trade types.Trade) {
|
||||||
|
tag, ok := orderTagHistory[trade.OrderID]
|
||||||
|
if !ok {
|
||||||
|
panic(fmt.Sprintf("cannot find order: %v", trade))
|
||||||
|
}
|
||||||
|
if tag == "close" {
|
||||||
|
if !buyPrice.IsZero() {
|
||||||
|
profit.Update(modify(trade.Price.Div(buyPrice)).Float64())
|
||||||
|
cumProfit.Update(cumProfit.Last() * profit.Last())
|
||||||
|
buyPrice = fixedpoint.Zero
|
||||||
|
if !sellPrice.IsZero() {
|
||||||
|
panic("sellprice shouldn't be zero")
|
||||||
|
}
|
||||||
|
} else if !sellPrice.IsZero() {
|
||||||
|
profit.Update(modify(sellPrice.Div(trade.Price)).Float64())
|
||||||
|
cumProfit.Update(cumProfit.Last() * profit.Last())
|
||||||
|
sellPrice = fixedpoint.Zero
|
||||||
|
if !buyPrice.IsZero() {
|
||||||
|
panic("buyprice shouldn't be zero")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic("no price available")
|
||||||
|
}
|
||||||
|
} else if tag == "short" {
|
||||||
|
if buyPrice.IsZero() {
|
||||||
|
if !sellPrice.IsZero() {
|
||||||
|
panic("sellPrice not zero")
|
||||||
|
}
|
||||||
|
sellPrice = trade.Price
|
||||||
|
} else {
|
||||||
|
profit.Update(modify(trade.Price.Div(buyPrice)).Float64())
|
||||||
|
cumProfit.Update(cumProfit.Last() * profit.Last())
|
||||||
|
buyPrice = fixedpoint.Zero
|
||||||
|
sellPrice = trade.Price
|
||||||
|
}
|
||||||
|
} else if tag == "long" {
|
||||||
|
if sellPrice.IsZero() {
|
||||||
|
if !buyPrice.IsZero() {
|
||||||
|
panic("buyPrice not zero")
|
||||||
|
}
|
||||||
|
buyPrice = trade.Price
|
||||||
|
} else {
|
||||||
|
profit.Update(modify(sellPrice.Div(trade.Price)).Float64())
|
||||||
|
cumProfit.Update(cumProfit.Last() * profit.Last())
|
||||||
|
sellPrice = fixedpoint.Zero
|
||||||
|
buyPrice = trade.Price
|
||||||
|
}
|
||||||
|
} else if tag == "sl" {
|
||||||
|
if !buyPrice.IsZero() {
|
||||||
|
profit.Update(modify(trade.Price.Div(buyPrice)).Float64())
|
||||||
|
cumProfit.Update(cumProfit.Last() * profit.Last())
|
||||||
|
buyPrice = fixedpoint.Zero
|
||||||
|
} else if !sellPrice.IsZero() {
|
||||||
|
profit.Update(modify(sellPrice.Div(trade.Price)).Float64())
|
||||||
|
cumProfit.Update(cumProfit.Last() * profit.Last())
|
||||||
|
sellPrice = fixedpoint.Zero
|
||||||
|
} else {
|
||||||
|
panic("no position to sl")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
s.BindStopLoss(ctx)
|
s.BindStopLoss(ctx)
|
||||||
|
|
||||||
|
@ -484,6 +603,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
||||||
StopPrice: stopPrice,
|
StopPrice: stopPrice,
|
||||||
Price: stopPrice,
|
Price: stopPrice,
|
||||||
Quantity: quantity,
|
Quantity: quantity,
|
||||||
|
Tag: "sl",
|
||||||
}
|
}
|
||||||
createdOrders, err := s.GeneralOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{
|
createdOrders, err := s.GeneralOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{
|
||||||
Symbol: s.Symbol,
|
Symbol: s.Symbol,
|
||||||
|
@ -491,6 +611,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
||||||
Type: types.OrderTypeLimit,
|
Type: types.OrderTypeLimit,
|
||||||
Price: source,
|
Price: source,
|
||||||
Quantity: quantity,
|
Quantity: quantity,
|
||||||
|
Tag: "short",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Errorf("cannot place sell order")
|
log.WithError(err).Errorf("cannot place sell order")
|
||||||
|
@ -534,6 +655,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
||||||
StopPrice: stopPrice,
|
StopPrice: stopPrice,
|
||||||
Price: stopPrice,
|
Price: stopPrice,
|
||||||
Quantity: quantity,
|
Quantity: quantity,
|
||||||
|
Tag: "sl",
|
||||||
}
|
}
|
||||||
createdOrders, err := s.GeneralOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{
|
createdOrders, err := s.GeneralOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{
|
||||||
Symbol: s.Symbol,
|
Symbol: s.Symbol,
|
||||||
|
@ -541,6 +663,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
||||||
Type: types.OrderTypeLimit,
|
Type: types.OrderTypeLimit,
|
||||||
Price: source,
|
Price: source,
|
||||||
Quantity: quantity,
|
Quantity: quantity,
|
||||||
|
Tag: "long",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Errorf("cannot place buy order")
|
log.WithError(err).Errorf("cannot place buy order")
|
||||||
|
@ -563,7 +686,9 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
||||||
|
|
||||||
defer fmt.Fprintln(os.Stdout, s.TradeStats.String())
|
defer fmt.Fprintln(os.Stdout, s.TradeStats.String())
|
||||||
|
|
||||||
s.Draw(dynamicKLine.StartTime, priceLine)
|
if s.GenerateGraph {
|
||||||
|
s.Draw(dynamicKLine.StartTime, priceLine, &profit, &cumProfit)
|
||||||
|
}
|
||||||
|
|
||||||
wg.Done()
|
wg.Done()
|
||||||
})
|
})
|
||||||
|
|
|
@ -1162,14 +1162,20 @@ type Canvas struct {
|
||||||
Interval Interval
|
Interval Interval
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCanvas(title string, interval Interval) *Canvas {
|
func NewCanvas(title string, intervals ...Interval) *Canvas {
|
||||||
valueFormatter := chart.TimeValueFormatter
|
valueFormatter := chart.TimeValueFormatter
|
||||||
if interval.Minutes() > 24*60 {
|
interval := Interval1m
|
||||||
valueFormatter = chart.TimeDateValueFormatter
|
if len(intervals) > 0 {
|
||||||
} else if interval.Minutes() > 60 {
|
interval = intervals[0]
|
||||||
valueFormatter = chart.TimeHourValueFormatter
|
if interval.Minutes() > 24*60 {
|
||||||
|
valueFormatter = chart.TimeDateValueFormatter
|
||||||
|
} else if interval.Minutes() > 60 {
|
||||||
|
valueFormatter = chart.TimeHourValueFormatter
|
||||||
|
} else {
|
||||||
|
valueFormatter = chart.TimeMinuteValueFormatter
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
valueFormatter = chart.TimeMinuteValueFormatter
|
valueFormatter = chart.IntValueFormatter
|
||||||
}
|
}
|
||||||
out := &Canvas{
|
out := &Canvas{
|
||||||
Chart: chart.Chart{
|
Chart: chart.Chart{
|
||||||
|
@ -1200,4 +1206,16 @@ func (canvas *Canvas) Plot(tag string, a Series, endTime Time, length int) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (canvas *Canvas) PlotRaw(tag string, a Series, length int) {
|
||||||
|
var x []float64
|
||||||
|
for i := 0; i < length; i++ {
|
||||||
|
x = append(x, float64(i))
|
||||||
|
}
|
||||||
|
canvas.Series = append(canvas.Series, chart.ContinuousSeries{
|
||||||
|
Name: tag,
|
||||||
|
XValues: x,
|
||||||
|
YValues: Reverse(a, length),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: ta.linreg
|
// TODO: ta.linreg
|
||||||
|
|
Loading…
Reference in New Issue
Block a user