Problem Description
Solution
To automatically reduce your position size to avoid a margin call in QuantConnect, you should utilize two specific event handlers provided by the LEAN engine:
on_margin_call_warning: This is your proactive defense. It triggers when your remaining margin drops below 5%. You can use this to liquidate a portion of your portfolio to free up buying power before a forced liquidation occurs.on_margin_call: This is your reactive defense. It triggers when a margin call is actually issued. By default, LEAN liquidates positions to satisfy the margin requirement. You can override this to control which assets are sold (e.g., selling losing positions first).
Strategy Implementation
The following algorithm demonstrates how to implement both handlers. It simulates a high-leverage scenario to demonstrate how the bot reacts by reducing positions.
# region imports
from AlgorithmImports import *
# endregion
class MarginManagementAlgorithm(QCAlgorithm):
def initialize(self):
self.set_start_date(2020, 1, 1)
self.set_end_date(2021, 1, 1)
self.set_cash(100000)
# Add a volatile asset and a stable asset
self.spy = self.add_equity("SPY", Resolution.MINUTE).symbol
self.tsla = self.add_equity("TSLA", Resolution.MINUTE).symbol
# Set leverage high to increase the probability of margin issues for demonstration
self.securities[self.spy].set_leverage(2.0)
self.securities[self.tsla].set_leverage(2.0)
# Schedule a buy event
self.schedule.on(self.date_rules.every_day(self.spy),
self.time_rules.after_market_open(self.spy, 30),
self.rebalance)
def rebalance(self):
# Intentionally use high leverage (near 100% of buying power)
if not self.portfolio.invested:
self.set_holdings([
PortfolioTarget(self.spy, 0.95),
PortfolioTarget(self.tsla, 0.95)
])
def on_data(self, data: Slice):
# Standard data processing
pass
def on_margin_call_warning(self):
"""
Triggered when there is less than 5% margin remaining.
We proactively reduce leverage here to avoid the actual margin call.
"""
self.log(f"MARGIN WARNING at {self.time}: Margin remaining < 5%. Reducing positions.")
# Strategy: Liquidate 10% of all invested positions to free up cash
for symbol, holding in self.portfolio.items():
if holding.invested:
# Calculate 10% of the current quantity
quantity_to_reduce = int(holding.quantity * 0.10)
# Ensure we don't submit an order for 0 shares
if quantity_to_reduce != 0:
self.market_order(symbol, -quantity_to_reduce)
self.log(f"Proactive Reduction: Sold {quantity_to_reduce} of {symbol}")
def on_margin_call(self, requests: List[SubmitOrderRequest]) -> List[SubmitOrderRequest]:
"""
Triggered when the portfolio has crossed the margin threshold.
LEAN generates a list of 'requests' (orders) to satisfy the margin call.
We can modify this list to choose WHICH assets to sell.
"""
self.log(f"MARGIN CALL at {self.time}: Executing forced liquidation logic.")
# Strategy: Sort the liquidation requests to sell the positions with the
# highest Unrealized Loss first (Stop the bleeding).
# Helper function to get unrealized profit percent
def get_unrealized_profit(request):
if request.symbol in self.portfolio:
return self.portfolio[request.symbol].unrealized_profit_percent
return 0
# Sort requests: Lowest profit (highest loss) first
sorted_requests = sorted(requests, key=get_unrealized_profit)
# Log our decision
for order in sorted_requests:
self.log(f"Margin Call Liquidation Priority: {order.symbol}")
# Return the sorted list. LEAN will execute these orders until margin is satisfied.
return sorted_requests
Key Components Explained
-
on_margin_call_warningLogic:- This method checks
self.portfolio.items()to find invested securities. - It calculates 10% of the current quantity (
holding.quantity * 0.10) and places amarket_orderto close that portion. - Benefit: This often prevents the broker (or the engine) from forcefully closing your entire position at an unfavorable price.
- This method checks
-
on_margin_callLogic:- This method receives
requests, which is a list ofSubmitOrderRequestobjects that the LEAN engine has calculated are necessary to fix the margin deficit. - We do not create new orders here; instead, we sort the existing requests.
- In the example,
sorted(requests, key=get_unrealized_profit)ensures that the algorithm sells the assets performing the worst first. - Benefit: This allows you to keep your winning trades open while sacrificing losing trades to satisfy margin requirements.
- This method receives
Important Considerations
- Execution Order:
on_margin_callorders are executed synchronously immediately after the method returns. - Buying Power: Ensure you understand the specific
BuyingPowerModelof the securities you are trading (e.g.,PatternDayTradingMarginModelvsSecurityMarginModel), as this dictates when these events are triggered. - Live Trading: In live trading, brokerage margin calls can be severe. The
on_margin_call_warningis a soft warning generated by LEAN's internal tracking, not necessarily a message from the broker. It relies on LEAN's data matching the broker's state accurately.
Q&A
Q: Can I use on_margin_call_warning to deposit virtual cash in a backtest?
A: No, you cannot inject cash during a backtest to solve a margin call. You must reduce holdings. However, in live trading, you could theoretically use the on_margin_call_warning to send an alert (email/SMS) to yourself to deposit funds, though the algorithm itself must still handle the immediate risk.
Q: What happens if I return an empty list in on_margin_call?
A: If you return an empty list or do not satisfy the margin requirement, LEAN will likely throw a runtime error or forcefully liquidate positions in subsequent steps to protect the brokerage model integrity. You must return enough orders to satisfy the margin debt.
Q: Does on_margin_call_warning work for crypto futures?
A: Yes, provided the BuyingPowerModel is correctly set for the crypto future. However, crypto markets are highly volatile; the gap between a 5% warning and liquidation can happen in milliseconds. It is safer to manage position sizing strictly in on_data rather than relying solely on the warning handler.