Refine signal formatting, simplify indicators, and fix chart layout
This commit is contained in:
@@ -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"),
|
||||
]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user