book
Checkout our new book! Hands on AI Trading with Python, QuantConnect, and AWS Learn More arrow

Trading and Orders

Position Sizing

Introduction

LEAN provides several methods to help you set specific portfolio weights for assets. These methods account for lot size and pre-calculated order fees.

Single Asset Targets

The set_holdings method calculates the number of asset units to purchase based on the portfolio weight you provide and then submits market orders. This method provides a quick way to set up a portfolio with a set of weights for assets. If you already have holdings, you may want to liquidate the existing holdings first to free up buying power.

Select Language:
 # Allocate 50% of buying power to IBM
self.set_holdings("IBM", 0.5)

# Allocate 50% of portfolio value to IBM, but liquidate other holdings first
self.set_holdings("IBM", 0.5, True)

# Provide a tag and order properties to the SetHoldings method
self.set_holdings(symbol, weight, liquidate_existing_holdings, tag, order_properties)

If the percentage you provide translates to an order quantity of 0 or to a small quantity that doesn't exceed the minimum_order_margin_portfolio_percentage algorithm setting, then the set_holdings method doesn’t place an order and doesn’t log anything.

Multiple Asset Targets

When you trade a weighted basket of assets, sometimes you must intelligently scale down existing positions before increasing allocations to other assets. If you call the set_holdings method with a list of PortfolioTarget objects, LEAN sorts the orders based on your position delta and then places the orders that reduce your position size in an asset before it places orders that increase your position size in an asset. When you call the set_holdings method with a list of PortfolioTarget objects, the decimal you pass to the PortfolioTarget constructor represents the portfolio weight. In this situation, don't use the PortfolioTarget.percent method.

Select Language:
# Purchase a portfolio of targets, processing orders intelligently.
self.set_holdings([PortfolioTarget("SPY", 0.8), PortfolioTarget("IBM", 0.2)]))

If you want the targets you pass to define the target portfolio state, enable the liquidate_existing_holdings argument.

Select Language:
self.set_holdings([PortfolioTarget("SPY", 0.8), PortfolioTarget("IBM", 0.2)], True)

The method also accepts tag and order properties arguments.

Select Language:
self.set_holdings(targets, liquidate_existing_holdings, tag, order_properties)

If the percentage you provide translates to an order quantity of 0 or to a small quantity that doesn't exceed the minimum_order_margin_portfolio_percentage algorithm setting, then the set_holdings method doesn’t place an order and doesn’t log anything.

Calculate Order Quantities

The set_holdings method uses market order to reach the target portfolio weight you provide. If you want to use a different order type, you need to specify the quantity to trade. To calculate the number of units to trade based on a portfolio weight, call the calculate_order_quantity method. The method calculates the quantity based on the current price of the asset and adjusts it for the fee model of the security. The target weight you provide is an unleveraged value. For instance, if you have 2x leverage and request a 100% weight, the method calculates the quantity that uses half of your available margin.

Select Language:
 # Calculate the fee-adjusted quantity of shares with given buying power
quantity = self.calculate_order_quantity("IBM", 0.4)

Buying Power Buffer

If you place a market on open order near the market close, the market can gap overnight to a worse open price. If the gap is large enough against your trade direction, you may not have sufficient buying power at the open to fill your trade. To ensure a high probability of order fills through market gaps and discontinuities, the set_holdings method assumes a 0.25% cash buffer. If LEAN rejects your orders due to buying power, widen the cash buffer through the algorithm settings property.

Select Language:
# Set the cash buffer to 5%
self.settings.free_portfolio_value_percentage = 0.05

# Set the cash buffer to $10,000
self.settings.free_portfolio_value = 10000

If you use free_portfolio_value_percentage, you must set it in the initialize or post_initialize event handler. If you use free_portfolio_value, you must set it after the post_initialize event handler.

Examples

The following examples demonstrate some common practices for position sizing.

Example 1: Covered Call

Select Language:
class PositionSizingAlgorithm(QCAlgorithm):
    def initialize(self) -> None:
        self.set_start_date(2023, 1, 1)
        self.set_end_date(2023, 8, 1)
        # Seed the price to ensure securities have information for margin calculations.
        self.set_security_initializer(BrokerageModelSecurityInitializer(self.brokerage_model, FuncSecuritySeeder(self.get_last_known_prices)))
        
        # Request SPY data for trading. We use raw price data to compare the strike price fairly for ATM contract selection.
        self.spy = self.add_equity("SPY", data_normalization_mode=DataNormalizationMode.RAW).symbol
        
        # Set a scheduled event to rebalance the covered call positions.
        self.schedule.on(
            self.date_rules.week_start(self.spy),
            self.time_rules.after_market_open(self.spy, 1),
            self.rebalance
        )

    def rebalance(self) -> None:
        option_chain = self.option_chain(self.spy)
        # Filter for the calls within 1-week expiry.
        expiry = max(x.expiry for x in option_chain if x.expiry <= self.time + timedelta(6))
        filtered = [x for x in option_chain if x.right == OptionRight.CALL and x.expiry == expiry]
        # Select the ATM call.
        atm_call = sorted(filtered, key=lambda x: abs(x.strike - self.securities[self.spy].price))[0]
        atm_call = self.add_option_contract(atm_call).symbol

        # Calculate the order size for the covered call.
        quantity = self.calculate_order_quantity(self.spy, 0.5) // 100
        # Covered call involves 100 shares of SPY and -1 ATM call contract.
        self.combo_market_order([
            Leg.create(self.spy, 100),
            Leg.create(atm_call, -1)
        ], quantity,
        asynchronous=True)

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: