Pro tier upgrade: quant rearchitect + WebSocket level-breach monitor
- Restore XAU/USD (Pro plan), drop USD/CAD, raise rate limit to 55/min - Reduce signal engine to 4 uncorrelated pillars (EMA, RSI, BB, Candlestick) with regime-adaptive weights (trending boosts EMA, ranging boosts RSI+BB) - Add structure-based S/R (swing highs/lows) for trade setup targeting - Fix confidence death spiral: all adjustments now additive, not multiplicative - Dynamic R:R trade setup: SL behind swing level, TP at next structure level, reject if R:R < 1.5 - Fix news engine for all 4 pairs (was hardcoded to USDJPY) - Add WebSocket monitor: streams live ticks, triggers M15 scan on level breach with 5-min cooldown per pair — catches breakouts between candle boundaries - Add correlation filter (EUR/USD+GBP/USD, XAU/USD+USD/JPY exposure warnings) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+60
-33
@@ -334,54 +334,44 @@ class SignalAggregator:
|
||||
score = report.primary_score
|
||||
confidence = report.primary_confidence
|
||||
|
||||
# MTF
|
||||
if report.mtf_agreement:
|
||||
confidence = min(confidence + 15, 95)
|
||||
confidence += 10
|
||||
all_scores = list(report.mtf_scores.values())
|
||||
if all_scores:
|
||||
mtf_avg = sum(all_scores) / len(all_scores)
|
||||
score = score * 0.6 + mtf_avg * 0.4
|
||||
else:
|
||||
confidence *= 0.7
|
||||
confidence -= 15
|
||||
if len(report.mtf_scores) >= 2:
|
||||
scores = list(report.mtf_scores.values())
|
||||
has_bull = any(s > 10 for s in scores)
|
||||
has_bear = any(s < -10 for s in scores)
|
||||
if has_bull and has_bear:
|
||||
report.warnings.append("Timeframes CONFLICT — mixed signals across timeframes")
|
||||
confidence *= 0.6
|
||||
|
||||
# News sentiment
|
||||
if abs(report.news_sentiment) > 0.2:
|
||||
if (report.news_sentiment > 0 and score > 0) or \
|
||||
(report.news_sentiment < 0 and score < 0):
|
||||
confidence = min(confidence + 5, 95)
|
||||
confidence += 5
|
||||
elif (report.news_sentiment > 0 and score < -10) or \
|
||||
(report.news_sentiment < 0 and score > 10):
|
||||
report.warnings.append("News sentiment CONFLICTS with technical signal")
|
||||
confidence *= 0.85
|
||||
confidence -= 10
|
||||
|
||||
# Event risk
|
||||
if report.event_risk == "danger":
|
||||
report.warnings.append(f"EVENT RISK: {report.event_description}")
|
||||
confidence *= 0.3
|
||||
confidence = min(confidence, 30)
|
||||
report.action_recommendation = f"WAIT — {report.event_description}"
|
||||
elif report.event_risk == "caution":
|
||||
report.warnings.append(f"Upcoming event: {report.event_description}")
|
||||
confidence *= 0.7
|
||||
confidence -= 15
|
||||
|
||||
# Volatility regime
|
||||
if report.volatility_regime == "extreme":
|
||||
report.warnings.append("EXTREME volatility — wider stops needed, reduced position size")
|
||||
confidence *= 0.8
|
||||
confidence -= 10
|
||||
elif report.volatility_regime == "low":
|
||||
report.warnings.append("Low volatility — potential for breakout, tight ranges")
|
||||
|
||||
# EMA200 distance gate — empirically the strongest single discriminator
|
||||
# across 2024 trend / 2023 chop / 2026 hostile windows. Sweet spot is
|
||||
# 0.5–1.5% from EMA200: closer = false-breakout chop, farther = chase.
|
||||
# Within band keeps confidence; outside band cuts it sharply so the
|
||||
# trade falls below the B-grade threshold and is rejected.
|
||||
primary = report.primary_analysis
|
||||
if primary is not None:
|
||||
ema200 = primary.key_levels.get("ema_trend")
|
||||
@@ -389,17 +379,18 @@ class SignalAggregator:
|
||||
ema_dist_pct = abs(report.current_price - ema200) / ema200 * 100.0
|
||||
_lo = float(os.environ.get("FB2_EMA_LO", "0.5"))
|
||||
_hi = float(os.environ.get("FB2_EMA_HI", "1.5"))
|
||||
_mult = float(os.environ.get("FB2_EMA_MULT", "0.55"))
|
||||
if ema_dist_pct < _lo:
|
||||
report.warnings.append(
|
||||
f"NEAR EMA200 ({ema_dist_pct:.2f}%) — chop risk, signal damped"
|
||||
f"NEAR EMA200 ({ema_dist_pct:.2f}%) — chop risk"
|
||||
)
|
||||
confidence *= _mult
|
||||
confidence -= 20
|
||||
elif ema_dist_pct > _hi:
|
||||
report.warnings.append(
|
||||
f"FAR FROM EMA200 ({ema_dist_pct:.2f}%) — over-extended, signal damped"
|
||||
f"FAR FROM EMA200 ({ema_dist_pct:.2f}%) — over-extended"
|
||||
)
|
||||
confidence *= _mult
|
||||
confidence -= 20
|
||||
|
||||
confidence = max(0.0, min(95.0, confidence))
|
||||
|
||||
# Final signal
|
||||
if score >= 40 and confidence >= 65:
|
||||
@@ -452,21 +443,57 @@ class SignalAggregator:
|
||||
return None
|
||||
|
||||
is_buy = report.final_signal in (SignalStrength.BUY, SignalStrength.STRONG_BUY)
|
||||
rr = self.config.signals.default_rr_ratio
|
||||
sl_mult = self.config.signals.atr_sl_multiplier
|
||||
|
||||
sl_distance = atr * sl_mult
|
||||
tp1_distance = sl_distance * rr
|
||||
tp2_distance = sl_distance * rr * 1.5
|
||||
levels = report.primary_analysis.key_levels
|
||||
min_rr = self.config.signals.min_rr_ratio
|
||||
|
||||
if is_buy:
|
||||
entry, sl = price, price - sl_distance
|
||||
tp1, tp2 = price + tp1_distance, price + tp2_distance
|
||||
swing_lows = sorted(
|
||||
[v for k, v in levels.items() if k.startswith("swing_low_") and v < price],
|
||||
reverse=True,
|
||||
)
|
||||
if swing_lows:
|
||||
sl_price = swing_lows[0] - atr * 0.5
|
||||
else:
|
||||
sl_price = price - atr * 1.5
|
||||
if price - sl_price < atr * 1.0:
|
||||
sl_price = price - atr * 1.0
|
||||
|
||||
swing_highs = sorted(
|
||||
v for k, v in levels.items() if k.startswith("swing_high_") and v > price + atr * 0.3
|
||||
)
|
||||
tp1 = swing_highs[0] if len(swing_highs) >= 1 else price + atr * 2.0
|
||||
tp2 = swing_highs[1] if len(swing_highs) >= 2 else price + atr * 3.0
|
||||
|
||||
sl_distance = price - sl_price
|
||||
direction = "BUY"
|
||||
entry, sl = price, sl_price
|
||||
else:
|
||||
entry, sl = price, price + sl_distance
|
||||
tp1, tp2 = price - tp1_distance, price - tp2_distance
|
||||
swing_highs = sorted(
|
||||
v for k, v in levels.items() if k.startswith("swing_high_") and v > price
|
||||
)
|
||||
if swing_highs:
|
||||
sl_price = swing_highs[0] + atr * 0.5
|
||||
else:
|
||||
sl_price = price + atr * 1.5
|
||||
if sl_price - price < atr * 1.0:
|
||||
sl_price = price + atr * 1.0
|
||||
|
||||
swing_lows = sorted(
|
||||
[v for k, v in levels.items() if k.startswith("swing_low_") and v < price - atr * 0.3],
|
||||
reverse=True,
|
||||
)
|
||||
tp1 = swing_lows[0] if len(swing_lows) >= 1 else price - atr * 2.0
|
||||
tp2 = swing_lows[1] if len(swing_lows) >= 2 else price - atr * 3.0
|
||||
|
||||
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:
|
||||
return None
|
||||
|
||||
risk_pips = sl_distance / pip_value
|
||||
reward_pips = tp1_distance / pip_value
|
||||
@@ -479,7 +506,7 @@ class SignalAggregator:
|
||||
stop_loss=sl,
|
||||
take_profit_1=tp1,
|
||||
take_profit_2=tp2,
|
||||
risk_reward=rr,
|
||||
risk_reward=round(rr, 2),
|
||||
risk_pips=risk_pips,
|
||||
reward_pips=reward_pips,
|
||||
pip_value=pip_value,
|
||||
|
||||
Reference in New Issue
Block a user