Notebook

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

weights_example

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

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

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

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.

Dynamic Assets Selection

See Dynamic Assets Selection Page

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.

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