yfinance-ta-patterns 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- yfinance_ta_patterns-0.1.1/LICENSE +21 -0
- yfinance_ta_patterns-0.1.1/PKG-INFO +108 -0
- yfinance_ta_patterns-0.1.1/README.md +69 -0
- yfinance_ta_patterns-0.1.1/pyproject.toml +33 -0
- yfinance_ta_patterns-0.1.1/setup.cfg +4 -0
- yfinance_ta_patterns-0.1.1/tests/test_cli.py +15 -0
- yfinance_ta_patterns-0.1.1/yfinance_ta_patterns/__init__.py +13 -0
- yfinance_ta_patterns-0.1.1/yfinance_ta_patterns/cli.py +160 -0
- yfinance_ta_patterns-0.1.1/yfinance_ta_patterns/forex_data_loader.py +53 -0
- yfinance_ta_patterns-0.1.1/yfinance_ta_patterns/pattern_analyzer.py +101 -0
- yfinance_ta_patterns-0.1.1/yfinance_ta_patterns/pattern_tester.py +258 -0
- yfinance_ta_patterns-0.1.1/yfinance_ta_patterns.egg-info/PKG-INFO +108 -0
- yfinance_ta_patterns-0.1.1/yfinance_ta_patterns.egg-info/SOURCES.txt +15 -0
- yfinance_ta_patterns-0.1.1/yfinance_ta_patterns.egg-info/dependency_links.txt +1 -0
- yfinance_ta_patterns-0.1.1/yfinance_ta_patterns.egg-info/entry_points.txt +3 -0
- yfinance_ta_patterns-0.1.1/yfinance_ta_patterns.egg-info/requires.txt +5 -0
- yfinance_ta_patterns-0.1.1/yfinance_ta_patterns.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 eminsk
|
|
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.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: yfinance-ta-patterns
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: CLI and helpers to scan yfinance data for TA-Lib candlestick patterns.
|
|
5
|
+
Author: eminsk
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2025 eminsk
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Repository, https://github.com/eminsk/yfinance-ta-patterns
|
|
29
|
+
Project-URL: Homepage, https://github.com/eminsk/yfinance-ta-patterns
|
|
30
|
+
Requires-Python: >=3.12
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
License-File: LICENSE
|
|
33
|
+
Requires-Dist: ta-lib>=0.6.8
|
|
34
|
+
Requires-Dist: yfinance>=0.2.66
|
|
35
|
+
Requires-Dist: pandas>=2.2.0
|
|
36
|
+
Requires-Dist: pytz>=2024.1
|
|
37
|
+
Requires-Dist: numpy>=1.26
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
|
|
40
|
+
## Forex Candlestick Scanner
|
|
41
|
+
|
|
42
|
+
Python package + CLI that downloads Yahoo Finance data via `yfinance` and runs TA-Lib candlestick detectors for your symbol, timeframe, and date filters.
|
|
43
|
+
|
|
44
|
+
### Quick start
|
|
45
|
+
```bash
|
|
46
|
+
# create env
|
|
47
|
+
python -m venv .venv
|
|
48
|
+
.\.venv\Scripts\activate # PowerShell; adjust for your shell
|
|
49
|
+
|
|
50
|
+
# install the package (editable for local dev)
|
|
51
|
+
pip install -e .
|
|
52
|
+
|
|
53
|
+
# run CLI (two entrypoints)
|
|
54
|
+
yfinance-ta-patterns --pattern KICKING --symbol EURUSD --timeframe 5m --period 60d
|
|
55
|
+
# or
|
|
56
|
+
yftp --all-patterns --symbol EURUSD --timeframe 5m --period 60d --date 2025-04-01
|
|
57
|
+
```
|
|
58
|
+
`pyproject.toml` targets Python `>=3.12`.
|
|
59
|
+
|
|
60
|
+
### CLI usage
|
|
61
|
+
```
|
|
62
|
+
yfinance-ta-patterns [--pattern NAME | --all-patterns]
|
|
63
|
+
[--symbol EURUSD] [--period 60d]
|
|
64
|
+
[--timeframe 15m] [--date YYYY-MM-DD]
|
|
65
|
+
[--start-date YYYY-MM-DD] [--end-date YYYY-MM-DD]
|
|
66
|
+
```
|
|
67
|
+
- `--pattern`: Single candlestick name (with or without `CDL` prefix).
|
|
68
|
+
- `--all-patterns`: Scan every TA-Lib candlestick detector.
|
|
69
|
+
- `--symbol`: Ticker without suffix; `=X` is appended automatically for Forex (default `EURUSD`).
|
|
70
|
+
- `--period`: History window passed to `yfinance` (e.g., `60d`, `1mo`).
|
|
71
|
+
- `--timeframe`: Use `M1/M5/M15/M30/H1/D1` or raw `yfinance` intervals (`1m`, `5m`, `1h`, `1d`, etc.).
|
|
72
|
+
- `--date`: Filter signals for a single day.
|
|
73
|
+
- `--start-date` / `--end-date`: Inclusive range filter (cannot be combined with `--date`).
|
|
74
|
+
|
|
75
|
+
### Examples
|
|
76
|
+
- All patterns for a single day:
|
|
77
|
+
```bash
|
|
78
|
+
yftp --all-patterns --symbol EURUSD --timeframe 5m --period 60d --date 2025-04-01
|
|
79
|
+
```
|
|
80
|
+
- All patterns across a range:
|
|
81
|
+
```bash
|
|
82
|
+
yftp --all-patterns --symbol EURUSD --timeframe 5m --period 60d --start-date 2025-04-01 --end-date 2025-04-10
|
|
83
|
+
```
|
|
84
|
+
- One pattern without date filter:
|
|
85
|
+
```bash
|
|
86
|
+
yftp --pattern KICKING --symbol EURUSD --timeframe 5m --period 60d
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Data loader
|
|
90
|
+
`yfinance_ta_patterns/forex_data_loader.py` fetches and normalizes OHLC data. It appends `=X` to symbols when missing and converts timestamps to UTC before shifting to the configured timezone (`Europe/Moscow` by default).
|
|
91
|
+
|
|
92
|
+
### Pattern analysis
|
|
93
|
+
`yfinance_ta_patterns/pattern_analyzer.py` wraps TA-Lib's `CDL*` functions, returning non-zero signals and applying optional date filters. When `--all-patterns` is used, it iterates over the full catalog and prints hits per pattern.
|
|
94
|
+
|
|
95
|
+
### Pattern ranking helper (optional)
|
|
96
|
+
`yfinance_ta_patterns/pattern_tester.py` contains a backtesting-style ranking tool. It depends on `utils.pattern_helper.PatternHelper` to enumerate patterns; add that helper before running comparisons or exports.
|
|
97
|
+
|
|
98
|
+
### Project layout
|
|
99
|
+
- `yfinance_ta_patterns/cli.py`: CLI entry point and argument parsing.
|
|
100
|
+
- `yfinance_ta_patterns/forex_data_loader.py`: Data download and timezone normalization.
|
|
101
|
+
- `yfinance_ta_patterns/pattern_analyzer.py`: Candlestick signal extraction.
|
|
102
|
+
- `yfinance_ta_patterns/pattern_tester.py`: Experimental ranking/backtest utilities.
|
|
103
|
+
- `main.py`: Thin wrapper to launch the CLI.
|
|
104
|
+
|
|
105
|
+
### Packaging and releases
|
|
106
|
+
- Nightly GitHub Actions workflow builds onefile Nuitka binaries for Windows/macOS/Linux and publishes nightly prereleases.
|
|
107
|
+
- A PyPI publish workflow can be enabled by adding a secret `PYPI_API_TOKEN`; tags like `v0.1.0` will build sdist/wheel and upload.
|
|
108
|
+
- TA-Lib is installed from PyPI on Windows; Linux/macOS CI builds the TA-Lib C library from source for the binaries. For local installs, PyPI wheels (`ta-lib` >=0.6.8) cover CPython 3.9–3.14.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
## Forex Candlestick Scanner
|
|
2
|
+
|
|
3
|
+
Python package + CLI that downloads Yahoo Finance data via `yfinance` and runs TA-Lib candlestick detectors for your symbol, timeframe, and date filters.
|
|
4
|
+
|
|
5
|
+
### Quick start
|
|
6
|
+
```bash
|
|
7
|
+
# create env
|
|
8
|
+
python -m venv .venv
|
|
9
|
+
.\.venv\Scripts\activate # PowerShell; adjust for your shell
|
|
10
|
+
|
|
11
|
+
# install the package (editable for local dev)
|
|
12
|
+
pip install -e .
|
|
13
|
+
|
|
14
|
+
# run CLI (two entrypoints)
|
|
15
|
+
yfinance-ta-patterns --pattern KICKING --symbol EURUSD --timeframe 5m --period 60d
|
|
16
|
+
# or
|
|
17
|
+
yftp --all-patterns --symbol EURUSD --timeframe 5m --period 60d --date 2025-04-01
|
|
18
|
+
```
|
|
19
|
+
`pyproject.toml` targets Python `>=3.12`.
|
|
20
|
+
|
|
21
|
+
### CLI usage
|
|
22
|
+
```
|
|
23
|
+
yfinance-ta-patterns [--pattern NAME | --all-patterns]
|
|
24
|
+
[--symbol EURUSD] [--period 60d]
|
|
25
|
+
[--timeframe 15m] [--date YYYY-MM-DD]
|
|
26
|
+
[--start-date YYYY-MM-DD] [--end-date YYYY-MM-DD]
|
|
27
|
+
```
|
|
28
|
+
- `--pattern`: Single candlestick name (with or without `CDL` prefix).
|
|
29
|
+
- `--all-patterns`: Scan every TA-Lib candlestick detector.
|
|
30
|
+
- `--symbol`: Ticker without suffix; `=X` is appended automatically for Forex (default `EURUSD`).
|
|
31
|
+
- `--period`: History window passed to `yfinance` (e.g., `60d`, `1mo`).
|
|
32
|
+
- `--timeframe`: Use `M1/M5/M15/M30/H1/D1` or raw `yfinance` intervals (`1m`, `5m`, `1h`, `1d`, etc.).
|
|
33
|
+
- `--date`: Filter signals for a single day.
|
|
34
|
+
- `--start-date` / `--end-date`: Inclusive range filter (cannot be combined with `--date`).
|
|
35
|
+
|
|
36
|
+
### Examples
|
|
37
|
+
- All patterns for a single day:
|
|
38
|
+
```bash
|
|
39
|
+
yftp --all-patterns --symbol EURUSD --timeframe 5m --period 60d --date 2025-04-01
|
|
40
|
+
```
|
|
41
|
+
- All patterns across a range:
|
|
42
|
+
```bash
|
|
43
|
+
yftp --all-patterns --symbol EURUSD --timeframe 5m --period 60d --start-date 2025-04-01 --end-date 2025-04-10
|
|
44
|
+
```
|
|
45
|
+
- One pattern without date filter:
|
|
46
|
+
```bash
|
|
47
|
+
yftp --pattern KICKING --symbol EURUSD --timeframe 5m --period 60d
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Data loader
|
|
51
|
+
`yfinance_ta_patterns/forex_data_loader.py` fetches and normalizes OHLC data. It appends `=X` to symbols when missing and converts timestamps to UTC before shifting to the configured timezone (`Europe/Moscow` by default).
|
|
52
|
+
|
|
53
|
+
### Pattern analysis
|
|
54
|
+
`yfinance_ta_patterns/pattern_analyzer.py` wraps TA-Lib's `CDL*` functions, returning non-zero signals and applying optional date filters. When `--all-patterns` is used, it iterates over the full catalog and prints hits per pattern.
|
|
55
|
+
|
|
56
|
+
### Pattern ranking helper (optional)
|
|
57
|
+
`yfinance_ta_patterns/pattern_tester.py` contains a backtesting-style ranking tool. It depends on `utils.pattern_helper.PatternHelper` to enumerate patterns; add that helper before running comparisons or exports.
|
|
58
|
+
|
|
59
|
+
### Project layout
|
|
60
|
+
- `yfinance_ta_patterns/cli.py`: CLI entry point and argument parsing.
|
|
61
|
+
- `yfinance_ta_patterns/forex_data_loader.py`: Data download and timezone normalization.
|
|
62
|
+
- `yfinance_ta_patterns/pattern_analyzer.py`: Candlestick signal extraction.
|
|
63
|
+
- `yfinance_ta_patterns/pattern_tester.py`: Experimental ranking/backtest utilities.
|
|
64
|
+
- `main.py`: Thin wrapper to launch the CLI.
|
|
65
|
+
|
|
66
|
+
### Packaging and releases
|
|
67
|
+
- Nightly GitHub Actions workflow builds onefile Nuitka binaries for Windows/macOS/Linux and publishes nightly prereleases.
|
|
68
|
+
- A PyPI publish workflow can be enabled by adding a secret `PYPI_API_TOKEN`; tags like `v0.1.0` will build sdist/wheel and upload.
|
|
69
|
+
- TA-Lib is installed from PyPI on Windows; Linux/macOS CI builds the TA-Lib C library from source for the binaries. For local installs, PyPI wheels (`ta-lib` >=0.6.8) cover CPython 3.9–3.14.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "yfinance-ta-patterns"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "CLI and helpers to scan yfinance data for TA-Lib candlestick patterns."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
license = { file = "LICENSE" }
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "eminsk" },
|
|
10
|
+
]
|
|
11
|
+
dependencies = [
|
|
12
|
+
"ta-lib>=0.6.8",
|
|
13
|
+
"yfinance>=0.2.66",
|
|
14
|
+
"pandas>=2.2.0",
|
|
15
|
+
"pytz>=2024.1",
|
|
16
|
+
"numpy>=1.26",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.scripts]
|
|
20
|
+
yfinance-ta-patterns = "yfinance_ta_patterns.cli:main"
|
|
21
|
+
yftp = "yfinance_ta_patterns.cli:main"
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Repository = "https://github.com/eminsk/yfinance-ta-patterns"
|
|
25
|
+
Homepage = "https://github.com/eminsk/yfinance-ta-patterns"
|
|
26
|
+
|
|
27
|
+
[build-system]
|
|
28
|
+
requires = ["setuptools>=68", "wheel"]
|
|
29
|
+
build-backend = "setuptools.build_meta"
|
|
30
|
+
|
|
31
|
+
[tool.setuptools.packages.find]
|
|
32
|
+
where = ["."]
|
|
33
|
+
include = ["yfinance_ta_patterns*"]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from yfinance_ta_patterns.cli import normalize_timeframe
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_normalize_timeframe_aliases():
|
|
5
|
+
assert normalize_timeframe("M5") == "5m"
|
|
6
|
+
assert normalize_timeframe("h1") == "1h"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_normalize_timeframe_invalid():
|
|
10
|
+
try:
|
|
11
|
+
normalize_timeframe("invalid")
|
|
12
|
+
except ValueError as exc:
|
|
13
|
+
assert "Unsupported timeframe" in str(exc)
|
|
14
|
+
else:
|
|
15
|
+
raise AssertionError("Expected ValueError for invalid timeframe")
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""YFinance TA Patterns package."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
__version__ = version("yfinance-ta-patterns")
|
|
7
|
+
except PackageNotFoundError: # pragma: no cover - during editable installs
|
|
8
|
+
__version__ = "0.0.0"
|
|
9
|
+
|
|
10
|
+
from .forex_data_loader import ForexDataLoader
|
|
11
|
+
from .pattern_analyzer import PatternAnalyzer
|
|
12
|
+
|
|
13
|
+
__all__ = ["ForexDataLoader", "PatternAnalyzer", "__version__"]
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
|
|
3
|
+
from .forex_data_loader import ForexDataLoader
|
|
4
|
+
from .pattern_analyzer import PatternAnalyzer
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
TIMEFRAME_MAP = {
|
|
8
|
+
"M1": "1m",
|
|
9
|
+
"M5": "5m",
|
|
10
|
+
"M15": "15m",
|
|
11
|
+
"M30": "30m",
|
|
12
|
+
"H1": "1h",
|
|
13
|
+
"D1": "1d",
|
|
14
|
+
}
|
|
15
|
+
VALID_INTERVALS = {
|
|
16
|
+
"1m",
|
|
17
|
+
"2m",
|
|
18
|
+
"5m",
|
|
19
|
+
"15m",
|
|
20
|
+
"30m",
|
|
21
|
+
"60m",
|
|
22
|
+
"90m",
|
|
23
|
+
"1h",
|
|
24
|
+
"1d",
|
|
25
|
+
"5d",
|
|
26
|
+
"1wk",
|
|
27
|
+
"1mo",
|
|
28
|
+
"3mo",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def normalize_timeframe(timeframe: str) -> str:
|
|
33
|
+
"""Convert human-friendly timeframe names into yfinance intervals."""
|
|
34
|
+
tf = timeframe.upper()
|
|
35
|
+
interval = TIMEFRAME_MAP.get(tf, timeframe.lower())
|
|
36
|
+
if interval not in VALID_INTERVALS:
|
|
37
|
+
allowed = ", ".join(sorted(VALID_INTERVALS))
|
|
38
|
+
raise ValueError(f"Unsupported timeframe '{timeframe}'. Allowed: {allowed}")
|
|
39
|
+
return interval
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def parse_args() -> argparse.Namespace:
|
|
43
|
+
parser = argparse.ArgumentParser(
|
|
44
|
+
description="Show TA-Lib candlestick pattern signals for a symbol.",
|
|
45
|
+
formatter_class=argparse.RawTextHelpFormatter,
|
|
46
|
+
epilog=(
|
|
47
|
+
"Examples:\n"
|
|
48
|
+
" # All patterns on a single day\n"
|
|
49
|
+
" python main.py --all-patterns --symbol EURUSD --timeframe 5m --period 60d --date 2025-04-01\n\n"
|
|
50
|
+
" # All patterns over a date range\n"
|
|
51
|
+
" python main.py --all-patterns --symbol EURUSD --timeframe 5m --period 60d "
|
|
52
|
+
"--start-date 2025-04-01 --end-date 2025-04-10\n\n"
|
|
53
|
+
" # Single pattern without date filter\n"
|
|
54
|
+
" python main.py --pattern KICKING --symbol EURUSD --timeframe 5m --period 60d\n"
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
parser.add_argument(
|
|
58
|
+
"--symbol",
|
|
59
|
+
default="EURUSD",
|
|
60
|
+
help="Ticker without suffix (e.g. EURUSD, GBPUSD); '=X' will be appended automatically.",
|
|
61
|
+
)
|
|
62
|
+
parser.add_argument("--period", default="60d", help="History period, e.g. 60d")
|
|
63
|
+
parser.add_argument(
|
|
64
|
+
"--timeframe",
|
|
65
|
+
default="15m",
|
|
66
|
+
help="Timeframe (M1, M5, M15, M30, H1, D1) or raw yfinance interval (1m, 5m, 15m, 1h, 1d...).",
|
|
67
|
+
)
|
|
68
|
+
group = parser.add_mutually_exclusive_group(required=True)
|
|
69
|
+
group.add_argument(
|
|
70
|
+
"--pattern",
|
|
71
|
+
help="Single candlestick pattern, e.g. KICKING (CDL prefix optional).",
|
|
72
|
+
)
|
|
73
|
+
group.add_argument(
|
|
74
|
+
"--all-patterns",
|
|
75
|
+
action="store_true",
|
|
76
|
+
help="Scan and show signals for all TA-Lib candlestick patterns.",
|
|
77
|
+
)
|
|
78
|
+
parser.add_argument(
|
|
79
|
+
"--date",
|
|
80
|
+
help="Optional filter by date (YYYY-MM-DD). If omitted, show all signals.",
|
|
81
|
+
)
|
|
82
|
+
parser.add_argument(
|
|
83
|
+
"--start-date",
|
|
84
|
+
help="Optional start date (YYYY-MM-DD) for range filter (inclusive).",
|
|
85
|
+
)
|
|
86
|
+
parser.add_argument(
|
|
87
|
+
"--end-date",
|
|
88
|
+
help="Optional end date (YYYY-MM-DD) for range filter (inclusive).",
|
|
89
|
+
)
|
|
90
|
+
return parser.parse_args()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def main() -> None:
|
|
94
|
+
args = parse_args()
|
|
95
|
+
interval = normalize_timeframe(args.timeframe)
|
|
96
|
+
|
|
97
|
+
if args.date and (args.start_date or args.end_date):
|
|
98
|
+
raise ValueError("Use either --date or --start-date/--end-date, not both.")
|
|
99
|
+
|
|
100
|
+
data = ForexDataLoader(
|
|
101
|
+
args.symbol,
|
|
102
|
+
period=args.period,
|
|
103
|
+
interval=interval,
|
|
104
|
+
).get_data()
|
|
105
|
+
|
|
106
|
+
analyzer = PatternAnalyzer(data)
|
|
107
|
+
range_info = ""
|
|
108
|
+
if args.date:
|
|
109
|
+
range_info = f" on {args.date}"
|
|
110
|
+
elif args.start_date or args.end_date:
|
|
111
|
+
range_info = f" from {args.start_date or 'beginning'} to {args.end_date or 'end'}"
|
|
112
|
+
|
|
113
|
+
if args.pattern:
|
|
114
|
+
signals = analyzer.get_signals(
|
|
115
|
+
args.pattern,
|
|
116
|
+
date=args.date,
|
|
117
|
+
start_date=args.start_date,
|
|
118
|
+
end_date=args.end_date,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
if signals.empty:
|
|
122
|
+
print(
|
|
123
|
+
f"No signals for pattern {args.pattern} "
|
|
124
|
+
f"on period {args.period} timeframe {interval}{range_info}"
|
|
125
|
+
)
|
|
126
|
+
else:
|
|
127
|
+
print(f"Found signals for {args.pattern} ({interval}, {args.period}){range_info}:")
|
|
128
|
+
print(signals.to_string())
|
|
129
|
+
else:
|
|
130
|
+
print(
|
|
131
|
+
f"Scanning all patterns for {args.symbol} "
|
|
132
|
+
f"({interval}, {args.period}){range_info}..."
|
|
133
|
+
)
|
|
134
|
+
found_any = False
|
|
135
|
+
for pattern in sorted(analyzer.pattern_functions):
|
|
136
|
+
signals = analyzer.get_signals(
|
|
137
|
+
pattern,
|
|
138
|
+
date=args.date,
|
|
139
|
+
start_date=args.start_date,
|
|
140
|
+
end_date=args.end_date,
|
|
141
|
+
)
|
|
142
|
+
pattern_name = pattern.replace("CDL", "")
|
|
143
|
+
if signals.empty:
|
|
144
|
+
print(f"{pattern_name}: no signals")
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
found_any = True
|
|
148
|
+
print(f"{pattern_name}:")
|
|
149
|
+
print(signals.to_string())
|
|
150
|
+
print("-" * 40)
|
|
151
|
+
|
|
152
|
+
if not found_any:
|
|
153
|
+
print(
|
|
154
|
+
f"No signals for any pattern on period {args.period} "
|
|
155
|
+
f"timeframe {interval}{range_info}"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
if __name__ == "__main__":
|
|
160
|
+
main()
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import yfinance as yf
|
|
3
|
+
import pytz
|
|
4
|
+
|
|
5
|
+
class ForexDataLoader:
|
|
6
|
+
"""
|
|
7
|
+
Generic Data Loader for Yahoo Finance tickers (forex, stocks, crypto, etc.).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
symbol: str,
|
|
13
|
+
period: str = '60d',
|
|
14
|
+
interval: str = '15m',
|
|
15
|
+
timezone: str = 'Europe/Moscow'
|
|
16
|
+
):
|
|
17
|
+
# Allow full tickers or append suffix if missing
|
|
18
|
+
self.ticker = symbol if symbol.endswith(suffix := '=X') else f"{symbol}{suffix}"
|
|
19
|
+
self.period = period
|
|
20
|
+
self.interval = interval
|
|
21
|
+
self.timezone = timezone
|
|
22
|
+
|
|
23
|
+
def fetch(self) -> pd.DataFrame:
|
|
24
|
+
"""Fetch raw data via yfinance."""
|
|
25
|
+
return yf.download(
|
|
26
|
+
self.ticker,
|
|
27
|
+
period=self.period,
|
|
28
|
+
interval=self.interval,
|
|
29
|
+
auto_adjust=True
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def process(self, data: pd.DataFrame) -> pd.DataFrame:
|
|
33
|
+
"""Normalize columns and timezone."""
|
|
34
|
+
# Drop extra MultiIndex level if present
|
|
35
|
+
data.columns = data.columns.droplevel(1) if data.columns.nlevels > 1 else data.columns
|
|
36
|
+
|
|
37
|
+
if data.index.tz is None:
|
|
38
|
+
data.index = data.index.tz_localize(pytz.UTC)
|
|
39
|
+
else:
|
|
40
|
+
data.index = data.index.tz_convert(pytz.UTC)
|
|
41
|
+
# Потом конвертируем в нужную зону
|
|
42
|
+
data.index = data.index.tz_convert(self.timezone)
|
|
43
|
+
return data
|
|
44
|
+
|
|
45
|
+
def get_data(self) -> pd.DataFrame:
|
|
46
|
+
"""Full pipeline: fetch then process."""
|
|
47
|
+
return self.process(self.fetch())
|
|
48
|
+
|
|
49
|
+
if __name__ == "__main__":
|
|
50
|
+
# Example usage
|
|
51
|
+
loader = ForexDataLoader("EURUSD=X")
|
|
52
|
+
data = loader.get_data()
|
|
53
|
+
print(data)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from typing import Iterable, Optional, Tuple
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import talib
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PatternAnalyzer:
|
|
8
|
+
def __init__(self, data: pd.DataFrame):
|
|
9
|
+
self.data = data
|
|
10
|
+
self.pattern_functions = [f for f in dir(talib) if f.startswith("CDL")]
|
|
11
|
+
|
|
12
|
+
def _normalize_pattern(self, pattern: str) -> str:
|
|
13
|
+
pattern_upper = pattern.upper()
|
|
14
|
+
return pattern_upper if pattern_upper.startswith("CDL") else f"CDL{pattern_upper}"
|
|
15
|
+
|
|
16
|
+
def _normalize_dates(
|
|
17
|
+
self, date: Optional[str], start_date: Optional[str], end_date: Optional[str]
|
|
18
|
+
) -> Tuple[Optional[pd.Timestamp], Optional[pd.Timestamp], Optional[pd.Timestamp]]:
|
|
19
|
+
tz = self.data.index.tz
|
|
20
|
+
|
|
21
|
+
def convert(dt: Optional[str]) -> Optional[pd.Timestamp]:
|
|
22
|
+
if dt is None:
|
|
23
|
+
return None
|
|
24
|
+
parsed = pd.to_datetime(dt)
|
|
25
|
+
if parsed.tzinfo is None:
|
|
26
|
+
parsed = parsed.tz_localize(tz)
|
|
27
|
+
else:
|
|
28
|
+
parsed = parsed.tz_convert(tz)
|
|
29
|
+
return parsed.normalize()
|
|
30
|
+
|
|
31
|
+
return convert(date), convert(start_date), convert(end_date)
|
|
32
|
+
|
|
33
|
+
def get_signals(
|
|
34
|
+
self,
|
|
35
|
+
pattern: str,
|
|
36
|
+
date: Optional[str] = None,
|
|
37
|
+
start_date: Optional[str] = None,
|
|
38
|
+
end_date: Optional[str] = None,
|
|
39
|
+
) -> pd.Series:
|
|
40
|
+
"""Return non-zero signals for a single candlestick pattern with optional date filters."""
|
|
41
|
+
normalized = self._normalize_pattern(pattern)
|
|
42
|
+
if normalized not in self.pattern_functions:
|
|
43
|
+
available = ", ".join(p.replace("CDL", "") for p in self.pattern_functions)
|
|
44
|
+
raise ValueError(f"Unknown pattern '{pattern}'. Available: {available}")
|
|
45
|
+
|
|
46
|
+
pattern_func = getattr(talib, normalized)
|
|
47
|
+
result = pattern_func(
|
|
48
|
+
self.data["Open"],
|
|
49
|
+
self.data["High"],
|
|
50
|
+
self.data["Low"],
|
|
51
|
+
self.data["Close"],
|
|
52
|
+
)
|
|
53
|
+
series = pd.Series(result, index=self.data.index, name=normalized)
|
|
54
|
+
signals = series[series != 0]
|
|
55
|
+
|
|
56
|
+
target_date, start_dt, end_dt = self._normalize_dates(date, start_date, end_date)
|
|
57
|
+
|
|
58
|
+
if target_date:
|
|
59
|
+
day_index = signals.index.normalize()
|
|
60
|
+
signals = signals[day_index == target_date]
|
|
61
|
+
|
|
62
|
+
if start_dt or end_dt:
|
|
63
|
+
day_index = signals.index.normalize()
|
|
64
|
+
mask = pd.Series(True, index=signals.index)
|
|
65
|
+
if start_dt:
|
|
66
|
+
mask &= day_index >= start_dt
|
|
67
|
+
if end_dt:
|
|
68
|
+
mask &= day_index <= end_dt
|
|
69
|
+
signals = signals[mask]
|
|
70
|
+
|
|
71
|
+
return signals
|
|
72
|
+
|
|
73
|
+
def analyze_all_for_date(self, date: str) -> Iterable[str]:
|
|
74
|
+
"""Keep legacy all-patterns behavior for a specific date."""
|
|
75
|
+
messages = []
|
|
76
|
+
for pattern in self.pattern_functions:
|
|
77
|
+
pattern_func = getattr(talib, pattern)
|
|
78
|
+
try:
|
|
79
|
+
result = pattern_func(
|
|
80
|
+
self.data["Open"],
|
|
81
|
+
self.data["High"],
|
|
82
|
+
self.data["Low"],
|
|
83
|
+
self.data["Close"],
|
|
84
|
+
)
|
|
85
|
+
values = result.loc[date]
|
|
86
|
+
values_series = (
|
|
87
|
+
values
|
|
88
|
+
if isinstance(values, pd.Series)
|
|
89
|
+
else pd.Series([values], index=[pd.Timestamp(date)])
|
|
90
|
+
)
|
|
91
|
+
non_zero = values_series[values_series != 0]
|
|
92
|
+
messages.append(
|
|
93
|
+
f"{pattern} on {date} (non-zero values):\n{non_zero.to_string()}"
|
|
94
|
+
if not non_zero.empty
|
|
95
|
+
else f"{pattern} on {date}: all values are 0"
|
|
96
|
+
)
|
|
97
|
+
except KeyError:
|
|
98
|
+
messages.append(f"{pattern}: No data available for {date}")
|
|
99
|
+
except Exception as e:
|
|
100
|
+
messages.append(f"Error in {pattern}: {e}")
|
|
101
|
+
return messages
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""Pattern ranking tester - find best performing candlestick patterns."""
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import numpy as np
|
|
5
|
+
import talib
|
|
6
|
+
from typing import Dict, List, Tuple, Optional
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from datetime import datetime, timedelta
|
|
9
|
+
from utils.pattern_helper import PatternHelper
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class PatternResult:
|
|
14
|
+
"""Result for a single pattern test."""
|
|
15
|
+
pattern_name: str
|
|
16
|
+
total_signals: int
|
|
17
|
+
winning_trades: int
|
|
18
|
+
losing_trades: int
|
|
19
|
+
win_rate: float
|
|
20
|
+
total_pnl: float
|
|
21
|
+
avg_pnl: float
|
|
22
|
+
max_profit: float
|
|
23
|
+
max_loss: float
|
|
24
|
+
sharpe_ratio: float
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class PatternRankingTester:
|
|
28
|
+
"""
|
|
29
|
+
Test all candlestick patterns and rank them by performance.
|
|
30
|
+
Can filter by economic news events.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
__slots__ = ('_data', '_initial_capital', '_position_size', '_results', '_news_dates')
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
data: pd.DataFrame,
|
|
38
|
+
initial_capital: float = 10000.0,
|
|
39
|
+
position_size: float = 100.0,
|
|
40
|
+
news_dates: Optional[List[str]] = None
|
|
41
|
+
):
|
|
42
|
+
"""
|
|
43
|
+
Initialize pattern tester.
|
|
44
|
+
|
|
45
|
+
Parameters:
|
|
46
|
+
-----------
|
|
47
|
+
data : pd.DataFrame
|
|
48
|
+
OHLCV data
|
|
49
|
+
initial_capital : float
|
|
50
|
+
Initial capital
|
|
51
|
+
position_size : float
|
|
52
|
+
Position size per trade
|
|
53
|
+
news_dates : list, optional
|
|
54
|
+
List of dates with important news (YYYY-MM-DD)
|
|
55
|
+
"""
|
|
56
|
+
self._data = data
|
|
57
|
+
self._initial_capital = initial_capital
|
|
58
|
+
self._position_size = position_size
|
|
59
|
+
self._results: List[PatternResult] = []
|
|
60
|
+
self._news_dates = set(news_dates) if news_dates else set()
|
|
61
|
+
|
|
62
|
+
def test_all_patterns(self, filter_news: bool = False) -> List[PatternResult]:
|
|
63
|
+
"""
|
|
64
|
+
Test all patterns and return ranked results.
|
|
65
|
+
|
|
66
|
+
Parameters:
|
|
67
|
+
-----------
|
|
68
|
+
filter_news : bool
|
|
69
|
+
If True, exclude trades during news events
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
--------
|
|
73
|
+
list : Ranked pattern results
|
|
74
|
+
"""
|
|
75
|
+
self._results = []
|
|
76
|
+
|
|
77
|
+
# Get all patterns dynamically from TA-Lib
|
|
78
|
+
all_patterns = PatternHelper.get_all_patterns()
|
|
79
|
+
|
|
80
|
+
for pattern_name in all_patterns:
|
|
81
|
+
try:
|
|
82
|
+
result = self._test_single_pattern(pattern_name, filter_news)
|
|
83
|
+
if result and result.total_signals > 0:
|
|
84
|
+
self._results.append(result)
|
|
85
|
+
except Exception as e:
|
|
86
|
+
print(f"Error testing {pattern_name}: {e}")
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
# Sort by win rate, then by total PnL
|
|
90
|
+
self._results.sort(key=lambda x: (x.win_rate, x.total_pnl), reverse=True)
|
|
91
|
+
|
|
92
|
+
return self._results
|
|
93
|
+
|
|
94
|
+
def _test_single_pattern(
|
|
95
|
+
self,
|
|
96
|
+
pattern_name: str,
|
|
97
|
+
filter_news: bool
|
|
98
|
+
) -> Optional[PatternResult]:
|
|
99
|
+
"""Test a single pattern."""
|
|
100
|
+
# Detect pattern
|
|
101
|
+
pattern_func = getattr(talib, pattern_name, None)
|
|
102
|
+
if not pattern_func:
|
|
103
|
+
print(f"Pattern function not found: {pattern_name}")
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
pattern_values = pattern_func(
|
|
108
|
+
self._data['Open'].values,
|
|
109
|
+
self._data['High'].values,
|
|
110
|
+
self._data['Low'].values,
|
|
111
|
+
self._data['Close'].values
|
|
112
|
+
)
|
|
113
|
+
except Exception as e:
|
|
114
|
+
print(f"Error detecting pattern {pattern_name}: {e}")
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
# Generate signals
|
|
118
|
+
signals = np.zeros(len(self._data))
|
|
119
|
+
for i in range(len(pattern_values)):
|
|
120
|
+
if pattern_values[i] > 0: # Bullish pattern
|
|
121
|
+
signals[i] = 1
|
|
122
|
+
elif pattern_values[i] < 0: # Bearish pattern
|
|
123
|
+
signals[i] = -1
|
|
124
|
+
|
|
125
|
+
# Filter by news if requested
|
|
126
|
+
if filter_news and self._news_dates:
|
|
127
|
+
for i in range(len(signals)):
|
|
128
|
+
date_str = self._data.index[i].strftime('%Y-%m-%d')
|
|
129
|
+
if date_str in self._news_dates:
|
|
130
|
+
signals[i] = 0
|
|
131
|
+
|
|
132
|
+
# Calculate trades
|
|
133
|
+
trades = self._calculate_trades(signals)
|
|
134
|
+
|
|
135
|
+
# Return None if no trades or too few signals
|
|
136
|
+
if not trades:
|
|
137
|
+
# Check if there were any signals at all
|
|
138
|
+
total_signals = int(np.sum(signals != 0))
|
|
139
|
+
if total_signals == 0:
|
|
140
|
+
print(f"{pattern_name}: No signals generated")
|
|
141
|
+
else:
|
|
142
|
+
print(f"{pattern_name}: {total_signals} signals but no completed trades")
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
# Calculate statistics
|
|
146
|
+
winning_trades = [t for t in trades if t['pnl'] > 0]
|
|
147
|
+
losing_trades = [t for t in trades if t['pnl'] <= 0]
|
|
148
|
+
|
|
149
|
+
total_pnl = sum(t['pnl'] for t in trades)
|
|
150
|
+
avg_pnl = total_pnl / len(trades) if trades else 0
|
|
151
|
+
win_rate = len(winning_trades) / len(trades) * 100 if trades else 0
|
|
152
|
+
|
|
153
|
+
max_profit = max([t['pnl'] for t in trades]) if trades else 0
|
|
154
|
+
max_loss = min([t['pnl'] for t in trades]) if trades else 0
|
|
155
|
+
|
|
156
|
+
# Calculate Sharpe ratio
|
|
157
|
+
pnls = [t['pnl'] for t in trades]
|
|
158
|
+
sharpe = (np.mean(pnls) / np.std(pnls)) * np.sqrt(252) if len(pnls) > 1 and np.std(pnls) > 0 else 0
|
|
159
|
+
|
|
160
|
+
return PatternResult(
|
|
161
|
+
pattern_name=pattern_name.replace('CDL', ''),
|
|
162
|
+
total_signals=int(np.sum(signals != 0)),
|
|
163
|
+
winning_trades=len(winning_trades),
|
|
164
|
+
losing_trades=len(losing_trades),
|
|
165
|
+
win_rate=win_rate,
|
|
166
|
+
total_pnl=total_pnl,
|
|
167
|
+
avg_pnl=avg_pnl,
|
|
168
|
+
max_profit=max_profit,
|
|
169
|
+
max_loss=max_loss,
|
|
170
|
+
sharpe_ratio=sharpe
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
def _calculate_trades(self, signals: np.ndarray) -> List[Dict]:
|
|
174
|
+
"""Calculate trades from signals."""
|
|
175
|
+
trades = []
|
|
176
|
+
position = 0
|
|
177
|
+
entry_idx = None
|
|
178
|
+
|
|
179
|
+
closes = self._data['Close'].values
|
|
180
|
+
times = self._data.index.to_numpy()
|
|
181
|
+
|
|
182
|
+
for i in range(len(signals)):
|
|
183
|
+
signal = signals[i]
|
|
184
|
+
|
|
185
|
+
# BUY signal
|
|
186
|
+
if signal == 1 and position == 0:
|
|
187
|
+
entry_idx = i
|
|
188
|
+
position = self._position_size / closes[i]
|
|
189
|
+
|
|
190
|
+
# SELL signal
|
|
191
|
+
elif signal == -1 and position > 0 and entry_idx is not None:
|
|
192
|
+
exit_price = closes[i]
|
|
193
|
+
entry_price = closes[entry_idx]
|
|
194
|
+
pnl = position * (exit_price - entry_price)
|
|
195
|
+
|
|
196
|
+
trades.append({
|
|
197
|
+
'entry_time': times[entry_idx],
|
|
198
|
+
'exit_time': times[i],
|
|
199
|
+
'entry_price': entry_price,
|
|
200
|
+
'exit_price': exit_price,
|
|
201
|
+
'pnl': pnl
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
position = 0
|
|
205
|
+
entry_idx = None
|
|
206
|
+
|
|
207
|
+
return trades
|
|
208
|
+
|
|
209
|
+
def get_top_patterns(self, n: int = 10) -> List[PatternResult]:
|
|
210
|
+
"""Get top N patterns by performance."""
|
|
211
|
+
return self._results[:n]
|
|
212
|
+
|
|
213
|
+
def get_comparison_report(self) -> pd.DataFrame:
|
|
214
|
+
"""Get comparison report with/without news filter."""
|
|
215
|
+
# Test without news filter
|
|
216
|
+
results_no_filter = self.test_all_patterns(filter_news=False)
|
|
217
|
+
|
|
218
|
+
# Test with news filter
|
|
219
|
+
results_with_filter = self.test_all_patterns(filter_news=True)
|
|
220
|
+
|
|
221
|
+
# Create comparison DataFrame
|
|
222
|
+
data = []
|
|
223
|
+
for r_no, r_yes in zip(results_no_filter[:20], results_with_filter[:20]):
|
|
224
|
+
data.append({
|
|
225
|
+
'Pattern': r_no.pattern_name,
|
|
226
|
+
'Win Rate (No News Filter)': f"{r_no.win_rate:.1f}%",
|
|
227
|
+
'Win Rate (With News Filter)': f"{r_yes.win_rate:.1f}%",
|
|
228
|
+
'Total PnL (No Filter)': f"${r_no.total_pnl:.2f}",
|
|
229
|
+
'Total PnL (With Filter)': f"${r_yes.total_pnl:.2f}",
|
|
230
|
+
'Signals (No Filter)': r_no.total_signals,
|
|
231
|
+
'Signals (With Filter)': r_yes.total_signals,
|
|
232
|
+
'Sharpe (No Filter)': f"{r_no.sharpe_ratio:.2f}",
|
|
233
|
+
'Sharpe (With Filter)': f"{r_yes.sharpe_ratio:.2f}"
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
return pd.DataFrame(data)
|
|
237
|
+
|
|
238
|
+
def export_results(self, filename: str = 'pattern_ranking.csv') -> None:
|
|
239
|
+
"""Export results to CSV."""
|
|
240
|
+
data = []
|
|
241
|
+
for result in self._results:
|
|
242
|
+
data.append({
|
|
243
|
+
'Rank': len(data) + 1,
|
|
244
|
+
'Pattern': result.pattern_name,
|
|
245
|
+
'Total Signals': result.total_signals,
|
|
246
|
+
'Winning Trades': result.winning_trades,
|
|
247
|
+
'Losing Trades': result.losing_trades,
|
|
248
|
+
'Win Rate %': f"{result.win_rate:.2f}",
|
|
249
|
+
'Total PnL': f"{result.total_pnl:.2f}",
|
|
250
|
+
'Avg PnL': f"{result.avg_pnl:.2f}",
|
|
251
|
+
'Max Profit': f"{result.max_profit:.2f}",
|
|
252
|
+
'Max Loss': f"{result.max_loss:.2f}",
|
|
253
|
+
'Sharpe Ratio': f"{result.sharpe_ratio:.2f}"
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
df = pd.DataFrame(data)
|
|
257
|
+
df.to_csv(filename, index=False)
|
|
258
|
+
print(f"Results exported to {filename}")
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: yfinance-ta-patterns
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: CLI and helpers to scan yfinance data for TA-Lib candlestick patterns.
|
|
5
|
+
Author: eminsk
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2025 eminsk
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Repository, https://github.com/eminsk/yfinance-ta-patterns
|
|
29
|
+
Project-URL: Homepage, https://github.com/eminsk/yfinance-ta-patterns
|
|
30
|
+
Requires-Python: >=3.12
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
License-File: LICENSE
|
|
33
|
+
Requires-Dist: ta-lib>=0.6.8
|
|
34
|
+
Requires-Dist: yfinance>=0.2.66
|
|
35
|
+
Requires-Dist: pandas>=2.2.0
|
|
36
|
+
Requires-Dist: pytz>=2024.1
|
|
37
|
+
Requires-Dist: numpy>=1.26
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
|
|
40
|
+
## Forex Candlestick Scanner
|
|
41
|
+
|
|
42
|
+
Python package + CLI that downloads Yahoo Finance data via `yfinance` and runs TA-Lib candlestick detectors for your symbol, timeframe, and date filters.
|
|
43
|
+
|
|
44
|
+
### Quick start
|
|
45
|
+
```bash
|
|
46
|
+
# create env
|
|
47
|
+
python -m venv .venv
|
|
48
|
+
.\.venv\Scripts\activate # PowerShell; adjust for your shell
|
|
49
|
+
|
|
50
|
+
# install the package (editable for local dev)
|
|
51
|
+
pip install -e .
|
|
52
|
+
|
|
53
|
+
# run CLI (two entrypoints)
|
|
54
|
+
yfinance-ta-patterns --pattern KICKING --symbol EURUSD --timeframe 5m --period 60d
|
|
55
|
+
# or
|
|
56
|
+
yftp --all-patterns --symbol EURUSD --timeframe 5m --period 60d --date 2025-04-01
|
|
57
|
+
```
|
|
58
|
+
`pyproject.toml` targets Python `>=3.12`.
|
|
59
|
+
|
|
60
|
+
### CLI usage
|
|
61
|
+
```
|
|
62
|
+
yfinance-ta-patterns [--pattern NAME | --all-patterns]
|
|
63
|
+
[--symbol EURUSD] [--period 60d]
|
|
64
|
+
[--timeframe 15m] [--date YYYY-MM-DD]
|
|
65
|
+
[--start-date YYYY-MM-DD] [--end-date YYYY-MM-DD]
|
|
66
|
+
```
|
|
67
|
+
- `--pattern`: Single candlestick name (with or without `CDL` prefix).
|
|
68
|
+
- `--all-patterns`: Scan every TA-Lib candlestick detector.
|
|
69
|
+
- `--symbol`: Ticker without suffix; `=X` is appended automatically for Forex (default `EURUSD`).
|
|
70
|
+
- `--period`: History window passed to `yfinance` (e.g., `60d`, `1mo`).
|
|
71
|
+
- `--timeframe`: Use `M1/M5/M15/M30/H1/D1` or raw `yfinance` intervals (`1m`, `5m`, `1h`, `1d`, etc.).
|
|
72
|
+
- `--date`: Filter signals for a single day.
|
|
73
|
+
- `--start-date` / `--end-date`: Inclusive range filter (cannot be combined with `--date`).
|
|
74
|
+
|
|
75
|
+
### Examples
|
|
76
|
+
- All patterns for a single day:
|
|
77
|
+
```bash
|
|
78
|
+
yftp --all-patterns --symbol EURUSD --timeframe 5m --period 60d --date 2025-04-01
|
|
79
|
+
```
|
|
80
|
+
- All patterns across a range:
|
|
81
|
+
```bash
|
|
82
|
+
yftp --all-patterns --symbol EURUSD --timeframe 5m --period 60d --start-date 2025-04-01 --end-date 2025-04-10
|
|
83
|
+
```
|
|
84
|
+
- One pattern without date filter:
|
|
85
|
+
```bash
|
|
86
|
+
yftp --pattern KICKING --symbol EURUSD --timeframe 5m --period 60d
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Data loader
|
|
90
|
+
`yfinance_ta_patterns/forex_data_loader.py` fetches and normalizes OHLC data. It appends `=X` to symbols when missing and converts timestamps to UTC before shifting to the configured timezone (`Europe/Moscow` by default).
|
|
91
|
+
|
|
92
|
+
### Pattern analysis
|
|
93
|
+
`yfinance_ta_patterns/pattern_analyzer.py` wraps TA-Lib's `CDL*` functions, returning non-zero signals and applying optional date filters. When `--all-patterns` is used, it iterates over the full catalog and prints hits per pattern.
|
|
94
|
+
|
|
95
|
+
### Pattern ranking helper (optional)
|
|
96
|
+
`yfinance_ta_patterns/pattern_tester.py` contains a backtesting-style ranking tool. It depends on `utils.pattern_helper.PatternHelper` to enumerate patterns; add that helper before running comparisons or exports.
|
|
97
|
+
|
|
98
|
+
### Project layout
|
|
99
|
+
- `yfinance_ta_patterns/cli.py`: CLI entry point and argument parsing.
|
|
100
|
+
- `yfinance_ta_patterns/forex_data_loader.py`: Data download and timezone normalization.
|
|
101
|
+
- `yfinance_ta_patterns/pattern_analyzer.py`: Candlestick signal extraction.
|
|
102
|
+
- `yfinance_ta_patterns/pattern_tester.py`: Experimental ranking/backtest utilities.
|
|
103
|
+
- `main.py`: Thin wrapper to launch the CLI.
|
|
104
|
+
|
|
105
|
+
### Packaging and releases
|
|
106
|
+
- Nightly GitHub Actions workflow builds onefile Nuitka binaries for Windows/macOS/Linux and publishes nightly prereleases.
|
|
107
|
+
- A PyPI publish workflow can be enabled by adding a secret `PYPI_API_TOKEN`; tags like `v0.1.0` will build sdist/wheel and upload.
|
|
108
|
+
- TA-Lib is installed from PyPI on Windows; Linux/macOS CI builds the TA-Lib C library from source for the binaries. For local installs, PyPI wheels (`ta-lib` >=0.6.8) cover CPython 3.9–3.14.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
tests/test_cli.py
|
|
5
|
+
yfinance_ta_patterns/__init__.py
|
|
6
|
+
yfinance_ta_patterns/cli.py
|
|
7
|
+
yfinance_ta_patterns/forex_data_loader.py
|
|
8
|
+
yfinance_ta_patterns/pattern_analyzer.py
|
|
9
|
+
yfinance_ta_patterns/pattern_tester.py
|
|
10
|
+
yfinance_ta_patterns.egg-info/PKG-INFO
|
|
11
|
+
yfinance_ta_patterns.egg-info/SOURCES.txt
|
|
12
|
+
yfinance_ta_patterns.egg-info/dependency_links.txt
|
|
13
|
+
yfinance_ta_patterns.egg-info/entry_points.txt
|
|
14
|
+
yfinance_ta_patterns.egg-info/requires.txt
|
|
15
|
+
yfinance_ta_patterns.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
yfinance_ta_patterns
|