Key Concepts

Multi-Asset Modeling

Introduction

We designed LEAN as a multi-asset platform with out-of-the-box support for multiple securities, so you can model complex portfolios like hedge funds.

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 Symbol _btcusdt, _btcFuture;
    private decimal _threshold = 0.005m;

    public override void Initialize()
    {
        SetStartDate(2022, 5, 1);
        SetEndDate(2024, 1, 1);
        // 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");

        // Request BTCUSD spot and future data to trade their price discrepancies.
        var btcusdt = AddCrypto("BTCUSDT", Resolution.Minute, market: Market.Bybit);
        _btcusdt = btcusdt.Symbol;
        _btcFuture = AddCryptoFuture("BTCUSDT", Resolution.Minute, market: Market.Bybit).Symbol;

        // 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.
        Portfolio[_btcusdt].SetHoldings(averagePrice: btcusdt.Price, quantity: 2);
    }

    public override void OnData(Slice slice)
    {
        if (slice.QuoteBars.TryGetValue(_btcusdt, out var spot) && slice.QuoteBars.TryGetValue(_btcFuture, out var future))
        {
            // 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.Close >= future.Close * (1m + _threshold) && !Portfolio[_btcusdt].IsShort)
            {
                Sell(_btcusdt, 1m);
            }
            // 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.Close >= spot.Close * (1m + _threshold) && !Portfolio[_btcusdt].IsLong)
            {
                Sell(_btcFuture, 1m);
            }
        }
    }

    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)
        {
            var toBuySymbol = orderEvent.Symbol == _btcusdt ? _btcFuture : _btcusdt;
            // Calculate the initial margin needed. We must sell the same amount of BTC to obtain sufficient USDT for trade.
            var margin = CalculateInitialMargin(toBuySymbol);
            // Check if USDT cash is sufficient to open the position.
            if (Portfolio.CashBook["USDT"].Amount >= margin)
            {
                Buy(toBuySymbol, 1m);
            }
        }
    }

    private decimal CalculateInitialMargin(Symbol toBuy)
    {
        // Calculate the initial margin of the symbol on the long side.
        var security = Securities[toBuy];
        var parameter = new InitialMarginParameters(security, 1m);
        var initialMargin = security.BuyingPowerModel.GetInitialMarginRequirement(parameter);
        return initialMargin.Value;
    }
}
class MultiAssetModelingAlgorithm(QCAlgorithm):
    threshold = 0.005

    def initialize(self) -> None:
        self.set_start_date(2022, 5, 1)
        self.set_end_date(2024, 1, 1)
        # 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")

        # Request BTCUSD spot and future data to trade their price discrepancies.
        btcusdt = self.add_crypto("BTCUSDT", Resolution.MINUTE, market=Market.BYBIT)
        self.btcusdt = btcusdt.symbol
        self.btc_future = self.add_crypto_future("BTCUSDT", Resolution.MINUTE, market=Market.BYBIT).symbol

        # 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.portfolio[self.btcusdt].set_holdings(average_price=btcusdt.price, quantity=2)

    def on_data(self, slice: Slice) -> None:
        spot = slice.quote_bars.get(self.btcusdt)
        future = slice.quote_bars.get(self.btc_future)
        if spot and future:
            # 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.close >= future.close * (1 + self.threshold) and not self.portfolio[self.btcusdt].is_short:
                self.sell(self.btcusdt, 1)
            # 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.close >= spot.close * (1 + self.threshold) and not self.portfolio[self.btcusdt].is_long:
                self.sell(self.btc_future, 1)

    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 order_event.quantity < 0 and order_event.status == OrderStatus.FILLED:
            to_buy_symbol = self.btc_future if order_event.symbol == self.btcusdt else self.btcusdt
            # Calculate the initial margin needed, we need to sell the same amount of BTC to obtain sufficient USDT for trade.
            margin = self.calculate_initial_margin(to_buy_symbol)
            # Check if USDT cash is sufficient to open the position.
            if self.portfolio.cash_book["USDT"].amount >= margin:
                self.buy(to_buy_symbol, 1)

    def calculate_initial_margin(self, to_buy: Symbol) -> None:
        # Calculate the initial margin of the symbol on the long side.
        security = self.securities[to_buy]
        parameter = InitialMarginParameters(security, 1)
        initial_margin = security.buying_power_model.get_initial_margin_requirement(parameter)
        return initial_margin.value

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 Symbol _btcusd;
    private Future _btcFuture;
    private decimal _threshold = 0.005m;

    public override void Initialize()
    {
        SetStartDate(2021, 1, 1);
        SetEndDate(2024, 1, 1);
        // Set cash to sell Futures.
        SetCash(10000000);
        // Seed the last price to set the initial price of the BTCUSD holdings.
        SetSecurityInitializer(new BrokerageModelSecurityInitializer(BrokerageModel, new FuncSecuritySeeder(GetLastKnownPrices)));

        // Request BTCUSD spot and future data to trade their price discrepancies.
        _btcusd = AddCrypto("BTCUSD", Resolution.Minute, market: Market.Coinbase).Symbol;
        _btcFuture = AddFuture(Futures.Currencies.BTC, Resolution.Minute, dataMappingMode: DataMappingMode.OpenInterest);
    }

    public override void OnData(Slice slice)
    {
        var mappedFuture = _btcFuture.Mapped;
        if (slice.QuoteBars.TryGetValue(_btcusd, out var spot) && slice.QuoteBars.TryGetValue(_btcFuture.Symbol, out var future))
        {
            // Use forward price to compare to spot BTC price fairly.
            var discountFactor = RiskFreeInterestRateModel.GetInterestRate(slice.Time) * (mappedFuture.ID.Date - slice.Time).Days / 365m;
            var btcFutureFV = future.Close * Convert.ToDecimal(Math.Exp((double)discountFactor));
            // 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.Close >= btcFutureFV * (1m + _threshold) && !Portfolio[_btcusd].IsShort)
            {
                Sell(_btcusd, _btcFuture.SymbolProperties.ContractMultiplier);
                Buy(mappedFuture, 1m);
            }
            // 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.
            else if (btcFutureFV >= spot.Close * (1m + _threshold) && !Portfolio[_btcusd].IsLong)
            {
                Sell(mappedFuture, 1m);
                Buy(_btcusd, _btcFuture.SymbolProperties.ContractMultiplier);
            }
            // Capitalize the arbitrage positions when prices converge.
            else if ((spot.Close <= btcFutureFV && Portfolio[_btcusd].IsShort) || (btcFutureFV <= spot.Close && Portfolio[_btcusd].IsLong))
            {
                Liquidate();
            }
        }

        // Get Symbol Change Event of the Continuous Future (change in mapped contract) to roll over.
        if (slice.SymbolChangedEvents.TryGetValue(_btcFuture.Symbol, out var changedEvent))
        {
            var oldSymbol = changedEvent.OldSymbol;
            // Subscribe to the trading data for the new symbol.
            var newSymbol = AddFutureContract(changedEvent.NewSymbol).Symbol;
            var tag = $"Rollover - Symbol changed at {Time}: {oldSymbol} -> {newSymbol}";
            var quantity = Portfolio[oldSymbol].Quantity;
            // Rolling over: to liquidate any position of the old mapped contract and switch to the newly 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(2021, 1, 1)
        self.set_end_date(2024, 1, 1)
        # Set cash to sell Futures.
        self.set_cash(10000000)
        # Seed the last price to set the initial price of the BTCUSD holdings.
        self.set_security_initializer(BrokerageModelSecurityInitializer(self.brokerage_model, FuncSecuritySeeder(self.get_last_known_prices)))

        # Request BTCUSD spot and future data to trade their price discrepancies.
        self.btcusd = self.add_crypto("BTCUSD", Resolution.MINUTE, market=Market.COINBASE).symbol
        self.btc_future = self.add_future(Futures.Currencies.BTC, Resolution.MINUTE, data_mapping_mode=DataMappingMode.OPEN_INTEREST)

    def on_data(self, slice: Slice) -> None:
        mapped_future = self.btc_future.mapped
        spot = slice.quote_bars.get(self.btcusd)
        future = slice.quote_bars.get(mapped_future)
        if spot and future:
            # Use forward price fairly to compare to the spot BTC price.
            discount_factor = self.risk_free_interest_rate_model.get_interest_rate(slice.time) * (mapped_future.id.date - slice.time).total_seconds() / 60 / 60 / 24 / 365
            btc_future_fv = future.close * np.exp(discount_factor)
            # 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.close >= btc_future_fv * (1 + self.threshold) and not self.portfolio[self.btcusd].is_short:
                self.sell(self.btcusd, self.btc_future.symbol_properties.contract_multiplier)
                self.buy(mapped_future, 1)
            # 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.
            elif btc_future_fv >= spot.close * (1 + self.threshold) and not self.portfolio[self.btcusd].is_long:
                self.sell(mapped_future, 1)
                self.buy(self.btcusd, self.btc_future.symbol_properties.contract_multiplier)
            # Capitalize the arbitrage positions when prices converge.
            elif (btc_future_fv <= spot.close and self.portfolio[self.btcusd].is_long) or (spot.close <= btc_future_fv and self.portfolio[self.btcusd].is_short):
                self.liquidate()
                
        # Get Symbol Change Event of the Continuous Future (change in mapped contract) to roll over.
        changed_event = slice.symbol_changed_events.get(self.btc_future.symbol)
        if changed_event:
            old_symbol = changed_event.old_symbol
            # Subscribe to the trading data for the new symbol.
            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: to liquidate any position of the old mapped contract and switch to the newly mapped contract
            self.liquidate(old_symbol, tag = tag)
            if quantity != 0:
                self.market_order(new_symbol, quantity, tag = tag)

You can also see our Videos. You can also get in touch with us via Discord.

Did you find this page helpful?

Contribute to the documentation: