2024-03-06 12:31:53 +00:00
|
|
|
package common
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2024-07-08 06:16:40 +00:00
|
|
|
"fmt"
|
2024-03-06 12:31:53 +00:00
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"golang.org/x/sync/errgroup"
|
|
|
|
|
2024-07-08 06:16:40 +00:00
|
|
|
"github.com/c9s/bbgo/pkg/bbgo"
|
2024-03-06 12:31:53 +00:00
|
|
|
"github.com/c9s/bbgo/pkg/exchange/batch"
|
|
|
|
"github.com/c9s/bbgo/pkg/types"
|
|
|
|
)
|
|
|
|
|
|
|
|
// ProfitFixerConfig is used for fixing profitStats and position by re-playing the trade history
|
|
|
|
type ProfitFixerConfig struct {
|
|
|
|
TradesSince types.Time `json:"tradesSince,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// ProfitFixer implements a trade-history-based profit fixer
|
|
|
|
type ProfitFixer struct {
|
|
|
|
sessions map[string]types.ExchangeTradeHistoryService
|
|
|
|
}
|
|
|
|
|
2024-03-06 13:56:32 +00:00
|
|
|
func NewProfitFixer() *ProfitFixer {
|
2024-03-06 12:31:53 +00:00
|
|
|
return &ProfitFixer{
|
|
|
|
sessions: make(map[string]types.ExchangeTradeHistoryService),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (f *ProfitFixer) AddExchange(sessionName string, service types.ExchangeTradeHistoryService) {
|
|
|
|
f.sessions[sessionName] = service
|
|
|
|
}
|
|
|
|
|
|
|
|
func (f *ProfitFixer) batchQueryTrades(
|
|
|
|
ctx context.Context,
|
|
|
|
service types.ExchangeTradeHistoryService,
|
|
|
|
symbol string,
|
|
|
|
since, until time.Time,
|
2024-07-08 09:43:22 +00:00
|
|
|
) (chan types.Trade, chan error) {
|
2024-03-06 12:31:53 +00:00
|
|
|
q := &batch.TradeBatchQuery{ExchangeTradeHistoryService: service}
|
2024-07-08 09:43:22 +00:00
|
|
|
return q.Query(ctx, symbol, &types.TradeQueryOptions{
|
2024-03-06 12:31:53 +00:00
|
|
|
StartTime: &since,
|
|
|
|
EndTime: &until,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-03-06 13:56:32 +00:00
|
|
|
func (f *ProfitFixer) aggregateAllTrades(ctx context.Context, symbol string, since, until time.Time) ([]types.Trade, error) {
|
2024-03-06 12:31:53 +00:00
|
|
|
var mu sync.Mutex
|
|
|
|
var allTrades = make([]types.Trade, 0, 1000)
|
|
|
|
|
|
|
|
g, subCtx := errgroup.WithContext(ctx)
|
|
|
|
for n, s := range f.sessions {
|
|
|
|
// allocate a copy of the iteration variables
|
|
|
|
sessionName := n
|
|
|
|
service := s
|
|
|
|
g.Go(func() error {
|
2024-03-06 13:56:32 +00:00
|
|
|
log.Infof("batch querying %s trade history from %s since %s until %s", symbol, sessionName, since.String(), until.String())
|
2024-07-08 09:43:22 +00:00
|
|
|
tradeC, errC := f.batchQueryTrades(subCtx, service, symbol, since, until)
|
|
|
|
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
|
|
|
return ctx.Err()
|
|
|
|
|
|
|
|
case err := <-errC:
|
|
|
|
return err
|
2024-03-06 12:31:53 +00:00
|
|
|
|
2024-07-08 09:43:22 +00:00
|
|
|
case trade, ok := <-tradeC:
|
|
|
|
if !ok {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
mu.Lock()
|
|
|
|
allTrades = append(allTrades, trade)
|
|
|
|
mu.Unlock()
|
|
|
|
}
|
|
|
|
}
|
2024-03-06 12:31:53 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := g.Wait(); err != nil {
|
2024-03-06 12:36:21 +00:00
|
|
|
return nil, err
|
2024-03-06 12:31:53 +00:00
|
|
|
}
|
|
|
|
|
2024-07-08 09:43:22 +00:00
|
|
|
mu.Lock()
|
2024-03-06 12:31:53 +00:00
|
|
|
allTrades = types.SortTradesAscending(allTrades)
|
2024-07-08 09:43:22 +00:00
|
|
|
mu.Unlock()
|
|
|
|
|
2024-03-06 12:36:21 +00:00
|
|
|
return allTrades, nil
|
|
|
|
}
|
|
|
|
|
2024-03-06 13:56:32 +00:00
|
|
|
func (f *ProfitFixer) Fix(
|
|
|
|
ctx context.Context, symbol string, since, until time.Time, stats *types.ProfitStats, position *types.Position,
|
|
|
|
) error {
|
2024-03-06 12:36:21 +00:00
|
|
|
log.Infof("starting profitFixer with time range %s <=> %s", since, until)
|
2024-03-06 13:56:32 +00:00
|
|
|
allTrades, err := f.aggregateAllTrades(ctx, symbol, since, until)
|
2024-03-06 12:36:21 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2024-03-06 12:36:53 +00:00
|
|
|
return f.FixFromTrades(allTrades, stats, position)
|
2024-03-06 12:34:19 +00:00
|
|
|
}
|
|
|
|
|
2024-03-06 12:36:53 +00:00
|
|
|
func (f *ProfitFixer) FixFromTrades(allTrades []types.Trade, stats *types.ProfitStats, position *types.Position) error {
|
2024-03-06 12:31:53 +00:00
|
|
|
for _, trade := range allTrades {
|
|
|
|
profit, netProfit, madeProfit := position.AddTrade(trade)
|
|
|
|
if madeProfit {
|
|
|
|
p := position.NewProfit(trade, profit, netProfit)
|
|
|
|
stats.AddProfit(p)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-06 12:34:19 +00:00
|
|
|
log.Infof("profitFixer fix finished: profitStats and position are updated from %d trades", len(allTrades))
|
2024-03-06 12:36:53 +00:00
|
|
|
return nil
|
2024-03-06 12:31:53 +00:00
|
|
|
}
|
2024-07-08 06:16:40 +00:00
|
|
|
|
|
|
|
type ProfitFixerBundle struct {
|
|
|
|
ProfitFixerConfig *ProfitFixerConfig `json:"profitFixer,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (f *ProfitFixerBundle) Fix(
|
|
|
|
ctx context.Context,
|
|
|
|
symbol string,
|
|
|
|
position *types.Position,
|
|
|
|
profitStats *types.ProfitStats,
|
|
|
|
sessions ...*bbgo.ExchangeSession,
|
|
|
|
) error {
|
|
|
|
bbgo.Notify("Fixing %s profitStats and position...", symbol)
|
|
|
|
|
|
|
|
log.Infof("profitFixer is enabled, checking checkpoint: %+v", f.ProfitFixerConfig.TradesSince)
|
|
|
|
|
|
|
|
if f.ProfitFixerConfig.TradesSince.Time().IsZero() {
|
|
|
|
return fmt.Errorf("tradesSince time can not be zero")
|
|
|
|
}
|
|
|
|
|
|
|
|
fixer := NewProfitFixer()
|
|
|
|
for _, session := range sessions {
|
|
|
|
if ss, ok := session.Exchange.(types.ExchangeTradeHistoryService); ok {
|
|
|
|
log.Infof("adding makerSession %s to profitFixer", session.Name)
|
|
|
|
fixer.AddExchange(session.Name, ss)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return fixer.Fix(ctx,
|
|
|
|
symbol,
|
|
|
|
f.ProfitFixerConfig.TradesSince.Time(),
|
|
|
|
time.Now(),
|
|
|
|
profitStats,
|
|
|
|
position)
|
|
|
|
}
|