Refine signal formatting, simplify indicators, and fix chart layout

This commit is contained in:
Harwinder
2026-05-01 17:11:19 -04:00
parent de96af798c
commit 9f4a720e30
3 changed files with 337 additions and 149 deletions
+108 -54
View File
@@ -13,7 +13,7 @@ from telegram.ext import (
from config import SUPPORTED_PAIRS, SUPPORTED_TIMEFRAMES, TELEGRAM_TOKEN
from charts import generate as generate_chart
from indicators import bollinger, macd, rsi
from signals import HTF_OF, analyze, emoji
from signals import HTF_OF, analyze, ascii_indicator, to_pips
from twelvedata import fetch_ohlc
logging.basicConfig(
@@ -27,14 +27,14 @@ DISCLAIMER = "\n\n_Educational only. Not financial advice._"
WELCOME = (
"*Forex Indicator Bot*\n\n"
"Commands:\n"
"/pairs — list supported pairs/timeframes\n"
"/price <PAIR> — latest close (e.g. /price EURUSD)\n"
"/rsi <PAIR> <TF> — RSI value\n"
"/macd <PAIR> <TF> — MACD line/signal/histogram\n"
"/bb <PAIR> <TF> — Bollinger Bands\n"
"/signal <PAIR> <TF> — multi-indicator weighted verdict with chart\n"
"/chart <PAIR> <TF> — candlestick chart with EMA/BB/RSI\n"
"/help — this message"
"/pairs - list supported pairs/timeframes\n"
"/price <PAIR> - latest close (e.g. /price EURUSD)\n"
"/rsi <PAIR> <TF> - RSI value\n"
"/macd <PAIR> <TF> - MACD line/signal/histogram\n"
"/bb <PAIR> <TF> - Bollinger Bands\n"
"/signal <PAIR> <TF> - multi-indicator weighted verdict with chart\n"
"/chart <PAIR> <TF> - candlestick chart with EMA/BB/RSI\n"
"/help - this message"
) + DISCLAIMER
@@ -128,9 +128,57 @@ async def bb_cmd(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
await _reply(update, f"Error: {e}")
def _format_reading(r) -> str:
sign = "+" if r.contribution > 0 else ""
return f"• {r.name}: {r.value} ({sign}{r.contribution})"
def _fmt_reading(r) -> str:
"""Format one indicator row.
Shows: signal (raw) × weight → contribution
e.g. +1 × 3 → +3 HTF EMA 20/50: strong bullish
This makes it immediately clear that the number after → is
(raw signal strength) × (importance weight), not a raw count.
"""
sign = "+" if r.raw > 0 else ""
raw_str = f"{sign}{r.raw}"
csign = "+" if r.contribution > 0 else ""
contrib = f"{csign}{r.contribution}"
return (
f" `{raw_str:>3} × {r.weight} → {contrib:>3}`"
f" {r.name}: {r.value}"
)
def _score_bar(score: int, max_score: int, width: int = 10) -> str:
"""Return a filled ASCII bar showing directional strength.
e.g. score=+8, max=20 -> '####------'
We use a single-sided bar: filled squares show absolute strength.
"""
filled = round(abs(score) / max_score * width)
bar = "#" * filled + "-" * (width - filled)
return bar
def _score_label(score: int, max_score: int) -> str:
"""Translate numeric score into a plain-English strength phrase."""
pct = abs(score) / max_score * 100
if pct >= 80:
return "very strong"
if pct >= 50:
return "strong"
if pct >= 25:
return "moderate"
return "weak"
def _fmt_group(readings, group: str, header: str) -> list[str]:
rows = [r for r in readings if r.group == group]
if not rows:
return []
return [header] + [_fmt_reading(r) for r in rows]
def _fmt_price(pair: str, value: float) -> str:
"""5 decimal places for FX, 2 for XAU."""
return f"{value:.2f}" if pair == "XAUUSD" else f"{value:.5f}"
async def chart_cmd(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
@@ -138,7 +186,7 @@ async def chart_cmd(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
pair, tf = _parse_pair_tf(ctx.args)
df = await fetch_ohlc(pair, tf)
img = generate_chart(df, pair, tf)
caption = f"*{pair}* {tf} — candlestick · EMA20/50 · BB(20,2) · RSI(14)" + DISCLAIMER
caption = f"*{pair}* {tf} - candlestick | EMA20/50 | BB(20,2) | RSI(14)" + DISCLAIMER
await update.message.reply_photo(img, caption=caption, parse_mode="Markdown")
except Exception as e:
logger.exception("chart_cmd failed")
@@ -154,49 +202,55 @@ async def signal_cmd(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
htf_df = await fetch_ohlc(pair, htf) if htf and htf != tf else None
report = analyze(df, htf_df)
price = report["price"]
atr_val = report["atr"]
verdict = report["verdict"]
ts = report["timestamp"].strftime("%Y-%m-%d %H:%M UTC")
pips = to_pips(pair, atr_val)
sl_dist = 1.5 * report["atr"]
tp_dist = 2.5 * report["atr"]
price = report["price"]
lines = [
f"*Signal* {pair} {tf} (HTF: {htf or 'n/a'})",
f"Price: `{price:.5f}` ATR(14): `{report['atr']:.5f}`",
lines: list[str] = [
f"[SIGNAL] *{pair} {tf}* | HTF: {htf or 'n/a'}",
f"`{_fmt_price(pair, price)}` | "
f"ATR `{_fmt_price(pair, atr_val)}` ({pips} pips) | _{ts}_",
"",
"*Indicators:*",
*[_format_reading(r) for r in report["readings"]],
*_fmt_group(report["readings"], "trend",
f"*-- TREND* (ADX {report['adx_label']}"
+ (" (!) scores halved" if report["adx_weak"] else "") + ")"),
"",
f"Score: `{report['score']:+d} / ±{report['max_score']}`",
f"*Verdict:* {emoji(report['verdict'])} *{report['verdict']}*"
f" (confidence {report['confidence']}%)",
*_fmt_group(report["readings"], "momentum", "*-- MOMENTUM*"),
"",
*_fmt_group(report["readings"], "volatility", "*-- VOLATILITY*"),
"",
# Score line — bar + plain-English label so the number has context
f"`[{_score_bar(report['score'], report['max_score'])}]` "
f"{ascii_indicator(verdict)} *{verdict}*",
f"Score `{report['score']:+d}` of +/-`{report['max_score']}` "
f"({_score_label(report['score'], report['max_score'])} signal, "
f"{report['confidence']}% of max)",
]
if report["verdict"] in ("BUY", "STRONG BUY"):
lines += [
"",
f"_Suggested zones (ATR-based):_",
f"Entry near `{price:.5f}`",
f"Stop ≈ `{price - sl_dist:.5f}` ({sl_dist:.5f} below)",
f"Target ≈ `{price + tp_dist:.5f}` ({tp_dist:.5f} above)",
]
elif report["verdict"] in ("SELL", "STRONG SELL"):
lines += [
"",
f"_Suggested zones (ATR-based):_",
f"Entry near `{price:.5f}`",
f"Stop ≈ `{price + sl_dist:.5f}` ({sl_dist:.5f} above)",
f"Target ≈ `{price - tp_dist:.5f}` ({tp_dist:.5f} below)",
]
if report["context"]:
lines += ["", f"> _{report['context']}_"]
if report["notes"]:
lines += ["", *[f"_{n}_" for n in report["notes"]]]
sl_dist = 1.5 * atr_val
tp_dist = 2.5 * atr_val
if verdict in ("BUY", "STRONG BUY"):
sl = _fmt_price(pair, price - sl_dist)
tp = _fmt_price(pair, price + tp_dist)
lines += [
"",
f"_ATR-based zones:_ SL `{sl}` | TP `{tp}`",
]
elif verdict in ("SELL", "STRONG SELL"):
sl = _fmt_price(pair, price + sl_dist)
tp = _fmt_price(pair, price - tp_dist)
lines += [
"",
f"_ATR-based zones:_ SL `{sl}` | TP `{tp}`",
]
caption = "\n".join(lines) + DISCLAIMER
img = generate_chart(
df, pair, tf,
verdict=report["verdict"],
confidence=report["confidence"],
)
img = generate_chart(df, pair, tf, verdict=verdict, confidence=report["confidence"])
await update.message.reply_photo(img, caption=caption, parse_mode="Markdown")
except Exception as e:
logger.exception("signal_cmd failed")
@@ -207,12 +261,12 @@ BOT_COMMANDS: list[BotCommand] = [
BotCommand("start", "Show welcome and command list"),
BotCommand("help", "Show welcome and command list"),
BotCommand("pairs", "List supported pairs and timeframes"),
BotCommand("price", "Latest close price — /price EURUSD"),
BotCommand("rsi", "RSI(14) — /rsi EURUSD 1h"),
BotCommand("macd", "MACD(12,26,9) — /macd EURUSD 1h"),
BotCommand("bb", "Bollinger Bands(20,2) — /bb EURUSD 1h"),
BotCommand("signal", "Multi-indicator verdict + chart — /signal EURUSD 1h"),
BotCommand("chart", "Candlestick chart with EMA/BB/RSI — /chart EURUSD 1h"),
BotCommand("price", "Latest close price - /price EURUSD"),
BotCommand("rsi", "RSI(14) - /rsi EURUSD 1h"),
BotCommand("macd", "MACD(12,26,9) - /macd EURUSD 1h"),
BotCommand("bb", "Bollinger Bands(20,2) - /bb EURUSD 1h"),
BotCommand("signal", "Multi-indicator verdict + chart - /signal EURUSD 1h"),
BotCommand("chart", "Candlestick chart with EMA/BB/RSI - /chart EURUSD 1h"),
]