File size: 8,909 Bytes
0821f38 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 | """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] = []
# ββ Header ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
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("")
# ββ Signal details ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
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
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("")
# ββ Backtest results ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
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("")
# Highlight key metrics
strat = a.strategy_stats
bh = a.bh_stats
sma = a.sma_stats
lines.append("<b>Key takeaways:</b>")
# Sharpe vs benchmarks
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)"
)
# Drawdown reduction
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)"
)
# Correlation
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 parity
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("")
# ββ Wavelet scale reference ββββββββββββββββββββββββββββββββββββββββββββββββ
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("")
# ββ Warnings ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
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)
# Import after definition to avoid circular reference at module level
from .backtest import SIG_LEVELS
|