In this post, we will cover the basics of portfolio construction and alpha model trading strategies. We will show how to build a simple portfolio construction strategy using Python and Trading Strategy’s backtesting framework.
The target audience of this post is quantitative finance developers and people who want to learn algorithmic trading.
What is portfolio construction?
Portfolio construction is a trading strategy method method of selecting securities optimally to achieve maximum returns while taking a minimum risk.
It involves understanding how different asset classes, funds, and weightings impact each other and an investor’s objectives.
Portfolio construction consists of several phases:
- Asset allocation models - to determine the optimal mix of asset classes (stocks, bonds, and commodities) in a portfolio based on historical returns, volatility, and correlations.
- Optimization techniques - identifying the best combination of individual securities within each asset class based on factors such as expected return, risk, and liquidity.
- Risk management tools - such as stop-loss orders, hedging strategies, and diversification techniques, to manage portfolio risk and reduce exposure to individual assets or market risks
- Alpha generation strategies - so-called alpha models may include different trading strategies, such as factor investing, statistical arbitrage, and trend-following, to identify assets and ways to trade them to outperform or underperform the broader market.
How does portfolio construction work?
Portfolio construction works by rebalancing the portfolio assets based on the new allocation. Rebalancing usually occurs at fixed intervals, like weekly. The total equity (assets + cash) is redeployed - assets receiving less weight are sold, while those with a stronger alpha signal are bought. The individual per cent weight of an asset is decided by some optimisation and risk management criteria.
What is an alpha model?
An alpha model is a mathematical or quantitative framework used to generate trading signals that can be used in portfolio construction.
The alpha model seeks to identify assets likely to outperform or underperform their peers based on various factors and variables.
Alpha models can be constructed using various techniques, such as statistical analysis, machine learning algorithms, or financial modelling. The inputs to an alpha model may include company financial statements, price and volume data, macroeconomic indicators, and other market data.
Once an alpha model generates trading signals, the portfolio manager can use those signals to construct a portfolio that aims to generate alpha (i.e., excess returns) relative to a benchmark index.
What is Trading Strategy Framework?
The Trading Strategy Framework is a Python-based software development framework to develop automated trading strategies for decentralised finance markets.
The framework consists of
- Market data feed reader in the form of Trading Strategy Client
- Backtesting framework
- Live trade execution environment for decentralised markets
The core audience of the library are quants.
How does the Trading Strategy framework support creating portfolio construction strategies?
The Trading Strategy Framework provides functionality for:
- Setting up a tradeable asset universe from assets traded on decentralised finance markets
- Writing a trading strategy logic using a well-formed interface that transforms input data into trades
- Supports alpha model that allows easy writing strategies for portfolio construction
The workflow for the framework is as follows:
- Develop and backtest your strategies using Jupyter Notebook
- Visualise and analyse the performance of your trading strategy using various tools and methods, like performance summary statistics, equity curve, tracking the performance of individual positions
- Take your trading strategy backtested code unmodified to the live trading execution environment.
The strategy core logic
The Trading Strategy Framework offers two functions the developer must implement for the strategies:
- create_trading_universe() that returns an object representing all assets the strategy can trade. This data is used to set up and update backtesting and live market data feeds. This includes blockchains, exchanges, trading pairs, OHLCV data feeds, liquidity data feeds and some special data feeds e.g. used for stop loss triggers.
- decide_trades() takes in the current strategy cycle, timestamped trading universe and the current strategy state (open positions) as input. Based on this data, the function will return a list of new trades that will either open new or close existing positions
The strategy advances in ticks. Each tick length is the duration of a strategy cycle. Common strategy cycles include hourly, daily, and weekly trade decisions. In portfolio construction, this strategy cycle is called rebalance.
Overview of portfolio construction strategy architecture
The Trading Strategy framework offers Python “lego blocks” that allow you to easily assemble a strategy without developing the software plumbing yourself.
For a developer, this is seen as high-level Python classes and objects.
- decide_trades() and create_trading_universe() are interface functions that the strategy developer fills in
- The timestamp is the current strategy cycle tick of the trading strategy.
- Trading pairs are identified with a copy-by-value class
tradeexecutor.state.identifier.TradingPairIdentifier
that encapsulates blockchain id, exchange id, ERC-20 tokens and their smart contract addresses, which are all needed to identify trading pairs in decentralised markets uniquely. This is one of the core challenges in decentralised markets, as assets cannot be referred to by their three or four-letter stock tickers. tradeexecutor.strategy.trading_strategy_universe.TradingStrategyUniverse
contains all data that can be an input to the trade.tradeexecutor.state.state.State
contains all past and current data about the previous actions the strategy took, like opened and closed positions, trades, blockchain transaction execution details, technical indicators, uptime, and deposited capital.tradeexecutor.strategy.alpha_model.AlphaModel
offers a way to set weighted trading signals based on the data analysis. It has helper methods of tracking signals, choosing top signals, and generating rebalance trades automatically.tradeexecutor.strategy.pandas_trader.position_manager.PositionManager
is a high-level utility class that is used to generate trades. For example, you can call PositionManager.close_all, and it will return a list of trade orders that need to be executed to sell all assets and return to full cash.
Here is how decide_trades() interacts in the case of a portfolio construction strategy:
The state is a JSON-serialisable hierarchy of all persistent strategy objects.
The overview of state management:
An example portfolio constructions strategy in Python
Below is a Python strategy code for an example portfolio construction strategy. It is based on an example strategy that would trade a fixed set of decentralised finance assets across a given set of exchanges and trading pairs.
This particular example:
- Runs a portfolio construction strategy backtest for a momentum strategy
- Uses handpicked “DeFi bluechip tokens” - see Trading universe definition for the list
- Long only
- Picks top assets for each strategy cycle
- Trades across multiple blockchains
- Trades across multiple DEXes
- Is based on momentum (previous week price change %)
- Weekly rebalances
- Due to volatile cryptocurrency markets, the strategy uses take profit/stop loss triggers from managing risks and exit outside the normal rebalance cycle
- Ignores price impact and thus may cause unrealistic results
- Ignores available liquidity to trade and thus may cause unrealistic results
- Ignores any complications of trading assets cross-chain - we assume our reserve currency USDC is good on any chain and DEX and trading pair, which is not a realistic assumption
- Order routing is ignored; the strategy cannot be moved to live trading as is
- We use the oldest available DEX price data we have: Uniswap v2, others
- For live trade execution, one would want to choose execution on a newer DEX with fewer fees
Full code examples
- You can read the full backtesting Jupyter Notebook in Trading Strategy documentation.
- See the link for the syntax-highlighted version of the example code.
First, we define our strategy parameters:
import datetime
import pandas as pd
from tradingstrategy.chain import ChainId
from tradingstrategy.timebucket import TimeBucket
from tradeexecutor.strategy.cycle import CycleDuration
from tradeexecutor.strategy.strategy_module import StrategyType, TradeRouting, ReserveCurrency
# Tell what trade execution engine version this strategy needs to use
trading_strategy_engine_version = "0.1"
# What kind of strategy we are running.
# This tells we are going to use
trading_strategy_type = StrategyType.managed_positions
# How our trades are routed.
# PancakeSwap basic routing supports two way trades with BUSD
# and three way trades with BUSD-BNB hop.
trade_routing = TradeRouting.ignore
# Set cycle to 7 days and look back the momentum of the previous candle
trading_strategy_cycle = CycleDuration.cycle_7d
momentum_lookback_period = datetime.timedelta(days=7)
# Hold top 3 coins for every cycle
max_assets_in_portfolio = 4
# Leave 20% cash buffer
value_allocated_to_positions = 0.80
# Set 33% stop loss over mid price
stop_loss = 0.66
# Set 5% take profit over mid price
take_profit = 1.05
# The weekly price must be up 2.5% for us to take a long position
minimum_mometum_threshold = 0.025
# Don't bother with trades that would move position
# less than 300 USD
minimum_rebalance_trade_threshold = 300
# Use hourly candles to trigger the stop loss
stop_loss_data_granularity = TimeBucket.h1
# Strategy keeps its cash in USDC
reserve_currency = ReserveCurrency.usdc
# The duration of the backtesting period
start_at = datetime.datetime(2020, 11, 1)
end_at = datetime.datetime(2023, 1, 31)
# Start with 10,000 USD
initial_deposit = 10_000
Then we define our trading universe:
from tradingstrategy.client import Client
from tradeexecutor.strategy.trading_strategy_universe import TradingStrategyUniverse
from tradeexecutor.strategy.trading_strategy_universe import load_partial_data
from tradeexecutor.strategy.execution_context import ExecutionContext
from tradeexecutor.strategy.execution_context import ExecutionMode
from tradeexecutor.strategy.universe_model import UniverseOptions
# List of trading pairs that we consider "DeFi blueschips" for this strategy
# For token ordering, wrappign see https://tradingstrategy.ai/docs/programming/market-data/trading-pairs.html
pairs = (
(ChainId.ethereum, "uniswap-v2", "WETH", "USDC"), # ETH
(ChainId.ethereum, "sushi", "AAVE", "WETH"), # AAVE
(ChainId.ethereum, "uniswap-v2", "UNI", "WETH"), # UNI
(ChainId.ethereum, "uniswap-v2", "CRV", "WETH"), # Curve
(ChainId.ethereum, "sushi", "SUSHI", "WETH"), # Sushi
(ChainId.bsc, "pancakeswap-v2", "WBNB", "BUSD"), # BNB
(ChainId.bsc, "pancakeswap-v2", "Cake", "BUSD"), # Cake
(ChainId.polygon, "quickswap", "WMATIC", "USDC"), # Matic
(ChainId.avalanche, "trader-joe", "WAVAX", "USDC"), # Avax
(ChainId.avalanche, "trader-joe", "JOE", "WAVAX"), # TraderJoe
)
def create_trading_universe(
ts: datetime.datetime,
client: Client,
execution_context: ExecutionContext,
universe_options: UniverseOptions,
) -> TradingStrategyUniverse:
assert not execution_context.mode.is_live_trading(), \
f"Only strategy backtesting supported, got {execution_context.mode}"
# Load data for our trading pair whitelist
dataset = load_partial_data(
client=client,
time_bucket=trading_strategy_cycle.to_timebucket(),
pairs=pairs,
execution_context=execution_context,
universe_options=universe_options,
liquidity=False,
stop_loss_time_bucket=stop_loss_data_granularity,
start_at=start_at,
end_at=end_at,
)
# Filter down the dataset to the pairs we specified
universe = TradingStrategyUniverse.create_multichain_universe_by_pair_descriptions(
dataset,
pairs,
reserve_token_symbol="USDC" # Pick any USDC - does not matter as we do not route
)
return universe
And then, we define the actual strategy in decide_trades() function:
from typing import List, Dict, Counter
from tradingstrategy.universe import Universe
from tradeexecutor.strategy.trading_strategy_universe import translate_trading_pair
from tradeexecutor.strategy.weighting import weight_by_1_slash_n
from tradeexecutor.strategy.alpha_model import AlphaModel
from tradeexecutor.state.trade import TradeExecution
from tradeexecutor.strategy.pricing_model import PricingModel
from tradeexecutor.strategy.pandas_trader.position_manager import PositionManager
from tradeexecutor.state.state import State
def decide_trades(
timestamp: pd.Timestamp,
universe: Universe,
state: State,
pricing_model: PricingModel,
cycle_debug_data: Dict) -> List[TradeExecution]:
# Create a position manager helper class that allows us easily to create
# opening/closing trades for different positions
position_manager = PositionManager(timestamp, universe, state, pricing_model)
alpha_model = AlphaModel(timestamp)
# Watch out for the inclusive range and include and avoid peeking in the future
adjusted_timestamp = timestamp - pd.Timedelta(seconds=1)
start = adjusted_timestamp - momentum_lookback_period - datetime.timedelta(seconds=1)
end = adjusted_timestamp
candle_universe = universe.candles
pair_universe = universe.pairs
# Get candle data for all candles, inclusive time range
candle_data = candle_universe.iterate_samples_by_pair_range(start, end)
# Iterate over all candles for all pairs in this timestamp (ts)
for pair_id, pair_df in candle_data:
# We should have candles for range start - end,
# where end is the current strategy cycle timestamp
# and start is one week before end.
# Because of sparse data we may have 0, 1 or 2 candles
first_candle = pair_df.iloc[0]
last_candle = pair_df.iloc[-1]
# How many candles we are going to evaluate
candle_count = len(pair_df)
assert last_candle["timestamp"] < timestamp, "Something wrong with the data - we should not be able to peek the candle of the current timestamp, but always use the previous candle"
open = last_candle["open"]
close = last_candle["close"]
# DEXPair instance contains more data than internal TradingPairIdentifier
# we use to store this pair across the strategy
dex_pair = pair_universe.get_pair_by_id(pair_id)
pair = translate_trading_pair(dex_pair)
# We define momentum as how many % the trading pair price gained during
# the momentum window
momentum = (close - open) / open
# This pair has not positive momentum,
# we only buy when stuff goes up
if momentum <= minimum_mometum_threshold:
continue
alpha_model.set_signal(
pair,
momentum,
stop_loss=stop_loss,
take_profit=take_profit,
)
# Select max_assets_in_portfolio assets in which we are going to invest
# Calculate a weight for ecah asset in the portfolio using 1/N method based on the raw signal
alpha_model.select_top_signals(max_assets_in_portfolio)
alpha_model.assign_weights(method=weight_by_1_slash_n)
alpha_model.normalise_weights()
# Load in old weight for each trading pair signal,
# so we can calculate the adjustment trade size
alpha_model.update_old_weights(state.portfolio)
# Calculate how much dollar value we want each individual position to be on this strategy cycle,
# based on our total available equity
portfolio = position_manager.get_current_portfolio()
portfolio_target_value = portfolio.get_total_equity() * value_allocated_to_positions
alpha_model.calculate_target_positions(portfolio_target_value)
# Shift portfolio from current positions to target positions
# determined by the alpha signals (momentum)
trades = alpha_model.generate_rebalance_trades_and_triggers(
position_manager,
min_trade_threshold=minimum_rebalance_trade_threshold, # Don't bother with trades under 300 USD
)
# Record alpha model state so we can later visualise our alpha model thinking better
state.visualisation.add_calculations(timestamp, alpha_model.to_dict())
return trades
Analysing the backtest results
The backtesting results can be analysed in various ways. Some of the analysis goals are:
- To see if the strategy is profitable
- See if the strategy has performance issues in different market conditions
- See if the strategy has performance issues in different market conditions
Past performance is not indicative of future results.
The Trading Strategy Framework offers a few analysis tools you can use in Jupyter Notebooks. Below are some examples.
The equity curve of the backtest results that compare the example strategy to the “buy and hold ETH” index benchmark:
Summary statistics:
Individual position timeline:
Further discussion
If you want to study further, have questions or feedback:
- Join the Trading Strategy community Discord for discussion
- See our collection of learning resources for Python and trading strategy developers
- Read Getting started tutorial for Trading Strategy
- See the portfolio construction notebook example in the documentation
- See another portfolio construction example in the document