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:
+15
-9
@@ -470,19 +470,22 @@ class SignalAggregator:
|
|||||||
if price - sl_price < atr * 1.5:
|
if price - sl_price < atr * 1.5:
|
||||||
sl_price = price - atr * 1.5
|
sl_price = price - atr * 1.5
|
||||||
|
|
||||||
|
sl_distance = price - sl_price
|
||||||
|
|
||||||
swing_highs = sorted(
|
swing_highs = sorted(
|
||||||
v for k, v in levels.items() if k.startswith("swing_high_") and v > price + atr * 0.3
|
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]
|
tp1 = swing_highs[0]
|
||||||
else:
|
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:
|
if len(swing_highs) >= 2 and swing_highs[1] - price <= atr * 5.0:
|
||||||
tp2 = swing_highs[1]
|
tp2 = swing_highs[1]
|
||||||
else:
|
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"
|
direction = "BUY"
|
||||||
entry, sl = price, sl_price
|
entry, sl = price, sl_price
|
||||||
else:
|
else:
|
||||||
@@ -496,27 +499,30 @@ class SignalAggregator:
|
|||||||
if sl_price - price < atr * 1.5:
|
if sl_price - price < atr * 1.5:
|
||||||
sl_price = price + atr * 1.5
|
sl_price = price + atr * 1.5
|
||||||
|
|
||||||
|
sl_distance = sl_price - price
|
||||||
|
|
||||||
swing_lows = sorted(
|
swing_lows = sorted(
|
||||||
[v for k, v in levels.items() if k.startswith("swing_low_") and v < price - atr * 0.3],
|
[v for k, v in levels.items() if k.startswith("swing_low_") and v < price - atr * 0.3],
|
||||||
reverse=True,
|
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]
|
tp1 = swing_lows[0]
|
||||||
else:
|
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:
|
if len(swing_lows) >= 2 and price - swing_lows[1] <= atr * 5.0:
|
||||||
tp2 = swing_lows[1]
|
tp2 = swing_lows[1]
|
||||||
else:
|
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"
|
direction = "SELL"
|
||||||
entry, sl = price, sl_price
|
entry, sl = price, sl_price
|
||||||
|
|
||||||
tp1_distance = abs(tp1 - entry)
|
tp1_distance = abs(tp1 - entry)
|
||||||
rr = tp1_distance / sl_distance if sl_distance > 0 else 0
|
rr = tp1_distance / sl_distance if sl_distance > 0 else 0
|
||||||
|
|
||||||
if rr < min_rr:
|
if rr < min_rr - 1e-9:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
risk_pips = sl_distance / pip_value
|
risk_pips = sl_distance / pip_value
|
||||||
|
|||||||
+19
-12
@@ -5,23 +5,25 @@ ForexBot2 — Telegram Bot
|
|||||||
Async Telegram interface with candle-boundary auto-scanner and
|
Async Telegram interface with candle-boundary auto-scanner and
|
||||||
real-time WebSocket level-breach monitor.
|
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.
|
Auto-scan: Starts automatically on boot if TELEGRAM_CHAT_ID is set.
|
||||||
/autoscan toggles for additional chats or to stop.
|
/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.
|
M15 fires at :00/:15/:30/:45 + 10s buffer.
|
||||||
H1 fires at :00 + 10s buffer.
|
H1 fires at :00 + 10s buffer.
|
||||||
H4 fires at :00 every 4 hours + 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,
|
When price crosses a key structure level (swing S/R, BB,
|
||||||
EMA200) cached from the last boundary scan, triggers an
|
EMA200) cached from the last boundary scan, triggers an
|
||||||
immediate M15 scan and sends a Level Break Alert.
|
immediate M15 scan and sends a Level Break Alert.
|
||||||
5-minute cooldown per pair prevents spam.
|
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):
|
Credit budget (worst case, 4-hour boundary):
|
||||||
Unique fetches: 4 pairs × ~6 TFs = ~24 calls (rest cached) → fits in 55/min.
|
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.
|
Level breach scans: ~3 REST calls each, ~10/hour worst case.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
@@ -84,8 +86,6 @@ CORRELATION_GROUPS = {
|
|||||||
|
|
||||||
_WS_TO_PAIR = {
|
_WS_TO_PAIR = {
|
||||||
"EUR/USD": EURUSD,
|
"EUR/USD": EURUSD,
|
||||||
"GBP/USD": GBPUSD,
|
|
||||||
"USD/JPY": USDJPY,
|
|
||||||
"XAU/USD": XAUUSD,
|
"XAU/USD": XAUUSD,
|
||||||
}
|
}
|
||||||
_WS_SYMBOLS = list(_WS_TO_PAIR.keys())
|
_WS_SYMBOLS = list(_WS_TO_PAIR.keys())
|
||||||
@@ -97,19 +97,15 @@ _LEVEL_KEYS = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
SCAN_BUTTONS = [
|
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_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_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_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_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_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_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_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_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_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_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_h1", "symbol": XAUUSD, "tf": Timeframe.H1, "label": "XAU/USD 1H"},
|
||||||
{"cb": "scan_xauusd_h4", "symbol": XAUUSD, "tf": Timeframe.H4, "label": "XAU/USD 4H"},
|
{"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)
|
_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")]
|
actionable = [r for r in reports if r.signal_quality in ("A+", "A", "B", "C")]
|
||||||
|
|
||||||
if not actionable:
|
if not actionable:
|
||||||
@@ -720,7 +720,14 @@ async def cb_scan(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|||||||
# ── Boot-time auto-start ──────────────────────────────────────────────
|
# ── Boot-time auto-start ──────────────────────────────────────────────
|
||||||
|
|
||||||
async def _post_init(app: Application) -> None:
|
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:
|
if not CHAT_ID:
|
||||||
logger.info("TELEGRAM_CHAT_ID not set — auto-scan disabled on boot. "
|
logger.info("TELEGRAM_CHAT_ID not set — auto-scan disabled on boot. "
|
||||||
"Use /autoscan in chat to enable.")
|
"Use /autoscan in chat to enable.")
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ from datetime import datetime, timezone
|
|||||||
|
|
||||||
API_KEY = "f563b22ffdfd4924955decbe1a8c8701"
|
API_KEY = "f563b22ffdfd4924955decbe1a8c8701"
|
||||||
WS_URL = f"wss://ws.twelvedata.com/v1/quotes/price?apikey={API_KEY}"
|
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():
|
async def stream_ticks():
|
||||||
|
|||||||
Reference in New Issue
Block a user