-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Jan Škoda
committed
Sep 15, 2023
1 parent
9049e61
commit 8314b54
Showing
2 changed files
with
280 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,279 @@ | ||
{ | ||
"cells": [ | ||
{ | ||
"cell_type": "markdown", | ||
"metadata": {}, | ||
"source": [ | ||
"<div style=\"float:right; width:100px; text-align: center; margin: 10px;\">\n", | ||
"<img src=\"https://crypto-lake.com/assets/img/lake.png\" alt=\"Lake\"/>\n", | ||
"</div>\n", | ||
"\n", | ||
"# Momentum indicator\n", | ||
"\n", | ||
"Backtest of a simple momentum indicator on 1m candle data. The logic was proposed by @BeatzXBT on twitter.\n", | ||
"\n", | ||
"We use [crypto-lake.com](https://crypto-lake.com/#data) sample/free market data, FTRB-USDT market on Ascendex.\n", | ||
"\n", | ||
"Quick links:\n", | ||
"- [edit this notebook online](https://mybinder.org/v2/gh/crypto-lake/analysis-sharing/main?filepath=momentum_indicator.ipynb) using Binder\n", | ||
"- [follow our activity on twitter](https://twitter.com/intent/user?screen_name=crypto_lake_com)" | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": null, | ||
"metadata": {}, | ||
"outputs": [], | ||
"source": [ | ||
"import datetime\n", | ||
"\n", | ||
"import numpy as np\n", | ||
"import pandas as pd\n", | ||
"import cufflinks as cf\n", | ||
"import plotly.express as px\n", | ||
"import statsmodels.api as sm\n", | ||
"\n", | ||
"import lakeapi\n", | ||
"\n", | ||
"cf.go_offline()\n", | ||
"\n", | ||
"# This runs on the paid data only at the moment\n", | ||
"# lakeapi.use_sample_data(anonymous_access=True)" | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": null, | ||
"metadata": {}, | ||
"outputs": [], | ||
"source": [ | ||
"# Parameters\n", | ||
"symbol = 'ADA-USDT'\n", | ||
"exchange = 'BINANCE'\n", | ||
"\n", | ||
"# Free sample data contain subset of the below time period\n", | ||
"start = datetime.datetime(2023, 8, 1)\n", | ||
"end = datetime.datetime(2023, 9, 1)" | ||
] | ||
}, | ||
{ | ||
"cell_type": "markdown", | ||
"metadata": {}, | ||
"source": [ | ||
"## Data" | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": null, | ||
"metadata": {}, | ||
"outputs": [], | ||
"source": [ | ||
"print('Loading candles')\n", | ||
"candles = lakeapi.load_data(\n", | ||
" table = 'candles',\n", | ||
" start = start,\n", | ||
" end = end,\n", | ||
" symbols = [symbol],\n", | ||
" exchanges = [exchange],\n", | ||
" drop_partition_cols = True,\n", | ||
").sort_values('origin_time')" | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": null, | ||
"metadata": {}, | ||
"outputs": [], | ||
"source": [ | ||
"# Minimize the data\n", | ||
"df = candles[['origin_time', 'close']]" | ||
] | ||
}, | ||
{ | ||
"cell_type": "markdown", | ||
"metadata": {}, | ||
"source": [ | ||
"## Logic" | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": null, | ||
"metadata": {}, | ||
"outputs": [], | ||
"source": [ | ||
"depths = np.asarray([200, 100, 50, 25, 10]) # in minutes\n", | ||
"n = len(depths)\n", | ||
"# depths *= 10 # try a longer time frame?\n", | ||
"\n", | ||
"# Signal logic\n", | ||
"for depth in depths:\n", | ||
"\tdf[f'ema_{depth}'] = np.log(df['close'] / df['close'].ewm(span=depth).mean()) * 100\n", | ||
"df['trend_val'] = df[[f'ema_{depth}' for depth in depths]].ewm(span=n, axis = 1).mean().sum(axis=1)\n", | ||
"df['pure_sum'] = df[[f'ema_{depth}' for depth in depths]].sum(axis=1)\n", | ||
"\n", | ||
"\n", | ||
"# Future return for evaluation\n", | ||
"df['future_return'] = df['close'].pct_change(10).shift(-10) * 100\n", | ||
"df = df.dropna()\n", | ||
"\n", | ||
"df[::3600].head(20)" | ||
] | ||
}, | ||
{ | ||
"cell_type": "markdown", | ||
"metadata": {}, | ||
"source": [ | ||
"## Evaluation" | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": null, | ||
"metadata": {}, | ||
"outputs": [], | ||
"source": [ | ||
"df.set_index('origin_time')[['close', 'trend_val']].iplot(secondary_y='trend_val')" | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": null, | ||
"metadata": {}, | ||
"outputs": [], | ||
"source": [ | ||
"# df.sample(1000).iplot(kind='scatter', x='trend_val', y='future_return', mode='markers', size=3)\n", | ||
"px.scatter(df.sample(10_000), x='trend_val', y='future_return', trendline='ols')" | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": null, | ||
"metadata": {}, | ||
"outputs": [], | ||
"source": [ | ||
"# Optional: install statsmodels for the next cell to run\n", | ||
"!pip install -q statsmodels" | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": null, | ||
"metadata": {}, | ||
"outputs": [], | ||
"source": [ | ||
"# Numerical linreg fit statistics\n", | ||
"mod = sm.OLS(df['trend_val'], df['future_return'])\n", | ||
"res = mod.fit()\n", | ||
"print(res.summary())" | ||
] | ||
}, | ||
{ | ||
"cell_type": "markdown", | ||
"metadata": {}, | ||
"source": [ | ||
"## Conclusion\n", | ||
"\n" | ||
] | ||
}, | ||
{ | ||
"cell_type": "markdown", | ||
"metadata": {}, | ||
"source": [ | ||
"It seems the trend value has a slight negative correlation with future trend on our data. I don't consider the correlation significant enough for this signal to be used in real trading.\n", | ||
"\n", | ||
"I also tried different settings, mostly longer ewm spans, but the results were similar." | ||
] | ||
}, | ||
{ | ||
"cell_type": "markdown", | ||
"metadata": {}, | ||
"source": [ | ||
"---\n", | ||
"\n", | ||
"### For reference: the original logic" | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": null, | ||
"metadata": {}, | ||
"outputs": [], | ||
"source": [ | ||
"# This seems to be equivalent to pandas Series.ewm(span=window).mean()\n", | ||
"def ema(arr_in: np.ndarray, window: int) -> np.ndarray:\n", | ||
" \"\"\"\n", | ||
" Hyper-fast EMA implementation\n", | ||
" \"\"\"\n", | ||
" \n", | ||
" n = arr_in.shape[0]\n", | ||
" ewma = np.empty(n, dtype=float)\n", | ||
" alpha = 2 / float(window + 1)\n", | ||
" w = 1\n", | ||
" ewma_old = arr_in[0]\n", | ||
" ewma[0] = ewma_old\n", | ||
"\n", | ||
" for i in range(1, n):\n", | ||
" w += (1-alpha)**i\n", | ||
" ewma_old = ewma_old*(1-alpha) + arr_in[i]\n", | ||
" ewma[i] = ewma_old / w\n", | ||
" \n", | ||
" return ewma\n", | ||
"\n", | ||
"# This is replicated in the cell below in a batch/more-efficient way\n", | ||
"def trend_feature(klines: np.ndarray, lengths: np.ndarray) -> float:\n", | ||
" \"\"\"\n", | ||
" Make sure lengths are fed in from longest to shortest\n", | ||
" \"\"\"\n", | ||
"\n", | ||
" closes = klines[:, 4]\n", | ||
" curr_price = closes[-1]\n", | ||
" n = len(lengths)\n", | ||
" vals = np.empty(n, dtype=float)\n", | ||
"\n", | ||
" for i in range(n):\n", | ||
" length = lengths[i]\n", | ||
" ema_val = ema(closes[-length:], length)[-1]\n", | ||
"\n", | ||
" # Safety check\n", | ||
" if ema_val == 0:\n", | ||
" vals[i] = vals[i-1] \n", | ||
"\n", | ||
" else:\n", | ||
" vals[i] = np.log(curr_price / ema_val) * 100\n", | ||
"\n", | ||
" trend_val = ema(vals, n)\n", | ||
"\n", | ||
" return np.sum(trend_val)\n" | ||
] | ||
} | ||
], | ||
"metadata": { | ||
"kernelspec": { | ||
"display_name": "Python 3.10.6 ('env': venv)", | ||
"language": "python", | ||
"name": "python3" | ||
}, | ||
"language_info": { | ||
"codemirror_mode": { | ||
"name": "ipython", | ||
"version": 3 | ||
}, | ||
"file_extension": ".py", | ||
"mimetype": "text/x-python", | ||
"name": "python", | ||
"nbconvert_exporter": "python", | ||
"pygments_lexer": "ipython3", | ||
"version": "3.11.4" | ||
}, | ||
"orig_nbformat": 4, | ||
"vscode": { | ||
"interpreter": { | ||
"hash": "7291192e6945a3c8ed0f956f4124ad6de62ed7ea9152f7ae8e29637075883be3" | ||
} | ||
} | ||
}, | ||
"nbformat": 4, | ||
"nbformat_minor": 2 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters