Key Concepts
Multi-Asset Modeling
Asset Portfolio
The portfolio manages the individual securities it contains. It tracks the cost of holding each security. It aggregates the performance of the individual securities in the portfolio to produce statistics like net profit and drawdown. The portfolio also holds information about each currency in its cashbook.
Cashbooks
We designed LEAN to be a multi-currency platform. LEAN can trade Forex, Cryptocurrencies, and other assets that are quoted in other currencies. A benefit of supporting multiple currencies is that as we add new asset classes from new countries, LEAN is already prepared to transact in those assets by using their quote currency. For instance, we added the India Equity market, which quotes assets in the INR currency.
The portfolio manages your currencies in its cashbook, which models the cash as a ledger of transactions. When you buy assets, LEAN uses the currencies in your cashbook to purchase the asset and pay the transaction fees. For more information about the cashbook, see Cashbook.
Buying Power
We model the margin requirements of each asset and reflect that in the buying power available to the algorithm. We source Futures margins from CME SPAN margins. Equity margin is 2x for standard margin accounts and 4x intraday for Pattern Day Trading accounts. For more information about buying power modeling, see Buying Power.
Examples
The following examples demonstrate some common practices for multi-asset modeling.
Example 1: BTC Spot-Crypto Future Arbitration
This algorithm demonstrates an arbitration between Spot and Crypto Future BTCUSDT using a BTC cash account. If one's price is above 0.5% of another's, we sell the relatively overpriced one and buy the counter side. To ensure the cash position is sufficient to open the buy position, we order the buy side after the sell side is filled and the cash is replenished.
public class MultiAssetModelingAlgorithm : QCAlgorithm
{
private Crypto _spot;
private CryptoFuture _future;
private decimal _threshold = 0.005m, _tradeQuantity;
public override void Initialize()
{
SetStartDate(2024, 9, 1);
SetEndDate(2024, 12, 31);
// Seed the last price to set the initial price of the BTCUSDT holdings.
SetSecurityInitializer(new BrokerageModelSecurityInitializer(BrokerageModel, new FuncSecuritySeeder(GetLastKnownPrices)));
// Simulate a cash Bybit account.
SetBrokerageModel(BrokerageName.Bybit, AccountType.Cash);
SetAccountCurrency("USDT", 0);
var spotBalance = 2; // BTC
SetCash("BTC", spotBalance);
// Request BTCUSD spot and future data to trade their price discrepancies.
_spot = AddCrypto("BTCUSDT", market: Market.Bybit);
_future = AddCryptoFuture("BTCUSDT", market: Market.Bybit);
// Simulate the portfolio is holding BTC cash initially via BTCUSDT position.
// Note that the performance will also be affected by the BTC performance in the default account currency.
_spot.Holdings.SetHoldings(averagePrice: _spot.Price, quantity: spotBalance);
_tradeQuantity = spotBalance/2.0m;
}
public override void OnData(Slice slice)
{
// Wait for the Slice to contain QuoteBar objects for both assets.
if (!(slice.QuoteBars.ContainsKey(_spot.Symbol) && slice.QuoteBars.ContainsKey(_future.Symbol)))
{
return;
}
// If the spot price is higher than the future price more than the threshold,
// Do arbitration by selling the spot BTC and buying the future.
if (_spot.Price >= _future.Price * (1m + _threshold) && !_future.Holdings.IsLong)
{
Sell(_spot.Symbol, _tradeQuantity);
}
// If the future price is higher than the spot price more than the threshold,
// Do arbitration by buying the spot BTC and selling the future.
if (_future.Price >= _spot.Price * (1m + _threshold) && _future.Holdings.IsLong)
{
Sell(_future.Symbol, _tradeQuantity);
}
}
public override void OnOrderEvent(OrderEvent orderEvent)
{
// Order the buy-side of the arb only when the BTC is sold and USDT is obtained.
if (!(orderEvent.Quantity < 0 && orderEvent.Status == OrderStatus.Filled))
{
return;
}
Security security = orderEvent.Symbol == _spot.Symbol ? _future : _spot;
// Calculate the initial margin needed. We must sell the same amount of BTC to obtain sufficient USDT for trade.
var margin = security.BuyingPowerModel.GetInitialMarginRequirement(
new InitialMarginParameters(security, _tradeQuantity)
).Value;
// Check if USDT cash is sufficient to open the position.
if (Portfolio.CashBook["USDT"].Amount >= margin)
{
Buy(security.Symbol, _tradeQuantity);
}
}
} class MultiAssetModelingAlgorithm(QCAlgorithm):
_threshold = 0.005
def initialize(self) -> None:
self.set_start_date(2024, 9, 1)
self.set_end_date(2024, 12, 31)
# Seed the last price to set the initial price of the BTCUSDT holdings.
self.set_security_initializer(
BrokerageModelSecurityInitializer(
self.brokerage_model,
FuncSecuritySeeder(self.get_last_known_prices)
)
)
# Simulate a cash Bybit account.
self.set_brokerage_model(BrokerageName.BYBIT, AccountType.CASH)
self.set_account_currency("USDT", 0)
spot_balance = 2 # BTC
self.set_cash('BTC', spot_balance)
# Request BTCUSD spot and future data to trade their price discrepancies.
self._spot = self.add_crypto("BTCUSDT", market=Market.BYBIT)
self._future = self.add_crypto_future("BTCUSDT", market=Market.BYBIT)
# Simulate the portfolio is holding BTC cash initially via BTCUSDT position.
# Note that the performance will also be affected by the BTC performance in the default account currency.
self._spot.holdings.set_holdings(average_price=self._spot.price, quantity=spot_balance)
self._trade_quantity = spot_balance/2
def on_data(self, slice: Slice) -> None:
# Wait for the Slice to contain QuoteBar objects for both assets.
if not(self._spot.symbol in slice.quote_bars and self._future.symbol in slice.quote_bars):
return
# If the spot price is higher than the future price more than the threshold,
# Do arbitration by selling the spot BTC and buying the future.
if self._spot.price >= self._future.price * (1 + self._threshold) and not self._future.holdings.is_long:
self.sell(self._spot.symbol, self._trade_quantity)
# If the future price is higher than the spot price more than the threshold,
# Do arbitration by buying the spot BTC and selling the future.
if self._future.price >= self._spot.price * (1 + self._threshold) and self._future.holdings.is_long:
self.sell(self._future.symbol, self._trade_quantity)
def on_order_event(self, order_event: OrderEvent) -> None:
# Order the buy-side of the arb only when the BTC is sold and USDT is obtained.
if not(order_event.quantity < 0 and order_event.status == OrderStatus.FILLED):
return
security = self._future if order_event.symbol == self._spot.symbol else self._spot
# Calculate the initial margin needed, we need to sell the same amount of BTC to obtain sufficient USDT for trade.
margin = security.buying_power_model.get_initial_margin_requirement(
InitialMarginParameters(security, self._trade_quantity)
).value
# Check if USDT cash is sufficient to open the position.
if self.portfolio.cash_book["USDT"].amount >= margin:
self.buy(security.symbol, self._trade_quantity)
Example 2: BTC Spot-Future Arbitration
This algorithm implements a similar logic to the above example of an arbitration algorithm between spot BTCUSD and Future BTC using a cash account. If one's price is above 0.5% of another's, we sell the relatively overpriced one and buy the counter side. Note that we need to calculate the future value of the Future fairly compared to the spot BTC. Also, we need to rollover any Futures contracts.
public class MultiAssetModelingAlgorithm : QCAlgorithm
{
private Crypto _spot;
private Future _future;
private decimal _threshold = 0.005m;
public override void Initialize()
{
SetStartDate(2024, 9, 1);
SetEndDate(2024, 12, 31);
// Set cash to sell Futures.
SetCash(1000000);
// Seed the price of each asset with its last known price to
// avoid trading errors.
SetSecurityInitializer(
new BrokerageModelSecurityInitializer(
BrokerageModel, new FuncSecuritySeeder(GetLastKnownPrices)
)
);
// Add BTCUSD spot and Future data to trade their price
// discrepancies.
_spot = AddCrypto("BTCUSD", market: Market.Coinbase);
_future = AddFuture(Futures.Currencies.BTC, dataMappingMode: DataMappingMode.OpenInterest);
_future.SetFilter(0, 62);
}
public override void OnData(Slice slice)
{
// Get the current bar for both markets.
if (!slice.Bars.TryGetValue(_spot.Symbol, out var spot) || !slice.QuoteBars.TryGetValue(_future.Mapped, out var future))
{
return;
}
// Use forward price to compare to spot BTC price fairly.
var discountFactor = RiskFreeInterestRateModel.GetInterestRate(slice.Time) * (decimal)(_future.Mapped.ID.Date - slice.Time).TotalSeconds / 60m / 60m / 24m / 365m;
var btcFutureFV = future.Close * Convert.ToDecimal(Math.Exp((double)discountFactor));
// If the spot price is above the Future price by more than the
// threshold, sell the spot BTC and buy the Future to arbitrage.
if (!_spot.Holdings.IsShort && spot.Close >= btcFutureFV * (1m + _threshold))
{
Sell(_spot.Symbol, _future.SymbolProperties.ContractMultiplier);
Buy(_future.Mapped, 1m);
}
// If the Future price is above the spot price by more than the
// threshold, sell the Future BTC and buy the spot BTC to arbitrage.
else if (!_spot.Holdings.IsLong && btcFutureFV >= spot.Close * (1m + _threshold))
{
Sell(_future.Mapped, 1m);
Buy(_spot.Symbol, _future.SymbolProperties.ContractMultiplier);
}
// When prices converge, exit the arbitrage trade.
else if ((_spot.Holdings.IsLong && btcFutureFV <= spot.Close) || (_spot.Holdings.IsShort && spot.Close <= btcFutureFV))
{
Liquidate();
}
}
public override void OnSymbolChangedEvents(SymbolChangedEvents symbolChangedEvents)
{
// Get Symbol Change Event of the Continuous Future (change
// in mapped contract) to roll over.
if (!symbolChangedEvents.TryGetValue(_future.Symbol, out var changedEvent))
{
return;
}
var oldSymbol = changedEvent.OldSymbol;
// Add the new contract.
var newSymbol = AddFutureContract(changedEvent.NewSymbol).Symbol;
var tag = $"Rollover - Symbol changed at {Time}: {oldSymbol} -> {newSymbol}";
var quantity = Portfolio[oldSymbol].Quantity;
// Rolling over: liquidate the position of the old mapped
// contract and switch to the new mapped contract.
Liquidate(oldSymbol, tag: tag);
if (quantity != 0)
{
MarketOrder(newSymbol, quantity, tag: tag);
}
}
} class MultiAssetModelingAlgorithm(QCAlgorithm):
_threshold = 0.005
def initialize(self) -> None:
self.set_start_date(2024, 9, 1)
self.set_end_date(2024, 12, 31)
# Set cash to sell Futures.
self.set_cash(1_000_000)
# Seed the price of each asset with its last known price to
# avoid trading errors.
self.set_security_initializer(
BrokerageModelSecurityInitializer(
self.brokerage_model,
FuncSecuritySeeder(self.get_last_known_prices)
)
)
# Add BTCUSD spot and Future data to trade their price
# discrepancies.
self._spot = self.add_crypto("BTCUSD", market=Market.COINBASE)
self._future = self.add_future(
Futures.Currencies.BTC,
data_mapping_mode=DataMappingMode.OPEN_INTEREST
)
self._future.set_filter(0, 62)
def on_data(self, slice: Slice) -> None:
# Get the current bar for both markets.
spot = slice.bars.get(self._spot.symbol)
future = slice.quote_bars.get(self._future.mapped)
if not (spot and future):
return
# Use forward price fairly to compare to the spot BTC price.
discount_factor = (
self.risk_free_interest_rate_model.get_interest_rate(slice.time)
* (self._future.mapped.id.date - slice.time).total_seconds()
/ 60 / 60 / 24 / 365
)
btc_future_fv = future.close * np.exp(discount_factor)
# If the spot price is above the Future price by more than the
# threshold, sell the spot BTC and buy the Future to arbitrage.
if (not self._spot.holdings.is_short and
spot.close >= btc_future_fv * (1 + self._threshold)):
self.sell(
self._spot.symbol,
self._future.symbol_properties.contract_multiplier
)
self.buy(self._future.mapped, 1)
# If the Future price is above the spot price by more than the
# threshold, sell the Future BTC and buy the spot BTC to arbitrage.
elif (not self._spot.holdings.is_long and
btc_future_fv >= spot.close * (1 + self._threshold)):
self.sell(self._future.mapped, 1)
self.buy(
self._spot.symbol,
self._future.symbol_properties.contract_multiplier
)
# When prices converge, exit the arbitrage trade.
elif ((self._spot.holdings.is_long and btc_future_fv <= spot.close) or
(self._spot.holdings.is_short and spot.close <= btc_future_fv)):
self.liquidate()
def on_symbol_changed_events(
self, symbol_changed_events: SymbolChangedEvents) -> None:
# Get Symbol Change Event of the Continuous Future (change
# in mapped contract) to roll over.
changed_event = symbol_changed_events.get(self._future.symbol)
if not changed_event:
return
old_symbol = changed_event.old_symbol
# Add the new contract.
new_symbol = self.add_future_contract(changed_event.new_symbol).symbol
tag = f"Rollover - Symbol changed at {self.time}: {old_symbol} -> {new_symbol}"
quantity = self.portfolio[old_symbol].quantity
# Rolling over: liquidate the position of the old mapped
# contract and switch to the new mapped contract.
self.liquidate(old_symbol, tag=tag)
if quantity:
self.market_order(new_symbol, quantity, tag=tag)