Add files via upload

This commit is contained in:
Richard
2026-05-21 15:58:47 +01:00
committed by GitHub
parent 83ae839186
commit ec1e025ad6
2 changed files with 718 additions and 43 deletions
+77 -12
View File
@@ -1,4 +1,4 @@
# MT5 Multi-Timeframe Candlestick Pattern Scanner & Backtester v8
# MT5 Multi-Timeframe Candlestick Pattern Scanner & Backtester v9
A comprehensive tool to scan **M5, M15, H1, H4, D1** charts for classical candlestick patterns, backtest their performance with realistic entry/exit simulation, and run a live scanner that scores and alerts when a new pattern appears.
@@ -6,7 +6,7 @@ A comprehensive tool to scan **M5, M15, H1, H4, D1** charts for classical candle
## Features
- **20+ patterns**: Doji, Hammer, Shooting Star, Engulfing, Morning/Evening Star, Three White Soldiers, Three Black Crows, Marubozu, Harami, Tweezers, Rising/Falling Three Methods, Inverted Hammer, and more
- **50+ patterns**: classic single-candle (Doji, Dragonfly Doji, Gravestone Doji, Hammer, Shooting Star, Marubozu, Belt Hold, Inverted Hammer, Hanging Man, Spinning Top), two-candle (Engulfing, Near Engulfing, Harami, Kicker, Piercing Line, Dark Cloud Cover, Tweezers, Meeting Lines, Separating Lines, Doji Star), three-candle (Morning/Evening Star, Three White Soldiers, Three Black Crows, Three Inside Up/Down, Three Outside Up/Down, Abandoned Baby, Upside Gap Two Crows), four-candle (Three Line Strike, Concealing Baby Swallow), five-candle (Rising/Falling Three Methods, Mat Hold, Ladder Bottom), and **TheStrat** composite patterns (2-2, 3-1-2, 2-1-2, 1-2-2 Rev, 1-3 Rev)
- **Multi-timeframe backtesting** — backtest all 5 timeframes in a single run with per-TF statistics
- **Trade management modes** — `fixed` (static SL/TP), `breakeven` (move SL to entry at 1R), `trail` (ATR-based trailing stop), `partial` (close 50% at 1R, trail remainder)
- **Structure-based SL placement** — place stops at pattern invalidation levels (below Hammer low, below Engulfing candle extreme, etc.) instead of generic ATR offsets
@@ -40,8 +40,10 @@ A comprehensive tool to scan **M5, M15, H1, H4, D1** charts for classical candle
- **Session classification** (Asia, Pacific, London Open, London Morning, London/NY Overlap, NY Afternoon)
- **Full backtests** over date ranges — CSV reports, summary tables, JSON stats cache, and text reports
- **Live scanner** — monitors all active timeframes and prints formatted alerts when a new candle closes
- **Sound alerts** (Windows only) — high-Hz triple beep for STRONG BUY, low-Hz triple beep for STRONG SELL; starts muted, type `m` + Enter to toggle
- **Tier-aware sound alerts** (Windows only) — high-Hz triple beep for STRONG BUY, low-Hz triple beep for STRONG SELL; Tier A (Elite) always fires, Tier B only if score ≥ `sound_alert_tier_b_min_score` (default 60), Tier C/D never; starts muted, type `m` + Enter to toggle
- **Position sizing** (risk-based, standard lots) displayed in alerts
- **Auto-detected broker UTC offset** — broker UTC offset is auto-detected at startup by comparing the latest MT5 candle time against true UTC; handles DST transitions automatically; falls back to `broker_utc_offset` config value if detection fails
- **Local time display** — candle close times and "Next close" countdowns are converted from broker time to local machine time for display; log timestamps always use local time
- **Auto-reconnect** with exponential backoff if MT5 connection drops
---
@@ -197,8 +199,12 @@ The scanner includes Windows-only sound alerts so you don't have to stare at the
### How it works
- **STRONG BUY** (Bullish signal with score >= 65) — triple high-Hz beep (1200 Hz by default)
- **STRONG SELL** (Bearish signal with score >= 65) — triple low-Hz beep (400 Hz by default)
- **STRONG BUY** (Bullish signal) — triple high-Hz beep (1200 Hz by default)
- **STRONG SELL** (Bearish signal) — triple low-Hz beep (400 Hz by default)
- **Tier-aware triggering** — which patterns fire an alert depends on their tier:
- **Tier A (Elite)** — always triggers a sound alert
- **Tier B (Tradeable)** — triggers only if the signal score ≥ `sound_alert_tier_b_min_score` (default 60)
- **Tier C / D** — never triggers sound alerts
- The scanner starts **muted** — you must explicitly unmute to hear alerts
- Type `m` + Enter in the terminal to toggle mute/unmute at any time
- The keyboard listener runs in a background thread, so there is no delay — it responds instantly
@@ -208,7 +214,7 @@ The scanner includes Windows-only sound alerts so you don't have to stare at the
When the live scanner starts with sound enabled, you will see:
```
Sound Alerts: ENABLED | Buy: 1200Hz | Sell: 400Hz | Threshold: 65
Sound Alerts: ENABLED | Buy: 1200Hz | Sell: 400Hz | Alert Tier: B (score >= 60)
Sound is MUTED — Type "m" + Enter to unmute
```
@@ -242,7 +248,64 @@ These settings are in the `CFG` dict at the top of the script (not exposed as CL
| `sound_sell_hz` | `400` | Frequency in Hz for STRONG SELL triple beep |
| `sound_beep_duration` | `150` | Duration of each individual beep in milliseconds |
| `sound_beep_pause` | `100` | Pause between beeps in milliseconds |
| `sound_strong_threshold` | `65.0` | Signal score must be >= this to trigger a sound alert |
| `sound_alert_tier` | `'B'` | Minimum tier to trigger sound: `'A'` (Elite only), `'B'` (Tradeable+), `'C'` (all directional) |
| `sound_alert_tier_b_min_score` | `60.0` | Minimum signal score for Tier B patterns to trigger a sound alert |
---
## v9 Enhancements
### Massively Expanded Pattern Library (50+)
v9 adds 30+ new patterns across all candle counts, bringing the total to 50+:
**New single-candle patterns**: Dragonfly Doji, Gravestone Doji, Bullish Belt Hold, Bearish Belt Hold
**New two-candle patterns**: Near Engulfing, Piercing Line, Dark Cloud Cover, Bullish Kicker, Bearish Kicker, Meeting Lines (Bullish/Bearish), Bullish/Bearish Separating Lines, Bearish Doji Star
**New three-candle patterns**: Three Inside Up, Three Inside Down, Three Outside Up, Three Outside Down, Bullish Abandoned Baby, Bearish Abandoned Baby, Upside Gap Two Crows
**New four-candle patterns**: Bullish/Bearish Three Line Strike, Concealing Baby Swallow
**New five-candle patterns**: Mat Hold (Bullish/Bearish), Ladder Bottom
**TheStrat composite patterns**: 2-2 Continuation/Reversal, 3-1-2 Reversal, 2-1-2 Reversal, 1-2-2 Reversal, 1-3 Reversal — based on Rob Smith's TheStrat candle-type system (Type 1 = inside bar, Type 2 = directional break, Type 3 = outside bar)
New pattern thresholds are configurable in `CFG`:
| Key | Default | Description |
|---|---|---|
| `belt_hold_wick_ratio` | `0.1` | Max wick/body ratio for belt hold patterns |
| `kicker_min_body_ratio` | `0.6` | Min body ratio for kicker candles |
| `separating_lines_tolerance_pips` | `2` | Open price tolerance for separating lines |
| `meeting_lines_tolerance_pips` | `2` | Close price tolerance for meeting lines |
### Tier-Aware Sound Alerts
Sound alerts are now triggered based on pattern tier rather than a flat score threshold:
| Tier | Alert condition |
|---|---|
| A: ELITE | Always fires a sound alert |
| B: TRADEABLE | Fires only if signal score ≥ `sound_alert_tier_b_min_score` (default 60) |
| C: MARGINAL | Never fires |
| D: AVOID | Never fires |
This replaces the old `sound_strong_threshold` (65.0) flat cutoff. Configure via `sound_alert_tier` and `sound_alert_tier_b_min_score` in `CFG`.
### Auto-Detected Broker UTC Offset
At live scanner startup, the broker's UTC offset is automatically detected by comparing the timestamp of the latest MT5 candle against true UTC. This means DST transitions are handled correctly without any manual configuration change.
- The detected offset is logged at startup: `Timestamps: Local time (UTC+3, auto-detected broker UTC+3)`
- If detection fails, falls back to the `broker_utc_offset` value in `CFG` (default: 2)
- To disable auto-detection, set `broker_utc_offset` to `0` in `CFG`
### Local Time Display
Candle close times and "Next close" countdowns in the live scanner are now displayed in **local machine time** rather than broker server time. Log timestamps (`[HH:MM:SS]`) have always used local time — now the candle times match.
The conversion formula is: `local = broker_time broker_utc_offset + local_utc_offset`
---
@@ -511,6 +574,7 @@ ATR now uses Wilder's exponential smoothing (alpha = 1/period) instead of simple
| `--risk-percent` | Risk % of account per trade | `1.0` |
| `--min-signal-score` | Minimum signal score to display (0-100) | `0` |
| `--alert-only-strong` | Only alert on strong signals | `False` |
| `--broker-utc-offset` | Broker UTC offset (0 = auto-detect) | `2` |
| `--test-sound` | Play STRONG BUY and STRONG SELL test beeps, then exit | |
| `--output` | Backtest output directory | `./backtest_results` |
@@ -538,7 +602,8 @@ These are set in the `CFG` dict at the top of the script:
| `sound_sell_hz` | `400` | Hz for STRONG SELL triple beep |
| `sound_beep_duration` | `150` | Duration per beep in ms |
| `sound_beep_pause` | `100` | Pause between beeps in ms |
| `sound_strong_threshold` | `65.0` | Min signal score to trigger sound |
| `sound_alert_tier` | `'B'` | Minimum tier to trigger sound: `'A'` (Elite only), `'B'` (Tradeable+), `'C'` (all directional) |
| `sound_alert_tier_b_min_score` | `60.0` | Minimum score for Tier B patterns to trigger a sound alert |
### Pattern Thresholds
@@ -557,7 +622,7 @@ See `--help` for the full list of arguments.
---
## Signal Scoring System (v7)
## Signal Scoring System
Each live signal is scored 0-100 based on:
@@ -645,6 +710,6 @@ Patterns below `--min-signal-score` are filtered out (default: 0, i.e. show all)
## Timezone Notes
- Log timestamps (`[HH:MM:SS]`) use your **local computer time**
- Candle close times and "Next:" candle times use **broker server time**
- Session classification uses **broker server time** hours
- This means candle times will differ from your local clock by your timezone offset
- Candle close times and "Next:" candle times are converted to **local computer time** via the auto-detected broker UTC offset
- Session classification uses **broker server time** hours internally
- The broker UTC offset is auto-detected at startup; if detection fails, the `broker_utc_offset` config value is used (default: UTC+2)
+641 -31
View File
@@ -1,9 +1,11 @@
#!/usr/bin/env python3
"""
MT5 Multi-Timeframe Candlestick Pattern Scanner & Backtester v8
MT5 Multi-Timeframe Candlestick Pattern Scanner & Backtester v9
===============================================================
Expanded from v5: supports M5, M15, H1, H4, and D1 timeframes for both
live scanning and backtesting. All parameters are consolidated near the top.
50+ candlestick patterns across M5, M15, H1, H4, D1 with TheStrat composites,
multi-timeframe backtesting, live scanner with signal scoring, tier-aware sound
alerts, auto-detected broker timezone, and local time display.
All parameters are consolidated near the top.
Must run on Windows with MT5 installed.
Credentials are loaded exclusively from the .env file in the same directory.
@@ -18,53 +20,53 @@ Sound Alerts (Windows only):
Usage:
# Live scanner — all 5 timeframes (default)
python mt5_multitf_pattern_scanner_v8.py
python mt5_multitf_pattern_scanner.py
# Live scanner — specific timeframes only
python mt5_multitf_pattern_scanner_v8.py --timeframes M5 H1 H4
python mt5_multitf_pattern_scanner.py --timeframes M5 H1 H4
# One-shot scan of latest closed candle on all timeframes
python mt5_multitf_pattern_scanner_v8.py --mode scan
python mt5_multitf_pattern_scanner.py --mode scan
# Quick backtest (last 500 bars on H4)
python mt5_multitf_pattern_scanner_v8.py --mode backtest --bars 500
python mt5_multitf_pattern_scanner.py --mode backtest --bars 500
# Full backtest on one timeframe
python mt5_multitf_pattern_scanner_v8.py --mode fullbacktest --timeframes H4
python mt5_multitf_pattern_scanner.py --mode fullbacktest --timeframes H4
# Full backtest on ALL timeframes, date-ranged
python mt5_multitf_pattern_scanner_v8.py --mode fullbacktest --from 2024-01-01 --to 2024-12-31
python mt5_multitf_pattern_scanner.py --mode fullbacktest --from 2024-01-01 --to 2024-12-31
# Full backtest with filters
python mt5_multitf_pattern_scanner_v8.py --mode fullbacktest \\
python mt5_multitf_pattern_scanner.py --mode fullbacktest \\
--d1-trend-filter --volume-filter --forward 15 --sl 1.5 --tp 1.5
# Full backtest with structure-based SL (pattern invalidation levels)
python mt5_multitf_pattern_scanner_v8.py --mode fullbacktest --sl-mode structure
python mt5_multitf_pattern_scanner.py --mode fullbacktest --sl-mode structure
# Full backtest with breakeven trade management
python mt5_multitf_pattern_scanner_v8.py --mode fullbacktest --trade-management breakeven
python mt5_multitf_pattern_scanner.py --mode fullbacktest --trade-management breakeven
# Full backtest with trailing stop management
python mt5_multitf_pattern_scanner_v8.py --mode fullbacktest --trade-management trail
python mt5_multitf_pattern_scanner.py --mode fullbacktest --trade-management trail
# Full backtest with partial close + trailing management
python mt5_multitf_pattern_scanner_v8.py --mode fullbacktest --trade-management partial
python mt5_multitf_pattern_scanner.py --mode fullbacktest --trade-management partial
# Full backtest with expired timeout (0R flat) instead of marginal win/loss
python mt5_multitf_pattern_scanner_v8.py --mode fullbacktest --timeout-mode expired
python mt5_multitf_pattern_scanner.py --mode fullbacktest --timeout-mode expired
# Multi-symbol watchlist scan
python mt5_multitf_pattern_scanner_v8.py --mode scan --symbols EURUSD GBPUSD USDJPY
python mt5_multitf_pattern_scanner.py --mode scan --symbols EURUSD GBPUSD USDJPY
# Multi-symbol full backtest
python mt5_multitf_pattern_scanner_v8.py --mode fullbacktest --symbols EURUSD GBPUSD
python mt5_multitf_pattern_scanner.py --mode fullbacktest --symbols EURUSD GBPUSD
# Live scanner with custom account sizing
python mt5_multitf_pattern_scanner_v8.py --mode live --account-balance 25000 --risk-percent 0.5
python mt5_multitf_pattern_scanner.py --mode live --account-balance 25000 --risk-percent 0.5
# Test sound alerts (plays both BUY and SELL test beeps)
python mt5_multitf_pattern_scanner_v8.py --test-sound
python mt5_multitf_pattern_scanner.py --test-sound
"""
import MetaTrader5 as mt5
@@ -201,6 +203,16 @@ CFG = {
'tweezer_tolerance_pips': 3,
'engulf_tolerance_pips': 2.0,
# ── New Pattern Thresholds ──────────────────────────────────────
'belt_hold_wick_ratio': 0.1, # Max wick/body ratio for belt holds
'kicker_min_body_ratio': 0.6, # Min body ratio for kicker candles
'separating_lines_tolerance_pips': 2, # Tolerance for matching open prices
'meeting_lines_tolerance_pips': 2, # Tolerance for matching close prices
'gap_tolerance_pips': 2, # Minimum gap size in pips for gap patterns
# ── TheStrat Pattern Detection ─────────────────────────────────
'thestrat_enabled': True, # Enable TheStrat composite patterns
# ── Trend Detection ────────────────────────────────────────────
'trend_lookback': 20, # SMA-based lookback bars
@@ -309,17 +321,44 @@ DEFAULT_OUTPUT_DIR = os.path.join(_LOG_DIR, "backtest_results")
# ── Pattern Priority for Deduplication ─────────────────────────────
PATTERN_PRIORITY = {
# ── Neutral / Low Priority (1) ──
'Doji': 1, 'Spinning Top': 1,
'Dragonfly Doji': 1, 'Gravestone Doji': 1,
# ── Single-Candle Directional (2) ──
'Hammer': 2, 'Inverted Hammer': 2,
'Shooting Star': 2, 'Hanging Man': 2,
'Bullish Belt Hold': 2, 'Bearish Belt Hold': 2,
# ── Strong Single-Candle (3) ──
'Marubozu (Bullish)': 3, 'Marubozu (Bearish)': 3,
# ── Two-Candle Reversal (4) ──
'Tweezer Tops': 4, 'Tweezer Bottoms': 4,
'Near Bullish Engulfing': 4, 'Near Bearish Engulfing': 4,
'Bullish Harami': 4, 'Bearish Harami': 4,
'Piercing Line': 4, 'Dark Cloud Cover': 4,
'Meeting Lines (Bullish)': 4, 'Meeting Lines (Bearish)': 4,
'Bullish Separating Lines': 4, 'Bearish Separating Lines': 4,
'Bearish Doji Star': 4,
# ── Strong Two-Candle (5) ──
'Bullish Engulfing': 5, 'Bearish Engulfing': 5,
'Bullish Harami': 6, 'Bearish Harami': 6,
'Bullish Kicker': 5, 'Bearish Kicker': 5,
# ── Three-Candle (6-7) ──
'Three Inside Up': 6, 'Three Inside Down': 6,
'Three Outside Up': 6, 'Three Outside Down': 6,
'Morning Star': 7, 'Evening Star': 7,
'Bullish Abandoned Baby': 7, 'Bearish Abandoned Baby': 7,
'Upside Gap Two Crows': 7,
# ── Strong Three-Candle (8) ──
'Three White Soldiers': 8, 'Three Black Crows': 8,
'Rising Three Methods': 9, 'Falling Three Methods': 9,
# ── Four-Candle (9) ──
'Bullish Three-Line Strike': 9, 'Bearish Three-Line Strike': 9,
'Concealing Baby Swallow': 9,
# ── Five-Candle (10) ──
'Rising Three Methods': 10, 'Falling Three Methods': 10,
'Mat Hold (Bullish)': 10, 'Mat Hold (Bearish)': 10,
'Ladder Bottom': 10,
# ── TheStrat Composite (11-12) ──
'TheStrat 2-2': 11, 'TheStrat 2-1-2': 11, 'TheStrat 3-1-2': 11,
'TheStrat 1-2-2 Rev': 12, 'TheStrat 1-3 Rev': 12,
}
@@ -953,7 +992,7 @@ def mt5_rates_to_df(rates, cfg=None):
return df
# ── Scanner-specific detect_* functions removed (v8) ────────────
# ── Scanner-specific detect_* functions removed (v8, unified in v9) ────────────
# Pattern detection is now unified: scan_patterns() converts MT5
# structured arrays to DataFrames and uses the fb_detect_* functions
# that are also used by the backtest. This eliminates ~400 lines of
@@ -2415,6 +2454,496 @@ def fb_detect_tweezer(df, idx, cfg=None):
if abs(p['LOW'] - c['LOW']) <= tol and trend in ('downtrend', 'ranging'): return 'Tweezer Bottoms'
return None
# ── NEW SINGLE-CANDLE PATTERNS ──────────────────────────────────────
def fb_detect_dragonfly_doji(df, idx, cfg=None):
"""Dragonfly Doji: very small body at the top with long lower wick, nearly no upper wick.
Appears at bottom of downtrend → Bullish Reversal."""
if cfg is None: cfg = CFG
r = df.iloc[idx]
if r['RANGE'] == 0: return False
if r['BODY_RATIO'] > cfg['doji_body_ratio'] * 1.5: return False # Slightly looser than pure doji
if r['UPPER_WICK'] > r['BODY'] * 2: return False # Must have tiny/no upper wick
if r['LOWER_WICK'] < r['RANGE'] * 0.6: return False # Long lower wick is key
return True
def fb_detect_gravestone_doji(df, idx, cfg=None):
"""Gravestone Doji: very small body at the bottom with long upper wick, nearly no lower wick.
Appears at top of uptrend → Bearish Reversal."""
if cfg is None: cfg = CFG
r = df.iloc[idx]
if r['RANGE'] == 0: return False
if r['BODY_RATIO'] > cfg['doji_body_ratio'] * 1.5: return False
if r['LOWER_WICK'] > r['BODY'] * 2: return False # Must have tiny/no lower wick
if r['UPPER_WICK'] < r['RANGE'] * 0.6: return False # Long upper wick is key
return True
def fb_detect_bullish_belt_hold(df, idx, cfg=None):
"""Bullish Belt Hold: opens at/near the low, closes near the high, very small lower wick.
Appears at bottom of downtrend → Bullish Reversal."""
if cfg is None: cfg = CFG
r = df.iloc[idx]
if idx < 3: return False
trend = fb_detect_trend(df, idx, cfg)
if trend not in ('downtrend', 'ranging'): return False
if r['BODY_SIGN'] != 1: return False # Must be bullish candle
if r['BODY_RATIO'] < cfg['long_candle_ratio'] * 0.8: return False # Must be substantial body
if r['LOWER_WICK'] > r['BODY'] * cfg['belt_hold_wick_ratio']: return False # Tiny/no lower wick
return True
def fb_detect_bearish_belt_hold(df, idx, cfg=None):
"""Bearish Belt Hold: opens at/near the high, closes near the low, very small upper wick.
Appears at top of uptrend → Bearish Reversal."""
if cfg is None: cfg = CFG
r = df.iloc[idx]
if idx < 3: return False
trend = fb_detect_trend(df, idx, cfg)
if trend not in ('uptrend', 'ranging'): return False
if r['BODY_SIGN'] != -1: return False # Must be bearish candle
if r['BODY_RATIO'] < cfg['long_candle_ratio'] * 0.8: return False
if r['UPPER_WICK'] > r['BODY'] * cfg['belt_hold_wick_ratio']: return False
return True
# ── NEW TWO-CANDLE PATTERNS ─────────────────────────────────────────
def fb_detect_piercing_line(df, idx, cfg=None):
"""Piercing Line: bearish candle followed by bullish candle that opens below
prior close but closes above the midpoint of the bearish candle's body.
Bullish Reversal."""
if idx < 1: return False
c = df.iloc[idx]; p = df.iloc[idx-1]
if p['BODY_SIGN'] != -1: return False # Prior must be bearish
if c['BODY_SIGN'] != 1: return False # Current must be bullish
if p['BODY_RATIO'] < cfg.get('long_candle_ratio', 0.7) * 0.6: return False # Prior must be meaningful
p_mid = (p['OPEN'] + p['CLOSE']) / 2
if c['OPEN'] >= p['CLOSE']: return False # Must open below prior close
if c['CLOSE'] <= p_mid: return False # Must close above midpoint
return True
def fb_detect_dark_cloud_cover(df, idx, cfg=None):
"""Dark Cloud Cover: bullish candle followed by bearish candle that opens above
prior close but closes below the midpoint of the bullish candle's body.
Bearish Reversal."""
if idx < 1: return False
c = df.iloc[idx]; p = df.iloc[idx-1]
if p['BODY_SIGN'] != 1: return False # Prior must be bullish
if c['BODY_SIGN'] != -1: return False # Current must be bearish
if p['BODY_RATIO'] < cfg.get('long_candle_ratio', 0.7) * 0.6: return False
p_mid = (p['OPEN'] + p['CLOSE']) / 2
if c['OPEN'] <= p['CLOSE']: return False # Must open above prior close
if c['CLOSE'] >= p_mid: return False # Must close below midpoint
return True
def fb_detect_bullish_kicker(df, idx, cfg=None):
"""Bullish Kicker: long bearish candle followed by an even longer bullish candle
that opens above the prior close (gap up) and closes higher.
Strong Bullish Reversal."""
if idx < 1: return False
c = df.iloc[idx]; p = df.iloc[idx-1]
if p['BODY_SIGN'] != -1: return False
if c['BODY_SIGN'] != 1: return False
min_body = cfg.get('kicker_min_body_ratio', 0.6)
if p['BODY_RATIO'] < min_body: return False
if c['BODY_RATIO'] < min_body: return False
gap_tol = cfg.get('gap_tolerance_pips', 2) * cfg.get('pip_divisor', 0.0001)
if c['OPEN'] < p['CLOSE'] + gap_tol: return False # Must gap up above prior close
if c['CLOSE'] <= p['OPEN']: return False # Must close above prior open
return True
def fb_detect_bearish_kicker(df, idx, cfg=None):
"""Bearish Kicker: long bullish candle followed by an even longer bearish candle
that opens below the prior open (gap down) and closes lower.
Strong Bearish Reversal."""
if idx < 1: return False
c = df.iloc[idx]; p = df.iloc[idx-1]
if p['BODY_SIGN'] != 1: return False
if c['BODY_SIGN'] != -1: return False
min_body = cfg.get('kicker_min_body_ratio', 0.6)
if p['BODY_RATIO'] < min_body: return False
if c['BODY_RATIO'] < min_body: return False
gap_tol = cfg.get('gap_tolerance_pips', 2) * cfg.get('pip_divisor', 0.0001)
if c['OPEN'] > p['CLOSE'] - gap_tol: return False # Must gap down below prior close
if c['CLOSE'] >= p['OPEN']: return False # Must close below prior open
return True
def fb_detect_meeting_lines_bullish(df, idx, cfg=None):
"""Bullish Meeting Lines: long bearish candle followed by long bullish candle
that opens lower but closes at approximately the same level as the prior close.
Bullish Reversal."""
if idx < 1: return False
c = df.iloc[idx]; p = df.iloc[idx-1]
if p['BODY_SIGN'] != -1: return False
if c['BODY_SIGN'] != 1: return False
if p['BODY_RATIO'] < 0.5: return False
if c['BODY_RATIO'] < 0.5: return False
tol = cfg.get('meeting_lines_tolerance_pips', 2) * cfg.get('pip_divisor', 0.0001)
if abs(c['CLOSE'] - p['CLOSE']) > tol: return False # Closes must be approximately equal
return True
def fb_detect_meeting_lines_bearish(df, idx, cfg=None):
"""Bearish Meeting Lines: long bullish candle followed by long bearish candle
that opens higher but closes at approximately the same level as the prior close.
Bearish Reversal."""
if idx < 1: return False
c = df.iloc[idx]; p = df.iloc[idx-1]
if p['BODY_SIGN'] != 1: return False
if c['BODY_SIGN'] != -1: return False
if p['BODY_RATIO'] < 0.5: return False
if c['BODY_RATIO'] < 0.5: return False
tol = cfg.get('meeting_lines_tolerance_pips', 2) * cfg.get('pip_divisor', 0.0001)
if abs(c['CLOSE'] - p['CLOSE']) > tol: return False
return True
def fb_detect_bullish_separating_lines(df, idx, cfg=None):
"""Bullish Separating Lines: bearish candle followed by bullish candle that
opens at approximately the same price as the prior open. Bullish Continuation."""
if idx < 1: return False
c = df.iloc[idx]; p = df.iloc[idx-1]
if p['BODY_SIGN'] != -1: return False
if c['BODY_SIGN'] != 1: return False
if p['BODY_RATIO'] < 0.4: return False
if c['BODY_RATIO'] < 0.4: return False
tol = cfg.get('separating_lines_tolerance_pips', 2) * cfg.get('pip_divisor', 0.0001)
if abs(c['OPEN'] - p['OPEN']) > tol: return False # Opens must be approximately equal
return True
def fb_detect_bearish_separating_lines(df, idx, cfg=None):
"""Bearish Separating Lines: bullish candle followed by bearish candle that
opens at approximately the same price as the prior open. Bearish Continuation."""
if idx < 1: return False
c = df.iloc[idx]; p = df.iloc[idx-1]
if p['BODY_SIGN'] != 1: return False
if c['BODY_SIGN'] != -1: return False
if p['BODY_RATIO'] < 0.4: return False
if c['BODY_RATIO'] < 0.4: return False
tol = cfg.get('separating_lines_tolerance_pips', 2) * cfg.get('pip_divisor', 0.0001)
if abs(c['OPEN'] - p['OPEN']) > tol: return False
return True
def fb_detect_bearish_doji_star(df, idx, cfg=None):
"""Bearish Doji Star: long bullish candle followed by a Doji (small body).
Shows indecision after a strong move up → Bearish Reversal signal."""
if idx < 1: return False
c = df.iloc[idx]; p = df.iloc[idx-1]
if p['BODY_SIGN'] != 1: return False
if p['BODY_RATIO'] < cfg.get('long_candle_ratio', 0.7): return False
if c['BODY_RATIO'] > cfg.get('doji_body_ratio', 0.1) * 2: return False # Must be doji-like
return True
# ── NEW THREE-CANDLE PATTERNS ────────────────────────────────────────
def fb_detect_three_inside_up(df, idx, cfg=None):
"""Three Inside Up: large bearish → small bullish inside it (harami) →
third bullish closing above first candle's open. Confirms Bullish Harami."""
if idx < 2: return False
f, s, t = df.iloc[idx-2], df.iloc[idx-1], df.iloc[idx]
# First: large bearish
if f['BODY_SIGN'] != -1 or f['BODY_RATIO'] < cfg.get('long_candle_ratio', 0.7) * 0.8: return False
# Second: small bullish inside first's body
if s['BODY_SIGN'] != 1: return False
if s['BODY_RATIO'] > cfg.get('small_candle_ratio', 0.35) + 0.15: return False
if max(s['OPEN'], s['CLOSE']) > max(f['OPEN'], f['CLOSE']): return False
if min(s['OPEN'], s['CLOSE']) < min(f['OPEN'], f['CLOSE']): return False
# Third: bullish closing above first's open
if t['BODY_SIGN'] != 1: return False
if t['CLOSE'] <= f['OPEN']: return False
return True
def fb_detect_three_inside_down(df, idx, cfg=None):
"""Three Inside Down: large bullish → small bearish inside it (harami) →
third bearish closing below first candle's open. Confirms Bearish Harami."""
if idx < 2: return False
f, s, t = df.iloc[idx-2], df.iloc[idx-1], df.iloc[idx]
if f['BODY_SIGN'] != 1 or f['BODY_RATIO'] < cfg.get('long_candle_ratio', 0.7) * 0.8: return False
if s['BODY_SIGN'] != -1: return False
if s['BODY_RATIO'] > cfg.get('small_candle_ratio', 0.35) + 0.15: return False
if max(s['OPEN'], s['CLOSE']) > max(f['OPEN'], f['CLOSE']): return False
if min(s['OPEN'], s['CLOSE']) < min(f['OPEN'], f['CLOSE']): return False
if t['BODY_SIGN'] != -1: return False
if t['CLOSE'] >= f['OPEN']: return False
return True
def fb_detect_three_outside_up(df, idx, cfg=None):
"""Three Outside Up: bearish candle → bullish engulfing → another bullish
closing higher. Confirms Bullish Engulfing."""
if idx < 2: return False
f, s, t = df.iloc[idx-2], df.iloc[idx-1], df.iloc[idx]
# First: bearish
if f['BODY_SIGN'] != -1: return False
# Second: bullish engulfing
if s['BODY_SIGN'] != 1: return False
if s['OPEN'] > f['CLOSE'] or s['CLOSE'] < f['OPEN']: return False
# Third: bullish closing higher than second
if t['BODY_SIGN'] != 1: return False
if t['CLOSE'] <= s['CLOSE']: return False
return True
def fb_detect_three_outside_down(df, idx, cfg=None):
"""Three Outside Down: bullish candle → bearish engulfing → another bearish
closing lower. Confirms Bearish Engulfing."""
if idx < 2: return False
f, s, t = df.iloc[idx-2], df.iloc[idx-1], df.iloc[idx]
if f['BODY_SIGN'] != 1: return False
if s['BODY_SIGN'] != -1: return False
if s['OPEN'] < f['CLOSE'] or s['CLOSE'] > f['OPEN']: return False
if t['BODY_SIGN'] != -1: return False
if t['CLOSE'] >= s['CLOSE']: return False
return True
def fb_detect_bullish_abandoned_baby(df, idx, cfg=None):
"""Bullish Abandoned Baby: long bearish → Doji (gap down) → long bullish (gap up).
Rare, strong Bullish Reversal."""
if idx < 2: return False
f, s, t = df.iloc[idx-2], df.iloc[idx-1], df.iloc[idx]
if f['BODY_SIGN'] != -1 or f['BODY_RATIO'] < cfg.get('long_candle_ratio', 0.7): return False
if s['BODY_RATIO'] > cfg.get('doji_body_ratio', 0.1) * 2: return False # Doji
if t['BODY_SIGN'] != 1 or t['BODY_RATIO'] < cfg.get('long_candle_ratio', 0.7) * 0.6: return False
gap_tol = cfg.get('gap_tolerance_pips', 2) * cfg.get('pip_divisor', 0.0001)
# Doji gaps down from first (doji high < first low)
if s['HIGH'] > f['LOW'] + gap_tol: return False
# Third gaps up from doji (third low > doji high)
if t['LOW'] < s['HIGH'] + gap_tol: return False
return True
def fb_detect_bearish_abandoned_baby(df, idx, cfg=None):
"""Bearish Abandoned Baby: long bullish → Doji (gap up) → long bearish (gap down).
Rare, strong Bearish Reversal."""
if idx < 2: return False
f, s, t = df.iloc[idx-2], df.iloc[idx-1], df.iloc[idx]
if f['BODY_SIGN'] != 1 or f['BODY_RATIO'] < cfg.get('long_candle_ratio', 0.7): return False
if s['BODY_RATIO'] > cfg.get('doji_body_ratio', 0.1) * 2: return False
if t['BODY_SIGN'] != -1 or t['BODY_RATIO'] < cfg.get('long_candle_ratio', 0.7) * 0.6: return False
gap_tol = cfg.get('gap_tolerance_pips', 2) * cfg.get('pip_divisor', 0.0001)
# Doji gaps up from first (doji low > first high)
if s['LOW'] < f['HIGH'] - gap_tol: return False
# Third gaps down from doji (third high < doji low)
if t['HIGH'] > s['LOW'] - gap_tol: return False
return True
def fb_detect_upside_gap_two_crows(df, idx, cfg=None):
"""Upside Gap Two Crows: long bullish → small bearish gap up →
larger bearish that engulfs the second but closes below first's close.
Bearish Reversal."""
if idx < 2: return False
f, s, t = df.iloc[idx-2], df.iloc[idx-1], df.iloc[idx]
if f['BODY_SIGN'] != 1 or f['BODY_RATIO'] < cfg.get('long_candle_ratio', 0.7): return False
if s['BODY_SIGN'] != -1: return False # Second: small bearish
gap_tol = cfg.get('gap_tolerance_pips', 2) * cfg.get('pip_divisor', 0.0001)
if s['LOW'] < f['HIGH'] - gap_tol: return False # Must gap up
if t['BODY_SIGN'] != -1: return False # Third: bearish
if t['CLOSE'] >= s['CLOSE']: return False # Must close below second's close
if t['OPEN'] >= s['OPEN']: return False # Must open above second's open (engulf body)
if t['CLOSE'] >= f['CLOSE']: return False # Must close below first's close
return True
# ── NEW FOUR-CANDLE PATTERNS ─────────────────────────────────────────
def fb_detect_bullish_three_line_strike(df, idx, cfg=None):
"""Bullish Three-Line Strike: three consecutive bullish candles followed by
a long bearish candle that opens above third's close and closes below
first candle's open. Bullish Continuation (pullback before resumption)."""
if idx < 3: return False
c1, c2, c3, c4 = df.iloc[idx-3], df.iloc[idx-2], df.iloc[idx-1], df.iloc[idx]
# First three: consecutive bullish with progressive closes
if c1['BODY_SIGN'] != 1 or c2['BODY_SIGN'] != 1 or c3['BODY_SIGN'] != 1: return False
if c1['BODY_RATIO'] < 0.4 or c2['BODY_RATIO'] < 0.4 or c3['BODY_RATIO'] < 0.4: return False
if not (c3['CLOSE'] > c2['CLOSE'] > c1['CLOSE']): return False
# Fourth: long bearish opening above third, closing below first
if c4['BODY_SIGN'] != -1: return False
if c4['OPEN'] < c3['CLOSE']: return False
if c4['CLOSE'] > c1['OPEN']: return False
return True
def fb_detect_bearish_three_line_strike(df, idx, cfg=None):
"""Bearish Three-Line Strike: three consecutive bearish candles followed by
a long bullish candle that opens below third's close and closes above
first candle's open. Bearish Continuation."""
if idx < 3: return False
c1, c2, c3, c4 = df.iloc[idx-3], df.iloc[idx-2], df.iloc[idx-1], df.iloc[idx]
if c1['BODY_SIGN'] != -1 or c2['BODY_SIGN'] != -1 or c3['BODY_SIGN'] != -1: return False
if c1['BODY_RATIO'] < 0.4 or c2['BODY_RATIO'] < 0.4 or c3['BODY_RATIO'] < 0.4: return False
if not (c3['CLOSE'] < c2['CLOSE'] < c1['CLOSE']): return False
if c4['BODY_SIGN'] != 1: return False
if c4['OPEN'] > c3['CLOSE']: return False
if c4['CLOSE'] < c1['OPEN']: return False
return True
def fb_detect_concealing_baby_swallow(df, idx, cfg=None):
"""Concealing Baby Swallow: two long bearish → gap down with a small-bodied candle →
another long bearish that engulfs the small candle. Bullish Reversal.
Very rare pattern."""
if idx < 3: return False
c1, c2, c3, c4 = df.iloc[idx-3], df.iloc[idx-2], df.iloc[idx-1], df.iloc[idx]
# First two: long bearish
if c1['BODY_SIGN'] != -1 or c2['BODY_SIGN'] != -1: return False
if c1['BODY_RATIO'] < 0.5 or c2['BODY_RATIO'] < 0.5: return False
# Third: small body that gaps down
if c3['BODY_RATIO'] > cfg.get('small_candle_ratio', 0.35) + 0.1: return False
gap_tol = cfg.get('gap_tolerance_pips', 2) * cfg.get('pip_divisor', 0.0001)
if c3['HIGH'] > min(c1['LOW'], c2['LOW']) + gap_tol: return False # Must gap down
# Fourth: long bearish engulfing the third
if c4['BODY_SIGN'] != -1: return False
if c4['BODY_RATIO'] < 0.5: return False
if c4['HIGH'] < c3['HIGH'] or c4['LOW'] > c3['LOW']: return False # Engulfs third
return True
# ── NEW FIVE-CANDLE PATTERNS ─────────────────────────────────────────
def fb_detect_mat_hold_bullish(df, idx, cfg=None):
"""Mat Hold (Bullish): long bullish → small bearish candles that gap up
and stay within first candle's range → another long bullish closing
above first candle's close. Bullish Continuation (stronger than Rising Three)."""
if cfg is None: cfg = CFG
if idx < 4: return False
first = df.iloc[idx-4]; fifth = df.iloc[idx]
if first['BODY_SIGN'] != 1 or first['BODY_RATIO'] < cfg.get('long_candle_ratio', 0.7): return False
gap_tol = cfg.get('gap_tolerance_pips', 2) * cfg.get('pip_divisor', 0.0001)
for i in range(1, 4):
c = df.iloc[idx-4+i]
if c['BODY_RATIO'] > cfg.get('small_candle_ratio', 0.35) + 0.15: return False
if c['HIGH'] > first['HIGH'] or c['LOW'] < first['LOW']: return False
# Mat Hold: second candle must gap up from first
if i == 1 and c['LOW'] < first['CLOSE'] - gap_tol: return False
if fifth['BODY_SIGN'] != 1 or fifth['BODY_RATIO'] < cfg.get('long_candle_ratio', 0.7) * 0.7: return False
if fifth['CLOSE'] <= first['CLOSE']: return False
return True
def fb_detect_mat_hold_bearish(df, idx, cfg=None):
"""Mat Hold (Bearish): long bearish → small bullish candles that gap down
and stay within first candle's range → another long bearish closing
below first candle's close. Bearish Continuation."""
if cfg is None: cfg = CFG
if idx < 4: return False
first = df.iloc[idx-4]; fifth = df.iloc[idx]
if first['BODY_SIGN'] != -1 or first['BODY_RATIO'] < cfg.get('long_candle_ratio', 0.7): return False
gap_tol = cfg.get('gap_tolerance_pips', 2) * cfg.get('pip_divisor', 0.0001)
for i in range(1, 4):
c = df.iloc[idx-4+i]
if c['BODY_RATIO'] > cfg.get('small_candle_ratio', 0.35) + 0.15: return False
if c['HIGH'] > first['HIGH'] or c['LOW'] < first['LOW']: return False
if i == 1 and c['HIGH'] > first['CLOSE'] + gap_tol: return False
if fifth['BODY_SIGN'] != -1 or fifth['BODY_RATIO'] < cfg.get('long_candle_ratio', 0.7) * 0.7: return False
if fifth['CLOSE'] >= first['CLOSE']: return False
return True
def fb_detect_ladder_bottom(df, idx, cfg=None):
"""Ladder Bottom: three consecutive long bearish candles → small bearish/bullish
candle → long bullish candle. Bullish Reversal (buying pressure taking over)."""
if cfg is None: cfg = CFG
if idx < 4: return False
c1, c2, c3, c4, c5 = df.iloc[idx-4], df.iloc[idx-3], df.iloc[idx-2], df.iloc[idx-1], df.iloc[idx]
# First three: long bearish with progressive lower closes
if c1['BODY_SIGN'] != -1 or c2['BODY_SIGN'] != -1 or c3['BODY_SIGN'] != -1: return False
if c1['BODY_RATIO'] < 0.4 or c2['BODY_RATIO'] < 0.4 or c3['BODY_RATIO'] < 0.4: return False
if not (c3['CLOSE'] < c2['CLOSE'] < c1['CLOSE']): return False
# Fourth: small-bodied candle
if c4['BODY_RATIO'] > cfg.get('small_candle_ratio', 0.35) + 0.15: return False
# Fifth: long bullish
if c5['BODY_SIGN'] != 1: return False
if c5['BODY_RATIO'] < cfg.get('long_candle_ratio', 0.7) * 0.6: return False
return True
# ── THESTRAT PATTERN DETECTION ────────────────────────────────────────
def thestrat_classify_candle(df, idx, cfg=None):
"""Classify a candle using TheStrat 1-2-3 system.
Returns:
'1' — Inside bar: current high ≤ previous high AND current low ≥ previous low
'2↑' — Directional up: current high > previous high AND current low ≥ previous low
'2↓' — Directional down: current low < previous low AND current high ≤ previous high
'3' — Outside bar: current high > previous high AND current low < previous low
None — If idx < 1 or data invalid
"""
if idx < 1: return None
c = df.iloc[idx]; p = df.iloc[idx-1]
broke_high = c['HIGH'] > p['HIGH']
broke_low = c['LOW'] < p['LOW']
if broke_high and broke_low:
return '3' # Outside bar
elif broke_high and not broke_low:
return '2↑' # Directional up
elif broke_low and not broke_high:
return '2↓' # Directional down
else:
return '1' # Inside bar
def fb_detect_thestrat_22(df, idx, cfg=None):
"""TheStrat 2-2 Pattern: Two consecutive Type 2 candles.
Can be continuation (same direction) or reversal (opposite direction).
Returns dict with Pattern/Category/Direction or None.
"""
if idx < 2: return None
t1 = thestrat_classify_candle(df, idx-1, cfg)
t2 = thestrat_classify_candle(df, idx, cfg)
if t1 not in ('2↑', '2↓') or t2 not in ('2↑', '2↓'): return None
if t1 == t2:
# Same direction → continuation
d = 'Bullish' if t2 == '2↑' else 'Bearish'
return {'Pattern': 'TheStrat 2-2', 'Category': f'{d} Continuation', 'Direction': d, 'Candles': 2}
else:
# Opposite direction → reversal
d = 'Bullish' if t2 == '2↑' else 'Bearish'
return {'Pattern': 'TheStrat 2-2', 'Category': f'{d} Reversal', 'Direction': d, 'Candles': 2}
def fb_detect_thestrat_312(df, idx, cfg=None):
"""TheStrat 3-1-2 Reversal: Type 3 (outside) → Type 1 (inside) → Type 2 (directional breakout).
Direction is set by the final Type 2 candle's direction."""
if idx < 3: return None
t3 = thestrat_classify_candle(df, idx-2, cfg) # First candle: Type 3
t1 = thestrat_classify_candle(df, idx-1, cfg) # Second: Type 1
t2 = thestrat_classify_candle(df, idx, cfg) # Third: Type 2
if t3 != '3' or t1 != '1' or t2 not in ('2↑', '2↓'): return None
d = 'Bullish' if t2 == '2↑' else 'Bearish'
return {'Pattern': 'TheStrat 3-1-2', 'Category': f'{d} Reversal', 'Direction': d, 'Candles': 3}
def fb_detect_thestrat_212(df, idx, cfg=None):
"""TheStrat 2-1-2 Reversal: Type 2 (directional) → Type 1 (inside) → Type 2 (directional breakout).
Direction is set by the final Type 2 candle's direction."""
if idx < 3: return None
t2a = thestrat_classify_candle(df, idx-2, cfg)
t1 = thestrat_classify_candle(df, idx-1, cfg)
t2b = thestrat_classify_candle(df, idx, cfg)
if t2a not in ('2↑', '2↓') or t1 != '1' or t2b not in ('2↑', '2↓'): return None
d = 'Bullish' if t2b == '2↑' else 'Bearish'
return {'Pattern': 'TheStrat 2-1-2', 'Category': f'{d} Reversal', 'Direction': d, 'Candles': 3}
def fb_detect_thestrat_122_rev(df, idx, cfg=None):
"""TheStrat 1-2-2 Rev: Type 1 (inside) → Type 2 (breakout) → Type 2 (reversal).
The third candle must be in the opposite direction from the second.
Direction is set by the third candle."""
if idx < 3: return None
t1 = thestrat_classify_candle(df, idx-2, cfg)
t2a = thestrat_classify_candle(df, idx-1, cfg)
t2b = thestrat_classify_candle(df, idx, cfg)
if t1 != '1' or t2a not in ('2↑', '2↓') or t2b not in ('2↑', '2↓'): return None
# Must be opposite directions for reversal
if t2a == t2b: return None
d = 'Bullish' if t2b == '2↑' else 'Bearish'
return {'Pattern': 'TheStrat 1-2-2 Rev', 'Category': f'{d} Reversal', 'Direction': d, 'Candles': 3}
def fb_detect_thestrat_13_rev(df, idx, cfg=None):
"""TheStrat 1-3 Rev: Type 1 (inside) → Type 3 (outside/broadening).
A broadening formation suggests reversal potential.
Direction is inferred from where price goes after the outside bar.
We use the outside bar's body direction as a hint."""
if idx < 2: return None
t1 = thestrat_classify_candle(df, idx-1, cfg)
t3 = thestrat_classify_candle(df, idx, cfg)
if t1 != '1' or t3 != '3': return None
c = df.iloc[idx]
# Use body direction as a hint for the likely direction
d = 'Bullish' if c['CLOSE'] >= c['OPEN'] else 'Bearish'
return {'Pattern': 'TheStrat 1-3 Rev', 'Category': f'{d} Reversal', 'Direction': d, 'Candles': 2}
def fb_detect_rising_three_methods(df, idx, cfg=None):
if cfg is None: cfg = CFG
if idx < 4: return False
@@ -2739,13 +3268,20 @@ def fb_detect_all_patterns(df, cfg=None, d1_df=None, tf_label='H4', htf_atr_df=N
row = df.iloc[idx]
found = []
# ── Single-candle: Neutral ──
if fb_detect_doji(df, idx, cfg):
found.append({'Pattern': 'Doji', 'Category': 'Neutral', 'Direction': 'Neutral', 'Candles': 1})
if fb_detect_dragonfly_doji(df, idx, cfg):
found.append({'Pattern': 'Dragonfly Doji', 'Category': 'Bullish Reversal', 'Direction': 'Bullish', 'Candles': 1})
if fb_detect_gravestone_doji(df, idx, cfg):
found.append({'Pattern': 'Gravestone Doji', 'Category': 'Bearish Reversal', 'Direction': 'Bearish', 'Candles': 1})
if fb_detect_spinning_top(df, idx, cfg):
found.append({'Pattern': 'Spinning Top', 'Category': 'Neutral', 'Direction': 'Neutral', 'Candles': 1})
if fb_detect_marubozu(df, idx, cfg):
d = 'Bullish' if row['BODY_SIGN'] == 1 else 'Bearish'
found.append({'Pattern': f'Marubozu ({d})', 'Category': f'{d} Continuation', 'Direction': d, 'Candles': 1})
# ── Single-candle: Directional (trend-context required) ──
if fb_detect_hammer(df, idx, cfg):
found.append({'Pattern': 'Hammer', 'Category': 'Bullish Reversal', 'Direction': 'Bullish', 'Candles': 1})
if fb_detect_inverted_hammer(df, idx, cfg):
@@ -2754,7 +3290,12 @@ def fb_detect_all_patterns(df, cfg=None, d1_df=None, tf_label='H4', htf_atr_df=N
found.append({'Pattern': 'Shooting Star', 'Category': 'Bearish Reversal', 'Direction': 'Bearish', 'Candles': 1})
if fb_detect_hanging_man(df, idx, cfg):
found.append({'Pattern': 'Hanging Man', 'Category': 'Bearish Reversal', 'Direction': 'Bearish', 'Candles': 1})
if fb_detect_bullish_belt_hold(df, idx, cfg):
found.append({'Pattern': 'Bullish Belt Hold', 'Category': 'Bullish Reversal', 'Direction': 'Bullish', 'Candles': 1})
if fb_detect_bearish_belt_hold(df, idx, cfg):
found.append({'Pattern': 'Bearish Belt Hold', 'Category': 'Bearish Reversal', 'Direction': 'Bearish', 'Candles': 1})
# ── Two-candle patterns ──
eng = fb_detect_engulfing(df, idx, cfg)
if eng:
d = 'Bullish' if 'Bullish' in eng else 'Bearish'
@@ -2775,6 +3316,26 @@ def fb_detect_all_patterns(df, cfg=None, d1_df=None, tf_label='H4', htf_atr_df=N
d = 'Bearish' if 'Tops' in tw else 'Bullish'
found.append({'Pattern': tw, 'Category': f'{d} Reversal', 'Direction': d, 'Candles': 2})
if fb_detect_piercing_line(df, idx, cfg):
found.append({'Pattern': 'Piercing Line', 'Category': 'Bullish Reversal', 'Direction': 'Bullish', 'Candles': 2})
if fb_detect_dark_cloud_cover(df, idx, cfg):
found.append({'Pattern': 'Dark Cloud Cover', 'Category': 'Bearish Reversal', 'Direction': 'Bearish', 'Candles': 2})
if fb_detect_bullish_kicker(df, idx, cfg):
found.append({'Pattern': 'Bullish Kicker', 'Category': 'Bullish Reversal', 'Direction': 'Bullish', 'Candles': 2})
if fb_detect_bearish_kicker(df, idx, cfg):
found.append({'Pattern': 'Bearish Kicker', 'Category': 'Bearish Reversal', 'Direction': 'Bearish', 'Candles': 2})
if fb_detect_meeting_lines_bullish(df, idx, cfg):
found.append({'Pattern': 'Meeting Lines (Bullish)', 'Category': 'Bullish Reversal', 'Direction': 'Bullish', 'Candles': 2})
if fb_detect_meeting_lines_bearish(df, idx, cfg):
found.append({'Pattern': 'Meeting Lines (Bearish)', 'Category': 'Bearish Reversal', 'Direction': 'Bearish', 'Candles': 2})
if fb_detect_bullish_separating_lines(df, idx, cfg):
found.append({'Pattern': 'Bullish Separating Lines', 'Category': 'Bullish Continuation', 'Direction': 'Bullish', 'Candles': 2})
if fb_detect_bearish_separating_lines(df, idx, cfg):
found.append({'Pattern': 'Bearish Separating Lines', 'Category': 'Bearish Continuation', 'Direction': 'Bearish', 'Candles': 2})
if fb_detect_bearish_doji_star(df, idx, cfg):
found.append({'Pattern': 'Bearish Doji Star', 'Category': 'Bearish Reversal', 'Direction': 'Bearish', 'Candles': 2})
# ── Three-candle patterns ──
if fb_detect_morning_star(df, idx, cfg):
found.append({'Pattern': 'Morning Star', 'Category': 'Bullish Reversal', 'Direction': 'Bullish', 'Candles': 3})
if fb_detect_evening_star(df, idx, cfg):
@@ -2783,10 +3344,58 @@ def fb_detect_all_patterns(df, cfg=None, d1_df=None, tf_label='H4', htf_atr_df=N
found.append({'Pattern': 'Three White Soldiers', 'Category': 'Bullish Reversal', 'Direction': 'Bullish', 'Candles': 3})
if fb_detect_three_black_crows(df, idx, cfg):
found.append({'Pattern': 'Three Black Crows', 'Category': 'Bearish Reversal', 'Direction': 'Bearish', 'Candles': 3})
if fb_detect_three_inside_up(df, idx, cfg):
found.append({'Pattern': 'Three Inside Up', 'Category': 'Bullish Reversal', 'Direction': 'Bullish', 'Candles': 3})
if fb_detect_three_inside_down(df, idx, cfg):
found.append({'Pattern': 'Three Inside Down', 'Category': 'Bearish Reversal', 'Direction': 'Bearish', 'Candles': 3})
if fb_detect_three_outside_up(df, idx, cfg):
found.append({'Pattern': 'Three Outside Up', 'Category': 'Bullish Reversal', 'Direction': 'Bullish', 'Candles': 3})
if fb_detect_three_outside_down(df, idx, cfg):
found.append({'Pattern': 'Three Outside Down', 'Category': 'Bearish Reversal', 'Direction': 'Bearish', 'Candles': 3})
if fb_detect_bullish_abandoned_baby(df, idx, cfg):
found.append({'Pattern': 'Bullish Abandoned Baby', 'Category': 'Bullish Reversal', 'Direction': 'Bullish', 'Candles': 3})
if fb_detect_bearish_abandoned_baby(df, idx, cfg):
found.append({'Pattern': 'Bearish Abandoned Baby', 'Category': 'Bearish Reversal', 'Direction': 'Bearish', 'Candles': 3})
if fb_detect_upside_gap_two_crows(df, idx, cfg):
found.append({'Pattern': 'Upside Gap Two Crows', 'Category': 'Bearish Reversal', 'Direction': 'Bearish', 'Candles': 3})
# ── Four-candle patterns ──
if fb_detect_bullish_three_line_strike(df, idx, cfg):
found.append({'Pattern': 'Bullish Three-Line Strike', 'Category': 'Bullish Continuation', 'Direction': 'Bullish', 'Candles': 4})
if fb_detect_bearish_three_line_strike(df, idx, cfg):
found.append({'Pattern': 'Bearish Three-Line Strike', 'Category': 'Bearish Continuation', 'Direction': 'Bearish', 'Candles': 4})
if fb_detect_concealing_baby_swallow(df, idx, cfg):
found.append({'Pattern': 'Concealing Baby Swallow', 'Category': 'Bullish Reversal', 'Direction': 'Bullish', 'Candles': 4})
# ── Five-candle patterns ──
if fb_detect_rising_three_methods(df, idx, cfg):
found.append({'Pattern': 'Rising Three Methods', 'Category': 'Bullish Continuation', 'Direction': 'Bullish', 'Candles': 5})
if fb_detect_falling_three_methods(df, idx, cfg):
found.append({'Pattern': 'Falling Three Methods', 'Category': 'Bearish Continuation', 'Direction': 'Bearish', 'Candles': 5})
if fb_detect_mat_hold_bullish(df, idx, cfg):
found.append({'Pattern': 'Mat Hold (Bullish)', 'Category': 'Bullish Continuation', 'Direction': 'Bullish', 'Candles': 5})
if fb_detect_mat_hold_bearish(df, idx, cfg):
found.append({'Pattern': 'Mat Hold (Bearish)', 'Category': 'Bearish Continuation', 'Direction': 'Bearish', 'Candles': 5})
if fb_detect_ladder_bottom(df, idx, cfg):
found.append({'Pattern': 'Ladder Bottom', 'Category': 'Bullish Reversal', 'Direction': 'Bullish', 'Candles': 5})
# ── TheStrat composite patterns ──
if cfg.get('thestrat_enabled', True):
ts22 = fb_detect_thestrat_22(df, idx, cfg)
if ts22:
found.append(ts22)
ts312 = fb_detect_thestrat_312(df, idx, cfg)
if ts312:
found.append(ts312)
ts212 = fb_detect_thestrat_212(df, idx, cfg)
if ts212:
found.append(ts212)
ts122 = fb_detect_thestrat_122_rev(df, idx, cfg)
if ts122:
found.append(ts122)
ts13 = fb_detect_thestrat_13_rev(df, idx, cfg)
if ts13:
found.append(ts13)
# Deduplicate
if cfg.get('deduplicate_signals', True):
@@ -3506,7 +4115,7 @@ def run_scanner(cfg=None):
log_message(f"Multi-symbol watchlist: {', '.join(watchlist)}", cfg)
log_message(C('cyan', '=' * 70), cfg)
log_message(C('bold', f" {symbol} MULTI-TIMEFRAME PATTERN SCANNER v8 — STARTING"), cfg)
log_message(C('bold', f" {symbol} MULTI-TIMEFRAME PATTERN SCANNER v9 — STARTING"), cfg)
log_message(C('cyan', '=' * 70), cfg)
watchlist_display = ', '.join(cfg.get('watchlist', [symbol]))
if len(cfg.get('watchlist', [])) > 1:
@@ -3842,7 +4451,7 @@ def run_full_backtest(args, cfg=None):
out_dir = args.output
print("=" * 70)
print(f" {symbol} MULTI-TIMEFRAME BACKTESTER v8")
print(f" {symbol} MULTI-TIMEFRAME BACKTESTER v9")
print("=" * 70)
print(f" Timeframes : {', '.join(active_tfs)}")
print(f" From : {args.date_from}")
@@ -4020,6 +4629,7 @@ def run_full_backtest(args, cfg=None):
try:
combined_stats = {'symbol': symbol, 'generated_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'backtest_range': f"{args.date_from} to {args.date_to}",
'broker_utc_offset': cfg.get('broker_utc_offset', 2),
'timeframes': {}}
for tf_label, dets in all_results.items():
if not dets: continue
@@ -4128,27 +4738,27 @@ def run_full_backtest(args, cfg=None):
def parse_args(cfg=None):
if cfg is None: cfg = CFG
p = argparse.ArgumentParser(
description="MT5 Multi-Timeframe Candlestick Pattern Scanner & Backtester v8",
description="MT5 Multi-Timeframe Candlestick Pattern Scanner & Backtester v9",
formatter_class=argparse.RawTextHelpFormatter,
epilog="""
Examples:
# Live scanner — all timeframes
python mt5_multitf_pattern_scanner_v8.py
python mt5_multitf_pattern_scanner.py
# Live scanner — specific timeframes
python mt5_multitf_pattern_scanner_v8.py --timeframes M5 H1 H4
python mt5_multitf_pattern_scanner.py --timeframes M5 H1 H4
# One-shot scan
python mt5_multitf_pattern_scanner_v8.py --mode scan
python mt5_multitf_pattern_scanner.py --mode scan
# Quick backtest (500 bars) on H4 only
python mt5_multitf_pattern_scanner_v8.py --mode backtest --bars 500 --timeframes H4
python mt5_multitf_pattern_scanner.py --mode backtest --bars 500 --timeframes H4
# Full backtest on all TFs, 2024 full year
python mt5_multitf_pattern_scanner_v8.py --mode fullbacktest --from 2024-01-01 --to 2024-12-31
python mt5_multitf_pattern_scanner.py --mode fullbacktest --from 2024-01-01 --to 2024-12-31
# Full backtest with filters on H4 only
python mt5_multitf_pattern_scanner_v8.py --mode fullbacktest --timeframes H4 \\
python mt5_multitf_pattern_scanner.py --mode fullbacktest --timeframes H4 \\
--d1-trend-filter --volume-filter --forward 15
"""
)