strategy

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.

In [1]:
# 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.

data_example

Load daily stock data for the Q18 Nasdaq-100 contest

In [2]:
data = qndata.stocks.load_ndx_data(min_date="2005-06-01")
100% (364802 of 364802) |################| Elapsed Time: 0:00:00 Time:  0:00:00
100% (38058 of 38058) |##################| Elapsed Time: 0:00:00 Time:  0:00:00
100% (14601876 of 14601876) |############| Elapsed Time: 0:00:00 Time:  0:00:00
fetched chunk 1/6 1s
100% (14605044 of 14605044) |############| Elapsed Time: 0:00:00 Time:  0:00:00
fetched chunk 2/6 2s
ERROR:root:download error: data
Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/qnt/data/common.py", line 79, in request_with_retry
    with urllib.request.urlopen(req, timeout=TIMEOUT) as response:
  File "/usr/local/lib/python3.7/urllib/request.py", line 222, in urlopen
    return opener.open(url, data, timeout)
  File "/usr/local/lib/python3.7/urllib/request.py", line 531, in open
    response = meth(req, response)
  File "/usr/local/lib/python3.7/urllib/request.py", line 641, in http_response
    'http', request, response, code, msg, hdrs)
  File "/usr/local/lib/python3.7/urllib/request.py", line 569, in error
    return self._call_chain(*args)
  File "/usr/local/lib/python3.7/urllib/request.py", line 503, in _call_chain
    result = func(*args)
  File "/usr/local/lib/python3.7/urllib/request.py", line 649, in http_error_default
    raise HTTPError(req.full_url, code, msg, hdrs, fp)
urllib.error.HTTPError: HTTP Error 500: Internal Server Error
ERROR:root:download error: data
Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/qnt/data/common.py", line 79, in request_with_retry
    with urllib.request.urlopen(req, timeout=TIMEOUT) as response:
  File "/usr/local/lib/python3.7/urllib/request.py", line 222, in urlopen
    return opener.open(url, data, timeout)
  File "/usr/local/lib/python3.7/urllib/request.py", line 531, in open
    response = meth(req, response)
  File "/usr/local/lib/python3.7/urllib/request.py", line 641, in http_response
    'http', request, response, code, msg, hdrs)
  File "/usr/local/lib/python3.7/urllib/request.py", line 569, in error
    return self._call_chain(*args)
  File "/usr/local/lib/python3.7/urllib/request.py", line 503, in _call_chain
    result = func(*args)
  File "/usr/local/lib/python3.7/urllib/request.py", line 649, in http_error_default
    raise HTTPError(req.full_url, code, msg, hdrs, fp)
urllib.error.HTTPError: HTTP Error 500: Internal Server Error
100% (14601844 of 14601844) |############| Elapsed Time: 0:00:00 Time:  0:00:00
fetched chunk 3/6 5s
100% (14598584 of 14598584) |############| Elapsed Time: 0:00:00 Time:  0:00:00
fetched chunk 4/6 7s
100% (14598584 of 14598584) |############| Elapsed Time: 0:00:00 Time:  0:00:00
fetched chunk 5/6 8s
100% (7971552 of 7971552) |##############| Elapsed Time: 0:00:00 Time:  0:00:00
fetched chunk 6/6 8s
Data loaded 9s

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.

weights_example

In [3]:
# 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")
Output cleaning...
fix uniq
ffill if the current price is None...
Check liquidity...
Ok.
Check missed dates...
Ok.
Normalization...
Output cleaning is complete.

4) Performance estimation

Once we have our trading algorithm, we can assess its performance by calculating various statistics.

In [4]:
stats = qnstats.calc_stat(data, weights.sel(time=slice("2006-01-01", None)))
display(stats.to_pandas().tail())
field equity relative_return volatility underwater max_drawdown sharpe_ratio mean_return bias instruments avg_turnover avg_holding_time
time
2023-09-07 0.900506 -0.001243 0.140152 -0.257599 -0.413971 -0.042210 -0.005916 0.346535 239.0 0.035067 101.833037
2023-09-08 0.899635 -0.000967 0.140136 -0.258317 -0.413971 -0.042593 -0.005969 0.346535 239.0 0.035062 101.833037
2023-09-11 0.902644 0.003344 0.140123 -0.255836 -0.413971 -0.041247 -0.005780 0.326733 239.0 0.035056 101.833037
2023-09-12 0.898117 -0.005015 0.140112 -0.259568 -0.413971 -0.043259 -0.006061 0.326733 239.0 0.035055 101.854042
2023-09-13 0.897337 -0.000869 0.140097 -0.260212 -0.413971 -0.043603 -0.006109 0.111111 239.0 0.035048 102.019114

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.

In [5]:
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

In [6]:
weights = weights.sel(time=slice("2006-01-01",None))

qnout.check(weights, data, "stocks_nasdaq100")
qnout.write(weights) # to participate in the competition
Check liquidity...
Ok.
Check missed dates...
Ok.
Check the sharpe ratio...
Period: 2006-01-01 - 2023-09-13
Sharpe Ratio = -0.04360331410788745
ERROR! The Sharpe Ratio is too low. -0.04360331410788745 < 1
Improve the strategy and make sure that the in-sample Sharpe Ratio more than 1.
Check correlation.

Ok. This strategy does not correlate with other strategies.
Write output: /root/fractions.nc.gz

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:

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:

  1. Moving Averages: These indicators calculate the average price over a specified number of periods to help identify trends in the market.
  2. Oscillators: These indicators measure the momentum and trend of the market by comparing the current price to its historical average.
  3. 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.
  4. Volume Indicators: These indicators measure the strength or weakness of a price trend based on the volume of trades occurring in the market.
  5. 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.
  6. Momentum Indicators: These indicators measure the rate of change of an asset's price over time to help identify trend reversals.
  7. 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)

Optimization

How to find good parameters for my algorithm?

See examples

Read more on our article.

How do I get a list of the top 3 assets ranked by Sharpe ratio?

import qnt.stats as qnstats

data = qndata.stocks.load_ndx_data(tail = 17*365, dims = ("time", "field", "asset"))

def get_best_instruments(data, weights, top_size):
    # compute statistics:
    stats_per_asset = qnstats.calc_stat(data, weights, per_asset=True)
    # calculate ranks of assets by "sharpe_ratio":
    ranks = (-stats_per_asset.sel(field="sharpe_ratio")).rank("asset")
    # select top assets by rank "top_period" days ago:
    top_period = 1
    rank = ranks.isel(time=-top_period)
    top = rank.where(rank <= top_size).dropna("asset").asset

    # select top stats:
    top_stats = stats_per_asset.sel(asset=top.values)

    # print results:
    print("SR tail of the top assets:")
    display(top_stats.sel(field="sharpe_ratio").to_pandas().tail())

    print("avg SR = ", top_stats[-top_period:].sel(field="sharpe_ratio").mean("asset")[-1].item())
    display(top_stats)
    return top_stats.coords["asset"].values

get_best_instruments(data, weights, 3)

How can I check the results for only the top 3 assets ranked by Sharpe ratio?

Select the top assets and then load their data:

best_assets= get_best_instruments(data, weights, 3)

data= qndata.stocks.load_ndx_data(tail = 17*365, assets=best_assets)

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.

Detailed explanation with examples.

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

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))