diff --git a/pkg/bbgo/tradecollector.go b/pkg/bbgo/tradecollector.go index c648ec48c..88dd20533 100644 --- a/pkg/bbgo/tradecollector.go +++ b/pkg/bbgo/tradecollector.go @@ -18,17 +18,17 @@ type TradeCollector struct { tradeStore *TradeStore tradeC chan types.Trade - position *types.Position + position types.PositionInterface orderStore *OrderStore doneTrades map[types.TradeKey]struct{} recoverCallbacks []func(trade types.Trade) tradeCallbacks []func(trade types.Trade, profit, netProfit fixedpoint.Value) - positionUpdateCallbacks []func(position *types.Position) + positionUpdateCallbacks []func(position types.PositionInterface) profitCallbacks []func(trade types.Trade, profit, netProfit fixedpoint.Value) } -func NewTradeCollector(symbol string, position *types.Position, orderStore *OrderStore) *TradeCollector { +func NewTradeCollector(symbol string, position types.PositionInterface, orderStore *OrderStore) *TradeCollector { return &TradeCollector{ Symbol: symbol, orderSig: sigchan.New(1), @@ -47,7 +47,7 @@ func (c *TradeCollector) OrderStore() *OrderStore { } // Position returns the position used by the trade collector -func (c *TradeCollector) Position() *types.Position { +func (c *TradeCollector) Position() types.PositionInterface { return c.position } diff --git a/pkg/bbgo/tradecollector_callbacks.go b/pkg/bbgo/tradecollector_callbacks.go index af8bf1bd1..4a1d4255c 100644 --- a/pkg/bbgo/tradecollector_callbacks.go +++ b/pkg/bbgo/tradecollector_callbacks.go @@ -27,11 +27,11 @@ func (c *TradeCollector) EmitTrade(trade types.Trade, profit fixedpoint.Value, n } } -func (c *TradeCollector) OnPositionUpdate(cb func(position *types.Position)) { +func (c *TradeCollector) OnPositionUpdate(cb func(position types.PositionInterface)) { c.positionUpdateCallbacks = append(c.positionUpdateCallbacks, cb) } -func (c *TradeCollector) EmitPositionUpdate(position *types.Position) { +func (c *TradeCollector) EmitPositionUpdate(position types.PositionInterface) { for _, cb := range c.positionUpdateCallbacks { cb(position) } diff --git a/pkg/types/position.go b/pkg/types/position.go index c02c358ac..ca962db7b 100644 --- a/pkg/types/position.go +++ b/pkg/types/position.go @@ -29,6 +29,10 @@ type PositionRisk struct { LiquidationPrice fixedpoint.Value `json:"liquidationPrice"` } +type PositionInterface interface { + AddTrade(td Trade) (profit fixedpoint.Value, netProfit fixedpoint.Value, madeProfit bool) +} + type Position struct { Symbol string `json:"symbol" db:"symbol"` BaseCurrency string `json:"baseCurrency" db:"base"` @@ -453,3 +457,163 @@ func (p *Position) AddTrade(td Trade) (profit fixedpoint.Value, netProfit fixedp return fixedpoint.Zero, fixedpoint.Zero, false } + +type PositionStack struct { + *Position + Stack []*Position +} + +func (stack *PositionStack) Push(pos *Position) *PositionStack { + stack.Position = pos + stack.Stack = append(stack.Stack, pos) + return stack + +} + +func (stack *PositionStack) Pop() *PositionStack { + if len(stack.Stack) < 1 { + return nil + } + stack.Position = stack.Stack[len(stack.Stack)-1] + stack.Stack = stack.Stack[:len(stack.Stack)-1] + return stack +} + +func NewPositionStackFromMarket(market Market) *PositionStack { + pos := &Position{ + Symbol: market.Symbol, + BaseCurrency: market.BaseCurrency, + QuoteCurrency: market.QuoteCurrency, + Market: market, + TotalFee: make(map[string]fixedpoint.Value), + } + return &PositionStack{ + Position: pos, + Stack: []*Position{pos}, + } +} + +func (p *PositionStack) AddTrade(td Trade) (profit fixedpoint.Value, netProfit fixedpoint.Value, madeProfit bool) { + price := td.Price + quantity := td.Quantity + quoteQuantity := td.QuoteQuantity + fee := td.Fee + + // calculated fee in quote (some exchange accounts may enable platform currency fee discount, like BNB) + // convert platform fee token into USD values + var feeInQuote fixedpoint.Value = fixedpoint.Zero + + switch td.FeeCurrency { + + case p.BaseCurrency: + quantity = quantity.Sub(fee) + + case p.QuoteCurrency: + quoteQuantity = quoteQuantity.Sub(fee) + + default: + if p.ExchangeFeeRates != nil { + if exchangeFee, ok := p.ExchangeFeeRates[td.Exchange]; ok { + if td.IsMaker { + feeInQuote = feeInQuote.Add(exchangeFee.MakerFeeRate.Mul(quoteQuantity)) + } else { + feeInQuote = feeInQuote.Add(exchangeFee.TakerFeeRate.Mul(quoteQuantity)) + } + } + } else if p.FeeRate != nil { + if td.IsMaker { + feeInQuote = feeInQuote.Add(p.FeeRate.MakerFeeRate.Mul(quoteQuantity)) + } else { + feeInQuote = feeInQuote.Add(p.FeeRate.TakerFeeRate.Mul(quoteQuantity)) + } + } + } + + p.Lock() + defer p.Unlock() + + // update changedAt field before we unlock in the defer func + defer func() { + p.ChangedAt = td.Time.Time() + }() + + p.addTradeFee(td) + + // Base > 0 means we're in long position + // Base < 0 means we're in short position + switch td.Side { + + case SideTypeBuy: + if p.Base.Sign() < 0 { + // convert short position to long position + if p.Base.Add(quantity).Sign() > 0 { + profit = p.AverageCost.Sub(price).Mul(p.Base.Neg()) + netProfit = p.ApproximateAverageCost.Sub(price).Mul(p.Base.Neg()).Sub(feeInQuote) + p.Base = p.Base.Add(quantity) + p.Quote = p.Quote.Sub(quoteQuantity) + p.AverageCost = price + p.ApproximateAverageCost = price + p.AccumulatedProfit = p.AccumulatedProfit.Add(profit) + return profit, netProfit, true + } else { + // covering short position + p.Base = p.Base.Add(quantity) + p.Quote = p.Quote.Sub(quoteQuantity) + profit = p.AverageCost.Sub(price).Mul(quantity) + netProfit = p.ApproximateAverageCost.Sub(price).Mul(quantity).Sub(feeInQuote) + p.AccumulatedProfit = p.AccumulatedProfit.Add(profit) + return profit, netProfit, true + } + } + + divisor := p.Base.Add(quantity) + p.ApproximateAverageCost = p.ApproximateAverageCost.Mul(p.Base). + Add(quoteQuantity). + Add(feeInQuote). + Div(divisor) + p.AverageCost = p.AverageCost.Mul(p.Base).Add(quoteQuantity).Div(divisor) + p.Base = p.Base.Add(quantity) + p.Quote = p.Quote.Sub(quoteQuantity) + + return fixedpoint.Zero, fixedpoint.Zero, false + + case SideTypeSell: + if p.Base.Sign() > 0 { + // convert long position to short position + if p.Base.Compare(quantity) < 0 { + profit = price.Sub(p.AverageCost).Mul(p.Base) + netProfit = price.Sub(p.ApproximateAverageCost).Mul(p.Base).Sub(feeInQuote) + p.Base = p.Base.Sub(quantity) + p.Quote = p.Quote.Add(quoteQuantity) + p.AverageCost = price + p.ApproximateAverageCost = price + p.AccumulatedProfit = p.AccumulatedProfit.Add(profit) + return profit, netProfit, true + } else { + p.Base = p.Base.Sub(quantity) + p.Quote = p.Quote.Add(quoteQuantity) + profit = price.Sub(p.AverageCost).Mul(quantity) + netProfit = price.Sub(p.ApproximateAverageCost).Mul(quantity).Sub(feeInQuote) + p.AccumulatedProfit = p.AccumulatedProfit.Add(profit) + return profit, netProfit, true + } + } + + // handling short position, since Base here is negative we need to reverse the sign + divisor := quantity.Sub(p.Base) + p.ApproximateAverageCost = p.ApproximateAverageCost.Mul(p.Base.Neg()). + Add(quoteQuantity). + Sub(feeInQuote). + Div(divisor) + + p.AverageCost = p.AverageCost.Mul(p.Base.Neg()). + Add(quoteQuantity). + Div(divisor) + p.Base = p.Base.Sub(quantity) + p.Quote = p.Quote.Add(quoteQuantity) + + return fixedpoint.Zero, fixedpoint.Zero, false + } + + return fixedpoint.Zero, fixedpoint.Zero, false +}