| Overall Statistics |
|
Total Orders 184 Average Win 0.74% Average Loss -0.08% Compounding Annual Return 18.076% Drawdown 12.700% Expectancy 2.349 Start Equity 500000.00 End Equity 700923.49 Net Profit 40.185% Sharpe Ratio 0.435 Sortino Ratio 0.469 Probabilistic Sharpe Ratio 27.926% Loss Rate 66% Win Rate 34% Profit-Loss Ratio 8.74 Alpha 0.057 Beta 0.227 Annual Standard Deviation 0.206 Annual Variance 0.042 Information Ratio -0.236 Tracking Error 0.228 Treynor Ratio 0.393 Total Fees $182.00 Estimated Strategy Capacity $0 Lowest Capacity Asset SPY R735QTJ8XC9X Portfolio Turnover 2.85% Drawdown Recovery 172 |
#region imports
from AlgorithmImports import *
import numpy as np
from hmmlearn.hmm import GaussianHMM
from datetime import timedelta
from collections import defaultdict
#endregion
class RegimeAwareMultiStrategyAlgorithm(QCAlgorithm):
"""
Backtesting-Only Multi-Strategy Trading Algorithm
Strategies Implemented:
1. BTC Momentum (Long/Short based on RSI + Momentum)
2. QQQ Wheel (Covered Calls + Cash-Secured Puts)
3. VIX/SPY Gap Option Spreads (3-DTE Vertical Spreads)
4. Forex Basket Momentum (EUR/USD/GBP/CHF Basket vs. SMA)
REMOVED: All alpha models, insights, portfolio construction, and risk management
frameworks that were required for alphaStream licensing.
TRADING METHOD: Direct order tickets and position management.
"""
def Initialize(self):
"""Initialize algorithm: symbols, indicators, and strategy parameters."""
# ===== CONFIGURATION: Strategy Parameters =====
# BTC Momentum Strategy
self.btcMomentumPeriod = 30 # This is kept for the indicator definition
self.btcRsiPeriod = 30 # This is kept for the indicator definition
# BTC Trade Frequency Control
self.btcLastTradeTime = None
self.btcMinHoldingPeriod = timedelta(days=2)
self.btcMinReentryPeriod = timedelta(hours=24)
self.btcEntryTime = None
self.btcPreviousSignal = None
self.btcSignalConfirmationBars = 0
self.btcRequiredConfirmations = 2 # Need 2 bars of same signal
# Tighter thresholds
self.btcMomentumBullEntry = 1
self.btcMomentumBearEntry = -1
self.btcOverbought = 51
self.btcSold = 49
# QQQ is used by the Gap strategy for gap calculation
self.qqqSymbolTicker = "QQQ"
# Gap Spread Strategy
self.gapAllocation = 0.15
self.gapVixThreshold = 0.009
self.gapQqqThreshold = 0.001
self.gapDteTarget = 3
self.gapShortDelta = 0.16
self.gapLongDelta = 0.10
self.gapMinSpreadBid = 0.08
self.gapMinOi = 100
self.gapMaxSpreadPct = 0.10
self.gapMonthlyDdLimit = 0.02
# Gap Strategy State Tracking
self.gapYesterdayVixClose = None
self.gapYesterdayQqqClose = None
self.gapTodayProcessed = False
self.gapLastProcessedDate = None
self.gapCurrentMonth = None
self.gapMonthlyStartEquity = None
self.gapStopTradingMonth = False
self.openGapSpreadTickets = []
# Forex Basket Strategy
self.fxPairs = ["EURUSD", "USDCHF", "GBPUSD", "EURGBP"]
self.fxSymbols = []
self.fxSmaPeriods = 15 * 24 # 75 days in hourly bars
self.fxAtrPeriods = 12 * 24 # 25 days in hourly bars
self.fxSmaIndicators = {}
self.fxAtrIndicators = {}
self.fxVolatilityWindow = RollingWindow[float](14)
self.fxPreviousSignal = None
self.fxSignalThreshold = 0.05 # 1%
self.fxRiskRewardRatio = 5.0
self.fxStopLossAtrMultiplier = 5.0
self.fxOpenTradeTickets = []
self.fxOpenEntryTickets = []
self.fxPrevSmaAvg = None
self.fxVolatilityLowThreshold = 0.0003
self.fxSmaVolatilityHighThreshold = 0.03
# Regime Detection
self.regimeVixBullThreshold = 23.50
self.regimeVixBearThreshold = 24.50
self.marketRegime = None # -1 = Bear, 0 = Neutral, 1 = Bull
# ===== ALGORITHM SETUP =====
self.SetStartDate(2023, 11, 1)
self.SetEndDate(2025, 11, 11)
self.SetCash(500000)
#self.SetBrokerageModel(BrokerageName.INTERACTIVE_BROKERS_BROKERAGE, AccountType.MARGIN)
# ===== ADD SECURITIES & INDICATORS =====
# BTC Crypto (Daily resolution for momentum/RSI)
self.btcSymbol = self.AddCrypto("BTCUSD", Resolution.Daily, Market.Coinbase).Symbol
self.btcMomentum = self.MOM(self.btcSymbol, self.btcMomentumPeriod, Resolution.Daily)
self.btcRsi = self.RSI(self.btcSymbol, self.btcRsiPeriod, MovingAverageType.Simple, Resolution.Daily)
self.btcAtr = self.ATR(self.btcSymbol, 14, MovingAverageType.Simple, Resolution.Daily)
self.btcStoch = self.STO(self.btcSymbol, 14, 3, 3, Resolution.Daily)
# QQQ Equity & Options (Wheel Strategy)
self.qqqSymbol = self.AddEquity(self.qqqSymbolTicker, Resolution.Hour).Symbol
# SPY Equity & Options (Gap Strategy)
self.spySymbol = self.AddEquity("SPY", Resolution.Hour).Symbol
self.spyOption = self.AddOption("SPY", Resolution.Hour)
self.spyOption.SetFilter(lambda universe: universe.Strikes(-35, 35).Expiration(0, 3))
# VIX for Regime Detection
self.vixIndexSymbol = self.AddIndex("VIX", Resolution.Hour).Symbol
self.spyDailySymbol = self.AddEquity("SPY", Resolution.Daily).Symbol
self.vixDailySymbol = self.AddIndex("VIX", Resolution.Daily).Symbol
# Forex Basket Assets & Indicators
for pair in self.fxPairs:
symbol = self.AddForex(pair, Resolution.Hour, Market.Oanda).Symbol
self.fxSymbols.append(symbol)
self.fxSmaIndicators[symbol] = self.SMA(symbol, self.fxSmaPeriods, Resolution.Hour)
self.fxAtrIndicators[symbol] = self.ATR(symbol, self.fxAtrPeriods, MovingAverageType.Simple, Resolution.Hour)
warmup_periods = max(25, self.fxSmaPeriods)
self.SetWarmUp(warmup_periods)
# ===== SCHEDULED FUNCTIONS =====
self.Schedule.On(
self.DateRules.MonthStart(),
self.TimeRules.AfterMarketOpen(self.vixDailySymbol, 5),
self.updateregime
)
self.Schedule.On(
self.DateRules.EveryDay(),
self.TimeRules.BeforeMarketClose(self.spySymbol, 15),
self.gapliquidateandlog
)
# Schedule BTC once daily instead of hourly
self.Schedule.On(
self.DateRules.EveryDay(),
self.TimeRules.At(0, 5),
self.btc_scheduled_check
)
# Schedule for Forex Basket Strategy
self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.At(6, 0), self.fx_signal_check)
self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.At(18, 0), self.fx_signal_check)
def OnData(self, slice):
"""Main trading logic - executed on data events."""
if self.IsWarmingUp:
return
# Initialize regime if needed
if self.marketRegime is None:
self.updateregime()
# Execute strategies
# self.btc_momentum_trading(slice) # REMOVED - now runs on schedule
self.gap_option_spread(slice)
def btc_scheduled_check(self):
"""Scheduled daily BTC check."""
self.btc_momentum_trading(None)
def OnOrderEvent(self, orderEvent):
"""
Handle order events for all strategies.
Specifically used by the Forex strategy to place SL/TP orders after entry.
"""
if orderEvent.Status != OrderStatus.Filled:
return
# Check if the filled order is one of our Forex entry orders
is_entry_order = any(t.OrderId == orderEvent.OrderId for t in self.fxOpenEntryTickets)
if is_entry_order:
self.fx_place_bracket_orders(orderEvent.Symbol)
def updateregime(self):
"""Update market regime based on VIX thresholds."""
if self.IsWarmingUp:
return
if not self.Securities.ContainsKey(self.vixDailySymbol) or self.Securities[self.vixDailySymbol].Price == 0:
self.Log("Regime update failed - VIX data unavailable.")
return
vixlevel = self.Securities[self.vixDailySymbol].Price
if vixlevel >= self.regimeVixBearThreshold:
self.marketRegime = -1 # BEAR
elif vixlevel < self.regimeVixBullThreshold:
self.marketRegime = 1 # BULL
else:
self.marketRegime = 0 # NEUTRAL
regimename = {1: "BULL", 0: "NEUTRAL", -1: "BEAR"}[self.marketRegime]
self.Log(f"Regime Updated: {regimename} | VIX Level: {vixlevel:.2f}")
def btc_momentum_trading(self, data):
"""Execute BTC momentum strategy with RSI and momentum indicators.
Long/Short based on regime, momentum, and RSI levels with reduced frequency."""
if not all(indicator.IsReady for indicator in [self.btcRsi, self.btcMomentum, self.btcStoch]):
return
rsi = self.btcRsi.Current.Value
momentum = self.btcMomentum.Current.Value
holdings = self.Portfolio[self.btcSymbol]
stoch_k = self.btcStoch.StochK.Current.Value
# Cooldown checks
if not holdings.Invested and self.btcLastTradeTime:
if self.Time - self.btcLastTradeTime < self.btcMinReentryPeriod:
return
if holdings.Invested and self.btcEntryTime:
if self.Time - self.btcEntryTime < self.btcMinHoldingPeriod:
return
# Signal generation
current_signal = None
if self.marketRegime == -1:
if momentum < self.btcMomentumBearEntry and rsi < self.btcSold and stoch_k < 80:
current_signal = "SHORT"
elif rsi > self.btcSold or momentum > 1:
current_signal = "COVER"
else:
if holdings.IsShort:
current_signal = "COVER"
elif momentum > self.btcMomentumBullEntry and rsi > self.btcOverbought and stoch_k > 20:
current_signal = "BUY"
elif rsi < self.btcOverbought or momentum < -1 :
current_signal = "SELL"
# Execution
if not holdings.Invested and current_signal == "BUY":
quantity = int(self.Portfolio.TotalPortfolioValue * 0.25 / self.Securities[self.btcSymbol].Price)
self.MarketOrder(self.btcSymbol, quantity)
self.btcEntryTime = self.Time
self.Log(f"BTC LONG | Mom: {momentum:.0f} | RSI: {rsi:.0f}")
elif not holdings.Invested and current_signal == "SHORT":
quantity = int(self.Portfolio.TotalPortfolioValue * 0.25 / self.Securities[self.btcSymbol].Price)
self.MarketOrder(self.btcSymbol, -quantity)
self.btcEntryTime = self.Time
self.Log(f"BTC SHORT | Mom: {momentum:.0f} | RSI: {rsi:.0f}")
elif holdings.IsLong and current_signal == "SELL":
self.Liquidate(self.btcSymbol)
self.btcLastTradeTime = self.Time
self.btcEntryTime = None
self.Log(f"BTC EXIT LONG | Mom: {momentum:.0f} | RSI: {rsi:.0f}")
elif holdings.IsShort and current_signal == "COVER":
self.Liquidate(self.btcSymbol)
self.btcLastTradeTime = self.Time
self.btcEntryTime = None
self.Log(f"BTC COVER SHORT | Mom: {momentum:.0f} | RSI: {rsi:.0f}")
def gapliquidateandlog(self):
"""Liquidate expired gap spread positions and cancel open orders."""
# Cancel any open limit orders from the gap strategy
for ticket in self.openGapSpreadTickets:
if ticket.Status != OrderStatus.Filled and ticket.Status != OrderStatus.Canceled:
ticket.Cancel()
self.Log(f"[GAP STRATEGY] Canceled open limit order for {ticket.Symbol}")
self.openGapSpreadTickets.clear()
# Liquidate any remaining option positions from the gap strategy
openpositions = [pos for pos in self.Portfolio.Values
if pos.Invested and pos.Symbol.SecurityType == SecurityType.Option
and pos.Symbol.ID.Symbol == self.spyOption.Symbol.Value] # Only liquidate SPY options
for pos in openpositions:
self.MarketOrder(pos.Symbol, -pos.Quantity)
self.Log(f"GAP: Liquidated position {pos.Symbol} at market close.")
def gap_option_spread(self, data):
"""Sell iron condors on VIX/QQQ gap days."""
currentdate = self.Time.date()
# Use current bar's Open/Close for VIX and QQQ
currentvixopen = self.Securities[self.vixIndexSymbol].Open if self.Securities[self.vixIndexSymbol].HasData else None
currentvixclose = self.Securities[self.vixIndexSymbol].Close if self.Securities[self.vixIndexSymbol].HasData else None
currentqqqopen = self.Securities[self.qqqSymbol].Open if self.Securities[self.qqqSymbol].HasData else None
currentqqqclose = self.Securities[self.qqqSymbol].Close if self.Securities[self.qqqSymbol].HasData else None
# Ensure we have valid open/close prices
if currentvixopen is None or currentvixclose is None or currentqqqopen is None or currentqqqclose is None:
return
# Initialize yesterday's closes if not set
if self.gapYesterdayVixClose is None or self.gapYesterdayQqqClose is None:
self.gapYesterdayVixClose = currentvixclose
self.gapYesterdayQqqClose = currentqqqclose
self.gapLastProcessedDate = currentdate
return
# Reset daily flag at new day
if currentdate != self.gapLastProcessedDate:
self.gapTodayProcessed = False
self.gapLastProcessedDate = currentdate
if self.gapTodayProcessed:
# Update yesterday's close for the next day's calculation
self.gapYesterdayVixClose = currentvixclose
self.gapYesterdayQqqClose = currentqqqclose
return
# Calculate gap percentages
vixgappct = (currentvixopen - self.gapYesterdayVixClose) / self.gapYesterdayVixClose
qqqgappct = (currentqqqopen - self.gapYesterdayQqqClose) / self.gapYesterdayQqqClose
# Only trade if NOT in bear regime
if self.marketRegime == -1:
self.gapYesterdayVixClose = currentvixclose
self.gapYesterdayQqqClose = currentqqqclose
return
if not data.OptionChains or self.spyOption.Symbol not in data.OptionChains:
self.gapYesterdayVixClose = currentvixclose
self.gapYesterdayQqqClose = currentqqqclose
return
optionchain = data.OptionChains[self.spyOption.Symbol]
if not optionchain or len(optionchain) == 0:
self.gapYesterdayVixClose = currentvixclose
self.gapYesterdayQqqClose = currentqqqclose
return
# Call spread: VIX gap up, QQQ gap up (risk-on)
# FIX: Corrected logic for call spread.
if vixgappct > self.gapVixThreshold and qqqgappct < -self.gapQqqThreshold:
self.Log(f"GAP TRIGGERED - CALL SPREAD | VIX Gap: {vixgappct:.4f} | QQQ Gap: {qqqgappct:.4f}")
if self.submitspreadlimitorder(optionchain, call=True):
self.gapTodayProcessed = True
self.Log("HYBRID GAPSPREAD: SUBMITTED CALL SPREAD")
# Put spread: VIX gap up, QQQ gap down (fear/dip)
# FIX: Corrected logic for put spread.
elif vixgappct < -self.gapVixThreshold and qqqgappct > self.gapQqqThreshold:
self.Log(f"GAP TRIGGERED - PUT SPREAD | VIX Gap: {vixgappct:.4f} | QQQ Gap: {qqqgappct:.4f}")
if self.submitspreadlimitorder(optionchain, call=False):
self.gapTodayProcessed = True
self.Log("HYBRID GAPSPREAD: SUBMITTED PUT SPREAD")
self.gapYesterdayVixClose = currentvixclose
self.gapYesterdayQqqClose = currentqqqclose
def submitspreadlimitorder(self, optionchain, call=True):
"""Submit combo limit order for gap spreads."""
qty = 5 # Number of spreads to trade
targetdeltashort = self.gapShortDelta if call else -self.gapShortDelta
targetdeltalong = self.gapLongDelta if call else -self.gapLongDelta
optiontype = OptionRight.Call if call else OptionRight.Put
# Find short leg (selling this leg)
shorts = [x for x in optionchain
if x.Right == optiontype
and hasattr(x, 'Greeks') and x.Greeks.Delta is not None
and ((call and x.Greeks.Delta >= targetdeltashort) or (not call and x.Greeks.Delta <= targetdeltashort)) # Delta logic for short leg
and self.isliquid(x)
and 0 < (x.Expiry - self.Time).days <= self.gapDteTarget]
if not shorts:
self.Log(f"GAP: No liquid shorts found matching delta {targetdeltashort}")
return False
shortleg = min(shorts, key=lambda x: abs(x.Greeks.Delta - targetdeltashort))
# Find long leg (buying this leg)
longs = [x for x in optionchain
if x.Right == optiontype
and hasattr(x, 'Greeks') and x.Greeks.Delta is not None
and ((call and x.Greeks.Delta <= targetdeltalong) or (not call and x.Greeks.Delta >= targetdeltalong)) # Delta logic for long leg
and self.isliquid(x)
and 0 < (x.Expiry - self.Time).days <= self.gapDteTarget]
if not longs:
self.Log(f"GAP: No liquid longs found matching delta {targetdeltalong}")
return False
longleg = min(longs, key=lambda x: abs(x.Greeks.Delta - targetdeltalong))
# Verify same expiration and different strikes
if shortleg.Expiry != longleg.Expiry:
self.Log("GAP: Different expiries for short and long legs")
return False
if shortleg.Strike == longleg.Strike:
self.Log("GAP: Short and long leg strikes are the same")
return False
# Create combo order: sell 1 short leg, buy 1 long leg for each spread
legs = [
Leg.Create(shortleg.Symbol, -1), # Sell 1 short leg contract
Leg.Create(longleg.Symbol, 1) # Buy 1 long leg contract
]
# FIX: Simplified limit price calculation. It's always (bid of short - ask of long) for a credit spread.
limitprice = shortleg.BidPrice - longleg.AskPrice
# FIX: Pass 'qty' (number of spreads) to ComboLimitOrder
tickets = self.ComboLimitOrder(legs, qty, limitprice)
self.openGapSpreadTickets.extend(tickets)
return True
def isliquid(self, contract):
"""Check if option contract has sufficient liquidity."""
try:
bid = contract.BidPrice if hasattr(contract, 'BidPrice') and contract.BidPrice else 0
ask = contract.AskPrice if hasattr(contract, 'AskPrice') and contract.AskPrice else 0
if bid < self.gapMinSpreadBid:
return False
# FIX: Calculate spread as a percentage of the bid price
if bid <= 0: return False # Avoid division by zero
spread_pct = (ask - bid) / bid
oi = contract.OpenInterest if hasattr(contract, 'OpenInterest') else 0
if oi < self.gapMinOi:
return False
# FIX: Compare percentage spread to percentage threshold
if spread_pct > self.gapMaxSpreadPct:
return False
return True
except Exception as e:
self.Log(f"isliquid ERROR: {str(e)}")
return False
# ==================================================================================
# ===== FOREX BASKET STRATEGY ======================================================
# ==================================================================================
def fx_signal_check(self):
"""
Runs on a schedule to check for Forex Basket trading signals.
"""
if self.IsWarmingUp or not all(self.fxSmaIndicators[p].IsReady for p in self.fxSymbols):
return
basket_avg_price = np.mean([self.Securities[p].Price for p in self.fxSymbols if self.Securities[p].Price > 0])
if basket_avg_price == 0: return
sma_avg = np.mean([self.fxSmaIndicators[p].Current.Value for p in self.fxSymbols])
# Add current basket price to volatility window
self.fxVolatilityWindow.Add(basket_avg_price)
# --- Volatility Filter ---
if self.fxVolatilityWindow.IsReady:
prices = list(self.fxVolatilityWindow)
returns = [(prices[i] - prices[i-1]) / prices[i-1] for i in range(1, len(prices)) if prices[i-1] != 0]
if len(returns) > 1:
volatility = np.std(returns)
if volatility < self.fxVolatilityLowThreshold:
self.Log(f"FX Basket: Volatility too low ({volatility:.4f}), skipping signal check.")
return
# --- Market Extremes Filter ---
if self.fxPrevSmaAvg is not None:
sma_change = abs(sma_avg - self.fxPrevSmaAvg) / self.fxPrevSmaAvg if self.fxPrevSmaAvg != 0 else 0
if sma_change > self.fxSmaVolatilityHighThreshold:
self.Log(f"FX Basket: SMA too volatile (changed {sma_change:.2%}), skipping signal check.")
self.fxPrevSmaAvg = sma_avg
return
self.fxPrevSmaAvg = sma_avg
current_signal = None
if basket_avg_price > sma_avg * (1 + self.fxSignalThreshold):
current_signal = "BUY"
elif basket_avg_price < sma_avg * (1 - self.fxSignalThreshold):
current_signal = "SELL"
self.fxPreviousSignal = current_signal
is_invested = any(self.Portfolio[p].Invested for p in self.fxSymbols)
if not is_invested:
self.Log(f"FX Basket: Signal '{current_signal}' CONFIRMED. Placing trade. (Price: {basket_avg_price:.5f}, SMA: {sma_avg:.5f})")
self.fx_place_basket_trade(current_signal)
def fx_place_basket_trade(self, direction):
"""
Liquidates existing positions and places new limit orders to enter a position.
"""
# Cancel all open orders (entry, SL, TP) before placing new ones
for ticket in self.fxOpenTradeTickets: ticket.Cancel(f"FX Basket: New signal '{direction}' received.")
self.fxOpenTradeTickets.clear()
for ticket in self.fxOpenEntryTickets: ticket.Cancel(f"FX Basket: New signal '{direction}' received.")
self.fxOpenEntryTickets.clear()
# Liquidate any existing positions
for pair in self.fxSymbols:
if self.Portfolio[pair].Invested:
self.Liquidate(pair, f"FX Basket: New signal '{direction}' received.")
# --- AGGRESSIVE DYNAMIC POSITION SIZING ---
# WARNING: This sizing is very aggressive and likely to cause margin calls.
portfolio_value_increment = 25000
lots_per_basket_increment = 12500
num_baskets_to_trade = int(self.Portfolio.TotalPortfolioValue / portfolio_value_increment)
self.Log(f"FX Basket Sizing: Portfolio Value: ${self.Portfolio.TotalPortfolioValue:,.2f}. Affords {num_baskets_to_trade} baskets.")
if num_baskets_to_trade == 0:
self.Log("FX Basket: Trade SKIPPED. Portfolio value is less than the $25,000 required for one basket.")
return
final_lots_per_pair = lots_per_basket_increment * num_baskets_to_trade
self.Log(f"FX Basket: Placing BASKET {direction} limit orders. Lots per pair: {final_lots_per_pair:,} ({num_baskets_to_trade} baskets)")
for pair in self.fxSymbols:
price = self.Securities[pair].Price
if price == 0:
self.Log(f"FX Basket: Price for {pair} is zero. Skipping order.")
continue
order_quantity = final_lots_per_pair if direction == "BUY" else -final_lots_per_pair
ticket = self.LimitOrder(pair, order_quantity, price)
self.fxOpenEntryTickets.append(ticket)
def fx_place_bracket_orders(self, symbol):
"""
Places ATR-based Stop Loss and Take Profit orders for a given symbol.
"""
position = self.Portfolio[symbol]
if not position.Invested:
return
atr_value = self.fxAtrIndicators[symbol].Current.Value
if atr_value == 0:
self.Log(f"FX Basket: ATR for {symbol} is zero, cannot place bracket orders.")
return
entry_price = position.AveragePrice
stop_distance = atr_value * self.fxStopLossAtrMultiplier
profit_distance = stop_distance * self.fxRiskRewardRatio
if position.IsLong:
stop_price = entry_price - stop_distance
profit_price = entry_price + profit_distance
else: # IsShort
stop_price = entry_price + stop_distance
profit_price = entry_price - profit_distance
# Place Stop Loss Order
stop_loss_order = self.StopLimitOrder(symbol, -position.Quantity, stop_price, stop_price)
self.fxOpenTradeTickets.append(stop_loss_order)
# Place Take Profit Order
take_profit_order = self.LimitOrder(symbol, -position.Quantity, profit_price)
self.fxOpenTradeTickets.append(take_profit_order)
self.Log(f"FX Basket: Placed Bracket for {symbol}: Stop @ {stop_price:.5f}, TP @ {profit_price:.5f}")