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
This commit is contained in:
+15
-9
@@ -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
@@ -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
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user