FEATURE: rename and use specific profit stats

This commit is contained in:
chiahung.lin 2024-01-02 17:16:47 +08:00
parent 0d6c6666a1
commit faaaaabce3
7 changed files with 159 additions and 38 deletions

View File

@ -20,7 +20,7 @@ func (s *Strategy) placeOpenPositionOrders(ctx context.Context) error {
return err
}
orders, err := generateOpenPositionOrders(s.Market, s.Budget, price, s.PriceDeviation, s.MaxOrderNum, s.OrderGroupID)
orders, err := generateOpenPositionOrders(s.Market, s.QuoteInvestment, price, s.PriceDeviation, s.MaxOrderCount, s.OrderGroupID)
if err != nil {
return err
}
@ -44,12 +44,12 @@ func getBestPriceUntilSuccess(ctx context.Context, ex types.Exchange, symbol str
return ticker.Sell, nil
}
func generateOpenPositionOrders(market types.Market, budget, price, priceDeviation fixedpoint.Value, maxOrderNum int64, orderGroupID uint32) ([]types.SubmitOrder, error) {
func generateOpenPositionOrders(market types.Market, quoteInvestment, price, priceDeviation fixedpoint.Value, maxOrderCount int64, orderGroupID uint32) ([]types.SubmitOrder, error) {
factor := fixedpoint.One.Sub(priceDeviation)
// calculate all valid prices
var prices []fixedpoint.Value
for i := 0; i < int(maxOrderNum); i++ {
for i := 0; i < int(maxOrderCount); i++ {
if i > 0 {
price = price.Mul(factor)
}
@ -61,9 +61,9 @@ func generateOpenPositionOrders(market types.Market, budget, price, priceDeviati
prices = append(prices, price)
}
notional, orderNum := calculateNotionalAndNum(market, budget, prices)
notional, orderNum := calculateNotionalAndNum(market, quoteInvestment, prices)
if orderNum == 0 {
return nil, fmt.Errorf("failed to calculate notional and num of open position orders, price: %s, budget: %s", price, budget)
return nil, fmt.Errorf("failed to calculate notional and num of open position orders, price: %s, quote investment: %s", price, quoteInvestment)
}
side := types.SideTypeBuy
@ -89,9 +89,9 @@ func generateOpenPositionOrders(market types.Market, budget, price, priceDeviati
// calculateNotionalAndNum calculates the notional and num of open position orders
// DCA2 is notional-based, every order has the same notional
func calculateNotionalAndNum(market types.Market, budget fixedpoint.Value, prices []fixedpoint.Value) (fixedpoint.Value, int) {
func calculateNotionalAndNum(market types.Market, quoteInvestment fixedpoint.Value, prices []fixedpoint.Value) (fixedpoint.Value, int) {
for num := len(prices); num > 0; num-- {
notional := budget.Div(fixedpoint.NewFromInt(int64(num)))
notional := quoteInvestment.Div(fixedpoint.NewFromInt(int64(num)))
if notional.Compare(market.MinNotional) < 0 {
continue
}

View File

@ -47,10 +47,10 @@ func TestGenerateOpenPositionOrders(t *testing.T) {
strategy := newTestStrategy()
t.Run("case 1: all config is valid and we can place enough orders", func(t *testing.T) {
budget := Number("10500")
quoteInvestment := Number("10500")
askPrice := Number("30000")
margin := Number("0.05")
submitOrders, err := generateOpenPositionOrders(strategy.Market, budget, askPrice, margin, 4, strategy.OrderGroupID)
submitOrders, err := generateOpenPositionOrders(strategy.Market, quoteInvestment, askPrice, margin, 4, strategy.OrderGroupID)
if !assert.NoError(err) {
return
}

View File

@ -0,0 +1,90 @@
package dca2
import (
"time"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
)
type ProfitStats struct {
Symbol string `json:"symbol"`
Market types.Market `json:"market,omitempty"`
CreatedAt time.Time `json:"since,omitempty"`
UpdatedAt time.Time `json:"updatedAt,omitempty"`
Round int64 `json:"round,omitempty"`
QuoteInvestment fixedpoint.Value `json:"quoteInvestment,omitempty"`
RoundProfit fixedpoint.Value `json:"roundProfit,omitempty"`
RoundFee map[string]fixedpoint.Value `json:"roundFee,omitempty"`
TotalProfit fixedpoint.Value `json:"totalProfit,omitempty"`
TotalFee map[string]fixedpoint.Value `json:"totalFee,omitempty"`
// ttl is the ttl to keep in persistence
ttl time.Duration
}
func newProfitStats(market types.Market, quoteInvestment fixedpoint.Value) *ProfitStats {
return &ProfitStats{
Symbol: market.Symbol,
Market: market,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
Round: 0,
QuoteInvestment: quoteInvestment,
RoundFee: make(map[string]fixedpoint.Value),
TotalFee: make(map[string]fixedpoint.Value),
}
}
func (s *ProfitStats) SetTTL(ttl time.Duration) {
if ttl.Nanoseconds() <= 0 {
return
}
s.ttl = ttl
}
func (s *ProfitStats) Expiration() time.Duration {
return s.ttl
}
func (s *ProfitStats) AddTrade(trade types.Trade) {
if s.RoundFee == nil {
s.RoundFee = make(map[string]fixedpoint.Value)
}
if fee, ok := s.RoundFee[trade.FeeCurrency]; ok {
s.RoundFee[trade.FeeCurrency] = fee.Add(trade.Fee)
} else {
s.RoundFee[trade.FeeCurrency] = trade.Fee
}
if s.TotalFee == nil {
s.TotalFee = make(map[string]fixedpoint.Value)
}
if fee, ok := s.TotalFee[trade.FeeCurrency]; ok {
s.TotalFee[trade.FeeCurrency] = fee.Add(trade.Fee)
} else {
s.TotalFee[trade.FeeCurrency] = trade.Fee
}
switch trade.Side {
case types.SideTypeSell:
s.RoundProfit = s.RoundProfit.Add(trade.QuoteQuantity)
s.TotalProfit = s.TotalProfit.Add(trade.QuoteQuantity)
case types.SideTypeBuy:
s.RoundProfit = s.RoundProfit.Sub(trade.QuoteQuantity)
s.TotalProfit = s.TotalProfit.Sub(trade.QuoteQuantity)
default:
}
s.UpdatedAt = trade.Time.Time()
}
func (s *ProfitStats) FinishRound() {
s.Round++
s.RoundProfit = fixedpoint.Zero
s.RoundFee = make(map[string]fixedpoint.Value)
}

View File

@ -34,7 +34,8 @@ func (s *Strategy) recover(ctx context.Context) error {
return err
}
closedOrders, err := queryService.QueryClosedOrdersDesc(ctx, s.Symbol, time.Time{}, time.Now(), 0)
closedOrders, err := queryService.QueryClosedOrdersDesc(ctx, s.Symbol, time.Date(2024, time.January, 1, 0, 0, 0, 0, time.Local), time.Now(), 0)
// closedOrders, err := queryService.QueryClosedOrdersDesc(ctx, s.Symbol, time.Time{}, time.Now(), 0)
if err != nil {
return err
}
@ -46,7 +47,7 @@ func (s *Strategy) recover(ctx context.Context) error {
debugRoundOrders(s.logger, "current", currentRound)
// recover state
state, err := recoverState(ctx, s.Symbol, int(s.MaxOrderNum), openOrders, currentRound, s.OrderExecutor.ActiveMakerOrders(), s.OrderExecutor.OrderStore(), s.OrderGroupID)
state, err := recoverState(ctx, s.Symbol, int(s.MaxOrderCount), openOrders, currentRound, s.OrderExecutor.ActiveMakerOrders(), s.OrderExecutor.OrderStore(), s.OrderGroupID)
if err != nil {
return err
}
@ -56,15 +57,15 @@ func (s *Strategy) recover(ctx context.Context) error {
return err
}
// recover budget
budget := recoverBudget(currentRound)
// recover quote investment
quoteInvestment := recoverQuoteInvestment(currentRound)
// recover startTimeOfNextRound
startTimeOfNextRound := recoverStartTimeOfNextRound(ctx, currentRound, s.CoolDownInterval)
s.state = state
if !budget.IsZero() {
s.Budget = budget
if !quoteInvestment.IsZero() {
s.QuoteInvestment = quoteInvestment
}
s.startTimeOfNextRound = startTimeOfNextRound
@ -72,7 +73,7 @@ func (s *Strategy) recover(ctx context.Context) error {
}
// recover state
func recoverState(ctx context.Context, symbol string, maxOrderNum int, openOrders []types.Order, currentRound Round, activeOrderBook *bbgo.ActiveOrderBook, orderStore *core.OrderStore, groupID uint32) (State, error) {
func recoverState(ctx context.Context, symbol string, maxOrderCount int, openOrders []types.Order, currentRound Round, activeOrderBook *bbgo.ActiveOrderBook, orderStore *core.OrderStore, groupID uint32) (State, error) {
if len(currentRound.OpenPositionOrders) == 0 {
// new strategy
return WaitToOpenPosition, nil
@ -101,10 +102,10 @@ func recoverState(ctx context.Context, symbol string, maxOrderNum int, openOrder
}
numOpenPositionOrders := len(currentRound.OpenPositionOrders)
if numOpenPositionOrders > maxOrderNum {
if numOpenPositionOrders > maxOrderCount {
return None, fmt.Errorf("the number of open-position orders is > max order number")
} else if numOpenPositionOrders < maxOrderNum {
// The number of open-position orders should be the same as maxOrderNum
} else if numOpenPositionOrders < maxOrderCount {
// The number of open-position orders should be the same as maxOrderCount
// If not, it may be the following possible cause
// 1. This strategy at position opening, so it may not place all orders we want successfully
// 2. There are some errors when placing open-position orders. e.g. cannot lock fund.....
@ -192,7 +193,7 @@ func recoverPosition(ctx context.Context, position *types.Position, queryService
return nil
}
func recoverBudget(currentRound Round) fixedpoint.Value {
func recoverQuoteInvestment(currentRound Round) fixedpoint.Value {
if len(currentRound.OpenPositionOrders) == 0 {
return fixedpoint.Zero
}

View File

@ -181,12 +181,16 @@ func (s *Strategy) runTakeProfitReady(_ context.Context, next State) {
// wait 3 seconds to avoid position not update
time.Sleep(3 * time.Second)
s.logger.Info("[State] TakeProfitReady - start reseting position and calculate budget for next round")
s.Budget = s.Budget.Add(s.Position.Quote)
s.logger.Info("[State] TakeProfitReady - start reseting position and calculate quote investment for next round")
s.QuoteInvestment = s.QuoteInvestment.Add(s.Position.Quote)
// reset position
s.Position.Reset()
// reset
s.EmitProfit(s.ProfitStats)
s.ProfitStats.FinishRound()
// set the start time of the next round
s.startTimeOfNextRound = time.Now().Add(s.CoolDownInterval.Duration())
s.state = WaitToOpenPosition

View File

@ -9,7 +9,6 @@ import (
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/strategy/common"
"github.com/c9s/bbgo/pkg/types"
"github.com/c9s/bbgo/pkg/util"
"github.com/prometheus/client_golang/prometheus"
@ -28,16 +27,19 @@ func init() {
//go:generate callbackgen -type Strateg
type Strategy struct {
*common.Strategy
Position *types.Position `json:"position,omitempty" persistence:"position"`
ProfitStats *ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"`
Environment *bbgo.Environment
Market types.Market
Environment *bbgo.Environment
Session *bbgo.ExchangeSession
OrderExecutor *bbgo.GeneralOrderExecutor
Market types.Market
Symbol string `json:"symbol"`
// setting
Budget fixedpoint.Value `json:"budget"`
MaxOrderNum int64 `json:"maxOrderNum"`
QuoteInvestment fixedpoint.Value `json:"quoteInvestment"`
MaxOrderCount int64 `json:"maxOrderCount"`
PriceDeviation fixedpoint.Value `json:"priceDeviation"`
TakeProfitRatio fixedpoint.Value `json:"takeProfitRatio"`
CoolDownInterval types.Duration `json:"coolDownInterval"`
@ -68,7 +70,7 @@ type Strategy struct {
// callbacks
readyCallbacks []func()
positionCallbacks []func(*types.Position)
profitCallbacks []func(*types.ProfitStats)
profitCallbacks []func(*ProfitStats)
closedCallbacks []func()
errorCallbacks []func(error)
}
@ -78,8 +80,8 @@ func (s *Strategy) ID() string {
}
func (s *Strategy) Validate() error {
if s.MaxOrderNum < 1 {
return fmt.Errorf("maxOrderNum can not be < 1")
if s.MaxOrderCount < 1 {
return fmt.Errorf("maxOrderCount can not be < 1")
}
if s.TakeProfitRatio.Sign() <= 0 {
@ -106,7 +108,6 @@ func (s *Strategy) Defaults() error {
func (s *Strategy) Initialize() error {
s.logger = log.WithFields(s.LogFields)
s.Strategy = &common.Strategy{}
return nil
}
@ -119,8 +120,29 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
}
func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
s.Strategy.Initialize(ctx, s.Environment, session, s.Market, ID, s.InstanceID())
instanceID := s.InstanceID()
s.Session = session
if s.ProfitStats == nil {
s.ProfitStats = newProfitStats(s.Market, s.QuoteInvestment)
}
if s.Position == nil {
s.Position = types.NewPositionFromMarket(s.Market)
}
s.Position.Strategy = ID
s.Position.StrategyInstanceID = instanceID
if session.MakerFeeRate.Sign() > 0 || session.TakerFeeRate.Sign() > 0 {
s.Position.SetExchangeFeeRate(session.ExchangeName, types.ExchangeFee{
MakerFeeRate: session.MakerFeeRate,
TakerFeeRate: session.TakerFeeRate,
})
}
s.OrderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position)
s.OrderExecutor.BindEnvironment(s.Environment)
s.OrderExecutor.Bind()
if s.OrderGroupID == 0 {
s.OrderGroupID = util.FNV32(instanceID) % math.MaxInt32
@ -135,6 +157,10 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo.
s.updateTakeProfitPrice()
})
s.OrderExecutor.TradeCollector().OnTrade(func(trade types.Trade, profit, netProfit fixedpoint.Value) {
s.ProfitStats.AddTrade(trade)
})
s.OrderExecutor.ActiveMakerOrders().OnFilled(func(o types.Order) {
s.logger.Infof("[DCA] FILLED ORDER: %s", o.String())
openPositionSide := types.SideTypeBuy
@ -178,7 +204,7 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo.
s.logger.Infof("[DCA] recovered state: %d", s.state)
s.logger.Infof("[DCA] recovered position %s", s.Position.String())
s.logger.Infof("[DCA] recovered budget %s", s.Budget)
s.logger.Infof("[DCA] recovered quote investment %s", s.QuoteInvestment)
s.logger.Infof("[DCA] recovered startTimeOfNextRound %s", s.startTimeOfNextRound)
} else {
s.state = WaitToOpenPosition
@ -204,8 +230,8 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo.
}
balance := balances[s.Market.QuoteCurrency]
if balance.Available.Compare(s.Budget) < 0 {
return fmt.Errorf("the available balance of %s is %s which is less than budget setting %s, please check it", s.Market.QuoteCurrency, balance.Available, s.Budget)
if balance.Available.Compare(s.QuoteInvestment) < 0 {
return fmt.Errorf("the available balance of %s is %s which is less than quote investment setting %s, please check it", s.Market.QuoteCurrency, balance.Available, s.QuoteInvestment)
}
bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) {

View File

@ -26,11 +26,11 @@ func (s *Strategy) EmitPosition(position *types.Position) {
}
}
func (s *Strategy) OnProfit(cb func(*types.ProfitStats)) {
func (s *Strategy) OnProfit(cb func(*ProfitStats)) {
s.profitCallbacks = append(s.profitCallbacks, cb)
}
func (s *Strategy) EmitProfit(profitStats *types.ProfitStats) {
func (s *Strategy) EmitProfit(profitStats *ProfitStats) {
for _, cb := range s.profitCallbacks {
cb(profitStats)
}