Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions config/xpremium.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
---
# xpremium strategy config example
# This strategy compares the best bid/ask across two exchanges (premium vs base)
# and emits LONG/SHORT when the percentage spread exceeds minSpread.
#
# Spread algorithm (percentage):
# premium = (premiumBid - baseAsk) / baseAsk # LONG when premium >= minSpread
# discount = (premiumAsk - baseBid) / premiumAsk # SHORT when discount <= -minSpread
#
# Usage:
# - Configure your exchange API keys via environment variables (see sessions section envVarPrefix)
# - Run: bbgo run --config config/xpremium.yaml
#
# Notes:
# - tradingSession is where orders are executed. It can be the premium session (default)
# or another session that supports derivatives/leverage.
# - Set leverage only if your trading session supports it (e.g., Binance Futures).
# - minSpread is a percentage threshold. You can specify as a decimal (e.g., 0.002)
# or as a percent string (e.g., "0.20%").
# - On signal reversal (e.g. LONG while holding SHORT), the strategy closes the opposite
# position first, then opens the new position.
# - After entry, a stop-market order is placed using the previous 15m pivot (low for long,
# high for short) adjusted by stopLossSafetyRatio.
# - A ROI-based take-profit can be enabled via takeProfitROI (default 3%).

notifications:
slack:
defaultChannel: "dev-bbgo"
errorChannel: "bbgo-error"

switches:
trade: true
orderUpdate: false
submitOrder: false

persistence:
json:
directory: var/data

logging:
trade: true
order: true
fields:
env: local

sessions:
# Provide credentials via ENV using the given prefixes.
# For example, for binance: BINANCE_API_KEY, BINANCE_API_SECRET
binance:
exchange: binance
envVarPrefix: binance

max:
exchange: max
envVarPrefix: max

binance_futures:
exchange: binance
envVarPrefix: binance
futures: true

crossExchangeStrategies:
- xpremium:
# Symbols to compare between exchanges
premiumSession: binance
premiumSymbol: BTCUSDT

baseSession: max
baseSymbol: BTCUSDT

# Orders will be executed on this session/symbol
# If not set, tradingSession defaults to premiumSession
tradingSession: binance_futures
tradingSymbol: BTCUSDT

# Optional leverage for the trading session (if supported)
# Set to 0 to skip changing leverage
leverage: 10

# Minimum percentage spread to trigger LONG/SHORT
# LONG when (premiumBid - baseAsk)/baseAsk >= minSpread
# SHORT when (premiumAsk - baseBid)/premiumAsk <= -minSpread
# Example: 0.002 = 0.2% or "0.20%"
minSpread: "0.20%"

# Optional explicit sizing and risk parameters
# quantity: 0.001 # fixed base quantity to trade; if omitted, size is risk-based
# priceType: maker # maker|taker, used to choose price for sizing
maxLossLimit: 100 # quote currency risk per trade used for position sizing

# Optional risk controls (from common.Strategy). Uncomment to enable.
# positionHardLimit: 0 # maximum absolute position value (quote)
# maxPositionQuantity: 0 # maximum base quantity
# circuitBreakLossThreshold: 0 # maximum cumulative loss before halt (quote)

# Optional exits & risk params for xpremium
takeProfitROI: 3% # ROI threshold to take profit (default 3%)
stopLossSafetyRatio: 1% # pivot-based stop safety adjustment (1% => 0.01)

# Pivot-based stop-loss. When enabled, we derive stop loss from the most recent closed pivot on the configured interval:
# - For LONG: previous Pivot Low, adjusted by stopLossSafetyRatio (stop = pivotLow * (1 - ratio))
# - For SHORT: previous Pivot High, adjusted by stopLossSafetyRatio (stop = pivotHigh * (1 + ratio))
# Pivots are detected using left/right bars (look-back/look-forward confirmation).
# Larger left/right make pivots rarer and more significant; smaller values react faster.
# If disabled or no pivot yet, falls back to last closed kline high/low of the interval.
pivotStop:
enabled: true # set true to use pivot-derived stop-loss (defaults to false)
interval: 15m # kline interval used for pivot detection (e.g., 5m/15m/1h)
left: 10 # number of bars to the left required to confirm a pivot
right: 10 # number of bars to the right required to confirm a pivot

# Optional Engulfing pattern take-profit:
# - LONG positions react to bearish engulfing
# - SHORT positions react to bullish engulfing
engulfingTakeProfit:
enabled: true
interval: 1h # check on kline close of this interval
bodyMultiple: 3.0 # current engulfing body must be >= 3.0x previous body
bottomShadowMaxRatio: "0.1%" # for bearish: max lower shadow; for bullish: max upper shadow

## Backtest settings (optional). In backtest mode, premium=base=trading session.
## You need to provide a CSV file with bid/ask prices for both premium and base.
## This only works under the single exchange strategy settings in backtest mode.
# backtest:
# bidAskPriceCsv: bid_ask_price.csv # CSV columns: Time, base ask, base bid, premium ask, premium bid
# tradingInterval: 1m # kline interval to align with CSV times
8 changes: 8 additions & 0 deletions pkg/bbgo/order_executor_general.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,14 @@ func NewGeneralOrderExecutor(
tradeCollector: core.NewTradeCollector(symbol, position, orderStore),
}

// When it's futures trading, we disable the order filter in trade collector
// because in futures trading, there could be trades not from bot submitted orders,
// e.g., liquidation trades, trades from the mobile apps
// so we need to process all trades directly
if session.Futures {
executor.tradeCollector.DisableOrderFilter(true)
}

if executor.position != nil && session.Margin {
market := executor.position.Market
marginInfoUpdater := session.GetMarginInfoUpdater()
Expand Down
100 changes: 56 additions & 44 deletions pkg/core/tradecollector.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ type TradeCollector struct {

boundStream map[types.Stream]struct{}

disableOrderFilter bool

ConverterManager
}

Expand Down Expand Up @@ -157,6 +159,14 @@ func (c *TradeCollector) TradeStore() *TradeStore {
return c.tradeStore
}

// DisableOrderFilter disables the order filter when processing trades
// When disabled, all trades will be processed without checking the order store.
// This is useful when the session is futures trading,
// and we would like to process all trades in the same market.
func (c *TradeCollector) DisableOrderFilter(v bool) {
c.disableOrderFilter = v
}

func (c *TradeCollector) SetPosition(position *types.Position) {
c.position = position
}
Expand Down Expand Up @@ -254,35 +264,29 @@ func (c *TradeCollector) Process() bool {
}

// if it's the trade we're looking for, add it to the list and mark it as done
if trade.OrderID == 0 {
logrus.Errorf("[tradecollector] process trade %+v has no OrderID", trade)
}
if c.orderStore.Exists(trade.OrderID) {
trades = append(trades, trade)
c.doneTrades[key] = struct{}{}
return true
if !c.disableOrderFilter {
if trade.OrderID == 0 {
logrus.Errorf("[tradecollector] process trade %+v has no OrderID", trade)
}
if c.orderStore.Exists(trade.OrderID) {
trades = append(trades, trade)
c.doneTrades[key] = struct{}{}
return true
}
}

return false
})
c.mu.Unlock()

for _, trade := range trades {
var p types.Profit
if c.position != nil {
profit, netProfit, madeProfit := c.position.AddTrade(trade)
if madeProfit {
p = c.position.NewProfit(trade, profit, netProfit)
}
p, changed := c.applyTrade(trade)
if changed {
positionChanged = true

c.EmitTrade(trade, profit, netProfit)
} else {
c.EmitTrade(trade, fixedpoint.Zero, fixedpoint.Zero)
}

if !p.Profit.IsZero() {
c.EmitProfit(trade, &p)
c.EmitProfit(trade, p)
}
}

Expand All @@ -293,11 +297,30 @@ func (c *TradeCollector) Process() bool {
return positionChanged
}

func (c *TradeCollector) applyTrade(trade types.Trade) (*types.Profit, bool) {
var p types.Profit
if c.position != nil {
profit, netProfit, madeProfit := c.position.AddTrade(trade)
if madeProfit {
p = c.position.NewProfit(trade, profit, netProfit)
}
c.EmitTrade(trade, profit, netProfit)
return &p, true
} else {
c.EmitTrade(trade, fixedpoint.Zero, fixedpoint.Zero)
return nil, false
}
}

// processTrade takes a trade and see if there is a matched order
// if the order is found, then we add the trade to the position
// return true when the given trade is added
// return false when the given trade is not added
func (c *TradeCollector) processTrade(trade types.Trade) bool {
if c.symbol != "" && trade.Symbol != c.symbol {
return false
}

key := trade.Key()

c.mu.Lock()
Expand All @@ -308,34 +331,27 @@ func (c *TradeCollector) processTrade(trade types.Trade) bool {
return false
}

if trade.OrderID == 0 {
logrus.Errorf("[tradecollector] process trade %+v has no OrderID", trade)
}

if !c.orderStore.Exists(trade.OrderID) {
// not done yet
// add this trade to the trade store for the later processing
c.tradeStore.Add(trade)
c.mu.Unlock()
return false
if !c.disableOrderFilter {
if trade.OrderID == 0 {
logrus.Errorf("[tradecollector] process trade %+v has no OrderID", trade)
}
if !c.orderStore.Exists(trade.OrderID) {
// not done yet
// add this trade to the trade store for the later processing
c.tradeStore.Add(trade)
c.mu.Unlock()
return false
}
}

c.doneTrades[key] = struct{}{}
c.mu.Unlock()

if c.position != nil {
profit, netProfit, madeProfit := c.position.AddTrade(trade)
if madeProfit {
p := c.position.NewProfit(trade, profit, netProfit)
c.EmitTrade(trade, profit, netProfit)
c.EmitProfit(trade, &p)
} else {
c.EmitTrade(trade, fixedpoint.Zero, fixedpoint.Zero)
c.EmitProfit(trade, nil)
}
p, positionChanged := c.applyTrade(trade)
c.EmitProfit(trade, p) // emit for both non-nil and nil

if positionChanged {
c.EmitPositionUpdate(c.position)
} else {
c.EmitTrade(trade, fixedpoint.Zero, fixedpoint.Zero)
}

return true
Expand All @@ -344,10 +360,6 @@ func (c *TradeCollector) processTrade(trade types.Trade) bool {
// return true when the given trade is added
// return false when the given trade is not added
func (c *TradeCollector) ProcessTrade(trade types.Trade) bool {
if c.symbol != "" && trade.Symbol != c.symbol {
return false
}

return c.processTrade(c.ConvertTrade(trade))
}

Expand Down
Loading
Loading