Fix trade setup generation, M5 data-only mode, WS symbol limits, command menu

- Trade setup TP now scales to SL distance ensuring min RR is met when
  no viable swing target exists; skip swing targets with poor RR
- Add float tolerance to RR gate to prevent edge-case rejection
- M5 auto-scan runs for level cache only, no Telegram broadcast
- WebSocket limited to EUR/USD + XAU/USD (Grow plan constraint)
- Register bot command menu via set_my_commands on startup

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
harwinder
2026-05-11 18:58:40 -04:00
parent 5e12307b1f
commit 4db6121ca7
3 changed files with 35 additions and 22 deletions
+15 -9
View File
@@ -470,19 +470,22 @@ class SignalAggregator:
if price - sl_price < atr * 1.5:
sl_price = price - atr * 1.5
sl_distance = price - sl_price
swing_highs = sorted(
v for k, v in levels.items() if k.startswith("swing_high_") and v > price + atr * 0.3
)
if swing_highs and swing_highs[0] - price <= atr * tp_swing_cap:
if (swing_highs
and swing_highs[0] - price <= atr * tp_swing_cap
and swing_highs[0] - price >= sl_distance * min_rr):
tp1 = swing_highs[0]
else:
tp1 = price + atr * tp_mult
tp1 = price + max(atr * tp_mult, sl_distance * min_rr)
if len(swing_highs) >= 2 and swing_highs[1] - price <= atr * 5.0:
tp2 = swing_highs[1]
else:
tp2 = price + atr * (tp_mult + 1.5)
tp2 = price + max(atr * (tp_mult + 1.5), sl_distance * min_rr * 1.5)
sl_distance = price - sl_price
direction = "BUY"
entry, sl = price, sl_price
else:
@@ -496,27 +499,30 @@ class SignalAggregator:
if sl_price - price < atr * 1.5:
sl_price = price + atr * 1.5
sl_distance = sl_price - price
swing_lows = sorted(
[v for k, v in levels.items() if k.startswith("swing_low_") and v < price - atr * 0.3],
reverse=True,
)
if swing_lows and price - swing_lows[0] <= atr * tp_swing_cap:
if (swing_lows
and price - swing_lows[0] <= atr * tp_swing_cap
and price - swing_lows[0] >= sl_distance * min_rr):
tp1 = swing_lows[0]
else:
tp1 = price - atr * tp_mult
tp1 = price - max(atr * tp_mult, sl_distance * min_rr)
if len(swing_lows) >= 2 and price - swing_lows[1] <= atr * 5.0:
tp2 = swing_lows[1]
else:
tp2 = price - atr * (tp_mult + 1.5)
tp2 = price - max(atr * (tp_mult + 1.5), sl_distance * min_rr * 1.5)
sl_distance = sl_price - price
direction = "SELL"
entry, sl = price, sl_price
tp1_distance = abs(tp1 - entry)
rr = tp1_distance / sl_distance if sl_distance > 0 else 0
if rr < min_rr:
if rr < min_rr - 1e-9:
return None
risk_pips = sl_distance / pip_value
+19 -12
View File
@@ -5,23 +5,25 @@ ForexBot2 — Telegram Bot
Async Telegram interface with candle-boundary auto-scanner and
real-time WebSocket level-breach monitor.
Manual scan: 4 pairs × 4 timeframes via inline buttons.
Manual scan: 4 pairs × 3 timeframes (M15/H1/H4) via inline buttons.
Auto-scan: Starts automatically on boot if TELEGRAM_CHAT_ID is set.
/autoscan toggles for additional chats or to stop.
M5 fires at :00/:05/:10/… + 10s buffer.
M5 fires at :00/:05/:10/… + 10s buffer (data only, no signals).
M15 fires at :00/:15/:30/:45 + 10s buffer.
H1 fires at :00 + 10s buffer.
H4 fires at :00 every 4 hours + 10s buffer.
WebSocket: Streams live ticks from TwelveData for all 4 pairs.
WebSocket: Streams live ticks from TwelveData for EUR/USD & XAU/USD
(Grow plan supports only these 2 WS symbols).
When price crosses a key structure level (swing S/R, BB,
EMA200) cached from the last boundary scan, triggers an
immediate M15 scan and sends a Level Break Alert.
5-minute cooldown per pair prevents spam.
GBP/USD & USD/JPY are monitored via REST scans only.
Plan: TwelveData Pro (55 credits/min, 1500 WS credits).
Plan: TwelveData Grow (55 credits/min, 8 trial WS credits).
Credit budget (worst case, 4-hour boundary):
Unique fetches: 4 pairs × ~6 TFs = ~24 calls (rest cached) → fits in 55/min.
WebSocket: 4 symbols = 4 credits (one-time on connect).
WebSocket: 2 symbols = 2 credits (one-time on connect).
Level breach scans: ~3 REST calls each, ~10/hour worst case.
Usage:
@@ -84,8 +86,6 @@ CORRELATION_GROUPS = {
_WS_TO_PAIR = {
"EUR/USD": EURUSD,
"GBP/USD": GBPUSD,
"USD/JPY": USDJPY,
"XAU/USD": XAUUSD,
}
_WS_SYMBOLS = list(_WS_TO_PAIR.keys())
@@ -97,19 +97,15 @@ _LEVEL_KEYS = (
)
SCAN_BUTTONS = [
{"cb": "scan_eurusd_m5", "symbol": EURUSD, "tf": Timeframe.M5, "label": "EUR/USD 5M"},
{"cb": "scan_eurusd_m15", "symbol": EURUSD, "tf": Timeframe.M15, "label": "EUR/USD 15M"},
{"cb": "scan_eurusd_h1", "symbol": EURUSD, "tf": Timeframe.H1, "label": "EUR/USD 1H"},
{"cb": "scan_eurusd_h4", "symbol": EURUSD, "tf": Timeframe.H4, "label": "EUR/USD 4H"},
{"cb": "scan_usdjpy_m5", "symbol": USDJPY, "tf": Timeframe.M5, "label": "USD/JPY 5M"},
{"cb": "scan_usdjpy_m15", "symbol": USDJPY, "tf": Timeframe.M15, "label": "USD/JPY 15M"},
{"cb": "scan_usdjpy_h1", "symbol": USDJPY, "tf": Timeframe.H1, "label": "USD/JPY 1H"},
{"cb": "scan_usdjpy_h4", "symbol": USDJPY, "tf": Timeframe.H4, "label": "USD/JPY 4H"},
{"cb": "scan_gbpusd_m5", "symbol": GBPUSD, "tf": Timeframe.M5, "label": "GBP/USD 5M"},
{"cb": "scan_gbpusd_m15", "symbol": GBPUSD, "tf": Timeframe.M15, "label": "GBP/USD 15M"},
{"cb": "scan_gbpusd_h1", "symbol": GBPUSD, "tf": Timeframe.H1, "label": "GBP/USD 1H"},
{"cb": "scan_gbpusd_h4", "symbol": GBPUSD, "tf": Timeframe.H4, "label": "GBP/USD 4H"},
{"cb": "scan_xauusd_m5", "symbol": XAUUSD, "tf": Timeframe.M5, "label": "XAU/USD 5M"},
{"cb": "scan_xauusd_m15", "symbol": XAUUSD, "tf": Timeframe.M15, "label": "XAU/USD 15M"},
{"cb": "scan_xauusd_h1", "symbol": XAUUSD, "tf": Timeframe.H1, "label": "XAU/USD 1H"},
{"cb": "scan_xauusd_h4", "symbol": XAUUSD, "tf": Timeframe.H4, "label": "XAU/USD 4H"},
@@ -390,6 +386,10 @@ async def _scan_timeframe(
_update_level_cache(reports)
if tf == Timeframe.M5:
logger.info("M5 scan: data analysis only — no Telegram broadcast")
return
actionable = [r for r in reports if r.signal_quality in ("A+", "A", "B", "C")]
if not actionable:
@@ -720,7 +720,14 @@ async def cb_scan(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
# ── Boot-time auto-start ──────────────────────────────────────────────
async def _post_init(app: Application) -> None:
"""Start the candle-boundary scheduler on boot if CHAT_ID is set."""
"""Register command menu and start scheduler on boot."""
await app.bot.set_my_commands([
("start", "Show scan buttons"),
("autoscan", "Toggle auto-scan on/off"),
("status", "Bot status, WS connection, live prices"),
("help", "All commands"),
])
if not CHAT_ID:
logger.info("TELEGRAM_CHAT_ID not set — auto-scan disabled on boot. "
"Use /autoscan in chat to enable.")
+1 -1
View File
@@ -8,7 +8,7 @@ from datetime import datetime, timezone
API_KEY = "f563b22ffdfd4924955decbe1a8c8701"
WS_URL = f"wss://ws.twelvedata.com/v1/quotes/price?apikey={API_KEY}"
SYMBOLS = ["EUR/USD", "GBP/USD", "USD/JPY", "XAU/USD"]
SYMBOLS = ["EUR/USD", "XAU/USD"]
async def stream_ticks():