Trading and Orders

Crypto Trades

Introduction

All fiat and Crypto currencies are individual assets. When you buy a pair like BTCUSDT, you trade USDT for BTC. In this case, LEAN removes some USDT from your portfolio cash book and adds some BTC. The virtual pair BTCUSDT represents your position in the trade, but the virtual pair doesn't actually exist. It simply represents an open trade.

Place Trades

When you place Crypto trades, don't use the CalculateOrderQuantitycalculate_order_quantity or SetHoldingsset_holdings methods. Instead, calculate the order quantity based on the currency amounts in your cash book and place manual orders.

The following code snippet demonstrates how to allocate 90% of your portfolio to BTC.

public override void OnData(Slice data)
{
    SetCryptoHoldings(_symbol, 0.9m);
}

private void SetCryptoHoldings(Symbol symbol, decimal percentage)
{
    var crypto = Securities[symbol] as Crypto;
    var baseCurrency = crypto.BaseCurrency;

    // Calculate the target quantity in the base currency
    var targetQuantity = percentage * (Portfolio.TotalPortfolioValue - Settings.FreePortfolioValue) / baseCurrency.ConversionRate;
    var quantity = targetQuantity - baseCurrency.Amount;

    // Round down to observe the lot size
    var lotSize = crypto.SymbolProperties.LotSize;
    quantity = Math.Round(quantity / lotSize) * lotSize;

    if (IsValidOrderSize(crypto, quantity))
    {
        MarketOrder(symbol, quantity);
    }
}

// Brokerages have different order size rules
// Binance considered the minimum volume (price x quantity):
private bool IsValidOrderSize(Crypto crypto, decimal quantity)
{
    return Math.Abs(crypto.Price * quantity) > crypto.SymbolProperties.MinimumOrderSize;
}
def on_data(self, data: Slice):
    self.set_crypto_holdings(self._symbol, .9)

def set_crypto_holdings(self, symbol, percentage):
    crypto = self.securities[symbol]
    base_currency = crypto.base_currency

    # Calculate the target quantity in the base currency
    target_quantity = percentage * (self.portfolio.total_portfolio_value - self.settings.free_portfolio_value) / base_currency.conversion_rate    
    quantity = target_quantity - base_currency.amount

    # Round down to observe the lot size
    lot_size = crypto.symbol_properties.lot_size
    quantity = round(quantity / lot_size) * lot_size

    if self.is_valid_order_size(crypto, quantity):
        self.market_order(symbol, quantity)

# Brokerages have different order size rules
# Binance considers the minimum volume (price x quantity):
def is_valid_order_size(self, crypto, quantity):
    return abs(crypto.price * quantity) > crypto.symbol_properties.minimum_order_size

The preceding example doesn't take into account order fees. You can add a 0.1% buffer to accommodate it.

The following example demonstrates how to form an equal-weighted Crypto portfolio and stay within the cash buffer.

public override void OnData(Slice data)
{
    var percentage = (1m - Settings.FreePortfolioValuePercentage) / _symbols.Count;
    foreach (var symbol in _symbols)
    {
        SetCryptoHoldings(_symbol, percentage);
    }
}
def on_data(self, data: Slice):
    percentage = (1 - self.settings.free_portfolio_value_percentage) / len(self.symbols);
    for symbol in self.symbols:
        self.set_crypto_holdings(symbol, percentage)

You can replace the Settings.FreePortfolioValuePercentagesettings.free_portfolio_value_percentage for a class variable (e.g. self.cash_buffer_cashBuffer).

When you place Crypto trades, ensure you have a sufficient balance of the base or quote currency before each trade. If you hold multiple assets and you want to put all of your capital into BTCUSDT, you need to first convert all your non-BTC assets into USDT and then purchase BTCUSDT.

By default, the account currency is USD and your starting cash is $100,000. Some Crypto exchanges don't support fiat currencies, which means you can't convert the $100,000 USD to the pair's quote currency. In this case, set your account currency to the quote currency with a positive quantity.

SetAccountCurrency("USDT", 1000);   // Set the account currency to Tether and its quantity to 1,000 USDT
self.set_account_currency("USDT", 1000)   # Set the account currency to Tether and its quantity to 1,000 USDT

For a full example of placing crypto trades, see the BasicTemplateCryptoAlgorithmBasicTemplateCryptoAlgorithm.

Liquidate Positions

If you use the Liquidateliquidate method to liquidate a Crypto position, it only liquidates the quantity of the virtual pair. Since the virtual pair BTCUSDT may not represent all of your BTC holdings, don't use the Liquidateliquidate method to liquidate Crypto positions. Instead, calculate the order quantity based on the currency amounts in your cash book and place manual orders. The following code snippet demonstrates how to liquidate a BTCUSDT position.

public override void OnData(Slice data)
{
    LiquidateCrypto(_symbol);
}

private void LiquidateCrypto(Symbol symbol)
{
    var crypto = Securities[symbol] as Crypto;
    var baseCurrency = crypto.BaseCurrency;

    // Avoid negative amount after liquidate
    var quantity = Math.Min(crypto.Holdings.Quantity, baseCurrency.Amount);
    
    // Round down to observe the lot size
    var lotSize = crypto.SymbolProperties.LotSize;
    quantity = (Math.Round(quantity / lotSize) - 1) * lotSize;

    if (IsValidOrderSize(crypto, quantity))
    {
        MarketOrder(symbol, -quantity);
    }
}
def on_data(self, data: Slice):
    self.liquidate_crypto(self._symbol)

def liquidate_crypto(self, symbol):
    crypto = self.securities[symbol]
    base_currency = crypto.base_currency

    # Avoid negative amount after liquidate
    quantity = min(crypto.holdings.quantity, base_currency.amount)
        
    # Round down to observe the lot size
    lot_size = crypto.symbol_properties.lot_size;
    quantity = (round(quantity / lot_size) - 1) * lot_size

    if self.is_valid_order_size(crypto, quantity):
        self.market_order(symbol, -quantity)

The order fees don't respect the lot size. When you try to liquidate a position, the absolute value of the base currency quantity can be less than the lot size and greater than zero. In this case, your algorithm holds a position that you can't liquidate, yet self.portfolio[symbol].investedPortfolio[symbol].Invested is Falsefalse. For more information about self.portfolio[symbol].investedPortfolio[symbol].Invested, see Holdings Status. The following code snippet demonstrates how to determine if you can liquidate a position:

public override void OnData(Slice data)
{
    var crypto = Securities[_symbol];
    if (Math.Abs(crypto.Holdings.Quantity) > crypto.SymbolProperties.LotSize)
    {
        LiquidateCrypto(_symbol);
    }
}
def on_data(self, data: Slice):
    crypto = self.securities[self._symbol]
    if abs(crypto.holdings.quantity) > crypto.symbol_properties.lot_size:
        self.liquidate_crypto(self._symbol)

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: