Download src/transformations/wavelets_lite/interpreter.py from ResearchEngineering/financial_news_bot: direct link, hf CLI and curl.
- Browser
- Download file 10.6 kB
-
https://huggingface.co/spaces/ResearchEngineering/financial_news_bot/resolve/main/src/transformations/wavelets_lite/interpreter.py
- Command line
-
hf download hf://spaces/ResearchEngineering/financial_news_bot/src/transformations/wavelets_lite/interpreter.py
-
curl -L -o interpreter.py https://huggingface.co/spaces/ResearchEngineering/financial_news_bot/resolve/main/src/transformations/wavelets_lite/interpreter.py
10.6 kB
| """ | |
| Frequency component interpreter for wavelets_lite. | |
| Takes a LiteSignal (or raw MODWT arrays) and maps the zone alignment into one | |
| of 12 named market patterns, a conviction score, and an action bias. | |
| Zones: | |
| Zone 1 (noise): D1, D2, D3 | |
| Zone 2 (signal): sig_levels (default D4, D5) | |
| Zone 3 (trend): D6, A6 | |
| Public API: | |
| interpret_signal(sig: LiteSignal) -> FrequencyInterpretation | |
| interpret_signal_from_arrays(details, approx, timeframe, sig_levels) -> FrequencyInterpretation | |
| format_interpretation(interp: FrequencyInterpretation) -> str | |
| """ | |
| from __future__ import annotations | |
| from dataclasses import dataclass | |
| import numpy as np | |
| from .analyzer import LiteSignal | |
| from .core import linear_slope, midband as _midband, safe_slope_window | |
| # ββ Pattern registry βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| _PATTERN_DESC: dict[str, str] = { | |
| "FULL_BULL": "All time scales trending up β strongest bull alignment", | |
| "FULL_BEAR": "All time scales trending down β strongest bear alignment", | |
| "BULL_WITH_NOISE_HEADWIND": "Pullback within a multi-scale uptrend β best long entry", | |
| "BEAR_WITH_NOISE_TAILWIND": "Bounce within a multi-scale downtrend β best short entry", | |
| "COUNTER_TREND_RALLY": "Intermediate rally against a structural downtrend β caution on longs", | |
| "PULLBACK_IN_UPTREND": "Intermediate correction within a structural uptrend β dip-buy candidate", | |
| "ACCELERATION": "Momentum building: noise and signal aligned, structural trend turning up", | |
| "TREND_EXHAUSTION": "Mid-band energy fading while structural uptrend still holds", | |
| "BEAR_EXHAUSTION": "Downtrend losing mid-band energy β potential base forming", | |
| "TRANSITION": "Mid-band stalled, short-term bounce against structural downtrend", | |
| "NOISE_REGIME": "No mid-band or structural trend β sideways consolidation", | |
| "STRUCTURAL_DIVERGENCE": "Intermediate trend up but semi-annual component weakening", | |
| "UNKNOWN": "Zone combination does not match any known pattern", | |
| } | |
| _PATTERN_ACTION: dict[str, str] = { | |
| "FULL_BULL": "FULL_SIZE", | |
| "FULL_BEAR": "FULL_SIZE", | |
| "BULL_WITH_NOISE_HEADWIND": "FULL_SIZE", | |
| "BEAR_WITH_NOISE_TAILWIND": "FULL_SIZE", | |
| "COUNTER_TREND_RALLY": "HALF_SIZE", | |
| "PULLBACK_IN_UPTREND": "HALF_SIZE", | |
| "ACCELERATION": "FULL_SIZE", | |
| "TREND_EXHAUSTION": "FADE", | |
| "BEAR_EXHAUSTION": "FADE", | |
| "TRANSITION": "PASS", | |
| "NOISE_REGIME": "PASS", | |
| "STRUCTURAL_DIVERGENCE": "HALF_SIZE", | |
| "UNKNOWN": "PASS", | |
| } | |
| _ZONE1_LABELS = ["D1", "D2", "D3"] | |
| _ZONE3_LABELS = ["D6", "A6"] | |
| _ZONE_WEIGHTS = (0.15, 0.35, 0.50) # Z1, Z2, Z3 | |
| # ββ Dataclass ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class FrequencyInterpretation: | |
| """Result of a frequency-zone pattern analysis. | |
| Attributes: | |
| pattern: Named market pattern (one of 12 + UNKNOWN). | |
| conviction: 0.0β1.0 weighted zone alignment score. | |
| action_bias: FULL_SIZE / HALF_SIZE / PASS / FADE. | |
| direction: UP / DOWN / FLAT β derived from Zone 2 (mid-band). | |
| description: One-line human-readable description of the pattern. | |
| zone1_vote: Majority direction of D1, D2, D3 (noise zone). | |
| zone2_vote: Direction of the mid-band signal levels (signal zone). | |
| zone3_vote: Majority direction of D6, A6 (trend zone). | |
| """ | |
| pattern: str | |
| conviction: float | |
| action_bias: str | |
| direction: str | |
| description: str | |
| zone1_vote: str | |
| zone2_vote: str | |
| zone3_vote: str | |
| # ββ Internal helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _zone_vote(signals: dict[str, float], labels: list[str]) -> str: | |
| """Majority vote of level_signals over the given labels.""" | |
| values = [signals[lb] for lb in labels if lb in signals] | |
| if not values: | |
| return "FLAT" | |
| up = sum(1 for v in values if v > 0) | |
| down = sum(1 for v in values if v < 0) | |
| flat = len(values) - up - down | |
| if up > down and up > flat: | |
| return "UP" | |
| if down > up and down > flat: | |
| return "DOWN" | |
| return "FLAT" | |
| def _sig_vote(raw_signal: float) -> str: | |
| if raw_signal > 0: | |
| return "UP" | |
| if raw_signal < 0: | |
| return "DOWN" | |
| return "FLAT" | |
| def _detect_pattern(z1: str, z2: str, z3: str) -> str: | |
| """Map (zone1, zone2, zone3) votes to a named pattern.""" | |
| if z2 == "FLAT": | |
| if z3 == "FLAT": | |
| return "NOISE_REGIME" | |
| if z3 == "UP": | |
| return "TREND_EXHAUSTION" | |
| # z3 == "DOWN" | |
| return "TRANSITION" if z1 == "UP" else "BEAR_EXHAUSTION" | |
| if z2 == "UP": | |
| if z3 == "UP": | |
| return "FULL_BULL" if z1 == "UP" else "BULL_WITH_NOISE_HEADWIND" | |
| if z3 == "DOWN": | |
| return "COUNTER_TREND_RALLY" | |
| # z3 == "FLAT" | |
| return "ACCELERATION" if z1 == "UP" else "STRUCTURAL_DIVERGENCE" | |
| # z2 == "DOWN" | |
| if z3 == "DOWN": | |
| return "FULL_BEAR" if z1 == "DOWN" else "BEAR_WITH_NOISE_TAILWIND" | |
| if z3 == "UP": | |
| return "PULLBACK_IN_UPTREND" | |
| # z3 == "FLAT" | |
| return "BEAR_EXHAUSTION" | |
| def _agreement(zone_vote: str, direction: str) -> float: | |
| """Zone agreement score: 1.0 (agrees) / 0.5 (flat) / 0.0 (opposes).""" | |
| if direction == "FLAT" or zone_vote == "FLAT": | |
| return 0.5 | |
| return 1.0 if zone_vote == direction else 0.0 | |
| def _compute_conviction(z1: str, z2: str, z3: str) -> float: | |
| """Weighted zone alignment β Zone 3 (0.50) > Zone 2 (0.35) > Zone 1 (0.15).""" | |
| if z2 == "FLAT": | |
| return 0.0 | |
| direction = "UP" if z2 == "UP" else "DOWN" | |
| return round( | |
| _agreement(z1, direction) * _ZONE_WEIGHTS[0] | |
| + 1.0 * _ZONE_WEIGHTS[1] # Z2 always agrees with itself | |
| + _agreement(z3, direction) * _ZONE_WEIGHTS[2], | |
| 3, | |
| ) | |
| def _build(z1: str, z2: str, z3: str) -> FrequencyInterpretation: | |
| pattern = _detect_pattern(z1, z2, z3) | |
| return FrequencyInterpretation( | |
| pattern = pattern, | |
| conviction = _compute_conviction(z1, z2, z3), | |
| action_bias = _PATTERN_ACTION[pattern], | |
| direction = z2, | |
| description = _PATTERN_DESC[pattern], | |
| zone1_vote = z1, | |
| zone2_vote = z2, | |
| zone3_vote = z3, | |
| ) | |
| # ββ Public API βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def interpret_signal(sig: LiteSignal) -> FrequencyInterpretation: | |
| """Interpret a LiteSignal's frequency zones into a named market pattern. | |
| Uses precomputed level_signals from the LiteSignal β no re-computation. | |
| Args: | |
| sig: Output of WaveletLiteAnalyzer.analyze() or _analyze_sync(). | |
| Returns: | |
| FrequencyInterpretation with pattern, conviction, action_bias, votes. | |
| """ | |
| z1 = _zone_vote(sig.level_signals, _ZONE1_LABELS) | |
| z2 = _sig_vote(sig.raw_signal) | |
| z3 = _zone_vote(sig.level_signals, _ZONE3_LABELS) | |
| return _build(z1, z2, z3) | |
| def interpret_signal_from_arrays( | |
| details: dict[int, np.ndarray], | |
| approx: np.ndarray, | |
| sig_levels: list[int], | |
| slope_window: int = 40, | |
| ) -> FrequencyInterpretation: | |
| """Interpret raw MODWT arrays into a named market pattern. | |
| Use this when you have atrous_swt() output but no LiteSignal. | |
| Args: | |
| details: {j: D_j array} dict from atrous_swt(). | |
| approx: Final approximation array A_N from atrous_swt(). | |
| sig_levels: Detail levels that form the mid-band, e.g. [4, 5]. | |
| slope_window: Bars for OLS slope estimation (default 40). | |
| Returns: | |
| FrequencyInterpretation with pattern, conviction, action_bias, votes. | |
| """ | |
| n = len(approx) | |
| decomp_levels = max(details.keys()) if details else 6 | |
| level_signals: dict[str, float] = {} | |
| for j in sorted(details.keys()): | |
| sw = safe_slope_window(j, n, slope_window) | |
| level_signals[f"D{j}"] = float(np.sign(linear_slope(details[j], sw))) | |
| sw_a = safe_slope_window(decomp_levels, n, slope_window) | |
| level_signals["A6"] = float(np.sign(linear_slope(approx, sw_a))) | |
| mb = _midband(details, sig_levels) | |
| sw_mb = safe_slope_window(max(sig_levels), n, slope_window) | |
| raw = float(np.sign(linear_slope(mb, sw_mb))) | |
| z1 = _zone_vote(level_signals, _ZONE1_LABELS) | |
| z2 = _sig_vote(raw) | |
| z3 = _zone_vote(level_signals, _ZONE3_LABELS) | |
| return _build(z1, z2, z3) | |
| # ββ Formatter ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| _ACTION_EMOJI = { | |
| "FULL_SIZE": "π’", | |
| "HALF_SIZE": "π‘", | |
| "PASS": "β«", | |
| "FADE": "π΄", | |
| } | |
| _DIR_ARROW = {"UP": "β²", "DOWN": "βΌ", "FLAT": "β"} | |
| _ZONE_EMOJI = {"UP": "π’", "DOWN": "π΄", "FLAT": "β«"} | |
| def format_interpretation(interp: FrequencyInterpretation) -> str: | |
| """Render a FrequencyInterpretation as a Telegram HTML string.""" | |
| action_emoji = _ACTION_EMOJI.get(interp.action_bias, "β") | |
| dir_arrow = _DIR_ARROW.get(interp.direction, "β") | |
| z1e = _ZONE_EMOJI.get(interp.zone1_vote, "β«") | |
| z2e = _ZONE_EMOJI.get(interp.zone2_vote, "β«") | |
| z3e = _ZONE_EMOJI.get(interp.zone3_vote, "β«") | |
| lines: list[str] = [ | |
| f"π <b>Pattern:</b> <code>{interp.pattern}</code>", | |
| f" {dir_arrow} <b>{interp.direction}</b> " | |
| f"conviction <b>{interp.conviction:.0%}</b> " | |
| f"{action_emoji} <b>{interp.action_bias}</b>", | |
| f" <i>{interp.description}</i>", | |
| "", | |
| "<code>Zone 1 (noise) D1βD3 </code>" + f"{z1e} {interp.zone1_vote}", | |
| "<code>Zone 2 (signal) D4βD5 </code>" + f"{z2e} {interp.zone2_vote}", | |
| "<code>Zone 3 (trend) D6+A6 </code>" + f"{z3e} {interp.zone3_vote}", | |
| ] | |
| return "\n".join(lines) | |