├── .gitignore ├── LICENSE ├── README.md ├── examples ├── HarmoCurrent.ipynb └── run_detect.py ├── requirements.txt ├── res ├── plot_0.png └── predict_0.png ├── setup.py └── src ├── __init__.py ├── harmonic_functions.py ├── harmonic_patterns.py └── settings.py /.gitignore: -------------------------------------------------------------------------------- 1 | # py-test cache 2 | .pytest* 3 | 4 | # ignore database 5 | *.sqlite 6 | *.sqlite3 7 | 8 | # I have no idea 9 | .idea 10 | 11 | # Byte-compiled / optimized / DLL files 12 | __pycache__/ 13 | *.py[cod] 14 | *$py.class 15 | 16 | # C extensions 17 | *.so 18 | 19 | *.h5 20 | 21 | # Distribution / packaging 22 | .Python 23 | env/ 24 | build/ 25 | develop-eggs/ 26 | dist/ 27 | downloads/ 28 | eggs/ 29 | .eggs/ 30 | lib/ 31 | lib64/ 32 | parts/ 33 | sdist/ 34 | var/ 35 | wheels/ 36 | *.egg-info/ 37 | .installed.cfg 38 | *.egg 39 | 40 | # PyInstaller 41 | # Usually these files are written by a python script from a template 42 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 43 | *.manifest 44 | *.spec 45 | 46 | # Installer logs 47 | pip-log.txt 48 | pip-delete-this-directory.txt 49 | 50 | # Unit test / coverage reports 51 | htmlcov/ 52 | .tox/ 53 | .coverage 54 | .coverage.* 55 | .cache 56 | nosetests.xml 57 | coverage.xml 58 | *.cover 59 | .hypothesis/ 60 | 61 | # Translations 62 | *.mo 63 | *.pot 64 | 65 | # Django stuff: 66 | *.log 67 | local_settings.py 68 | 69 | !./static/.gitkeep 70 | 71 | static/ 72 | 73 | !.gitignore 74 | 75 | # Flask stuff: 76 | instance/ 77 | .webassets-cache 78 | 79 | # Scrapy stuff: 80 | .scrapy 81 | 82 | # Sphinx documentation 83 | docs/_build/ 84 | 85 | # PyBuilder 86 | target/ 87 | 88 | # Jupyter Notebook 89 | .ipynb_checkpoints 90 | 91 | # pyenv 92 | .python-version 93 | 94 | # celery beat schedule file 95 | celerybeat-schedule 96 | 97 | # SageMath parsed files 98 | *.sage.py 99 | 100 | # dotenv 101 | .env 102 | 103 | # virtualenv 104 | .venv 105 | venv/ 106 | ENV/ 107 | 108 | # Spyder project settings 109 | .spyderproject 110 | .spyproject 111 | 112 | # Rope project settings 113 | .ropeproject 114 | 115 | # mkdocs documentation 116 | /site 117 | 118 | # mypy 119 | .mypy_cache/ 120 | 121 | # OS generated files # 122 | ###################### 123 | .DS_Store 124 | .DS_Store? 125 | .Spotlight-V100 126 | .Trashes -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Joffrey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Harmonic Pattern Detector 2 | 3 | ``` 4 | In short, this project filters ZIGZAG patterns that fit-in Harmonic Patterns. 5 | ``` 6 | 7 | ### Search these patterns in ZIG-ZAG(parameters are configurable) patterns: 8 | 9 | [Harmonic Trading reference](https://harmonicpattern.com/blog/harmonic-pattern-and-elliot-wave-theory-advanced-technique/) 10 | 11 | + ABCD 12 | + Gartley 13 | + Bat 14 | + AltBat 15 | + ButterFly 16 | + Crab 17 | + DeepCrab 18 | + Shark 19 | + Cypper 20 | 21 | + all supports predict and deepsearch 22 | 23 | ### patterns found 24 | 25 | 26 | ![plot_0](res/plot_0.png) 27 | 28 | 29 | 30 | ### patterns predict 31 | 32 | ![predict_0](res/predict_0.png) 33 | 34 | 35 | 36 | #### Reqirements 37 | 38 | + TA-Lib 39 | 40 |
41 | 42 | Setup 43 |

44 | 45 | 46 | ```bash 47 | cd 48 | pip install -r requirements.txt 49 | pip install -e . # or python setup.py install 50 | ``` 51 | 52 |

53 |
54 | 55 | 56 | ### Features 57 | 58 | #### Visualize 59 | 60 | + Draw Harmonic Patterns in the graph using mplfinance + ipympl 61 | 62 | 63 | #### Predict 64 | 65 | + Predict harmonic patterns according to current kline 66 | 67 | #### Else: 68 | 69 | + go to examples/*.ipynb 70 | + [example](examples/HarmoCurrent.ipynb) 71 | -------------------------------------------------------------------------------- /examples/HarmoCurrent.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "### init" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 13, 13 | "metadata": { 14 | "ExecuteTime": { 15 | "end_time": "2023-01-07T23:12:25.659999Z", 16 | "start_time": "2023-01-07T23:12:25.545687Z" 17 | }, 18 | "init_cell": true 19 | }, 20 | "outputs": [ 21 | { 22 | "name": "stdout", 23 | "output_type": "stream", 24 | "text": [ 25 | "The autoreload extension is already loaded. To reload it, use:\n", 26 | " %reload_ext autoreload\n" 27 | ] 28 | } 29 | ], 30 | "source": [ 31 | "%load_ext autoreload\n", 32 | "import matplotlib\n", 33 | "%reload_ext autoreload\n", 34 | "%autoreload 2\n", 35 | "\n", 36 | "#%matplotlib notebook\n", 37 | "#%matplotlib widget\n", 38 | "\n", 39 | "import warnings\n", 40 | "warnings.simplefilter(\"ignore\")" 41 | ] 42 | }, 43 | { 44 | "cell_type": "code", 45 | "execution_count": 14, 46 | "metadata": { 47 | "ExecuteTime": { 48 | "end_time": "2023-01-07T23:12:25.748099Z", 49 | "start_time": "2023-01-07T23:12:25.663793Z" 50 | }, 51 | "init_cell": true 52 | }, 53 | "outputs": [], 54 | "source": [ 55 | "import logging\n", 56 | "import signal, threading, os, time\n", 57 | "import logging\n", 58 | "\n", 59 | "from IPython.core.debugger import set_trace\n", 60 | "from IPython.terminal.embed import embed\n", 61 | "\n", 62 | "import os, sys\n", 63 | "\n", 64 | "\n", 65 | "import ccxt\n", 66 | "import pandas as pd\n", 67 | "\n", 68 | "logger = logging.getLogger('notebook')\n", 69 | "logger.setLevel(logging.INFO)" 70 | ] 71 | }, 72 | { 73 | "cell_type": "code", 74 | "execution_count": 15, 75 | "metadata": { 76 | "ExecuteTime": { 77 | "end_time": "2023-01-07T23:12:25.842974Z", 78 | "start_time": "2023-01-07T23:12:25.751353Z" 79 | }, 80 | "init_cell": true 81 | }, 82 | "outputs": [], 83 | "source": [ 84 | "import mplfinance as mpf\n", 85 | "#matplotlib.use('Agg')\n", 86 | "import matplotlib.pyplot as plt\n", 87 | "plt.rcParams['figure.figsize'] = [8, 12]\n" 88 | ] 89 | }, 90 | { 91 | "cell_type": "code", 92 | "execution_count": 16, 93 | "metadata": { 94 | "ExecuteTime": { 95 | "end_time": "2023-01-07T23:12:25.939296Z", 96 | "start_time": "2023-01-07T23:12:25.846489Z" 97 | }, 98 | "code_folding": [ 99 | 9 100 | ], 101 | "init_cell": true 102 | }, 103 | "outputs": [], 104 | "source": [ 105 | "\n", 106 | "try:\n", 107 | " from settings import HTTP_PROXY\n", 108 | "except ImportError:\n", 109 | " HTTP_PROXY = None\n", 110 | " \n", 111 | "# (!!!)You should change this according to your network environment\n", 112 | "if HTTP_PROXY is None:\n", 113 | " HTTP_PROXY = 'http://127.0.0.1:1087' \n", 114 | "\n", 115 | "def kline_to_df(arr) -> pd.DataFrame:\n", 116 | " kline = pd.DataFrame(\n", 117 | " arr,\n", 118 | " columns=['ts', 'open', 'high', 'low', 'close', 'volume' ])\n", 119 | " kline.index = pd.to_datetime(kline.ts, unit='ms')\n", 120 | " kline.drop('ts', axis=1, inplace=True)\n", 121 | " return kline\n", 122 | " " 123 | ] 124 | }, 125 | { 126 | "cell_type": "code", 127 | "execution_count": 17, 128 | "metadata": { 129 | "ExecuteTime": { 130 | "end_time": "2023-01-07T23:12:26.022225Z", 131 | "start_time": "2023-01-07T23:12:25.942116Z" 132 | }, 133 | "code_folding": [], 134 | "init_cell": true, 135 | "scrolled": false 136 | }, 137 | "outputs": [], 138 | "source": [ 139 | "\n", 140 | "PROXIES = {\n", 141 | " 'http': HTTP_PROXY,\n", 142 | " 'https': HTTP_PROXY,\n", 143 | "}\n", 144 | "\n", 145 | "ccxt_options = {'proxies': PROXIES}\n", 146 | "\n", 147 | "ok = 'okex'\n", 148 | "bn = 'binance'\n", 149 | "\n", 150 | "client_list = [bn, ok]" 151 | ] 152 | }, 153 | { 154 | "cell_type": "markdown", 155 | "metadata": {}, 156 | "source": [ 157 | "## Step by Step" 158 | ] 159 | }, 160 | { 161 | "cell_type": "code", 162 | "execution_count": 19, 163 | "metadata": { 164 | "ExecuteTime": { 165 | "end_time": "2023-01-07T23:12:35.936358Z", 166 | "start_time": "2023-01-07T23:12:35.160500Z" 167 | } 168 | }, 169 | "outputs": [], 170 | "source": [ 171 | "# prepare client\n", 172 | "binance = getattr(ccxt, 'binance')(ccxt_options)\n", 173 | "markerts = binance.load_markets()\n", 174 | "symbol = 'BTC/USDT'\n", 175 | "period = '4h'" 176 | ] 177 | }, 178 | { 179 | "cell_type": "code", 180 | "execution_count": 20, 181 | "metadata": { 182 | "ExecuteTime": { 183 | "end_time": "2023-01-07T23:12:36.660188Z", 184 | "start_time": "2023-01-07T23:12:36.278189Z" 185 | } 186 | }, 187 | "outputs": [], 188 | "source": [ 189 | "# fetch raw kline data via ccxt unified api\n", 190 | "kline_data = binance.fetch_ohlcv(symbol, period, limit=1000)\n", 191 | "\n", 192 | "kline = kline_to_df(kline_data)" 193 | ] 194 | }, 195 | { 196 | "cell_type": "code", 197 | "execution_count": 21, 198 | "metadata": { 199 | "ExecuteTime": { 200 | "end_time": "2023-01-07T23:12:37.083300Z", 201 | "start_time": "2023-01-07T23:12:37.002885Z" 202 | } 203 | }, 204 | "outputs": [ 205 | { 206 | "name": "stdout", 207 | "output_type": "stream", 208 | "text": [ 209 | "raw ccxt kline data:\n", 210 | " [[1658736000000, 21962.33, 22110.98, 21868.36, 21941.25, 22048.18138], [1658750400000, 21942.18, 22003.12, 21683.4, 21895.79, 32159.10297], [1658764800000, 21893.94, 22021.47, 21552.53, 21890.17, 27200.4675], [1658779200000, 21892.66, 22259.98, 21250.0, 21310.9, 43229.18467]]\n" 211 | ] 212 | } 213 | ], 214 | "source": [ 215 | "print('raw ccxt kline data:\\n', kline_data[:4])" 216 | ] 217 | }, 218 | { 219 | "cell_type": "code", 220 | "execution_count": 22, 221 | "metadata": { 222 | "ExecuteTime": { 223 | "end_time": "2023-01-07T23:12:37.503897Z", 224 | "start_time": "2023-01-07T23:12:37.432331Z" 225 | }, 226 | "scrolled": true 227 | }, 228 | "outputs": [ 229 | { 230 | "name": "stdout", 231 | "output_type": "stream", 232 | "text": [ 233 | "convert to dataframe:\n", 234 | " open high low close volume\n", 235 | "ts \n", 236 | "2022-07-25 08:00:00 21962.33 22110.98 21868.36 21941.25 22048.18138\n", 237 | "2022-07-25 12:00:00 21942.18 22003.12 21683.40 21895.79 32159.10297\n", 238 | "2022-07-25 16:00:00 21893.94 22021.47 21552.53 21890.17 27200.46750\n", 239 | "2022-07-25 20:00:00 21892.66 22259.98 21250.00 21310.90 43229.18467\n" 240 | ] 241 | } 242 | ], 243 | "source": [ 244 | "print('convert to dataframe:\\n', kline[:4])" 245 | ] 246 | }, 247 | { 248 | "cell_type": "code", 249 | "execution_count": 23, 250 | "metadata": { 251 | "ExecuteTime": { 252 | "end_time": "2023-01-07T23:12:39.898580Z", 253 | "start_time": "2023-01-07T23:12:37.843840Z" 254 | } 255 | }, 256 | "outputs": [], 257 | "source": [ 258 | "patterns, predict_patterns = detector.search_patterns(kline, only_last=False, last_n=4, plot=False, predict=True)" 259 | ] 260 | }, 261 | { 262 | "cell_type": "code", 263 | "execution_count": 24, 264 | "metadata": { 265 | "ExecuteTime": { 266 | "end_time": "2023-01-07T23:12:40.288715Z", 267 | "start_time": "2023-01-07T23:12:40.216987Z" 268 | } 269 | }, 270 | "outputs": [ 271 | { 272 | "name": "stdout", 273 | "output_type": "stream", 274 | "text": [ 275 | "BTC/USDT 4h \n", 276 | "patterns found: [Timestamp('2022-08-17 04:00:00'), Timestamp('2022-08-20 20:00:00'), Timestamp('2022-08-21 20:00:00'), Timestamp('2022-08-23 04:00:00'), Timestamp('2022-08-24 16:00:00')], [['H', 24446.71, 137], ['L', 20761.9, 159], ['H', 21800.0, 165], ['L', 20890.14, 173], ['H', 21900.0, 182]], \n", 277 | " bearish abcd, {'AB': 0.8764666217127463, 'BC': 1.1099070186622118, 'AB=CD': 0.9727964550621349}\n", 278 | "BTC/USDT 4h \n", 279 | "patterns found: [Timestamp('2022-08-15 00:00:00'), Timestamp('2022-08-20 20:00:00'), Timestamp('2022-08-21 20:00:00'), Timestamp('2022-08-23 04:00:00'), Timestamp('2022-08-24 16:00:00')], [['H', 25211.32, 124], ['L', 20761.9, 159], ['H', 21800.0, 165], ['L', 20890.14, 173], ['H', 21900.0, 182]], \n", 280 | " bearish abcd, {'AB': 0.8764666217127463, 'BC': 1.1099070186622118, 'AB=CD': 0.9727964550621349}\n", 281 | "BTC/USDT 4h \n", 282 | "patterns found: [Timestamp('2022-10-04 20:00:00'), Timestamp('2022-10-13 12:00:00'), Timestamp('2022-10-14 00:00:00'), Timestamp('2022-10-21 12:00:00'), Timestamp('2022-10-29 08:00:00')], [['H', 20475.0, 429], ['L', 18190.0, 481], ['H', 19951.87, 484], ['L', 18650.0, 529], ['H', 21085.0, 576]], \n", 283 | " bearish butterfly, {'XAB': 0.7710590809628004, 'XAD': 1.2669584245076586, 'ABC': 0.7389137677581205, 'BCD': 1.8703864441150053, 'AB=CD': 1.3820542945847316}\n" 284 | ] 285 | } 286 | ], 287 | "source": [ 288 | "for pat in patterns:\n", 289 | " msg = f'{symbol} {period} \\npatterns found: {pat[1]}, {pat[0]}, \\n {pat[2]}, {pat[3]}'\n", 290 | " print(msg)\n", 291 | "\n", 292 | "\n", 293 | "for pat in predict_patterns:\n", 294 | " msg = '\\n'.join([f'{p} {v}' for p,v in list(zip([str(dt) for dt in pat[1]], [p for p in pat[0]]))])\n", 295 | " msg = f'{symbol} {period} {msg} {pat[2]} {pat[3]}'\n", 296 | " print(msg)" 297 | ] 298 | }, 299 | { 300 | "cell_type": "markdown", 301 | "metadata": { 302 | "heading_collapsed": true 303 | }, 304 | "source": [ 305 | "### Parameters" 306 | ] 307 | }, 308 | { 309 | "cell_type": "code", 310 | "execution_count": 18, 311 | "metadata": { 312 | "ExecuteTime": { 313 | "end_time": "2023-01-07T23:12:26.120221Z", 314 | "start_time": "2023-01-07T23:12:26.025441Z" 315 | }, 316 | "code_folding": [], 317 | "hidden": true, 318 | "init_cell": true, 319 | "run_control": { 320 | "marked": true 321 | } 322 | }, 323 | "outputs": [], 324 | "source": [ 325 | "from harmonic_functions import HarmonicDetector\n", 326 | "#from HarmonicPatterns.harmonic import send_alert, search_function\n", 327 | "from functools import partial\n", 328 | "import time\n", 329 | "from multiprocessing import Pool\n", 330 | "\n", 331 | "PERIODS = ['1h', '4h', '1d']\n", 332 | "\n", 333 | "################\n", 334 | "# HarmonicDetector params\n", 335 | "# err_allowed: The error rate that detector would allow, usually 0.05 < err_rate < 0.1\n", 336 | "# strict: for example 0.618 * (1 - err_rate) < pattern < 0.618 * (1 + err_rate), the base should be a fibbonacci number, like 0.382, 0.618, 0.786, 1.618....\n", 337 | "# predict_err_rate: # similiar to err_allowed but used in predict\n", 338 | "\n", 339 | "\n", 340 | "################\n", 341 | "# search params\n", 342 | "# only_last: find patterns in history or not\n", 343 | "# symbols: symbols list to find\n", 344 | "# periods: periods that used in ccxt\n", 345 | "# last_n: if only_last is set, how near should the bar be\n", 346 | "# alert: send alerts when patterns found\n", 347 | "# plot: plot patterns when patterns found\n", 348 | "def search_function(detector, exchange_id, symbols, periods=PERIODS, ccxt_args={}, savefig=False, predict=True, only_last=False, alert=False, plot=True):\n", 349 | " client = getattr(ccxt, exchange_id)(ccxt_args)\n", 350 | " client.load_markets()\n", 351 | " RETRY_TIMES=3\n", 352 | " for symbol in symbols:\n", 353 | " for period in periods:\n", 354 | " print(f'------------------calculating {symbol} {period}------------------')\n", 355 | " retry = RETRY_TIMES\n", 356 | " while retry>0:\n", 357 | " try:\n", 358 | " df = kline_to_df(client.fetch_ohlcv(symbol, period, limit=1000))\n", 359 | "\n", 360 | " patterns, predict_patterns = detector.search_patterns(df, only_last=only_last, last_n=4, plot=plot, predict=predict)\n", 361 | " break\n", 362 | " except Exception as e:\n", 363 | " print(e)\n", 364 | " retry -= 1\n", 365 | " if retry==0: raise\n", 366 | " continue\n", 367 | " for pat in patterns:\n", 368 | " msg = f'{symbol} {period} \\npatterns found: {pat[1]}, {pat[0]}, \\n {pat[2]}, {pat[3]}'\n", 369 | " print(msg)\n", 370 | " if alert and pat[0][-1][2] == len(df)-1:\n", 371 | " send_alert(f'Pattern_Found_{symbol}_{period}', msg)\n", 372 | "\n", 373 | " for pat in predict_patterns:\n", 374 | " msg = '\\n'.join([f'{p} {v}' for p,v in list(zip([str(dt) for dt in pat[1]], [p for p in pat[0]]))])\n", 375 | " msg = f'{symbol} {period} {msg} {pat[2]} {pat[3]}'\n", 376 | " print(msg)\n", 377 | " if alert:\n", 378 | " send_alert(f'Pattern_Predict_{symbol}_{period}', msg)\n", 379 | "\n", 380 | "\n", 381 | "detector = HarmonicDetector(error_allowed=0.07, strict=True, predict_err_rate=0.07)\n", 382 | "search = partial(search_function, detector, ccxt_args=ccxt_options, only_last=False, plot=True)\n", 383 | "\n", 384 | "#PERIODS = ['5m','15m', '30m']\n" 385 | ] 386 | }, 387 | { 388 | "cell_type": "markdown", 389 | "metadata": {}, 390 | "source": [ 391 | "### DETECT BTC" 392 | ] 393 | }, 394 | { 395 | "cell_type": "code", 396 | "execution_count": null, 397 | "metadata": { 398 | "ExecuteTime": { 399 | "end_time": "2023-01-07T22:59:52.333500Z", 400 | "start_time": "2023-01-07T22:59:42.268411Z" 401 | }, 402 | "scrolled": true 403 | }, 404 | "outputs": [], 405 | "source": [ 406 | "s = ['BTC/USDT', 'ETH/USDT']\n", 407 | "search('binance', s, periods = ['1h', '4h'])" 408 | ] 409 | }, 410 | { 411 | "cell_type": "markdown", 412 | "metadata": {}, 413 | "source": [ 414 | "### BTC USDT" 415 | ] 416 | }, 417 | { 418 | "cell_type": "code", 419 | "execution_count": null, 420 | "metadata": { 421 | "ExecuteTime": { 422 | "end_time": "2023-01-07T22:47:19.358619Z", 423 | "start_time": "2023-01-07T22:47:17.448137Z" 424 | }, 425 | "scrolled": true 426 | }, 427 | "outputs": [], 428 | "source": [ 429 | "s = ['BTC/USDT']\n", 430 | "\n", 431 | "search(ok, s, periods=['1h', '4h'], predict=True, only_last=False, alert=False, plot=True)\n" 432 | ] 433 | } 434 | ], 435 | "metadata": { 436 | "kernelspec": { 437 | "display_name": "Python 3", 438 | "language": "python", 439 | "name": "python3" 440 | }, 441 | "language_info": { 442 | "codemirror_mode": { 443 | "name": "ipython", 444 | "version": 3 445 | }, 446 | "file_extension": ".py", 447 | "mimetype": "text/x-python", 448 | "name": "python", 449 | "nbconvert_exporter": "python", 450 | "pygments_lexer": "ipython3", 451 | "version": "3.7.4" 452 | }, 453 | "varInspector": { 454 | "cols": { 455 | "lenName": 16, 456 | "lenType": 16, 457 | "lenVar": 40 458 | }, 459 | "kernels_config": { 460 | "python": { 461 | "delete_cmd_postfix": "", 462 | "delete_cmd_prefix": "del ", 463 | "library": "var_list.py", 464 | "varRefreshCmd": "print(var_dic_list())" 465 | }, 466 | "r": { 467 | "delete_cmd_postfix": ") ", 468 | "delete_cmd_prefix": "rm(", 469 | "library": "var_list.r", 470 | "varRefreshCmd": "cat(var_dic_list()) " 471 | } 472 | }, 473 | "types_to_exclude": [ 474 | "module", 475 | "function", 476 | "builtin_function_or_method", 477 | "instance", 478 | "_Feature" 479 | ], 480 | "window_display": false 481 | } 482 | }, 483 | "nbformat": 4, 484 | "nbformat_minor": 4 485 | } 486 | -------------------------------------------------------------------------------- /examples/run_detect.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import warnings 3 | warnings.simplefilter("ignore") 4 | 5 | from datetime import datetime 6 | 7 | import signal, threading, os, time 8 | import logging 9 | 10 | from IPython.core.debugger import set_trace 11 | from IPython.terminal.embed import embed 12 | 13 | import time 14 | from multiprocessing import Pool, TimeoutError 15 | from functools import partial 16 | 17 | import os, sys 18 | import asyncio 19 | 20 | import nest_asyncio 21 | nest_asyncio.apply() 22 | 23 | import inspect 24 | #import ccxt.async_support as ccxt 25 | import ccxt 26 | 27 | import pandas as pd 28 | 29 | # from djow_core.base.logger import get_logger 30 | from logging import Logger 31 | 32 | HTTP_PROXY = None 33 | 34 | import httpx 35 | 36 | logger = Logger('Harmonic') 37 | 38 | from .harmonic_functions import HarmonicDetector 39 | 40 | 41 | from .settings import NOTIFY_URL 42 | from .settings import MAIN_SYMBOLS, ALT_SYMBOLS, PERIODS, ERROR_RATE 43 | from .settings import PROCESS_COUNT 44 | 45 | 46 | import redis 47 | redis_client = redis.Redis() 48 | 49 | 50 | import sys 51 | def get_frame_fname(level=0): 52 | return sys._getframe(level+1).f_code.co_name 53 | 54 | def send_alert(title: str, body: str): 55 | """ 56 | 用Redis缓存一下,主要是为了不重复发送 57 | """ 58 | body = body.replace('\n', '\n\n') 59 | template=''' 60 | \n\n 61 | {body} 62 | \n\n 63 | ''' 64 | if not redis_client.exists(body): 65 | r = httpx.post(NOTIFY_URL, data={'text': title, 'desp': template.format(body=body)}) 66 | redis_client.setex(body, 60 * 60 * 30, 1) 67 | return r.status_code 68 | else: 69 | return None 70 | 71 | 72 | def kline_to_df(arr) -> pd.DataFrame: 73 | kline = pd.DataFrame( 74 | arr, 75 | columns=['ts', 'open', 'high', 'low', 'close', 'volume' ]) 76 | kline.index = pd.to_datetime(kline.ts, unit='ms') 77 | kline.drop('ts', axis=1, inplace=True) 78 | return kline 79 | 80 | 81 | def search_function(detector, exchange_id, symbols, periods=PERIODS, ccxt_args={}, savefig=False, predict=True, only_last=False, alert=False, plot=False): 82 | client = getattr(ccxt, exchange_id)(ccxt_args) 83 | client.load_markets() 84 | RETRY_TIMES=3 85 | for symbol in symbols: 86 | for period in periods: 87 | logger.info(f'------------------calculating {symbol} {period}------------------') 88 | retry = RETRY_TIMES 89 | while retry>0: 90 | try: 91 | df = kline_to_df(client.fetch_ohlcv(symbol, period, limit=1000)) 92 | 93 | patterns, predict_patterns = detector.search_patterns(df, only_last=only_last, last_n=4, plot=plot, predict=predict) 94 | break 95 | except Exception as e: 96 | logger.error(e) 97 | retry -= 1 98 | if retry==0: raise 99 | continue 100 | for pat in patterns: 101 | msg = f'{symbol} {period} \npatterns found: {pat[1]}, {pat[0]}, \n {pat[2]}, {pat[3]}' 102 | logger.info(msg) 103 | if alert and pat[0][-1][2] == len(df)-1: 104 | send_alert(f'Pattern_Found_{symbol}_{period}', msg) 105 | 106 | for pat in predict_patterns: 107 | msg = '\n'.join([f'{p} {v}' for p,v in list(zip([str(dt) for dt in pat[1]], [p for p in pat[0]]))]) 108 | msg = f'{symbol} {period} {msg} {pat[2]} {pat[3]}' 109 | logger.info(msg) 110 | if alert: 111 | send_alert(f'Pattern_Predict_{symbol}_{period}', msg) 112 | 113 | 114 | def main(): 115 | #signal.signal(signal.SIGINT, partial(debug_handler, engine)) 116 | 117 | PROXIES = { 118 | 'http': HTTP_PROXY, 119 | 'https': HTTP_PROXY, 120 | } 121 | 122 | ccxt_options = {'proxies': PROXIES} 123 | 124 | ok = 'okex' 125 | bn = 'binance' 126 | hb = 'huobipro' 127 | 128 | notify_msgs = [] 129 | while True: 130 | epoch_start_time = datetime.now() 131 | predict_results = [] 132 | #call_repl(engine) 133 | 134 | detector = HarmonicDetector(error_allowed=ERROR_RATE, strict=True) 135 | client = hb 136 | 137 | symbols = [*MAIN_SYMBOLS, *ALT_SYMBOLS] 138 | 139 | search = partial(search_function, detector, ccxt_args=ccxt_options) 140 | 141 | try: 142 | with Pool(PROCESS_COUNT) as p: 143 | # 检测主流币和山寨是否出现谐波模式 144 | r = p.map_async(partial(search, client, periods=PERIODS, predict=PREDICT, only_last=True, alert=True, plot=False), [[si] for si in symbols]) 145 | # 检测平台币 146 | r1 = p.map_async(partial(search, hb, periods=PERIODS, predict=PREDICT, only_last=True, alert=True, plot=False), [['HT/USDT']]) 147 | r2 = p.map_async(partial(search, ok, periods=PERIODS, predict=PREDICT, only_last=True, alert=True, plot=False), [['OKB/USDT']]) 148 | r3 = p.map_async(partial(search, bn, periods=PERIODS, predict=PREDICT, only_last=True, alert=True, plot=False), [['BNB/USDT']]) 149 | r.get(timeout=360) 150 | r1.get(timeout=120) 151 | r2.get(timeout=120) 152 | r3.get(timeout=120) 153 | except TimeoutError as e: 154 | logger.error(e) 155 | continue 156 | except Exception as e: 157 | logger.error(e) 158 | continue 159 | finally: 160 | pass 161 | 162 | 163 | epoch_end_time = datetime.now() 164 | run_time = (epoch_end_time - epoch_start_time).total_seconds() 165 | print(f'------------|Total seconds: {run_time}s|---------------') 166 | if len(predict_results)>0: 167 | send_alert('Patterns predict', '\n\n'.join(predict_results)) 168 | #time.sleep(10) 169 | 170 | 171 | if __name__ == '__main__': 172 | # 从异步改成同步,用multiprocessing来达到并发效果 173 | #loop = asyncio.get_event_loop() 174 | #loop.run_until_complete(main()) 175 | main() 176 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | 2 | numpy>=1.14.2 3 | #scipy>=1.3.1 4 | pandas>=0.22.0 5 | ipython 6 | 7 | ccxt 8 | jupyter 9 | 10 | matplotlib 11 | 12 | #nest-asyncio 13 | mplfinance 14 | 15 | ipympl 16 | 17 | TA-Lib 18 | 19 | fire 20 | -------------------------------------------------------------------------------- /res/plot_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djoffrey/HarmonicPatterns/9ebdcab2cb598dc8636957e92a27cef93e1aa4ca/res/plot_0.png -------------------------------------------------------------------------------- /res/predict_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djoffrey/HarmonicPatterns/9ebdcab2cb598dc8636957e92a27cef93e1aa4ca/res/predict_0.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os, glob 2 | 3 | from setuptools import setup, find_packages 4 | 5 | HERE = os.path.dirname(os.path.abspath(__file__)) 6 | 7 | 8 | # with open('requirements.txt') as f: 9 | # requirements = f.read().splitlines() 10 | # requirements = [r for r in requirements if not r.startswith('-e')] 11 | 12 | 13 | # packages = ['djow'] + glob.glob('djow_*') 14 | 15 | setup( 16 | name='HarmonicPatterns', 17 | version='0.0.1', 18 | # install_requires=requirements, 19 | # packages=packages, 20 | url='https://github.com/djoffrey/HarmonicPatterns', 21 | author='djoffrey', 22 | author_email='joffrey.oh@gmail.com', 23 | description='A Python Library for Harmonic Trading' 24 | ) 25 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djoffrey/HarmonicPatterns/9ebdcab2cb598dc8636957e92a27cef93e1aa4ca/src/__init__.py -------------------------------------------------------------------------------- /src/harmonic_functions.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | Author Djoffrey 4 | some harmonic pattern scanner functions 5 | (TODO: calculate using (high+low)/2 or etc, not just close price) 6 | (TODO: should return each peak distance) 7 | """ 8 | 9 | import pandas as pd 10 | import numpy as np 11 | import talib.abstract as ta 12 | # from scipy.signal import argrelextrema 13 | import mplfinance as mpf 14 | 15 | MAIN_FIBB_RATIOS = [0.618, 1.618] 16 | SECOND_FIBB_RATIOS = [0.786, 0.886, 1.13, 1.27] 17 | ALT_FIBB_RATIOS = [0.382, 0.5, 0.707, 1.41, 2.0, 2.24, 2.618, 3.14, 3.618] 18 | AB_CD = [1.27, 1.618, 2.236] 19 | 20 | FIBB_RATIOS = [*MAIN_FIBB_RATIOS, *SECOND_FIBB_RATIOS, *ALT_FIBB_RATIOS] 21 | 22 | from IPython.core.debugger import set_trace 23 | import os 24 | 25 | 26 | def kline_to_df(arr) -> pd.DataFrame: 27 | kline = pd.DataFrame( 28 | arr, 29 | columns=['ts', 'open', 'high', 'low', 'close', 'volume' ]) 30 | kline.index = pd.to_datetime(kline.ts, unit='ms') 31 | kline.drop('ts', axis=1, inplace=True) 32 | return kline 33 | 34 | 35 | class HarmonicDetector(object): 36 | def __init__(self, error_allowed:float=0.05, strict:bool=True, predict_err_rate:float=None): 37 | self.err = error_allowed 38 | self.predict_err_rate = self.err if predict_err_rate is None else predict_err_rate 39 | 40 | self.strict = strict 41 | 42 | def is_eq(self, n: float, m: float, err:float=None, l_closed:bool=False, r_closed:bool=True) -> bool: 43 | _err = self.err if err is None else err 44 | left = m if l_closed else m * (1 - _err) 45 | right = m if r_closed else m * (1 + _err) 46 | return (n >= left) and (n <= right) 47 | 48 | def is_in(self, n: float, l: float, r: float, err:float=None, l_closed:bool=True, r_closed:bool=True) -> bool: 49 | _err = self.err if err is None else err 50 | left = l if l_closed else l * (1 - _err) 51 | right = r if r_closed else r * (1 + _err) 52 | if self.strict: 53 | fibb_rates = [ 54 | self.is_eq(n, f_rate) for f_rate in FIBB_RATIOS 55 | if (f_rate >= left) and (f_rate <= right) 56 | ] 57 | return np.any(fibb_rates) 58 | else: 59 | return (n >= left) and (n <= right) 60 | 61 | def get_zigzag(self, df: pd.DataFrame, period: int): 62 | 63 | # translated from https://www.tradingview.com/script/mRbjBGdL-Double-Zig-Zag-with-HHLL/ 64 | 65 | zigzag_pattern = [] 66 | direction = 0 67 | changed = False 68 | for idx in range(1, len(df)): 69 | highest_high = ta.MAX(df.high[:idx], timeperiod=period)[-1] 70 | lowest_low = ta.MIN(df.low[:idx], timeperiod=period)[-1] 71 | 72 | 73 | new_high = df.high[idx] >= highest_high 74 | new_low = df.low[idx] <= lowest_low 75 | 76 | if new_high and not new_low: 77 | if direction != 1: 78 | direction = 1 79 | changed = True 80 | elif direction == 1: 81 | changed = False 82 | elif not new_high and new_low: 83 | if direction != -1: 84 | direction = -1 85 | changed = True 86 | elif direction == -1: 87 | changed = False 88 | 89 | if new_high or new_low: 90 | if changed or len(zigzag_pattern)==0: 91 | if direction == 1: 92 | pat = ['H', df.high[idx], idx] 93 | zigzag_pattern.append(pat) 94 | elif direction == -1: 95 | pat = ['L', df.low[idx], idx] 96 | zigzag_pattern.append(pat) 97 | else: 98 | if direction == 1 and df.high[idx] > zigzag_pattern[-1][1]: 99 | pat = ['H', df.high[idx], idx] 100 | zigzag_pattern[-1] = pat 101 | elif direction == -1 and df.low[idx] < zigzag_pattern[-1][1]: 102 | pat = ['L', df.low[idx], idx] 103 | zigzag_pattern[-1] = pat 104 | else: 105 | pass 106 | return zigzag_pattern 107 | 108 | def detect_abcd(self, current_pat: list, predict:bool=False, predict_mode:str='direct'): 109 | """ 110 | AB=CD is a common pattern in harmonic trading, it is many pattern's inner structure 111 | AB=CD has two main alternatives 1.27AB=CD and 1.618AB=CD 112 | """ 113 | if not predict: 114 | X, A, B, C, D = [pat[1] for pat in current_pat] 115 | last_direction = current_pat[-1][0] 116 | XAB = abs(B-A) / abs(X-A) 117 | XAD = abs(A-D) / abs(X-A) 118 | ABC = abs(B-C) / abs(A-B) 119 | BCD = abs(C-D) / abs(B-C) 120 | 121 | ABCD = BCD / (1/ABC) 122 | 123 | ret_dict = { 124 | 'AB': ABC, 125 | 'BC': BCD, 126 | 'AB=CD': ABCD 127 | } 128 | 129 | # Detect 130 | pattern_found = np.all(np.array([ 131 | self.is_in(ABC, 0.382, 0.886), # AB 132 | self.is_in(BCD, 1.13, 1.168), # CD 133 | self.is_eq(ABCD, 1) # strictly 1/AB = CD 134 | ])) 135 | direction = 1 if D origin_D): 205 | pattern_predict_s3 = True 206 | 207 | if pattern_predict_s2 and pattern_predict_s3: 208 | return [direction, ret_dict] 209 | else: 210 | return None 211 | else: 212 | return None 213 | 214 | def detect_gartley(self, current_pat: list, predict: bool=False, predict_mode:str='direct'): 215 | # current_pat: [['H', new_high, idx],...] 216 | # Legs 217 | if not predict: 218 | X, A, B, C, D = [pat[1] for pat in current_pat] 219 | last_direction = current_pat[-1][0] 220 | XAB = abs(B-A) / abs(X-A) 221 | XAD = abs(A-D) / abs(X-A) 222 | ABC = abs(B-C) / abs(A-B) 223 | BCD = abs(C-D) / abs(B-C) 224 | 225 | ABCD = BCD / (1/ABC) 226 | 227 | ret_dict = { 228 | 'XAB': XAB, 229 | 'XAD': XAD, 230 | 'ABC': ABC, 231 | 'BCD': BCD, 232 | 'AB=CD': ABCD 233 | } 234 | 235 | # Detect 236 | pattern_found = np.all(np.array([ 237 | self.is_eq(XAB, 0.618), # LEG 1 238 | self.is_in(ABC, 0.382, 0.886), # LEG 2 239 | self.is_in(BCD, 1.13, 1.168), # LEG 3 240 | self.is_eq(XAD, 0.786), # LEG 4 241 | self.is_eq(ABCD, 1) # AB=CD 242 | ])) 243 | direction = 1 if D origin_D): 328 | pattern_predict_s3 = True 329 | 330 | if pattern_predict_s2 and pattern_predict_s3: 331 | return [direction, ret_dict] 332 | else: 333 | return None 334 | else: 335 | return None 336 | 337 | def detect_bat(self, current_pat: list, predict: bool=False, predict_mode:str='direct'): 338 | # current_pat: [['H', new_high, idx],...] 339 | # Legs 340 | if not predict: 341 | X, A, B, C, D = [pat[1] for pat in current_pat] 342 | last_direction = current_pat[-1][0] 343 | XAB = abs(B-A) / abs(X-A) 344 | XAD = abs(A-D) / abs(X-A) 345 | ABC = abs(B-C) / abs(A-B) 346 | BCD = abs(C-D) / abs(B-C) 347 | 348 | ABCD = BCD / (1/ABC) 349 | 350 | ret_dict = { 351 | 'XAB': XAB, 352 | 'XAD': XAD, 353 | 'ABC': ABC, 354 | 'BCD': BCD, 355 | 'AB=CD': ABCD 356 | } 357 | 358 | # Detect 359 | pattern_found = np.all(np.array([ 360 | self.is_in(XAB, 0.382, 0.5), # LEG 1 361 | self.is_in(ABC, 0.382, 0.886), # LEG 2 362 | self.is_in(BCD, 1.618, 2.168), # LEG 3 363 | self.is_eq(XAD, 0.886), # LEG 4 364 | self.is_eq(ABCD, 1) or self.is_in(ABCD, 0.786, 1.618), # AB=CD and its alternatives 365 | ])) 366 | direction = 1 if D origin_D): 449 | pattern_predict_s3 = True 450 | 451 | if pattern_predict_s2 and pattern_predict_s3: 452 | return [direction, ret_dict] 453 | else: 454 | return None 455 | else: 456 | return None 457 | def detect_altbat(self, current_pat: list, predict: bool=False, predict_mode:str='direct'): 458 | # current_pat: [['H', new_high, idx],...] 459 | # Legs 460 | if not predict: 461 | X, A, B, C, D = [pat[1] for pat in current_pat] 462 | last_direction = current_pat[-1][0] 463 | XAB = abs(B-A) / abs(X-A) 464 | XAD = abs(A-D) / abs(X-A) 465 | ABC = abs(B-C) / abs(A-B) 466 | BCD = abs(C-D) / abs(B-C) 467 | 468 | ret_dict = { 469 | 'XAB': XAB, 470 | 'XAD': XAD, 471 | 'ABC': ABC, 472 | 'BCD': BCD 473 | } 474 | 475 | # Detect 476 | pattern_found = np.all(np.array([ 477 | self.is_eq(XAB, 0.382), # LEG 1 478 | self.is_in(ABC, 0.382, 0.886), # LEG 2 479 | self.is_in(BCD, 2, 3.168), # LEG 3 480 | self.is_eq(XAD, 1.13) # LEG 4 481 | ])) 482 | direction = 1 if D origin_D): 558 | pattern_predict_s3 = True 559 | 560 | if pattern_predict_s2 and pattern_predict_s3: 561 | return [direction, ret_dict] 562 | else: 563 | return None 564 | 565 | else: 566 | return None 567 | 568 | def detect_butterfly(self, current_pat: list, predict: bool=False, predict_mode:str='direct'): 569 | # current_pat: [['H', new_high, idx],...] 570 | # Legs 571 | if not predict: 572 | X, A, B, C, D = [pat[1] for pat in current_pat] 573 | last_direction = current_pat[-1][0] 574 | XAB = abs(B-A) / abs(X-A) 575 | XAD = abs(A-D) / abs(X-A) 576 | ABC = abs(B-C) / abs(A-B) 577 | BCD = abs(C-D) / abs(B-C) 578 | 579 | ABCD = BCD / (1/ABC) 580 | 581 | ret_dict = { 582 | 'XAB': XAB, 583 | 'XAD': XAD, 584 | 'ABC': ABC, 585 | 'BCD': BCD, 586 | 'AB=CD': ABCD 587 | } 588 | 589 | # Detect 590 | pattern_found = np.all(np.array([ 591 | self.is_eq(XAB, 0.786), # LEG 1 592 | self.is_in(ABC, 0.382, 0.886), # LEG 2 593 | self.is_in(BCD, 1.618, 2.24), # LEG 3 594 | self.is_eq(XAD, 1.27), # LEG 4 595 | self.is_eq(ABCD, 1) or self.is_in(ABCD, 0.786, 1.618), # AB=CD and its alternatives 596 | ])) 597 | direction = 1 if D origin_D): 676 | pattern_predict_s3 = True 677 | 678 | if pattern_predict_s2 and pattern_predict_s3: 679 | return [direction, ret_dict] 680 | else: 681 | return None 682 | 683 | else: 684 | return None 685 | 686 | def detect_crab(self, current_pat: list, predict: bool=False, predict_mode:str='direct'): 687 | # current_pat: [['H', new_high, idx],...] 688 | # Legs 689 | if not predict: 690 | X, A, B, C, D = [pat[1] for pat in current_pat] 691 | last_direction = current_pat[-1][0] 692 | XAB = abs(B-A) / abs(X-A) 693 | XAD = abs(A-D) / abs(X-A) 694 | ABC = abs(B-C) / abs(A-B) 695 | BCD = abs(C-D) / abs(B-C) 696 | 697 | ABCD = BCD / (1/ABC) 698 | ret_dict = { 699 | 'XAB': XAB, 700 | 'XAD': XAD, 701 | 'ABC': ABC, 702 | 'BCD': BCD, 703 | 'AB=CD': ABCD 704 | } 705 | 706 | # Detect 707 | pattern_found = np.all(np.array([ 708 | self.is_in(XAB, 0.382, 0.618),# LEG 1 709 | self.is_in(ABC, 0.382, 0.886), # LEG 2 710 | self.is_in(BCD, 2.618, 3.618), # LEG 3 711 | self.is_eq(XAD, 1.618), # LEG 4 712 | self.is_eq(ABCD, 1) or self.is_in(ABCD, 0.786, 2.236), # AB=CD and its alternatives 713 | ])) 714 | direction = 1 if D origin_D): 800 | pattern_predict_s3 = True 801 | 802 | if pattern_predict_s2 and pattern_predict_s3: 803 | return [direction, ret_dict] 804 | else: 805 | return None 806 | else: 807 | return None 808 | def detect_deepcrab(self, current_pat: list, predict: bool=False, predict_mode:str='direct'): 809 | # current_pat: [['H', new_high, idx],...] 810 | # Legs 811 | if not predict: 812 | X, A, B, C, D = [pat[1] for pat in current_pat] 813 | last_direction = current_pat[-1][0] 814 | XAB = abs(B-A) / abs(X-A) 815 | XAD = abs(A-D) / abs(X-A) 816 | ABC = abs(B-C) / abs(A-B) 817 | BCD = abs(C-D) / abs(B-C) 818 | 819 | ABCD = BCD / (1/ABC) 820 | 821 | ret_dict = { 822 | 'XAB': XAB, 823 | 'XAD': XAD, 824 | 'ABC': ABC, 825 | 'BCD': BCD, 826 | 'AB=CD': ABCD 827 | } 828 | 829 | # Detect 830 | pattern_found = np.all(np.array([ 831 | self.is_eq(XAB, 0.886),# LEG 1 832 | self.is_in(ABC, 0.382, 0.886), # LEG 2 833 | self.is_in(BCD, 2.24, 3.618), # LEG 3 834 | self.is_eq(XAD, 1.618), # LEG 4 835 | self.is_eq(ABCD, 1) or self.is_in(ABCD, 0.786, 2.236), # AB=CD and its alternatives 836 | ])) 837 | direction = 1 if D origin_D): 921 | pattern_predict_s3 = True 922 | 923 | if pattern_predict_s2 and pattern_predict_s3: 924 | return [direction, ret_dict] 925 | else: 926 | return None 927 | 928 | else: 929 | return None 930 | 931 | def detect_shark(self, current_pat: list, predict: bool=False, predict_mode:str='direct'): 932 | # current_pat: [['H', new_high, idx],...] 933 | # Legs 934 | if not predict: 935 | X, A, B, C, D = [pat[1] for pat in current_pat] 936 | last_direction = current_pat[-1][0] 937 | XAB = abs(B-A) / abs(X-A) 938 | XAD = abs(A-D) / abs(X-A) 939 | ABC = abs(B-C) / abs(A-B) 940 | BCD = abs(C-D) / abs(B-C) 941 | 942 | ret_dict = { 943 | 'XAB': XAB, 944 | 'XAD': XAD, 945 | 'ABC': ABC, 946 | 'BCD': BCD 947 | } 948 | 949 | # Detect 950 | pattern_found = np.all(np.array([ 951 | self.is_in(XAB, 0.5, 0.886), # LEG 1 952 | self.is_in(ABC, 1.13, 1.618), # LEG 2 953 | self.is_in(BCD, 1.618, 2.24), # LEG 3 954 | self.is_in(XAD, 0.886, 1.13) # LEG 4 955 | ])) 956 | direction = 1 if D origin_D): 1031 | pattern_predict_s3 = True 1032 | 1033 | if pattern_predict_s2 and pattern_predict_s3: 1034 | return [direction, ret_dict] 1035 | else: 1036 | return None 1037 | else: 1038 | return None 1039 | def detect_5o(self, current_pat: list, predict: bool=False, predict_mode:str='direct'): 1040 | # current_pat: [['H', new_high, idx],...] 1041 | # Legs 1042 | if not predict: 1043 | X, A, B, C, D = [pat[1] for pat in current_pat] 1044 | last_direction = current_pat[-1][0] 1045 | XAB = abs(B-A) / abs(X-A) 1046 | XAD = abs(A-D) / abs(X-A) 1047 | ABC = abs(B-C) / abs(A-B) 1048 | BCD = abs(C-D) / abs(B-C) 1049 | 1050 | ABCD = BCD / (1/ABC) 1051 | 1052 | ret_dict = { 1053 | 'XAB': XAB, 1054 | 'XAD': XAD, 1055 | 'ABC': ABC, 1056 | 'BCD': BCD, 1057 | 'AB=CD': ABCD 1058 | } 1059 | 1060 | # Detect 1061 | pattern_found = np.all(np.array([ 1062 | self.is_in(XAB, 1.13, 1.618), # LEG 1 1063 | self.is_in(ABC, 1.618, 2.24), # LEG 2 1064 | self.is_eq(BCD, 0.5), # LEG 3 1065 | #self.is_eq(XAD, 0.5), # LEG 4 1066 | self.is_eq(ABCD, 1) or self.is_in(ABCD, 0.786, 1.618), # AB=CD and its alternatives 1067 | ])) 1068 | direction = 1 if D origin_D): 1152 | pattern_predict_s3 = True 1153 | 1154 | if pattern_predict_s2 and pattern_predict_s3: 1155 | return [direction, ret_dict] 1156 | else: 1157 | return None 1158 | 1159 | else: 1160 | return None 1161 | def detect_cypher(self, current_pat: list, predict: bool=False, predict_mode:str='direct'): 1162 | # current_pat: [['H', new_high, idx],...] 1163 | # Legs 1164 | if not predict: 1165 | X, A, B, C, D = [pat[1] for pat in current_pat] 1166 | last_direction = current_pat[-1][0] 1167 | XAB = abs(B-A) / abs(X-A) 1168 | XAD = abs(A-D) / abs(X-A) 1169 | ABC = abs(B-C) / abs(A-B) 1170 | BCD = abs(C-D) / abs(B-C) 1171 | XCD = abs(D-C) / abs(X-C) 1172 | 1173 | ret_dict = { 1174 | 'XAB': XAB, 1175 | 'XAD': XAD, 1176 | 'ABC': ABC, 1177 | 'BCD': BCD, 1178 | 'XCD': XCD 1179 | } 1180 | 1181 | # Detect 1182 | pattern_found = np.all(np.array([ 1183 | self.is_in(XAB, 0.382, 0.786), # LEG 1 1184 | self.is_in(ABC, 1.272, 1.414), # LEG 2 1185 | #self.is_in(BCD, 1.618, 2.24), # LEG 3 1186 | self.is_eq(XCD, 0.786) or self.is_eq(XAD, 0.786) , # LEG 4 1187 | ])) 1188 | direction = 1 if D origin_D): 1267 | pattern_predict_s3 = True 1268 | 1269 | if pattern_predict_s3: 1270 | return [direction, ret_dict] 1271 | else: 1272 | return None 1273 | 1274 | else: 1275 | return None 1276 | 1277 | def detect_pattern(self, zigzag_pattern): 1278 | res = [] 1279 | # price default to close 1280 | for idx in range(0, len(zigzag_pattern) - 5 + 1): 1281 | current_pat = [pat for pat in zigzag_pattern[idx:idx+5]] 1282 | current_idx = [pat[2] for pat in zigzag_pattern[idx:idx+5]] 1283 | detect_funcions = [ 1284 | (self.detect_gartley, 'gartley'), 1285 | (self.detect_bat, 'bat'), 1286 | (self.detect_altbat, 'altbat'), 1287 | (self.detect_butterfly, 'butterfly'), 1288 | (self.detect_crab, 'crab'), 1289 | (self.detect_deepcrab, 'deepcrab'), 1290 | (self.detect_shark, 'shark'), 1291 | (self.detect_5o, '5o'), 1292 | (self.detect_cypher, 'cypher'), 1293 | (self.detect_abcd, 'abcd'), 1294 | ] 1295 | if len(current_pat) == 5: 1296 | for func, func_name in detect_funcions: 1297 | r = func(current_pat) 1298 | if r is not None: 1299 | direction, ret_dict = r 1300 | bull_or_bear = 'bullish' if direction==1 else 'bearish' 1301 | label = f'{bull_or_bear} {func_name}' 1302 | res.append([current_pat, current_idx, label, ret_dict]) 1303 | return res 1304 | 1305 | def predict_pattern(self, zigzag_pattern): 1306 | res = [] 1307 | # predict reverse pattern 1308 | current_pat = zigzag_pattern[-5:] 1309 | current_idx = [pat[2] for pat in zigzag_pattern[-5:]] 1310 | 1311 | detect_funcions = [ 1312 | (self.detect_gartley, 'gartley'), 1313 | (self.detect_bat, 'bat'), 1314 | (self.detect_altbat, 'altbat'), 1315 | (self.detect_butterfly, 'butterfly'), 1316 | (self.detect_crab, 'crab'), 1317 | (self.detect_deepcrab, 'deepcrab'), 1318 | (self.detect_shark, 'shark'), 1319 | (self.detect_5o, '5o'), 1320 | (self.detect_cypher, 'cypher'), 1321 | ] 1322 | if len(current_pat) == 5: 1323 | for func, func_name in detect_funcions: 1324 | r = func(current_pat, predict=True, predict_mode='reverse') 1325 | if r is not None: 1326 | direction, ret_dict = r 1327 | _, X, A, B, C = [pat[1] for pat in current_pat] 1328 | D = ret_dict['predict_D'] 1329 | bull_or_bear = 'bullish' if direction==1 else 'bearish' 1330 | label = f'{bull_or_bear} {func_name} predict next D: {D}' 1331 | # Funny ha? but shit happens 1332 | if D > 0: 1333 | p_pattern = [X, A, B, C, D] 1334 | p_idx = [*current_idx[1:], -1] 1335 | res.append([p_pattern, p_idx, label, ret_dict]) 1336 | 1337 | for func, func_name in detect_funcions: 1338 | r = func(current_pat, predict=True, predict_mode='direct') 1339 | if r is not None: 1340 | direction, ret_dict = r 1341 | X, A, B, C, origin_D = [pat[1] for pat in current_pat] 1342 | D = ret_dict['predict_D'] 1343 | bull_or_bear = 'bullish' if direction==1 else 'bearish' 1344 | label = f'{bull_or_bear} {func_name} predict current D: {D}' 1345 | # Funny ha? but shit happens 1346 | if D > 0: 1347 | p_pattern = [X, A, B, C, D] 1348 | res.append([p_pattern, current_idx, label, ret_dict]) 1349 | return res 1350 | 1351 | def get_patterns(self, df: pd.DataFrame, window: int, predict: bool=False, plot:bool=False): 1352 | zigzag_pattern = self.get_zigzag(df, window) 1353 | patterns = self.detect_pattern(zigzag_pattern) 1354 | if plot: 1355 | points = [(df.index[k[2]], k[1]) for k in zigzag_pattern] 1356 | patterns_line = [list(zip( 1357 | [df.index[dt_idx] for dt_idx in pat[1]], 1358 | [val_idx[1] for val_idx in pat[0]] 1359 | )) for pat in patterns] 1360 | mpf.plot(df,type='candle', alines=dict(alines=patterns_line, colors=['b','r','c','k','g'])) 1361 | if predict: 1362 | predict_res = self.predict_pattern(zigzag_pattern) 1363 | return patterns, predict_res 1364 | return patterns, None 1365 | 1366 | def filter_duplicats(self, patterns, predict:bool=False): 1367 | pat_set = set() 1368 | ret = [] 1369 | if not predict: 1370 | for pat in patterns: 1371 | X,A,B,C,D = pat[1] 1372 | pat_str = f'{X}-{A}-{B}-{C}-{D}' 1373 | if pat_str not in pat_set: 1374 | ret.append(pat) 1375 | pat_set.add(pat_str) 1376 | else: 1377 | # predict 1378 | for pat in patterns: 1379 | X,A,B,C,D = pat[1] # get index 1380 | label = pat[2] # get_label 1381 | #predict_D = pat[3]['predict_D'] # get_predict_D 1382 | pat_str = f'{X}-{A}-{B}-{C}-{D}-{label}' 1383 | if pat_str not in pat_set: 1384 | ret.append(pat) 1385 | pat_set.add(pat_str) 1386 | return ret 1387 | 1388 | def plot_patterns(self, df, patterns, predict_patterns, plot_predict:bool=True, save_fig:bool=False, file_name:str=None, file_data_path='./data/'): 1389 | """ 1390 | plot results, if save_fig, will save_fig to {file_name} 1391 | """ 1392 | # points = [(df.index[k[2]], k[1]) for k in zigzag_pattern] 1393 | patterns_line = [list(zip( 1394 | [df.index[dt_idx] for dt_idx in pat[1]], 1395 | [val_idx[1] for val_idx in pat[0]] 1396 | )) for pat in patterns if pat[2][-4:]!='abcd'] 1397 | if plot_predict: 1398 | new_index = df.index.append(pd.Index([ 1399 | df.index[-1] + 10*(df.index[-1] - df.index[-2]) 1400 | ])) 1401 | df = pd.DataFrame(df, index=new_index) 1402 | df.fillna(0) 1403 | patterns_predict_line = [list(zip( 1404 | [df.index[dt_idx] for dt_idx in pat[1]], 1405 | [val_idx for val_idx in pat[0]] 1406 | )) for pat in predict_patterns if pat[2][-4:]!='abcd'] 1407 | 1408 | predict_Ds = [pat[3]['predict_D'] for pat in predict_patterns] 1409 | patterns_line.extend(patterns_predict_line) 1410 | else: 1411 | predict_Ds = [] 1412 | 1413 | mc = mpf.make_marketcolors(base_mpf_style='yahoo') 1414 | # Create a style based on `seaborn` using those market colors: 1415 | style = mpf.make_mpf_style(base_mpl_style='seaborn', marketcolors=mc) 1416 | 1417 | if len(patterns_line) > 0: 1418 | if save_fig and file_name is not None: 1419 | if not os.path.exists(file_data_path): 1420 | os.mkdir(file_data_path) 1421 | file_path = file_data_path + file_name 1422 | mpf.plot(df, 1423 | type='candle', 1424 | mav=(10,21,55,120), 1425 | style=style, 1426 | alines=dict(alines=patterns_line, 1427 | colors=['b','r','c','k','g'], 1428 | linewidths=3, 1429 | alpha=0.5 1430 | ), 1431 | hlines=dict(hlines=predict_Ds, colors=['g'],linestyle='-.'), 1432 | savefig=file_path 1433 | ) 1434 | else: 1435 | mpf.plot(df, 1436 | type='candle', 1437 | mav=(10,21,55,120), 1438 | style=style, 1439 | alines=dict(alines=patterns_line, 1440 | colors=['b','r','c','k','g'], 1441 | linewidths=3, 1442 | alpha=0.5 1443 | ), 1444 | hlines=dict(hlines=predict_Ds, colors=['g'],linestyle='-.'), 1445 | #savefig='test.svg' 1446 | ) 1447 | 1448 | def search_patterns(self, df: pd.DataFrame, predict: bool=False, only_last:bool=False, last_n:int=0, plot:bool=False, plot_predict:bool=True, return_dt_idx=True, save_fig_name:str=None): 1449 | assert last_n < 5 1450 | all_patterns = list() 1451 | all_predict_patterns = list() 1452 | for window in [8, 13, 21, 34, 55]: 1453 | zigzag_pattern = self.get_zigzag(df, window) 1454 | if predict: 1455 | ori_err = self.err 1456 | self.err = self.predict_err_rate 1457 | predict_res = self.predict_pattern(zigzag_pattern) 1458 | all_predict_patterns.extend(predict_res) 1459 | self.err = ori_err 1460 | patterns = self.detect_pattern(zigzag_pattern) 1461 | if only_last: 1462 | all_patterns.extend(patterns[-1:]) 1463 | else: 1464 | all_patterns.extend(patterns) 1465 | 1466 | if only_last and last_n>0: 1467 | all_patterns = [pat for pat in all_patterns if pat[0][-1][2] == len(df) - last_n] 1468 | 1469 | all_patterns = self.filter_duplicats(all_patterns) 1470 | all_predict_patterns = self.filter_duplicats(all_predict_patterns, predict=True) 1471 | if plot: 1472 | if save_fig_name is not None: 1473 | self.plot_patterns(df, all_patterns, all_predict_patterns, save_fig=True, file_name=save_fig_name) 1474 | else: 1475 | self.plot_patterns(df, all_patterns, all_predict_patterns, save_fig=False) 1476 | 1477 | if return_dt_idx: 1478 | all_patterns = [ 1479 | [pat[0], 1480 | [df.index[dt_idx] for dt_idx in pat[1]], 1481 | pat[2], 1482 | pat[3]] 1483 | for pat in all_patterns] 1484 | all_predict_patterns = [ 1485 | [pat[0], 1486 | [df.index[dt_idx] for dt_idx in pat[1]], 1487 | pat[2], 1488 | pat[3]] 1489 | for pat in all_predict_patterns] 1490 | 1491 | return all_patterns, all_predict_patterns 1492 | 1493 | 1494 | if __name__ == '__main__': 1495 | detector = HarmonicDetector() 1496 | print('done') 1497 | -------------------------------------------------------------------------------- /src/harmonic_patterns.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | Author Djoffrey 4 | some harmonic pattern scanner functions 5 | (TODO: calculate using (high+low)/2 or etc, not just close price) 6 | (TODO: should return each peak distance) 7 | """ 8 | 9 | import pandas as pd 10 | import numpy as np 11 | import talib.abstract as ta 12 | # from scipy.signal import argrelextrema 13 | import mplfinance as mpf 14 | 15 | MAIN_FIBB_RATIOS = [0.618, 1.618] 16 | SECOND_FIBB_RATIOS = [0.786, 0.886, 1.13, 1.27] 17 | ALT_FIBB_RATIOS = [0.382, 0.5, 0.707, 1.41, 2.0, 2.24, 2.618, 3.14, 3.618] 18 | AB_CD = [1.27, 1.618, 2.236] 19 | 20 | FIBB_RATIOS = [*MAIN_FIBB_RATIOS, *SECOND_FIBB_RATIOS, *ALT_FIBB_RATIOS] 21 | 22 | from IPython.core.debugger import set_trace 23 | import os 24 | 25 | 26 | def kline_to_df(arr) -> pd.DataFrame: 27 | kline = pd.DataFrame( 28 | arr, 29 | columns=['ts', 'open', 'high', 'low', 'close', 'volume' ]) 30 | kline.index = pd.to_datetime(kline.ts, unit='ms') 31 | kline.drop('ts', axis=1, inplace=True) 32 | return kline 33 | 34 | 35 | class HarmonicDetector(object): 36 | def __init__(self, error_allowed:float=0.05, strict:bool=True, predict_err_rate:float=None): 37 | self.err = error_allowed 38 | self.predict_err_rate = self.err if predict_err_rate is None else predict_err_rate 39 | 40 | self.strict = strict 41 | 42 | def is_eq(self, n: float, m: float, err:float=None, l_closed:bool=False, r_closed:bool=True) -> bool: 43 | _err = self.err if err is None else err 44 | left = m if l_closed else m * (1 - _err) 45 | right = m if r_closed else m * (1 + _err) 46 | return (n >= left) and (n <= right) 47 | 48 | def is_in(self, n: float, l: float, r: float, err:float=None, l_closed:bool=True, r_closed:bool=True) -> bool: 49 | _err = self.err if err is None else err 50 | left = l if l_closed else l * (1 - _err) 51 | right = r if r_closed else r * (1 + _err) 52 | if self.strict: 53 | fibb_rates = [ 54 | self.is_eq(n, f_rate) for f_rate in FIBB_RATIOS 55 | if (f_rate >= left) and (f_rate <= right) 56 | ] 57 | return np.any(fibb_rates) 58 | else: 59 | return (n >= left) and (n <= right) 60 | 61 | def get_zigzag(self, df: pd.DataFrame, period: int): 62 | 63 | # translated from https://www.tradingview.com/script/mRbjBGdL-Double-Zig-Zag-with-HHLL/ 64 | 65 | zigzag_pattern = [] 66 | direction = 0 67 | changed = False 68 | for idx in range(1, len(df)): 69 | highest_high = ta.MAX(df.high[:idx], timeperiod=period)[-1] 70 | lowest_low = ta.MIN(df.low[:idx], timeperiod=period)[-1] 71 | 72 | 73 | new_high = df.high[idx] >= highest_high 74 | new_low = df.low[idx] <= lowest_low 75 | 76 | if new_high and not new_low: 77 | if direction != 1: 78 | direction = 1 79 | changed = True 80 | elif direction == 1: 81 | changed = False 82 | elif not new_high and new_low: 83 | if direction != -1: 84 | direction = -1 85 | changed = True 86 | elif direction == -1: 87 | changed = False 88 | 89 | if new_high or new_low: 90 | if changed or len(zigzag_pattern)==0: 91 | if direction == 1: 92 | pat = ['H', df.high[idx], idx] 93 | zigzag_pattern.append(pat) 94 | elif direction == -1: 95 | pat = ['L', df.low[idx], idx] 96 | zigzag_pattern.append(pat) 97 | else: 98 | if direction == 1 and df.high[idx] > zigzag_pattern[-1][1]: 99 | pat = ['H', df.high[idx], idx] 100 | zigzag_pattern[-1] = pat 101 | elif direction == -1 and df.low[idx] < zigzag_pattern[-1][1]: 102 | pat = ['L', df.low[idx], idx] 103 | zigzag_pattern[-1] = pat 104 | else: 105 | pass 106 | return zigzag_pattern 107 | 108 | def detect_abcd(self, current_pat: list, predict:bool=False, predict_mode:str='direct'): 109 | """ 110 | AB=CD is a common pattern in harmonic trading, it is many pattern's inner structure 111 | AB=CD has two main alternatives 1.27AB=CD and 1.618AB=CD 112 | """ 113 | if not predict: 114 | X, A, B, C, D = [pat[1] for pat in current_pat] 115 | last_direction = current_pat[-1][0] 116 | XAB = abs(B-A) / abs(X-A) 117 | XAD = abs(A-D) / abs(X-A) 118 | ABC = abs(B-C) / abs(A-B) 119 | BCD = abs(C-D) / abs(B-C) 120 | 121 | ABCD = BCD / (1/ABC) 122 | 123 | ret_dict = { 124 | 'AB': ABC, 125 | 'BC': BCD, 126 | 'AB=CD': ABCD 127 | } 128 | 129 | # Detect 130 | pattern_found = np.all(np.array([ 131 | self.is_in(ABC, 0.382, 0.886), # AB 132 | self.is_in(BCD, 1.13, 1.168), # CD 133 | self.is_eq(ABCD, 1) # strictly 1/AB = CD 134 | ])) 135 | direction = 1 if D origin_D): 205 | pattern_predict_s3 = True 206 | 207 | if pattern_predict_s2 and pattern_predict_s3: 208 | return [direction, ret_dict] 209 | else: 210 | return None 211 | else: 212 | return None 213 | 214 | def detect_gartley(self, current_pat: list, predict: bool=False, predict_mode:str='direct'): 215 | # current_pat: [['H', new_high, idx],...] 216 | # Legs 217 | if not predict: 218 | X, A, B, C, D = [pat[1] for pat in current_pat] 219 | last_direction = current_pat[-1][0] 220 | XAB = abs(B-A) / abs(X-A) 221 | XAD = abs(A-D) / abs(X-A) 222 | ABC = abs(B-C) / abs(A-B) 223 | BCD = abs(C-D) / abs(B-C) 224 | 225 | ABCD = BCD / (1/ABC) 226 | 227 | ret_dict = { 228 | 'XAB': XAB, 229 | 'XAD': XAD, 230 | 'ABC': ABC, 231 | 'BCD': BCD, 232 | 'AB=CD': ABCD 233 | } 234 | 235 | # Detect 236 | pattern_found = np.all(np.array([ 237 | self.is_eq(XAB, 0.618), # LEG 1 238 | self.is_in(ABC, 0.382, 0.886), # LEG 2 239 | self.is_in(BCD, 1.13, 1.168), # LEG 3 240 | self.is_eq(XAD, 0.786), # LEG 4 241 | self.is_eq(ABCD, 1) # AB=CD 242 | ])) 243 | direction = 1 if D origin_D): 328 | pattern_predict_s3 = True 329 | 330 | if pattern_predict_s2 and pattern_predict_s3: 331 | return [direction, ret_dict] 332 | else: 333 | return None 334 | else: 335 | return None 336 | 337 | def detect_bat(self, current_pat: list, predict: bool=False, predict_mode:str='direct'): 338 | # current_pat: [['H', new_high, idx],...] 339 | # Legs 340 | if not predict: 341 | X, A, B, C, D = [pat[1] for pat in current_pat] 342 | last_direction = current_pat[-1][0] 343 | XAB = abs(B-A) / abs(X-A) 344 | XAD = abs(A-D) / abs(X-A) 345 | ABC = abs(B-C) / abs(A-B) 346 | BCD = abs(C-D) / abs(B-C) 347 | 348 | ABCD = BCD / (1/ABC) 349 | 350 | ret_dict = { 351 | 'XAB': XAB, 352 | 'XAD': XAD, 353 | 'ABC': ABC, 354 | 'BCD': BCD, 355 | 'AB=CD': ABCD 356 | } 357 | 358 | # Detect 359 | pattern_found = np.all(np.array([ 360 | self.is_in(XAB, 0.382, 0.5), # LEG 1 361 | self.is_in(ABC, 0.382, 0.886), # LEG 2 362 | self.is_in(BCD, 1.618, 2.168), # LEG 3 363 | self.is_eq(XAD, 0.886), # LEG 4 364 | self.is_eq(ABCD, 1) or self.is_in(ABCD, 0.786, 1.618), # AB=CD and its alternatives 365 | ])) 366 | direction = 1 if D origin_D): 449 | pattern_predict_s3 = True 450 | 451 | if pattern_predict_s2 and pattern_predict_s3: 452 | return [direction, ret_dict] 453 | else: 454 | return None 455 | else: 456 | return None 457 | def detect_altbat(self, current_pat: list, predict: bool=False, predict_mode:str='direct'): 458 | # current_pat: [['H', new_high, idx],...] 459 | # Legs 460 | if not predict: 461 | X, A, B, C, D = [pat[1] for pat in current_pat] 462 | last_direction = current_pat[-1][0] 463 | XAB = abs(B-A) / abs(X-A) 464 | XAD = abs(A-D) / abs(X-A) 465 | ABC = abs(B-C) / abs(A-B) 466 | BCD = abs(C-D) / abs(B-C) 467 | 468 | ret_dict = { 469 | 'XAB': XAB, 470 | 'XAD': XAD, 471 | 'ABC': ABC, 472 | 'BCD': BCD 473 | } 474 | 475 | # Detect 476 | pattern_found = np.all(np.array([ 477 | self.is_eq(XAB, 0.382), # LEG 1 478 | self.is_in(ABC, 0.382, 0.886), # LEG 2 479 | self.is_in(BCD, 2, 3.168), # LEG 3 480 | self.is_eq(XAD, 1.13) # LEG 4 481 | ])) 482 | direction = 1 if D origin_D): 558 | pattern_predict_s3 = True 559 | 560 | if pattern_predict_s2 and pattern_predict_s3: 561 | return [direction, ret_dict] 562 | else: 563 | return None 564 | 565 | else: 566 | return None 567 | 568 | def detect_butterfly(self, current_pat: list, predict: bool=False, predict_mode:str='direct'): 569 | # current_pat: [['H', new_high, idx],...] 570 | # Legs 571 | if not predict: 572 | X, A, B, C, D = [pat[1] for pat in current_pat] 573 | last_direction = current_pat[-1][0] 574 | XAB = abs(B-A) / abs(X-A) 575 | XAD = abs(A-D) / abs(X-A) 576 | ABC = abs(B-C) / abs(A-B) 577 | BCD = abs(C-D) / abs(B-C) 578 | 579 | ABCD = BCD / (1/ABC) 580 | 581 | ret_dict = { 582 | 'XAB': XAB, 583 | 'XAD': XAD, 584 | 'ABC': ABC, 585 | 'BCD': BCD, 586 | 'AB=CD': ABCD 587 | } 588 | 589 | # Detect 590 | pattern_found = np.all(np.array([ 591 | self.is_eq(XAB, 0.786), # LEG 1 592 | self.is_in(ABC, 0.382, 0.886), # LEG 2 593 | self.is_in(BCD, 1.618, 2.24), # LEG 3 594 | self.is_eq(XAD, 1.27), # LEG 4 595 | self.is_eq(ABCD, 1) or self.is_in(ABCD, 0.786, 1.618), # AB=CD and its alternatives 596 | ])) 597 | direction = 1 if D origin_D): 676 | pattern_predict_s3 = True 677 | 678 | if pattern_predict_s2 and pattern_predict_s3: 679 | return [direction, ret_dict] 680 | else: 681 | return None 682 | 683 | else: 684 | return None 685 | 686 | def detect_crab(self, current_pat: list, predict: bool=False, predict_mode:str='direct'): 687 | # current_pat: [['H', new_high, idx],...] 688 | # Legs 689 | if not predict: 690 | X, A, B, C, D = [pat[1] for pat in current_pat] 691 | last_direction = current_pat[-1][0] 692 | XAB = abs(B-A) / abs(X-A) 693 | XAD = abs(A-D) / abs(X-A) 694 | ABC = abs(B-C) / abs(A-B) 695 | BCD = abs(C-D) / abs(B-C) 696 | 697 | ABCD = BCD / (1/ABC) 698 | ret_dict = { 699 | 'XAB': XAB, 700 | 'XAD': XAD, 701 | 'ABC': ABC, 702 | 'BCD': BCD, 703 | 'AB=CD': ABCD 704 | } 705 | 706 | # Detect 707 | pattern_found = np.all(np.array([ 708 | self.is_in(XAB, 0.382, 0.618),# LEG 1 709 | self.is_in(ABC, 0.382, 0.886), # LEG 2 710 | self.is_in(BCD, 2.618, 3.618), # LEG 3 711 | self.is_eq(XAD, 1.618), # LEG 4 712 | self.is_eq(ABCD, 1) or self.is_in(ABCD, 0.786, 2.236), # AB=CD and its alternatives 713 | ])) 714 | direction = 1 if D origin_D): 800 | pattern_predict_s3 = True 801 | 802 | if pattern_predict_s2 and pattern_predict_s3: 803 | return [direction, ret_dict] 804 | else: 805 | return None 806 | else: 807 | return None 808 | def detect_deepcrab(self, current_pat: list, predict: bool=False, predict_mode:str='direct'): 809 | # current_pat: [['H', new_high, idx],...] 810 | # Legs 811 | if not predict: 812 | X, A, B, C, D = [pat[1] for pat in current_pat] 813 | last_direction = current_pat[-1][0] 814 | XAB = abs(B-A) / abs(X-A) 815 | XAD = abs(A-D) / abs(X-A) 816 | ABC = abs(B-C) / abs(A-B) 817 | BCD = abs(C-D) / abs(B-C) 818 | 819 | ABCD = BCD / (1/ABC) 820 | 821 | ret_dict = { 822 | 'XAB': XAB, 823 | 'XAD': XAD, 824 | 'ABC': ABC, 825 | 'BCD': BCD, 826 | 'AB=CD': ABCD 827 | } 828 | 829 | # Detect 830 | pattern_found = np.all(np.array([ 831 | self.is_eq(XAB, 0.886),# LEG 1 832 | self.is_in(ABC, 0.382, 0.886), # LEG 2 833 | self.is_in(BCD, 2.24, 3.618), # LEG 3 834 | self.is_eq(XAD, 1.618), # LEG 4 835 | self.is_eq(ABCD, 1) or self.is_in(ABCD, 0.786, 2.236), # AB=CD and its alternatives 836 | ])) 837 | direction = 1 if D origin_D): 921 | pattern_predict_s3 = True 922 | 923 | if pattern_predict_s2 and pattern_predict_s3: 924 | return [direction, ret_dict] 925 | else: 926 | return None 927 | 928 | else: 929 | return None 930 | 931 | def detect_shark(self, current_pat: list, predict: bool=False, predict_mode:str='direct'): 932 | # current_pat: [['H', new_high, idx],...] 933 | # Legs 934 | if not predict: 935 | X, A, B, C, D = [pat[1] for pat in current_pat] 936 | last_direction = current_pat[-1][0] 937 | XAB = abs(B-A) / abs(X-A) 938 | XAD = abs(A-D) / abs(X-A) 939 | ABC = abs(B-C) / abs(A-B) 940 | BCD = abs(C-D) / abs(B-C) 941 | 942 | ret_dict = { 943 | 'XAB': XAB, 944 | 'XAD': XAD, 945 | 'ABC': ABC, 946 | 'BCD': BCD 947 | } 948 | 949 | # Detect 950 | pattern_found = np.all(np.array([ 951 | self.is_in(XAB, 0.5, 0.886), # LEG 1 952 | self.is_in(ABC, 1.13, 1.618), # LEG 2 953 | self.is_in(BCD, 1.618, 2.24), # LEG 3 954 | self.is_in(XAD, 0.886, 1.13) # LEG 4 955 | ])) 956 | direction = 1 if D origin_D): 1031 | pattern_predict_s3 = True 1032 | 1033 | if pattern_predict_s2 and pattern_predict_s3: 1034 | return [direction, ret_dict] 1035 | else: 1036 | return None 1037 | else: 1038 | return None 1039 | def detect_5o(self, current_pat: list, predict: bool=False, predict_mode:str='direct'): 1040 | # current_pat: [['H', new_high, idx],...] 1041 | # Legs 1042 | if not predict: 1043 | X, A, B, C, D = [pat[1] for pat in current_pat] 1044 | last_direction = current_pat[-1][0] 1045 | XAB = abs(B-A) / abs(X-A) 1046 | XAD = abs(A-D) / abs(X-A) 1047 | ABC = abs(B-C) / abs(A-B) 1048 | BCD = abs(C-D) / abs(B-C) 1049 | 1050 | ABCD = BCD / (1/ABC) 1051 | 1052 | ret_dict = { 1053 | 'XAB': XAB, 1054 | 'XAD': XAD, 1055 | 'ABC': ABC, 1056 | 'BCD': BCD, 1057 | 'AB=CD': ABCD 1058 | } 1059 | 1060 | # Detect 1061 | pattern_found = np.all(np.array([ 1062 | self.is_in(XAB, 1.13, 1.618), # LEG 1 1063 | self.is_in(ABC, 1.618, 2.24), # LEG 2 1064 | self.is_eq(BCD, 0.5), # LEG 3 1065 | #self.is_eq(XAD, 0.5), # LEG 4 1066 | self.is_eq(ABCD, 1) or self.is_in(ABCD, 0.786, 1.618), # AB=CD and its alternatives 1067 | ])) 1068 | direction = 1 if D origin_D): 1152 | pattern_predict_s3 = True 1153 | 1154 | if pattern_predict_s2 and pattern_predict_s3: 1155 | return [direction, ret_dict] 1156 | else: 1157 | return None 1158 | 1159 | else: 1160 | return None 1161 | def detect_cypher(self, current_pat: list, predict: bool=False, predict_mode:str='direct'): 1162 | # current_pat: [['H', new_high, idx],...] 1163 | # Legs 1164 | if not predict: 1165 | X, A, B, C, D = [pat[1] for pat in current_pat] 1166 | last_direction = current_pat[-1][0] 1167 | XAB = abs(B-A) / abs(X-A) 1168 | XAD = abs(A-D) / abs(X-A) 1169 | ABC = abs(B-C) / abs(A-B) 1170 | BCD = abs(C-D) / abs(B-C) 1171 | XCD = abs(D-C) / abs(X-C) 1172 | 1173 | ret_dict = { 1174 | 'XAB': XAB, 1175 | 'XAD': XAD, 1176 | 'ABC': ABC, 1177 | 'BCD': BCD, 1178 | 'XCD': XCD 1179 | } 1180 | 1181 | # Detect 1182 | pattern_found = np.all(np.array([ 1183 | self.is_in(XAB, 0.382, 0.786), # LEG 1 1184 | self.is_in(ABC, 1.272, 1.414), # LEG 2 1185 | #self.is_in(BCD, 1.618, 2.24), # LEG 3 1186 | self.is_eq(XCD, 0.786) or self.is_eq(XAD, 0.786) , # LEG 4 1187 | ])) 1188 | direction = 1 if D origin_D): 1267 | pattern_predict_s3 = True 1268 | 1269 | if pattern_predict_s3: 1270 | return [direction, ret_dict] 1271 | else: 1272 | return None 1273 | 1274 | else: 1275 | return None 1276 | 1277 | def detect_pattern(self, zigzag_pattern): 1278 | res = [] 1279 | # price default to close 1280 | for idx in range(0, len(zigzag_pattern) - 5 + 1): 1281 | current_pat = [pat for pat in zigzag_pattern[idx:idx+5]] 1282 | current_idx = [pat[2] for pat in zigzag_pattern[idx:idx+5]] 1283 | detect_funcions = [ 1284 | (self.detect_gartley, 'gartley'), 1285 | (self.detect_bat, 'bat'), 1286 | (self.detect_altbat, 'altbat'), 1287 | (self.detect_butterfly, 'butterfly'), 1288 | (self.detect_crab, 'crab'), 1289 | (self.detect_deepcrab, 'deepcrab'), 1290 | (self.detect_shark, 'shark'), 1291 | (self.detect_5o, '5o'), 1292 | (self.detect_cypher, 'cypher'), 1293 | (self.detect_abcd, 'abcd'), 1294 | ] 1295 | if len(current_pat) == 5: 1296 | for func, func_name in detect_funcions: 1297 | r = func(current_pat) 1298 | if r is not None: 1299 | direction, ret_dict = r 1300 | bull_or_bear = 'bullish' if direction==1 else 'bearish' 1301 | label = f'{bull_or_bear} {func_name}' 1302 | res.append([current_pat, current_idx, label, ret_dict]) 1303 | return res 1304 | 1305 | def predict_pattern(self, zigzag_pattern): 1306 | res = [] 1307 | # predict reverse pattern 1308 | current_pat = zigzag_pattern[-5:] 1309 | current_idx = [pat[2] for pat in zigzag_pattern[-5:]] 1310 | 1311 | detect_funcions = [ 1312 | (self.detect_gartley, 'gartley'), 1313 | (self.detect_bat, 'bat'), 1314 | (self.detect_altbat, 'altbat'), 1315 | (self.detect_butterfly, 'butterfly'), 1316 | (self.detect_crab, 'crab'), 1317 | (self.detect_deepcrab, 'deepcrab'), 1318 | (self.detect_shark, 'shark'), 1319 | (self.detect_5o, '5o'), 1320 | (self.detect_cypher, 'cypher'), 1321 | ] 1322 | if len(current_pat) == 5: 1323 | for func, func_name in detect_funcions: 1324 | r = func(current_pat, predict=True, predict_mode='reverse') 1325 | if r is not None: 1326 | direction, ret_dict = r 1327 | _, X, A, B, C = [pat[1] for pat in current_pat] 1328 | D = ret_dict['predict_D'] 1329 | bull_or_bear = 'bullish' if direction==1 else 'bearish' 1330 | label = f'{bull_or_bear} {func_name} predict next D: {D}' 1331 | # Funny ha? but shit happens 1332 | if D > 0: 1333 | p_pattern = [X, A, B, C, D] 1334 | p_idx = [*current_idx[1:], -1] 1335 | res.append([p_pattern, p_idx, label, ret_dict]) 1336 | 1337 | for func, func_name in detect_funcions: 1338 | r = func(current_pat, predict=True, predict_mode='direct') 1339 | if r is not None: 1340 | direction, ret_dict = r 1341 | X, A, B, C, origin_D = [pat[1] for pat in current_pat] 1342 | D = ret_dict['predict_D'] 1343 | bull_or_bear = 'bullish' if direction==1 else 'bearish' 1344 | label = f'{bull_or_bear} {func_name} predict current D: {D}' 1345 | # Funny ha? but shit happens 1346 | if D > 0: 1347 | p_pattern = [X, A, B, C, D] 1348 | res.append([p_pattern, current_idx, label, ret_dict]) 1349 | return res 1350 | 1351 | def get_patterns(self, df: pd.DataFrame, window: int, predict: bool=False, plot:bool=False): 1352 | zigzag_pattern = self.get_zigzag(df, window) 1353 | patterns = self.detect_pattern(zigzag_pattern) 1354 | if plot: 1355 | points = [(df.index[k[2]], k[1]) for k in zigzag_pattern] 1356 | patterns_line = [list(zip( 1357 | [df.index[dt_idx] for dt_idx in pat[1]], 1358 | [val_idx[1] for val_idx in pat[0]] 1359 | )) for pat in patterns] 1360 | mpf.plot(df,type='candle', alines=dict(alines=patterns_line, colors=['b','r','c','k','g'])) 1361 | if predict: 1362 | predict_res = self.predict_pattern(zigzag_pattern) 1363 | return patterns, predict_res 1364 | return patterns, None 1365 | 1366 | def filter_duplicats(self, patterns, predict:bool=False): 1367 | pat_set = set() 1368 | ret = [] 1369 | if not predict: 1370 | for pat in patterns: 1371 | X,A,B,C,D = pat[1] 1372 | pat_str = f'{X}-{A}-{B}-{C}-{D}' 1373 | if pat_str not in pat_set: 1374 | ret.append(pat) 1375 | pat_set.add(pat_str) 1376 | else: 1377 | # predict 1378 | for pat in patterns: 1379 | X,A,B,C,D = pat[1] # get index 1380 | label = pat[2] # get_label 1381 | #predict_D = pat[3]['predict_D'] # get_predict_D 1382 | pat_str = f'{X}-{A}-{B}-{C}-{D}-{label}' 1383 | if pat_str not in pat_set: 1384 | ret.append(pat) 1385 | pat_set.add(pat_str) 1386 | return ret 1387 | 1388 | def plot_patterns(self, df, patterns, predict_patterns, plot_predict:bool=True, save_fig:bool=False, file_name:str=None, file_data_path='./data/'): 1389 | """ 1390 | plot results, if save_fig, will save_fig to {file_name} 1391 | """ 1392 | # points = [(df.index[k[2]], k[1]) for k in zigzag_pattern] 1393 | patterns_line = [list(zip( 1394 | [df.index[dt_idx] for dt_idx in pat[1]], 1395 | [val_idx[1] for val_idx in pat[0]] 1396 | )) for pat in patterns if pat[2][-4:]!='abcd'] 1397 | if plot_predict: 1398 | new_index = df.index.append(pd.Index([ 1399 | df.index[-1] + 10*(df.index[-1] - df.index[-2]) 1400 | ])) 1401 | df = pd.DataFrame(df, index=new_index) 1402 | df.fillna(0) 1403 | patterns_predict_line = [list(zip( 1404 | [df.index[dt_idx] for dt_idx in pat[1]], 1405 | [val_idx for val_idx in pat[0]] 1406 | )) for pat in predict_patterns if pat[2][-4:]!='abcd'] 1407 | 1408 | predict_Ds = [pat[3]['predict_D'] for pat in predict_patterns] 1409 | patterns_line.extend(patterns_predict_line) 1410 | else: 1411 | predict_Ds = [] 1412 | 1413 | mc = mpf.make_marketcolors(base_mpf_style='yahoo') 1414 | # Create a style based on `seaborn` using those market colors: 1415 | style = mpf.make_mpf_style(base_mpl_style='seaborn', marketcolors=mc) 1416 | 1417 | if len(patterns_line) > 0: 1418 | if save_fig and file_name is not None: 1419 | if not os.path.exists(file_data_path): 1420 | os.mkdir(file_data_path) 1421 | file_path = file_data_path + file_name 1422 | mpf.plot(df, 1423 | type='candle', 1424 | mav=(10,21,55,120), 1425 | style=style, 1426 | alines=dict(alines=patterns_line, 1427 | colors=['b','r','c','k','g'], 1428 | linewidths=3, 1429 | alpha=0.5 1430 | ), 1431 | hlines=dict(hlines=predict_Ds, colors=['g'],linestyle='-.'), 1432 | savefig=file_path 1433 | ) 1434 | else: 1435 | mpf.plot(df, 1436 | type='candle', 1437 | mav=(10,21,55,120), 1438 | style=style, 1439 | alines=dict(alines=patterns_line, 1440 | colors=['b','r','c','k','g'], 1441 | linewidths=3, 1442 | alpha=0.5 1443 | ), 1444 | hlines=dict(hlines=predict_Ds, colors=['g'],linestyle='-.'), 1445 | #savefig='test.svg' 1446 | ) 1447 | 1448 | def search_patterns(self, df: pd.DataFrame, predict: bool=False, only_last:bool=False, last_n:int=0, plot:bool=False, plot_predict:bool=True, return_dt_idx=True, save_fig_name:str=None): 1449 | assert last_n < 5 1450 | all_patterns = list() 1451 | all_predict_patterns = list() 1452 | for window in [8, 13, 21, 34, 55]: 1453 | zigzag_pattern = self.get_zigzag(df, window) 1454 | if predict: 1455 | ori_err = self.err 1456 | self.err = self.predict_err_rate 1457 | predict_res = self.predict_pattern(zigzag_pattern) 1458 | all_predict_patterns.extend(predict_res) 1459 | self.err = ori_err 1460 | patterns = self.detect_pattern(zigzag_pattern) 1461 | if only_last: 1462 | all_patterns.extend(patterns[-1:]) 1463 | else: 1464 | all_patterns.extend(patterns) 1465 | 1466 | if only_last and last_n>0: 1467 | all_patterns = [pat for pat in all_patterns if pat[0][-1][2] == len(df) - last_n] 1468 | 1469 | all_patterns = self.filter_duplicats(all_patterns) 1470 | all_predict_patterns = self.filter_duplicats(all_predict_patterns, predict=True) 1471 | if plot: 1472 | if save_fig_name is not None: 1473 | self.plot_patterns(df, all_patterns, all_predict_patterns, save_fig=True, file_name=save_fig_name) 1474 | else: 1475 | self.plot_patterns(df, all_patterns, all_predict_patterns, save_fig=False) 1476 | 1477 | if return_dt_idx: 1478 | all_patterns = [ 1479 | [pat[0], 1480 | [df.index[dt_idx] for dt_idx in pat[1]], 1481 | pat[2], 1482 | pat[3]] 1483 | for pat in all_patterns] 1484 | all_predict_patterns = [ 1485 | [pat[0], 1486 | [df.index[dt_idx] for dt_idx in pat[1]], 1487 | pat[2], 1488 | pat[3]] 1489 | for pat in all_predict_patterns] 1490 | 1491 | return all_patterns, all_predict_patterns 1492 | 1493 | 1494 | if __name__ == '__main__': 1495 | detector = HarmonicDetector() 1496 | print('done') 1497 | -------------------------------------------------------------------------------- /src/settings.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | MAIN_SYMBOLS = ['BTC/USDT', 'ETH/USDT', 'BCH/USDT', 'ETC/USDT', 'ADA/USDT','XRP/USDT', 'DOT/USDT', 'EOS/USDT', 'LTC/USDT'] 4 | ALT_SYMBOLS = ['UNI/USDT', 'XMR/USDT', 'ATOM/USDT', 'COMP/USDT', 'SOL/USDT', 'ALGO/USDT', 'FIL/USDT', 'XLM/USDT', 'AAVE/USDT'] 5 | PERIODS = ['30m', '1h', '4h'] 6 | ERROR_RATE = 0.05 7 | 8 | 9 | PROCESS_COUNT=4 10 | 11 | PREDICT=False 12 | 13 | # HTTP_PROXY = 'http://127.0.0.1:1087' 14 | HTTP_PROXY = None 15 | NOTIFY_URL = 'https://sc.ftqq.com/[****]' 16 | 17 | --------------------------------------------------------------------------------