Dynamic Assets Selection¶
This chapter outlines methods for dynamic stock selection designed to enhance trading strategies. You can tailor these methods to suit your specific needs, using the provided examples to help you develop your own stock selection criteria. It is important to aim for diversity in your portfolio by including a multitude of stocks.
Example Strategy in Python¶
Let’s start with a basic strategy that uses moving averages to determine the moments for buying and selling stocks:
import xarray as xr
import qnt.data as qndata
import qnt.ta as qnta
import qnt.stats as qnstats
import qnt.filter as qnfilter
# Load stock data
data = qndata.stocks.load_ndx_data(min_date="2005-01-01")
def strategy(data):
close = data.sel(field="close")
ma_slow = qnta.sma(close, 30)
ma_fast = qnta.sma(close, 10)
return xr.where(ma_fast > ma_slow, 1, 0)
weights = strategy(data)
Applying to Liquid Assets¶
This liquidity filter, focusing specifically on Nasdaq 100 assets, is the main filter you are highly recommended to use, especially if you plan to participate in competitions. It multiplies the strategy weights by a liquidity indicator, effectively filtering out non-liquid stocks:
weights = weights * data.sel(field="is_liquid")
Trading Stocks with Different Volatilities¶
You can choose stocks with different levels of volatility:
Low Volatility: Trade 150 stocks with the lowest volatility over the past 60 days. High Volatility: Focus on the 150 most volatile stocks.
import qnt.filter as qnfilter
# Low Volatility
low_volatility = qnfilter.filter_volatility(data=data, rolling_window=60, top_assets=150, metric="std", ascending=True)
weights = weights * low_volatility
# High Volatility
high_volatility = qnfilter.filter_volatility(data=data, rolling_window=60, top_assets=150, metric="std",
ascending=False)
weights = weights * high_volatility
Selecting Stocks by Sharpe Ratio¶
Select stocks that show the best results by Sharpe ratio:
import qnt.stats as qnstats
import qnt.filter as qnfilter
def filter_sharpe_ratio(data, weights, top_assets):
stats_per_asset = qnstats.calc_stat(data, weights, per_asset=True)
sharpe_ratio = stats_per_asset.sel(field="sharpe_ratio")
return qnfilter.rank_assets_by(data, sharpe_ratio, top_assets, ascending=False)
asset_filter = filter_sharpe_ratio(data, weights, 150)
weights = weights * asset_filter
# weights = weights * qnfilter.filter_sharpe_ratio(data, weights, 150) # this can be done in one line
Volatility Using a Rolling Window¶
This method allows filtering stocks based on volatility calculated over a specified time window:
import qnt.stats as qnstats
import qnt.filter as qnfilter
def filter_volatility_rolling(data, weights, top_assets, rolling_window, metric="std", ascending=True):
stats_per_asset = qnstats.calc_stat(data, weights, per_asset=True)
volatility = stats_per_asset.sel(field="volatility")
volatility = qnfilter.calc_rolling_metric(volatility, rolling_window, metric)
return qnfilter.rank_assets_by(data, volatility, top_assets, ascending)
asset_filter = filter_volatility_rolling(data, weights, 150, 60, "std", True)
weights = weights * asset_filter
# Same way calculate Low Volatility
# weights = weights * qnfilter.filter_volatility_rolling(data=data,
# weights=strategy(data),
# top_assets=150,
# rolling_window=60,
# metric="std",
# ascending=True)
Filtering Stocks by Normalized Average True Range (NATR)¶
The Normalized Average True Range (NATR) is a volatility metric that adjusts the Average True Range (ATR) for the price level of the asset, providing a percentage-based measure that makes it easier to compare volatility across different priced stocks.
import qnt.filter as qnfilter
import qnt.ta as qnta
def filter_by_normalized_atr(data, top_assets, ma_period=90, ascending=True):
high = data.sel(field='high')
low = data.sel(field='low')
close = data.sel(field='close')
# Calculating ATR and then Normalized ATR
atr = qnta.atr(high=high, low=low, close=close, ma=ma_period)
natr = 100 * (atr / close) # Normalized ATR
# Ranking assets based on NATR
natr_ranks = qnfilter.rank_assets_by(data=data, criterion=natr, top_assets=top_assets, ascending=ascending)
return natr_ranks
asset_filter = filter_by_normalized_atr(data, top_assets=150, ma_period=90, ascending=True)
weights = weights * asset_filter
# Same way calculate Low Volatility
# weights = weights * qnfilter.filter_by_normalized_atr(data, top_assets=50, ma_period=90, ascending=True)
These methods allow for more flexible and adaptive stock trading strategies, which can significantly enhance the efficiency of your portfolio.
Risk Management¶
In this section, we explore various risk management strategies to ensure stable and controlled trading outcomes. These techniques aim to mitigate financial exposure and optimize portfolio performance through systematic checks and balances.
Exposure Check¶
Managing the exposure of each asset in a portfolio is a crucial component of risk management. Exposure refers to the absolute value of each asset relative to the total value of the portfolio, which is essential for assessing the risk each asset contributes.
Below is an example Python function that calculates the exposure for each position within a portfolio and checks if the exposure exceeds predefined limits:
import qnt.stats as qnstats
import qnt.exposure as qnexp
qnstats.check_exposure(portfolio_history=weights,
soft_limit=0.05, hard_limit=0.1,
days_tolerance=0.02, excess_tolerance=0.02,
avg_period=252, check_period=252 * 5
)
How It Works¶
Exposure Calculation: Computes the ratio of the absolute value of each position to the total portfolio value.
Soft and Hard Limits: Defines thresholds that should not be exceeded to adequately control risk.
Soft Limit (0.05): This is a threshold indicating a cautionary level of exposure. If the exposure of any asset exceeds this limit, it may warrant attention but isn’t critical yet.
Hard Limit (0.1): This threshold indicates a critical level of exposure. Exceeding this limit is considered risky and requires immediate action.
Tolerance Levels: Specifies the acceptable levels for brief periods of increased risk.
Days Tolerance (0.02): Represents the proportion of the checking period that can tolerate exposures between the soft and hard limits.
Excess Tolerance (0.02): The maximum allowable average excess exposure above the soft limit during the check period.
Historical Analysis: Uses historical data to evaluate the risk profile over time and ensure compliance with set limits, considering the past data specified by
avg_period
(252 days) andcheck_period
(1260 days, or 252 days multiplied by 5).
Detailed Function Explanation¶
The check_exposure
function evaluates portfolio risk by comparing each asset’s exposure against established risk
thresholds. The function performs several checks:
Maximum Exposure Analysis: Identifies periods where the exposure of any asset exceeds the soft limit and logs these occurrences.
Compliance Checks:
Checks if the proportion of days with exposures exceeding the soft limit is within the days tolerance.
Analyzes if the average excess exposure is within the excess tolerance.
Ensures that no asset’s exposure ever exceeds the hard limit during the check period.
This systematic approach allows for a dynamic and responsive risk management strategy, ensuring that the portfolio maintains a balanced risk profile in accordance with predefined risk parameters.
Applying Exposure Filters¶
Adjusting weights based on exposure checks is crucial for maintaining the desired risk profile. Here are functions that help manage exposure by modifying the investment weights:
normalize_by_max_exposure¶
Helper function which normalizes weights based on the highest daily exposure, ensuring that the exposure of each asset does not exceed a specified maximum exposure limit, while keeping daily weights allocation ratio among assets.
def normalize_by_max_exposure(weights, max_exposure=0.1):
daily_max = abs(weights).max("asset")
normalizer = xr.where(daily_max > max_exposure, daily_max / max_exposure, 1)
return weights / normalizer
weights.to_pandas().tail(2)
asset NAS:AAPL NAS:GOOG NAS:AMGN
time
2024-04-23 0.319466 0.095525 0.022927
2024-04-24 0.317989 0.095365 0.022855
normalize_by_max_exposure(weights).to_pandas().tail(2)
asset NAS:AAPL NAS:GOOG NAS:AMGN
time
2024-04-23 0.1 0.029902 0.007177
2024-04-24 0.1 0.029990 0.007187
drop_bad_days¶
Removes positions exceeding the maximum weight for any asset, thus reducing exposure.
import qnt.exposure as qnexp
weights_filtered = qnexp.drop_bad_days(weights=weights, max_weight=0.049)
mix_weights¶
Combines two sets of weights (primary and secondary) while ensuring the maximum exposure does not exceed a specific threshold.
import qnt.exposure as qnexp
weights_filtered = qnexp.mix_weights(primary=weights1, secondary=weights2, max_weight=0.049)
cut_big_positions¶
Caps the weights of positions that exceed the maximum allowable weight to limit exposure.
import qnt.exposure as qnexp
weights_filtered = qnexp.cut_big_positions(weights=weights, max_weight=0.049)
These methods form an integral part of a robust risk management system, helping to safeguard against market volatility and maintain portfolio stability.
Full code example¶
import xarray as xr
import qnt.data as qndata
import qnt.output as qnout
import qnt.ta as qnta
import qnt.stats as qnstats
import qnt.filter as qnfilter
import qnt.exposure as qnexp
# Load stock data
data = qndata.stocks.load_ndx_data(min_date="2005-01-01")
def strategy(data):
close = data.sel(field="close")
ma_slow = qnta.sma(close, 30)
ma_fast = qnta.sma(close, 10)
return xr.where(ma_fast > ma_slow, 1, 0)
weights = strategy(data)
# Applying to Liquid Assets
weights = weights * data.sel(field="is_liquid")
# Trading Stocks with Different Volatilities
# Low Volatility
# low_volatility = qnfilter.filter_volatility(data=data, rolling_window=60, top_assets=150, metric="std", ascending=True)
# weights_low_volatility = weights * low_volatility
# High Volatility
# high_volatility = qnfilter.filter_volatility(data=data, rolling_window=60, top_assets=150, metric="std",
# ascending=False)
# weights_high_volatility = weights * high_volatility
# Selecting Stocks by Sharpe Ratio
# weights_filter_sharpe_ratio = weights * qnfilter.filter_sharpe_ratio(data, weights, 150)
# Same way calculate Low Volatility
# weights_roll = weights * qnfilter.filter_volatility_rolling(data=data,
# weights=strategy(data),
# top_assets=150,
# rolling_window=60,
# metric="std",
# ascending=True)
# Same way calculate Low Volatility
# weights = weights * qnfilter.filter_by_normalized_atr(data, top_assets=50, ma_period=90, ascending=True)
qnstats.check_exposure(portfolio_history=weights,
soft_limit=0.05, hard_limit=0.1,
days_tolerance=0.02, excess_tolerance=0.02,
avg_period=252, check_period=252 * 5
)
# Removes positions exceeding the maximum weight for any asset, thus reducing exposure.
# weights_filtered = qnexp.drop_bad_days(weights=weights, max_weight=0.049)
# Combines two sets of weights (primary and secondary) while ensuring the maximum exposure does not exceed a specific
# threshold.
# weights_filtered = qnexp.mix_weights(primary=weights1, secondary=weights2, max_weight=0.049)
# Caps the weights of positions that exceed the maximum allowable weight to limit exposure.
weights_filtered = qnexp.cut_big_positions(weights=weights, max_weight=0.049)
stats = qnstats.calc_stat(data, weights.sel(time=slice("2006-01-01", None)))
display(stats.to_pandas().tail())
qnout.check(weights_filtered, data, "stocks_nasdaq100")
qnout.write(weights_filtered) # to participate in the competition. run this line in a separate cell