| """Telegram HTML formatter for WaveletAnalysis output.""" |
| from __future__ import annotations |
| import pandas as pd |
| from .analyzer import WaveletAnalysis, SignalSnapshot |
| from .stats import Stats |
|
|
| _SIGNAL_EMOJI = {1.0: "π’ LONG", -1.0: "π΄ SHORT", 0.0: "β« FLAT"} |
| _SIGNAL_LABEL = {1.0: "UP-TREND", -1.0: "DOWN-TREND", 0.0: "NO SIGNAL"} |
|
|
|
|
| def _fmt_pct(v: float) -> str: |
| return f"{v:+.2f}%" |
|
|
|
|
| def _fmt_price(p: float) -> str: |
| if p >= 1000: |
| return f"{p:,.2f}" |
| if p >= 10: |
| return f"{p:.2f}" |
| if p >= 1: |
| return f"{p:.4f}" |
| return f"{p:.6f}" |
|
|
|
|
| def _bar(value: float, min_v: float, max_v: float, width: int = 10) -> str: |
| """ASCII progress bar scaled between min_v and max_v.""" |
| rng = max_v - min_v |
| if rng == 0: |
| filled = width // 2 |
| else: |
| filled = int((value - min_v) / rng * width) |
| filled = max(0, min(filled, width)) |
| return "β" * filled + "β" * (width - filled) |
|
|
|
|
| def _stats_block(s: Stats) -> str: |
| lines = [] |
| lines.append(f"<b>{s.name}</b>") |
| lines.append( |
| f" CAGR: <b>{s.cagr:+.2%}</b> | Vol: {s.volatility:.2%} | " |
| f"Sharpe: <b>{s.sharpe:.3f}</b> Sortino: {s.sortino:.3f}" |
| ) |
| lines.append( |
| f" Max DD: <b>{s.max_drawdown:.2%}</b> | Calmar: {s.calmar:.3f}" |
| ) |
| lines.append( |
| f" Hit Rate: {s.hit_rate:.1%} | Turnover: {s.annual_turnover:.1f}Γ/yr | " |
| f"In Market: {s.time_in_market:.1%}" |
| ) |
| lines.append( |
| f" Avg Leverage: {s.avg_gross_leverage:.2f}Γ | " |
| f"Corr to SPY: {'β' if s.correlation_to_spy is None else f'{s.correlation_to_spy:.2f}'}" |
| ) |
| return "\n".join(lines) |
|
|
|
|
| def _comparison_table(strat: Stats, bh: Stats, sma: Stats) -> str: |
| rows = [ |
| ("CAGR", f"{strat.cagr:+.2%}", f"{bh.cagr:+.2%}", f"{sma.cagr:+.2%}"), |
| ("Vol", f"{strat.volatility:.2%}", f"{bh.volatility:.2%}", f"{sma.volatility:.2%}"), |
| ("Sharpe", f"{strat.sharpe:.3f}", f"{bh.sharpe:.3f}", f"{sma.sharpe:.3f}"), |
| ("Sortino", f"{strat.sortino:.3f}", f"{bh.sortino:.3f}", f"{sma.sortino:.3f}"), |
| ("Max DD", f"{strat.max_drawdown:.2%}", f"{bh.max_drawdown:.2%}", f"{sma.max_drawdown:.2%}"), |
| ("Calmar", f"{strat.calmar:.3f}", f"{bh.calmar:.3f}", f"{sma.calmar:.3f}"), |
| ("Turnover", f"{strat.annual_turnover:.1f}Γ", f"{bh.annual_turnover:.1f}Γ", f"{sma.annual_turnover:.1f}Γ"), |
| ("In Mkt", f"{strat.time_in_market:.1%}", f"{bh.time_in_market:.1%}", f"{sma.time_in_market:.1%}"), |
| ("Corr SPY", |
| f"{'β' if strat.correlation_to_spy is None else f'{strat.correlation_to_spy:.2f}'}", |
| "1.00", |
| f"{'β' if sma.correlation_to_spy is None else f'{sma.correlation_to_spy:.2f}'}"), |
| ] |
|
|
| header = f"<code>{'Metric':<10} {'MODWT':>10} {'B&H':>8} {'SMA':>8}</code>" |
| divider = "<code>" + "β" * 40 + "</code>" |
| table_rows = [header, divider] |
| for name, v_strat, v_bh, v_sma in rows: |
| table_rows.append(f"<code>{name:<10} {v_strat:>10} {v_bh:>8} {v_sma:>8}</code>") |
|
|
| return "\n".join(table_rows) |
|
|
|
|
| def format_analysis(a: WaveletAnalysis, compact: bool = False) -> str: |
| sig = a.signal |
| lines: list[str] = [] |
|
|
| |
| signal_emoji = _SIGNAL_EMOJI.get(sig.raw_signal, "β") |
| signal_label = _SIGNAL_LABEL.get(sig.raw_signal, "UNKNOWN") |
|
|
| lines.append( |
| f"π <b>Wavelet Analysis: {a.ticker}</b> | <code>{a.timeframe}</code> | " |
| f"{signal_emoji} <b>{signal_label}</b>" |
| ) |
| lines.append( |
| f"Price: <b>{_fmt_price(sig.current_price)}</b> " |
| f"<i>β± close last bar</i> " |
| f"Vol(60d ann): <b>{sig.realized_vol_ann:.1%}</b>" |
| ) |
| lines.append( |
| f"Bars used: {sig.bars_used} | " |
| f"{sig.last_bar_time.strftime('%Y-%m-%d') if hasattr(sig.last_bar_time, 'strftime') else sig.last_bar_time}" |
| ) |
| lines.append("") |
|
|
| |
| lines.append("π‘ <b>MODWT Mid-Band Signal (D4+D5, 16β64 day cycle)</b>") |
|
|
| slope_direction = "β" if sig.midband_slope > 0 else ("β" if sig.midband_slope < 0 else "β") |
| lines.append( |
| f" Mid-band slope: <b>{sig.midband_slope:.6f}</b> {slope_direction} " |
| f"(last safe value: {sig.midband_last:.6f})" |
| ) |
|
|
| pos_desc = ( |
| f"<b>{sig.sized_position:+.2f}Γ</b> notional" |
| if sig.sized_position != 0 else "<b>FLAT (no position)</b>" |
| ) |
| lines.append(f" Sized position: {pos_desc} (vol-targeted to 10% ann.)") |
|
|
| |
| vol_bar = _bar(sig.realized_vol_ann, 0.05, 0.50) |
| lines.append(f" Realized vol: [{vol_bar}] {sig.realized_vol_ann:.1%}") |
| lines.append("") |
|
|
| |
| if a.strategy_stats is not None and a.bh_stats is not None and a.sma_stats is not None: |
| lines.append("π <b>Walk-Forward Backtest Results</b>") |
| lines.append(f" <i>{a.strategy_stats.years:.1f} years | sym8 wavelet | weekly rebalance | 1bp/side cost</i>") |
| lines.append("") |
|
|
| if not compact: |
| lines.append(_comparison_table(a.strategy_stats, a.bh_stats, a.sma_stats)) |
| lines.append("") |
|
|
| |
| strat = a.strategy_stats |
| bh = a.bh_stats |
| sma = a.sma_stats |
|
|
| lines.append("<b>Key takeaways:</b>") |
|
|
| |
| sharpe_vs_bh = strat.sharpe - bh.sharpe |
| sharpe_vs_sma = strat.sharpe - sma.sharpe |
| lines.append( |
| f" β’ Sharpe <b>{strat.sharpe:.3f}</b> " |
| f"({'β' if sharpe_vs_bh > 0 else 'β'}{abs(sharpe_vs_bh):.3f} vs B&H, " |
| f"{'β' if sharpe_vs_sma > 0 else 'β'}{abs(sharpe_vs_sma):.3f} vs SMA)" |
| ) |
|
|
| |
| dd_reduction = (abs(bh.max_drawdown) - abs(strat.max_drawdown)) / abs(bh.max_drawdown) * 100 |
| lines.append( |
| f" β’ Max DD <b>{strat.max_drawdown:.2%}</b> " |
| f"(vs B&H {bh.max_drawdown:.2%} β " |
| f"{'β' if dd_reduction > 0 else 'β'}{abs(dd_reduction):.0f}% reduction)" |
| ) |
|
|
| |
| if strat.correlation_to_spy is not None: |
| lines.append( |
| f" β’ Corr to SPY: <b>{strat.correlation_to_spy:.2f}</b> " |
| f"<i>(diversification vs pure long)</i>" |
| ) |
|
|
| |
| cagr_diff = strat.cagr - bh.cagr |
| lines.append( |
| f" β’ CAGR <b>{strat.cagr:+.2%}</b> " |
| f"({'β' if abs(cagr_diff) < 0.005 else ('β' if cagr_diff > 0 else 'β')}" |
| f"{abs(cagr_diff):.2%} vs B&H), vol {strat.volatility:.1%} " |
| f"vs {bh.volatility:.1%} B&H" |
| ) |
| lines.append("") |
|
|
| elif not a.strategy_stats: |
| lines.append("<i>β Insufficient history for full backtest β signal snapshot only.</i>") |
| lines.append("") |
|
|
| |
| if not compact: |
| lines.append("π¬ <b>MODWT Scale Reference (sym8, 6 levels)</b>") |
| scale_table = [ |
| ("D1", "2β4d", "microstructure noise"), |
| ("D2", "4β8d", "short reversals"), |
| ("D3", "8β16d", "news-driven swings"), |
| ("D4", "16β32d", "monthly momentum β <b>signal</b>"), |
| ("D5", "32β64d", "quarterly trend β <b>signal</b>"), |
| ("D6", "64β128d", "macro positioning"), |
| ("A6", ">128d", "secular drift"), |
| ] |
| for lvl, period, desc in scale_table: |
| lines.append(f" <code>{lvl}</code> {period:<10} {desc}") |
| lines.append("") |
|
|
| |
| if a.warnings: |
| lines.append("β οΈ <b>Warnings:</b>") |
| for w in a.warnings: |
| lines.append(f" <i>{w}</i>") |
| lines.append("") |
|
|
| lines.append( |
| f"<i>Generated {a.timestamp.strftime('%Y-%m-%d %H:%M UTC')} | " |
| f"MODWT mid-band (D{min(SIG_LEVELS)}+D{max(SIG_LEVELS)}) | " |
| "Not financial advice</i>" |
| ) |
|
|
| return "\n".join(lines) |
|
|
|
|
| |
| from .backtest import SIG_LEVELS |
|
|