strategy

Quick Start Fundamental Data

This example showcases a trading strategy based on fundamental data on the Quantiacs platform. The strategy uses Nasdaq 100 index data and focuses on liquid stocks.

You can clone and edit this example there (tab Examples).


Strategy idea: We will buy shares of companies whose total revenue has increased over the last 65 days.

Full code

Below is the complete code snippet for this strategy:

import xarray as xr
import qnt.data    as qndata
import qnt.output as qnout
import qnt.stats   as qnstats
import qnt.graph as qngraph
import qnt.data.secgov_fundamental as fundamental

market_data = qndata.stocks.load_ndx_data(min_date="2005-01-01")
indicators_data = fundamental.load_indicators_for(market_data, indicator_names=['total_revenue'])


def calculate_weights(data, fundamental_data):
    """
    Calculate weights for the strategy based on a simple revenue growth check.

    If the total revenue for a given time period is greater than 65 days ago, assign a weight of 1 (buy), otherwise 0.

    """
    total_revenue = fundamental_data.sel(field="total_revenue")
    total_revenue_days_ago = total_revenue.shift(time=65)

    buy = 1
    is_up = xr.where(total_revenue > total_revenue_days_ago, buy, 0)

    return is_up  * data.sel(field='is_liquid') # use only liquidity assets


def add_buy_and_hold_enough_bid_for(data, weights_):
    """Add buy and hold condition based on the liquidity of the assets."""
    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_


def plot_performance(stats):
    """Plot the performance of the strategy."""
    performance = stats.to_pandas()["equity"]
    qngraph.make_plot_filled(performance.index, performance, name="PnL (Equity)", type="log")


weights = calculate_weights(market_data, indicators_data)
# Fundamental data is available from 2010 onwards
# Add a simple "buy and hold" strategy.
weights = add_buy_and_hold_enough_bid_for(market_data, weights)
weights = qnout.clean(weights, market_data, "stocks_nasdaq100")

stats = qnstats.calc_stat(market_data, weights.sel(time=slice("2006-01-01", None)))
display(stats.to_pandas().tail())
plot_performance(stats)

weights = weights.sel(time=slice("2006-01-01", None))
qnout.check(weights, market_data, "stocks_nasdaq100")
qnout.write(weights)  # to participate in the competition

1) Load libraries

Start by importing all the essential libraries.

In [1]:
import xarray as xr
import qnt.data as qndata
import qnt.output as qnout
import qnt.stats as qnstats
import qnt.graph as qngraph
import qnt.data.secgov_fundamental as fundamental

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

market_data - contains a list of assets by which indicators will be loaded

market_data.asset.to_pandas().to_list()

indicators_data: This dataset houses the principal fundamental indicators which are, by default, represented as LTM (Last Twelve Months). It essentially provides indicator values calculated for the last 4 quarters for each given date.

List of available indicators

display(fundamental.get_standard_indicator_names())
display(fundamental.get_complex_indicator_names())
display(fundamental.get_annual_indicator_names())

Loading data

# indicators_data = fundamental.load_indicators_for(market_data)
# indicators_data = fundamental.load_indicators_for(market_data, fundamental.get_standard_indicator_names())
# indicators_data = fundamental.load_indicators_for(market_data, fundamental.get_complex_indicator_names(),time_period = 'ltm')
# indicators_data = fundamental.load_indicators_for(market_data, fundamental.get_annual_indicator_names())

To construct fundamental indicators (equity, EV, EBITDA, etc.) fundamental facts are used (e.g., 'us-gaap:Revenues', 'us-gaap:StockholdersEquity', etc.).

You can check the source code of the library. It presents how fundamental indicators are constructed and how data is recovered in case of errors. You can create your own algorithm, here is an example

Data provider - https://www.sec.gov/. For example, Walmart, Inc. - List Reports and Annual report 2021-03-19

Load daily stock data for the Nasdaq-100 contest

In [2]:
market_data = qndata.stocks.load_ndx_data(min_date="2005-01-01")
indicators_data = fundamental.load_indicators_for(market_data, indicator_names=['total_revenue'])
100% (367973 of 367973) |################| Elapsed Time: 0:00:00 Time:  0:00:00
100% (39443 of 39443) |##################| Elapsed Time: 0:00:00 Time:  0:00:00
100% (14717216 of 14717216) |############| Elapsed Time: 0:00:00 Time:  0:00:00
fetched chunk 1/6 1s
100% (14720244 of 14720244) |############| Elapsed Time: 0:00:00 Time:  0:00:00
fetched chunk 2/6 2s
100% (14717184 of 14717184) |############| Elapsed Time: 0:00:00 Time:  0:00:00
fetched chunk 3/6 3s
100% (14717100 of 14717100) |############| Elapsed Time: 0:00:00 Time:  0:00:00
fetched chunk 4/6 4s
100% (14717100 of 14717100) |############| Elapsed Time: 0:00:00 Time:  0:00:00
fetched chunk 5/6 6s
100% (13667388 of 13667388) |############| Elapsed Time: 0:00:00 Time:  0:00:00
fetched chunk 6/6 7s
Data loaded 7s
100% (39443 of 39443) |##################| Elapsed Time: 0:00:00 Time:  0:00:00
load secgov facts...
100% (17044689 of 17044689) |############| Elapsed Time: 0:00:00 Time:  0:00:00
fetched chunk 1 / 5 9 s
100% (10862688 of 10862688) |############| Elapsed Time: 0:00:00 Time:  0:00:00
fetched chunk 2 / 5 13 s
100% (11962193 of 11962193) |############| Elapsed Time: 0:00:00 Time:  0:00:00
fetched chunk 3 / 5 18 s
100% (6787607 of 6787607) |##############| Elapsed Time: 0:00:00 Time:  0:00:00
fetched chunk 4 / 5 23 s
100% (5991391 of 5991391) |##############| Elapsed Time: 0:00:00 Time:  0:00:00
fetched chunk 5 / 5 27 s
facts loaded.

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

Fundamental data is available from 2010 onwards. However, to participate in the competition, the strategy needs to generate weights from 2006. In this context, we have decided to apply a simple "buy and hold" strategy. You are free to choose any other strategy for use.

In [3]:
def calculate_weights(data, fundamental_data):
    """
    Calculate weights for the strategy based on a simple revenue growth check.

    If the total revenue for a given time period is greater than 65 days ago, assign a weight of 1 (buy), otherwise 0.

    """
    total_revenue = fundamental_data.sel(field="total_revenue")
    total_revenue_days_ago = total_revenue.shift(time=65)

    buy = 1
    is_up = xr.where(total_revenue > total_revenue_days_ago, buy, 0)

    return is_up * data.sel(field='is_liquid')


def add_buy_and_hold_enough_bid_for(data, weights_):
    """Add buy and hold condition based on the liquidity of the assets."""
    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 = calculate_weights(market_data, indicators_data)
weights = add_buy_and_hold_enough_bid_for(market_data, weights)
weights = qnout.clean(weights, market_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(market_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
2024-04-18 4.458629 -0.005823 0.210355 -0.151425 -0.586099 0.405225 0.085241 1.0 172.0 0.038412 79.466880
2024-04-19 4.420653 -0.008517 0.210343 -0.158653 -0.586099 0.402744 0.084714 1.0 172.0 0.038407 79.466880
2024-04-22 4.449716 0.006574 0.210325 -0.153122 -0.586099 0.404536 0.085084 1.0 172.0 0.038402 79.466880
2024-04-23 4.488072 0.008620 0.210311 -0.145822 -0.586099 0.406894 0.085574 1.0 172.0 0.038396 79.466880
2024-04-24 4.506116 0.004020 0.210290 -0.142387 -0.586099 0.407975 0.085793 1.0 172.0 0.038391 78.986866

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

4) Submit Your strategy to the competition

To send the strategy, use the Submit button.

In [6]:
weights = weights.sel(time=slice("2006-01-01", None))
qnout.check(weights, market_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 - 2024-04-24
Sharpe Ratio = 0.407975367229318
ERROR! The Sharpe Ratio is too low. 0.407975367229318 < 1
Improve the strategy and make sure that the in-sample Sharpe Ratio more than 1.
Check correlation.
WARNING! Can't calculate correlation.
Correlation check failed.
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:

Potential Issues in Working with Fundamental Data:

  • Inconsistency in fact publication among companies:

    • One company might not publish a specific fact but might provide other data from which this fact can be derived.
    • Another company, on the contrary, might directly provide the fact, omitting intermediary data.
  • Lack of standardized formulas for indicators:

    • Not all indicators have standard calculation formulas.
    • For some of them, each company decides on its own which fundamental facts should be used to form the indicator.
    • This can lead to the same company using different data at different times for one indicator.
    • It's not accurate to compare companies based on such indicators.
  • Changing the strategy of indicator construction:

    • When updating financial statements, a company may change the methodology or calculation formulas for indicators, introducing an element of uncertainty.
  • Errors and corrections in reports:

    • Reports can contain errors, which are corrected later, but the initial data can distort the analysis.
  • Data omissions:

    • Some facts might be missing in the reports.
    • Companies might release their reports on different dates.
  • Issues with indicators based on stock prices:

    • If a company conducts a stock split before publishing a report, indicators can show unexpected changes, distorting the analysis.

The current implementation of Quantiacs partially resolve these issues:

  • When constructing an indicator, one formula is used for all companies, allowing them to be compared under "similar" conditions.
  • If key data for calculation is missing, the algorithm tries to restore it using other facts or indicators.
  • If data from the SEC gov report is missing, the algorithm tries to restore the missing information based on annual and quarterly reports, or if absent, uses average values.
  • By default, the strategy for constructing indicators is over 12 months (LTM). Users can build indicators for the quarter (QF) or use annual values (AF).

You can discover the available attributes in the us-gaap taxonomy here. Introduction to Financial Statements here