Q24 Crypto Guide Strategy¶
A detailed guideline for the Q24 Crypto Contest, explaining key rules and presenting the new performance and payout model with a practical example.
You can clone and edit this example there (tab Examples).
Description¶
The strategy can open only long positions throughout the entire period of evaluation. Positions are allocated only to top10 cryptocurrencies determined by their market capitalization – liquid crypto assets. Liquidity is defined on monthly basis – the top 10 crypto assets with the highest market capitalization on the last day of month will be liquid for next month. Market Capitalization data is taken from coinmarketcap.com. Stable coins are excluded from any calculation and liquidity definition.
In further simple example strategy, for signal definition we use two simple moving average crossover - fast sma (15 bars) and slow sma (34 bars), so when fast_sma is higher than slow sma, the weights are set (long only). Another condition that must be fulfiled is Relative strenght index value of close price have to be in certain levels.
Important Considerations:
- The in-sample period begins on 2016-01-01. Earlier data may be used for testing and training purposes.
- The strategy must achieve a minimum in-sample Sharpe Ratio of 1.0 to be considered valid.
- There is no limit to exposure by crypto asset.
- Manual asset selection or direct hand-picking is not permitted. The allocation process must be automatic.
- The strategy can open only long positions.
- Only data provided by Quantiacs can be used.
- Top 7 unique-user eligible strategies ranked by highest Sharpe ratio are winners.
For official Q24 rules, click here.
Crypto daily datasets¶
Main dataset with historical cryptocurrency End Of Day quotes (OHLCV) can be obtained:
data = qndata.cryptodaily_load_data(min_date='2015-01-01')
Quantiacs also provides additional dataset for blockchain data which can be used in analysis:
### load list of metrics
blockchain_metrics = qndata.blockchaincom_load_list()
### load single metric
miners_rev = qndata.blockchaincom_load_data(id='miners-revenue')
The data is provided in xarray.DataArray format. Check here for more details on manipulating xarray data.
Strategy - trading algorithm¶
There is Strategy Builder feature available for no coding strategy generator. It can be found under Personal page, button +Create.
# Necessary imports
import xarray as xr
import numpy as np
import pandas as pd
import qnt.stats as qnstats
import qnt.data as qndata
import qnt.output as qnout
import qnt.ta as qnta
import qnt.backtester as qnbt
import qnt.graph as qngraph
def load_data(period):
return qndata.cryptodaily_load_data(tail=period)
def strategy(data):
close = data.sel(field='close')
is_liquid = data.sel(field='is_liquid')
sma_fast = qnta.sma(close, 15)
sma_slow = qnta.sma(close, 34)
rsi = qnta.rsi(close, 14)
sma_signal = xr.where(sma_fast > sma_slow, 1, 0) * is_liquid ### allocation to liquid assets only
rsi_signal = xr.where((rsi < 34) | (rsi > 68), 1, 0)
return sma_signal * rsi_signal
Weights - Single / Multi pass approach¶
In Single pass backtesting, which is significantly faster, weights are calculated using the entire dataset in a single run.
Multi pass backtesting evaluates weights on a day-by-day basis by slicing the dataset for each individual day.
If applicable, we recommend using the single pass approach for efficiency, while verifying strategy statistics with the multi pass backtester. If the statistics from single pass and multi pass match exactly, it indicates that forward-looking bias has likely not been introduced, and the strategy can be confidently submitted as single pass.
In rare cases, even if results are identical between single and multi pass, forward-looking bias might unintentionally occur (e.g., by incorporating global data variables into the logic). Such issues are generally mitigated in production but can result in discrepancies in statistics comparing development and production results.
### Disable warnings (dependancies)
import warnings
warnings.filterwarnings('ignore')
### SINGLE PASS
data = qndata.cryptodaily_load_data(min_date='2015-01-01')
weights = strategy(data)
weights = qnout.clean(weights, data, "crypto_daily_long")
stats = qnstats.calc_stat(data, weights.sel(time=slice('2016-01-01', None))).sel(time=slice('2016-01-01', None))
# stats.to_pandas().tail(10)
0% (0 of 15080244) | | Elapsed Time: 0:00:00 ETA: --:--:--
16% (2563634 of 15080244) |## | Elapsed Time: 0:00:00 ETA: 0:00:00
51% (7841704 of 15080244) |###### | Elapsed Time: 0:00:00 ETA: 0:00:00
89% (13572180 of 15080244) |########## | Elapsed Time: 0:00:00 ETA: 0:00:00
100% (15080244 of 15080244) |############| Elapsed Time: 0:00:00 Time: 0:00:00
Output cleaning...
Fix unique timestamps
Forward filling missing prices...
Check liquidity...
Ok.
Check for missed dates...
Ok.
Check positive positions...
Ok.
Final normalization...
Output cleaning complete.
### MULTI PASS
# w=qnbt.backtest(
# competition_type="crypto_daily_long",
# lookback_period=365,
# start_date="2016-01-01",
# strategy=strategy,
# analyze=True,
# check_correlation=True
# )
Benchmark¶
For the Q24 competition, we introduced a benchmark that will be used both for comparing submission performance and as part of the pricing model for potential earnings. As benchmark we use Crypto10 index created on the way, similar as liquidity is set, on the last day of each month, the weights of the top 10 cryptocurrencies are calculated for the allocation of the following month. The weights are determined as the ratio of each asset’s market capitalization to the total market capitalization of the top 10 cryptocurrencies.
$$ benchmark = \frac { MarketCap(x)} {TotalTop10MarketCap} $$
The benchmark weights for each upcoming month will be available on the last day of the previous month and can be obtained as follows:
benchmark = qndata.index_load_weights(index_name='CRYPTO10', min_date='2016-01-01')
ERROR:root:download error: idx-weights/data
Traceback (most recent call last):
File "/usr/local/lib/python3.11/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.11/urllib/request.py", line 216, in urlopen
return opener.open(url, data, timeout)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 525, in open
response = meth(req, response)
^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 634, in http_response
response = self.parent.error(
^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 563, in error
return self._call_chain(*args)
^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 496, in _call_chain
result = func(*args)
^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 643, in http_error_default
raise HTTPError(req.full_url, code, msg, hdrs, fp)
urllib.error.HTTPError: HTTP Error 404: Not Found
ERROR:root:download error: idx-weights/data
Traceback (most recent call last):
File "/usr/local/lib/python3.11/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.11/urllib/request.py", line 216, in urlopen
return opener.open(url, data, timeout)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 525, in open
response = meth(req, response)
^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 634, in http_response
response = self.parent.error(
^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 563, in error
return self._call_chain(*args)
^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 496, in _call_chain
result = func(*args)
^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 643, in http_error_default
raise HTTPError(req.full_url, code, msg, hdrs, fp)
urllib.error.HTTPError: HTTP Error 404: Not Found
ERROR:root:download error: idx-weights/data
Traceback (most recent call last):
File "/usr/local/lib/python3.11/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.11/urllib/request.py", line 216, in urlopen
return opener.open(url, data, timeout)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 525, in open
response = meth(req, response)
^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 634, in http_response
response = self.parent.error(
^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 563, in error
return self._call_chain(*args)
^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 496, in _call_chain
result = func(*args)
^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 643, in http_error_default
raise HTTPError(req.full_url, code, msg, hdrs, fp)
urllib.error.HTTPError: HTTP Error 404: Not Found
ERROR:root:download error: idx-weights/data
Traceback (most recent call last):
File "/usr/local/lib/python3.11/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.11/urllib/request.py", line 216, in urlopen
return opener.open(url, data, timeout)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 525, in open
response = meth(req, response)
^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 634, in http_response
response = self.parent.error(
^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 563, in error
return self._call_chain(*args)
^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 496, in _call_chain
result = func(*args)
^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 643, in http_error_default
raise HTTPError(req.full_url, code, msg, hdrs, fp)
urllib.error.HTTPError: HTTP Error 404: Not Found
ERROR:root:download error: idx-weights/data
Traceback (most recent call last):
File "/usr/local/lib/python3.11/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.11/urllib/request.py", line 216, in urlopen
return opener.open(url, data, timeout)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 525, in open
response = meth(req, response)
^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 634, in http_response
response = self.parent.error(
^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 563, in error
return self._call_chain(*args)
^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 496, in _call_chain
result = func(*args)
^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 643, in http_error_default
raise HTTPError(req.full_url, code, msg, hdrs, fp)
urllib.error.HTTPError: HTTP Error 404: Not Found
ERROR:root:download error: idx-weights/data
Traceback (most recent call last):
File "/usr/local/lib/python3.11/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.11/urllib/request.py", line 216, in urlopen
return opener.open(url, data, timeout)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 525, in open
response = meth(req, response)
^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 634, in http_response
response = self.parent.error(
^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 563, in error
return self._call_chain(*args)
^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 496, in _call_chain
result = func(*args)
^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 643, in http_error_default
raise HTTPError(req.full_url, code, msg, hdrs, fp)
urllib.error.HTTPError: HTTP Error 404: Not Found
ERROR:root:download error: idx-weights/data
Traceback (most recent call last):
File "/usr/local/lib/python3.11/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.11/urllib/request.py", line 216, in urlopen
return opener.open(url, data, timeout)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 525, in open
response = meth(req, response)
^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 634, in http_response
response = self.parent.error(
^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 563, in error
return self._call_chain(*args)
^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 496, in _call_chain
result = func(*args)
^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 643, in http_error_default
raise HTTPError(req.full_url, code, msg, hdrs, fp)
urllib.error.HTTPError: HTTP Error 404: Not Found
ERROR:root:download error: idx-weights/data
Traceback (most recent call last):
File "/usr/local/lib/python3.11/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.11/urllib/request.py", line 216, in urlopen
return opener.open(url, data, timeout)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 525, in open
response = meth(req, response)
^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 634, in http_response
response = self.parent.error(
^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 563, in error
return self._call_chain(*args)
^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 496, in _call_chain
result = func(*args)
^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 643, in http_error_default
raise HTTPError(req.full_url, code, msg, hdrs, fp)
urllib.error.HTTPError: HTTP Error 404: Not Found
ERROR:root:download error: idx-weights/data
Traceback (most recent call last):
File "/usr/local/lib/python3.11/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.11/urllib/request.py", line 216, in urlopen
return opener.open(url, data, timeout)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 525, in open
response = meth(req, response)
^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 634, in http_response
response = self.parent.error(
^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 563, in error
return self._call_chain(*args)
^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 496, in _call_chain
result = func(*args)
^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 643, in http_error_default
raise HTTPError(req.full_url, code, msg, hdrs, fp)
urllib.error.HTTPError: HTTP Error 404: Not Found
ERROR:root:download error: idx-weights/data
Traceback (most recent call last):
File "/usr/local/lib/python3.11/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.11/urllib/request.py", line 216, in urlopen
return opener.open(url, data, timeout)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 525, in open
response = meth(req, response)
^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 634, in http_response
response = self.parent.error(
^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 563, in error
return self._call_chain(*args)
^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 496, in _call_chain
result = func(*args)
^^^^^^^^^^^
File "/usr/local/lib/python3.11/urllib/request.py", line 643, in http_error_default
raise HTTPError(req.full_url, code, msg, hdrs, fp)
urllib.error.HTTPError: HTTP Error 404: Not Found
0% (0 of 1099628) | | Elapsed Time: 0:00:00 ETA: --:--:--
100% (1099628 of 1099628) |##############| Elapsed Time: 0:00:00 Time: 0:00:00
### Benchmark stats
# bench_stats = qnstats.calc_stat(data, benchmark.sel(time=slice('2016-01-01', None))).sel(time=slice('2016-01-01', None))
# bench_stats.to_pandas().tail(10)
Volatility normalization and pricing model¶
In the Q24 contest, a new pricing model will be applied for payouts to the winners (the top 7 eligible submissions from unique users ranked by the highest Sharpe ratio). On the last day of the Contest period, a scaling factor will be determined for each winning submission and for the Crypto10 benchmark. This factor normalizes annualized volatility to 10% if it exceeds that threshold. The same factor will then be used to scale all future weights during the entire Live period. The submission performance (equity) in the Live period will be compared to the benchmark’s equity to produce the final score:
$$ PayoutScore = \frac {SubmissionEquity} {max(BenchmarkEquity, 1.0)} $$
Negative performance will not be rewarded, even if the submission outperforms the benchmark.
To simulate the process and compare submission performance to the benchmark, the helper functions below can be used.
### Get annualized volatility on certain date
def get_volatility(data, weights, start=None, end=None):
return qnstats.calc_stat(data, weights.sel(time=slice(start,end))).sel(time=slice(start,end)).sel(field='volatility').isel(time=-1).item()
### Compare submission equity to benchmark, with or without normalizing volatility
def compare_to_benchmark(data, weights, benchmark, start=None, end=None, scale_date=None, vola_level=None):
if not scale_date:
scale_date = end
if vola_level:
weights = weights * min(vola_level / get_volatility(data, weights, start, scale_date), 1)
benchmark = benchmark * min(vola_level / get_volatility(data, benchmark, start, scale_date), 1)
eq_str = qnstats.calc_stat(data, weights.sel(time=slice(start, end))).sel(time=slice(start, end)).sel(field='equity')
eq_ben = qnstats.calc_stat(data, benchmark.sel(time=slice(start, end))).sel(time=slice(start, end)).sel(field='equity')
qngraph.make_plot_double(index=eq_str.time, data1=eq_str, data2=eq_ben, name1='strategy', name2='benchmark')
### Compare submission equity to benchmark equity simulating Live (payout) period performance, or simply show Score
def simulate_live(data, weights, benchmark, start=None, end=None, scale_date=None, vola_level=None, show_score=False):
in_sample_start_date = '2016-01-01' ### IS start date for Q24
if vola_level:
weights = weights * min(vola_level / get_volatility(data, weights, in_sample_start_date, scale_date), 1)
benchmark = benchmark * min(vola_level / get_volatility(data, benchmark, in_sample_start_date, scale_date), 1)
eq_str = qnstats.calc_stat(data, weights.sel(time=slice(start, end))).sel(time=slice(start, end)).sel(field='equity')
eq_ben = qnstats.calc_stat(data, benchmark.sel(time=slice(start, end))).sel(time=slice(start, end)).sel(field='equity')
if show_score:
eq_ben = xr.where(eq_ben < 1, 1, eq_ben)
score = eq_str / eq_ben
qngraph.make_plot(index=eq_str.time, data=eq_str / eq_ben, name="Score")
else:
qngraph.make_plot_double(index=eq_str.time, data1=eq_str, data2=eq_ben, name1='strategy', name2='benchmark')
### Compare equities when volatility is normalized to 10% on last available date
compare_to_benchmark(data, weights, benchmark, start='2016-01-01', vola_level=0.1)
WARNING: Strategy trades non-liquid assets.
WARNING: Strategy trades non-liquid assets.
### Simulation of Score movement for submission using periods of last Quantiacs Crypto Contest (Q17)
### last date of Contest period: 2022-08-31, Live (payout) period: 2022-10-01 ----- 2023-10-01
simulate_live(data, weights, benchmark, scale_date='2022-08-31', start='2022-10-01', end='2023-10-01', vola_level=0.1, show_score=True)
WARNING: some dates are missed in the portfolio_history
WARNING: some dates are missed in the portfolio_history
WARNING: Strategy trades non-liquid assets.
WARNING: some dates are missed in the portfolio_history
WARNING: some dates are missed in the portfolio_history
WARNING: Strategy trades non-liquid assets.
Submit strategy to the competition¶
Use Submit button on my strategies page
Make sure that qnout.write(weights) has been added to cell, and the weights have been written. It is not required when using Multi pass backtester.
qnout.write(weights)
Write output: /root/fractions.nc.gz