Q20 Quick Start Strategy¶
This template shows how to make a submission to the Q20 Nasdaq-100 contest and contains some useful code snippets.
You can clone and edit this example there (tab Examples).
This is a Dual Simple Moving Average Crossover strategy using the Nasdaq 100 index data on the Quantiacs platform. It goes long on a stock when its 20-day SMA exceeds the 200-day SMA and shorts when the opposite occurs, only considering liquid stocks. This strategy aims to capitalize on momentum changes in stock prices.
Full code¶
Below is the complete code snippet for this strategy:
import xarray as xr
import qnt.ta as qnta
import qnt.data as qndata
import qnt.output as qnout
import qnt.stats as qnstats
# Load daily stock data for the Q18 Nasdaq-100 contest
data = qndata.stocks.load_ndx_data(min_date="2005-06-01")
# Strategy
close = data.sel(field="close")
sma_slow = qnta.sma(close, 200)
sma_fast = qnta.sma(close, 20)
weights = xr.where(sma_slow < sma_fast, 1, -1)
# Liquidity filter and clean
is_liquid = data.sel(field="is_liquid")
weights = weights * is_liquid
weights = qnout.clean(weights, data, "stocks_nasdaq100")
# Calc stats
stats = qnstats.calc_stat(data, weights.sel(time=slice("2006-01-01", None)))
display(stats.to_pandas().tail())
# Graph
performance = stats.to_pandas()["equity"]
import qnt.graph as qngraph
qngraph.make_plot_filled(performance.index, performance, name="PnL (Equity)", type="log")
weights = weights.sel(time=slice("2006-01-01",None))
qnout.check(weights, data, "stocks_nasdaq100")
qnout.write(weights) # to participate in the competition
1) Load libraries¶
Start by importing all the essential libraries.
# Import basic libraries.
import xarray as xr
import pandas as pd
import numpy as np
# Import Quantiacs libraries.
import qnt.data as qndata # load and manipulate data
import qnt.output as qnout # manage output
import qnt.backtester as qnbt # backtester
import qnt.stats as qnstats # statistical functions for analysis
import qnt.graph as qngraph # graphical tools
import qnt.ta as qnta # indicators library
import qnt.xr_talib as xr_talib # indicators library
2) Data¶
The variable qndata.stocks.load_ndx_data(tail=period) is an xarray.DataArray structure which contains historical market data for the last (tail=period) days and whose coordinates are:
- time: a date in format yyyy-mm-dd;
- field: an attribute, for example the opening daily price;
- asset: the identifying symbol for the asset, for example NAS:APPL for Apple.
Load daily stock data for the Q18 Nasdaq-100 contest
data = qndata.stocks.load_ndx_data(min_date="2005-06-01")
3) Strategy. Weights allocation¶
Every day, the algorithm determines how much of each asset should be in the portfolio for the next trading day. These are called the portfolio weights.
A positive weight means you'll be buying that asset, while a negative weight means you'll be selling it.
These decisions are made at the end of each day and put into effect at the beginning of the next trading day.
# Strategy
close = data.sel(field="close")
sma_slow = qnta.sma(close, 200)
sma_fast = qnta.sma(close, 20)
weights = xr.where(sma_slow < sma_fast, 1, -1)
# Liquidity filter and clean
is_liquid = data.sel(field="is_liquid")
weights = weights * is_liquid
weights = qnout.clean(weights, data, "stocks_nasdaq100")
4) Performance estimation¶
Once we have our trading algorithm, we can assess its performance by calculating various statistics.
stats = qnstats.calc_stat(data, weights.sel(time=slice("2006-01-01", None)))
display(stats.to_pandas().tail())
These stats show how well the algorithm is doing if you started with 1M USD. They include:
- equity: the cumulative value of profits and losses since inception (1M USD);
- relative_return: the relative daily variation of equity;
- volatility: the volatility of the investment since inception (i.e. the annualized standard deviation of the daily returns);
- underwater: the time evolution of drawdowns;
- max_drawdown: the absolute minimum of the underwater chart;
- sharpe_ratio: the annualized Sharpe ratio since inception; the value must be larger than 1 for taking part to contests;
- mean_return: the annualized mean return of the investment since inception;
- bias: the daily asymmetry between long and short exposure: 1 for a long-only system, -1 for a short-only one;
- instruments: the number of instruments which get allocations on a given day;
- avg_turnover: the average turnover;
- avg_holding_time: the average holding time in days.
We can also plot a chart to show how profits and losses have accumulated over time.
performance = stats.to_pandas()["equity"]
qngraph.make_plot_filled(performance.index, performance, name="PnL (Equity)", type="log")
5) Submit Your strategy to the competition¶
Send strategy use Submit button
weights = weights.sel(time=slice("2006-01-01",None))
qnout.check(weights, data, "stocks_nasdaq100")
qnout.write(weights) # to participate in the competition
Strategy Guidelines¶
Your trading algorithm can open both short and long positions.
At any given time, your algorithm can trade all or a subset of stocks that are or were part of the NASDAQ-100 stock index. Keep in mind that this index's composition changes over time. Quantiacs provides a suitable filter function for selecting these stocks.
The Sharpe ratio of your system since January 1, 2006, must be greater than 1.
Your system must not replicate the current examples. We use a correlation filter to identify and remove duplicates in the submissions.
For more detailed rules, please visit our competition rules page.
Working with Data¶
Quantiacs offers historical data for major financial markets, including stocks, futures (like Bitcoin futures), and cryptocurrencies. This section provides an overview of the data:
- Stocks: Market data for NASDAQ-listed companies, past and present.
- Futures: Market data for liquid global futures contracts with various underlying assets.
- Cryptocurrencies: Market data for top cryptocurrencies by market capitalization.
Additional Datasets:
- Indexes: Daily data for various stock market indices.
- U.S. Bureau of Labor Statistics (BLS Data): Offers macroeconomic data on prices, employment, unemployment, compensation, and working conditions.
- International Monetary Fund (IMF Data): Publishes time series data on IMF lending, exchange rates, economic and financial indicators, and commodity data.
- Fundamental Data: An experimental API for additional financial data.
Loading Data¶
import qnt.data as qndata
# Load daily stock data for the Q18 Nasdaq-100 contest
stocks = qndata.stocks.load_ndx_data(min_date="2005-06-01")
# Load cryptocurrency daily data for the Q16/Q17 contests
cryptodaily = qndata.cryptodaily.load_data(min_date="2005-06-01")
# Load futures data for the Q15 contest
futures = qndata.futures.load_data.load_data(min_date="2005-06-01")
# Load BTC futures data for the Q15 contest
crypto_futures = qndata.cryptofutures.load_data(min_date="2005-06-01")
print(stocks, cryptodaily, futures, crypto_futures)
Accessing Data Fields¶
The datasets contain details such as opening and closing prices, high and low prices, trading volumes, and more.
import qnt.data as qndata
data = qndata.stocks.load_ndx_data(min_date="2005-06-01")
price_open = data.sel(field="open")
price_close = data.sel(field="close")
price_high = data.sel(field="high")
price_low = data.sel(field="low")
volume_day = data.sel(field="vol")
is_liquid = data.sel(field="is_liquid")
Working with xarray and pandas¶
Quantiacs uses xarray for storing multi-dimensional data, and pandas for handling tabular data. Both libraries are powerful tools for data manipulation, selection, and computation.
You can also easily convert between xarray DataArrays and pandas DataFrames to leverage the unique capabilities of each library.
import qnt.data as qndata
import numpy as np
import qnt.ta as qnta
# Xarray usage
data = qndata.stocks.load_ndx_data(min_date="2005-06-01")
price_open = data.sel(field="open")
price_close = data.sel(field="close")
price_close_100 = price_close / 100.0
log_price = np.log(price_close)
close_price_sma = qnta.sma(price_close, 2)
# Conversion between xarray and pandas
prices_pandas = price_close.to_pandas()
prices_xarray = prices_pandas.unstack().to_xarray()
We provide two examples on how to calculate the percentage change of close prices and simple moving average:
Example 1¶
import qnt.data as qntdata
# Load data
data = qntdata.stocks.load_ndx_data(min_date="2005-06-01")
# Calculate percentage change of close prices
def get_price_pct_change(prices):
prices_pandas = prices.to_pandas()
assets = data.coords["asset"].values
for asset in assets:
prices_pandas[asset] = prices_pandas[asset].pct_change()
return prices_pandas
prices = data.sel(field="close") * 1.0
prices_pct_change = get_price_pct_change(prices).unstack().to_xarray()
Example 2¶
import qnt.data as qntdata
# Load data
data = qntdata.stocks.load_ndx_data(min_date="2005-06-01")
# Convert close prices to pandas DataFrame
close = data.sel(field="close").to_pandas()
# Calculate simple moving average (SMA) for close prices
close_sma = ((close - close.shift(10)) / close.shift(10)).rolling(30).mean()
# Normalize SMA values
norm = abs(close_sma).sum(axis=1)
weights = close_sma.div(norm, axis=0)
# Convert weights back to xarray DataArray
final_weights = weights.unstack().to_xarray()
QNT Technical Indicators¶
The qnt.ta module is a collection of technical analysis indicators and functions specially optimized for working with qnt, a platform for quantitative finance research and trading strategies.
Indicator groups:
- Moving Averages: These indicators calculate the average price over a specified number of periods to help identify trends in the market.
- Oscillators: These indicators measure the momentum and trend of the market by comparing the current price to its historical average.
- Volatility Indicators: These indicators help to identify how much the price of an asset is changing over time, which can be useful for managing risk.
- Volume Indicators: These indicators measure the strength or weakness of a price trend based on the volume of trades occurring in the market.
- Overlap Studies: These indicators are used to identify potential areas of support and resistance by analyzing the relationship between the current price and its historical moving averages.
- Momentum Indicators: These indicators measure the rate of change of an asset's price over time to help identify trend reversals.
- Cycle Indicators: These indicators help identify trends in the market by analyzing repeating patterns over a fixed period of time.
import qnt.data as qndata
import qnt.ta as qnta
data = qndata.stocks.load_ndx_data(min_date="2005-06-01")
high = data.sel(field='high')
low = data.sel(field='low')
close = data.sel(field='close')
volume = data.sel(field='vol')
# Moving Averages
sma_20 = qnta.sma(close, 20)
ema_20 = qnta.ema(close, 20)
wilder_ma_20 = qnta.wilder_ma(close, 20)
lwma_20 = qnta.lwma(close, 20)
dema_20 = qnta.dema(close, 20)
tema_20 = qnta.tema(close, 20)
# Oscillators
rsi_14 = qnta.rsi(close, 14)
roc_10 = qnta.roc(close, 10)
sroc_10 = qnta.sroc(close, 10)
macd_line, macd_signal, macd_hist = qnta.macd(close, 12, 26, 9)
trix_15 = qnta.trix(close, 15)
stoch_k = qnta.stochastic_k(high, low, close, 14)
stoch_d = qnta.stochastic(high, low, close, 14)
slow_stoch_d = qnta.slow_stochastic(high, low, close, 14)
# Index Indicators
atr_14 = qnta.atr(high, low, close, 14)
tr_1 = qnta.tr(high, low, close)
dms = qnta.dms(high, low, close, 14, 14, 14)
# Cumulative
obv_line = qnta.obv(close, volume)
chaikin_adl_line = qnta.chaikin_adl(high, low, close, volume)
chaikin_oscillator = qnta.chaikin_osc(chaikin_adl_line, 3, 10)
# Global
ad_line_result = qnta.ad_line(close * data.sel(field="is_liquid"))
ad_ratio_result = qnta.ad_ratio(close * data.sel(field="is_liquid"))
# Pivot Points
pivot_points_result = qnta.pivot_points(data, 2, 3)
top_pivot_points_result = qnta.top_pivot_points(data)
bottom_pivot_points_result = qnta.bottom_pivot_points(data)
# Other functions
price_change = qnta.change(close)
shifted_data = qnta.shift(close, periods=1)
std_dev = qnta.std(close, 20)
variance_value = qnta.variance(close, 20)
covariance_value = qnta.covariance(close, close, 20)
beta_value = qnta.beta(close, close, 20)
correlation_value = qnta.correlation(close, close, 20)
Frequently used functions¶
Description | Code Example |
---|---|
View a list of all tickers | data.asset.to_pandas().to_list() |
See which fields are available | data.field.to_pandas().to_list() |
Load specific tickers | data = qndata.stocks.load_ndx_data(min_date="2005-06-01", assets=["NAS:AAPL", "NAS:AMZN"]) |
Select specific tickers after loading all data | def get_data_filter(data, assets): filler= data.sel(asset=assets) return filler get_data_filter(data, ["NAS:AAPL", "NAS:AMZN"]) |
Loads a list of NASDAQ-listed stocks | stocks_list = qndata.stocks.load_ndx_list(min_date='2006-01-01') |
Loads a list of available futures contracts. | future_list = qndata.futures.load_list() |
List of sectors. | sectors = [x['sector'] for x in stocks_list] |
Filter list of asset IDs for the specified sector. | assets_for_sector = [x['id'] for x in stocks_list if x['sector'] == "Energy"] |
Load specific tickers for sector | data = qndata.stocks.load_ndx_data(min_date="2005-06-01", assets=assets_for_sector) |
Dynamic Assets Selection¶
Applying to Liquid Assets¶
Make sure that the selected stocks are liquid:
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.
# 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:
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:
asset_filter = qnfilter.filter_volatility_rolling(data=data,
weights=strategy(data),
top_assets=150,
rolling_window=60,
metric="std",
ascending=True)
weights = weights * asset_filter
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.
asset_filter = qnfilter.filter_by_normalized_atr(data, top_assets=150, ma_period=90, ascending=True)
weights = weights * asset_filter
How can you reduce slippage impace when trading?¶
Just apply some technique to reduce turnover:
def get_lower_slippage(weights, rolling_time=6):
return weights.rolling({"time": rolling_time}).max()
improved_weights = get_lower_slippage(weights, rolling_time=6)
How to get the Sharpe ratio?¶
import qnt.stats as qnstats
def get_sharpe(market_data, weights):
rr = qnstats.calc_relative_return(market_data, weights)
sharpe = qnstats.calc_sharpe_ratio_annualized(rr).values[-1]
return sharpe
sharpe = get_sharpe(data, weights) # weights.sel(time=slice("2006-01-01",None))
How can you check the quality of your strategy?¶
import qnt.output as qnout
qnout.check(weights, data, "stocks_nasdaq100")
or
stat= qnstats.calc_stat(data, weights)
display(stat.to_pandas().tail())
or
import qnt.graph as qngraph
statistics= qnstats.calc_stat(data, weights)
display(statistics.to_pandas().tail())
performance= statistics.to_pandas()["equity"]
qngraph.make_plot_filled(performance.index, performance, name="PnL (Equity)", type="log")
display(statistics[-1:].sel(field = ["sharpe_ratio"]).transpose().to_pandas())
qnstats.print_correlation(weights, data)
Common Reasons for Submission Rejection and Their Solutions¶
Here are some of the frequent reasons causing submission rejection in algorithmic trading competitions, and their corresponding remedies.
1) Missed call to write_output¶
Save algorithm weights, run code
qnt.output.write(weights)
2) Not eligible send to contest. In-Sample Sharpe must be larger than 1¶
Improve your algorithm. Аor example, you can use sections and get an algorithm that will pass the filter
- Example Trading System Optimization
- Example of a strategy using technical analysis indicators
- How do I get a list of the top 3 assets ranked by Sharpe ratio?
Need help? Check the Documentation and find solutions/report problems in the Forum section.
3) Not enough bid information.¶
Run code
min_time = weights.time[abs(weights).fillna(0).sum('asset')> 0].min()
min_time
min_time must be less than or equal to January 1, 2006.
If min_time is larger than the starting date, we recommend to fill the starting values of the time series with non-vanishing values, for example a simple buy-and-hold strategy.
def get_enough_bid_for(data_, weights_):
time_traded = weights_.time[abs(weights_).fillna(0).sum('asset') > 0]
is_strategy_traded = len(time_traded)
if is_strategy_traded:
return xr.where(weights_.time < time_traded.min(), data_.sel(field="is_liquid"), weights_)
return weights_
weights_new = get_enough_bid_for(data, weights)
weights_new = weights_new.sel(time=slice("2006-01-01",None))