GoshawkVortexAI commited on
Commit
150aaaa
Β·
verified Β·
1 Parent(s): 9a92ea1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +158 -191
app.py CHANGED
@@ -1,6 +1,6 @@
1
  # ══════════════════════════════════════════════════════════════════════════════
2
- # EUR/USD Β· ENSEMBLE FORECAST ENGINE Β· Ultra-Premium Edition v2
3
- # Model: Ensemble Ridge + GBM Β· ATR-normalised target Β· Regime filter Β· Threshold
4
  # Forecast horizon: 1 or 2 days ahead
5
  # TP/SL: ATR-based risk management Β· Signal scoring Β· Backtest
6
  # Data: Yahoo Finance (yfinance)
@@ -15,8 +15,6 @@ from sklearn.linear_model import Ridge, RidgeCV
15
  from sklearn.preprocessing import StandardScaler, PolynomialFeatures
16
  from sklearn.pipeline import Pipeline
17
  from sklearn.metrics import mean_absolute_error
18
- from sklearn.ensemble import GradientBoostingRegressor
19
- from sklearn.model_selection import TimeSeriesSplit
20
  import matplotlib
21
  matplotlib.use('Agg')
22
  import matplotlib.pyplot as plt
@@ -98,43 +96,6 @@ def compute_rsi(series: pd.Series, period: int = 14) -> pd.Series:
98
  return 100 - (100 / (1 + gain / (loss + 1e-10)))
99
 
100
 
101
- def detect_regime(df: pd.DataFrame) -> pd.Series:
102
- """
103
- Market regime detection:
104
- +1 = TREND (directional, EMA spread wide, ADX-proxy high)
105
- 0 = NEUTRAL
106
- -1 = RANGE (mean-reverting, BB squeeze, flat MAs)
107
- """
108
- close = df['Close']
109
- ema8 = close.ewm(span=8, adjust=False).mean()
110
- ema21 = close.ewm(span=21, adjust=False).mean()
111
- ema55 = close.ewm(span=55, adjust=False).mean()
112
-
113
- # Trend strength: normalised EMA spread
114
- spread = (ema8 - ema55).abs() / (close + 1e-10)
115
- spread_z = (spread - spread.rolling(40).mean()) / (spread.rolling(40).std() + 1e-10)
116
-
117
- # Volatility regime: BB width z-score
118
- mid = close.rolling(20).mean()
119
- bb_w = (close.rolling(20).std() * 4) / (mid + 1e-10)
120
- bb_z = (bb_w - bb_w.rolling(40).mean()) / (bb_w.rolling(40).std() + 1e-10)
121
-
122
- regime = pd.Series(0, index=df.index)
123
- regime[spread_z > 0.5] = 1 # trending
124
- regime[bb_z < -0.5] = -1 # range / squeeze
125
- return regime
126
-
127
-
128
- def normalise_target_by_atr(y_logret: np.ndarray, atr_vals: np.ndarray) -> np.ndarray:
129
- """
130
- Divide log-returns by ATR to make the target volatility-scaled.
131
- Model learns to predict 'how many ATRs will price move', not raw pips.
132
- This prevents near-zero predictions when volatility is high.
133
- """
134
- atr_norm = atr_vals / (atr_vals.mean() + 1e-10) # relative ATR
135
- return y_logret / (atr_norm + 1e-10)
136
-
137
-
138
  def create_features(data: pd.DataFrame) -> pd.DataFrame:
139
  """
140
  Premium feature set β€” 55+ engineered features designed to minimise MAE
@@ -257,16 +218,6 @@ def create_features(data: pd.DataFrame) -> pd.DataFrame:
257
  df['Volume_Ratio'] = 1.0
258
  df['OBV_Signal'] = 0.0
259
 
260
- # ── Regime features ──────────────────────────────────────────────────────
261
- df['Regime'] = detect_regime(df)
262
- df['Regime_Trend'] = (df['Regime'] == 1).astype(float)
263
- df['Regime_Range'] = (df['Regime'] == -1).astype(float)
264
-
265
- # BB squeeze: width below 20th percentile of last 60 days
266
- bb20 = df['BB_width_20'] if 'BB_width_20' in df.columns else (
267
- df['Close'].rolling(20).std() * 4 / (df['Close'].rolling(20).mean() + 1e-10))
268
- df['BB_Squeeze'] = (bb20 < bb20.rolling(60).quantile(0.20)).astype(float)
269
-
270
  # ── FIX 4: Direction Composite Signal (boosts directional accuracy) ─────
271
  # Encodes consensus of multiple indicators into a single direction score.
272
  # Positive β†’ bullish bias, Negative β†’ bearish bias.
@@ -348,8 +299,6 @@ FEATURE_COLS = [
348
  'Volume_Ratio', 'OBV_Signal',
349
  # Direction composite (FIX 4)
350
  'Direction_Score', 'Momentum_Consistency',
351
- # Regime
352
- 'Regime', 'Regime_Trend', 'Regime_Range', 'BB_Squeeze',
353
  ]
354
 
355
 
@@ -362,15 +311,19 @@ FEATURE_COLS = [
362
  def build_ridge_pipeline(alpha: float) -> Pipeline:
363
  """Ridge pipeline with degree-2 interactions (no bias terms in poly)."""
364
  return Pipeline([
365
- ('scaler', StandardScaler()),
366
- ('poly', PolynomialFeatures(degree=2, interaction_only=True,
367
- include_bias=False)),
368
- ('scaler2', StandardScaler(with_mean=False)),
369
- ('ridge', Ridge(alpha=alpha, fit_intercept=True)),
370
  ])
371
 
372
 
373
  def select_best_alpha(X_tr, y_tr) -> float:
 
 
 
 
374
  alphas = np.logspace(-3, 4, 60)
375
  scaler = StandardScaler()
376
  Xs = scaler.fit_transform(X_tr)
@@ -379,115 +332,119 @@ def select_best_alpha(X_tr, y_tr) -> float:
379
  return float(rcv.alpha_)
380
 
381
 
382
- # ══════════════════════════════════════════════════════════════════════════════
383
- # ENSEMBLE MODEL: Ridge + Gradient Boosting
384
- # ══════════════════════════════════════════════════════════════════════════════
385
-
386
- def train_ensemble(df: pd.DataFrame, horizon: int = 1):
387
  """
388
- Four upgrades over baseline:
389
- A) ATR-normalised log-return target β†’ prevents near-zero predictions
390
- B) Regime-conditioned training β†’ model sees market context
391
- C) GBM secondary model β†’ captures nonlinear patterns
392
- D) Ensemble blend (Ridge 55% + GBM 45%)
393
- Final signal passes through a threshold filter: if predicted move
394
- is < 0.3Γ— ATR the signal is tagged WEAK (no trade recommended).
395
  """
396
  avail = [c for c in FEATURE_COLS if c in df.columns]
397
  X = df[avail].values
398
  close = df['Close'].values
399
- atr = df['ATR'].values if 'ATR' in df.columns else np.full(len(close), 1e-4)
400
 
401
- # ── A. ATR-normalised log-return target ───────────────────────────────────
 
 
402
  y_logret = np.log(close[horizon:] / close[:len(close) - horizon])
403
- atr_aligned = atr[:len(y_logret)]
404
- y_norm = normalise_target_by_atr(y_logret, atr_aligned) # scaled target
405
 
406
- X = X[:len(y_norm)]
407
- close_aligned = close[:len(y_norm)]
408
- atr_aligned = atr_aligned[:len(y_norm)]
409
 
410
- split = -15
411
- X_train, X_test = X[:split], X[split:]
412
- y_train, y_norm_test = y_norm[:split], y_norm[split:]
413
- y_raw_test = y_logret[len(y_logret)+split:]
414
  close_test = close_aligned[split:]
415
- atr_test = atr_aligned[split:]
416
 
417
- # ── B. Ridge on full training set ────────────────────────────────────────
418
  best_alpha = select_best_alpha(X_train, y_train)
419
- ridge_pipe = build_ridge_pipeline(best_alpha)
420
- ridge_pipe.fit(X_train, y_train)
421
- ridge_pred_norm = ridge_pipe.predict(X_test)
422
-
423
- # ── C. GBM on residuals (boosting approach) ───────────────────────────────
424
- # Fit GBM on training residuals so it learns what Ridge misses
425
- ridge_train_pred = ridge_pipe.predict(X_train)
426
- residuals_train = y_train - ridge_train_pred
427
-
428
- scaler_gbm = StandardScaler()
429
- X_tr_sc = scaler_gbm.fit_transform(X_train)
430
- X_te_sc = scaler_gbm.transform(X_test)
431
-
432
- gbm = GradientBoostingRegressor(
433
- n_estimators=120, max_depth=3, learning_rate=0.05,
434
- subsample=0.8, min_samples_leaf=5, random_state=42,
435
- )
436
- gbm.fit(X_tr_sc, residuals_train)
437
- gbm_correction = gbm.predict(X_te_sc)
438
 
439
- # ── D. Ensemble blend ────────────────────────────────────────────────────
440
- RIDGE_W = 0.55
441
- GBM_W = 0.45
442
- ensemble_norm = ridge_pred_norm * RIDGE_W + (ridge_pred_norm + gbm_correction) * GBM_W
443
 
444
- # Convert normalised prediction back to log-return, then to price
445
- atr_norm_test = atr_test / (atr_aligned.mean() + 1e-10)
446
- logret_hat = ensemble_norm * atr_norm_test
447
- pred_abs = close_test * np.exp(logret_hat)
448
- y_test_abs = close_test * np.exp(y_raw_test)
449
 
450
- mae = mean_absolute_error(y_test_abs, pred_abs)
451
 
452
- return (ridge_pipe, gbm, scaler_gbm, atr_aligned.mean(),
453
- pred_abs, y_test_abs, avail, best_alpha, mae,
454
- RIDGE_W, GBM_W)
455
 
456
 
457
- def predict_next_ensemble(ridge_pipe, gbm, scaler_gbm, atr_mean,
458
- df: pd.DataFrame, avail: list,
459
- horizon: int = 1,
460
- ridge_w: float = 0.55,
461
- gbm_w: float = 0.45) -> tuple:
462
- """
463
- Returns (pred_price, logret, signal_strength, threshold_passed).
464
- signal_strength = |predicted move| / ATR (how many ATRs away)
465
- threshold_passed = True if signal_strength >= 0.30
466
- """
467
- cp = float(df['Close'].iloc[-1])
468
- atr = float(df['ATR'].iloc[-1]) if 'ATR' in df.columns else 1e-4
469
- x = df[avail].iloc[-1].values.reshape(1, -1)
470
 
471
- # Ridge prediction (normalised)
472
- ridge_norm = float(ridge_pipe.predict(x)[0])
473
 
474
- # GBM correction on residual
475
- x_sc = scaler_gbm.transform(x)
476
- gbm_corr = float(gbm.predict(x_sc)[0])
477
 
478
- ensemble_norm = ridge_norm * ridge_w + (ridge_norm + gbm_corr) * gbm_w
 
479
 
480
- # Denormalise
481
- atr_norm = atr / (atr_mean + 1e-10)
482
- logret = ensemble_norm * atr_norm * horizon
483
- pred_price = cp * np.exp(logret)
 
 
 
 
 
 
484
 
485
- # ── Sinyal eşiği ─────────────────────────────────────────────────────────
486
- signal_strength = abs(pred_price - cp) / (atr + 1e-10)
487
- THRESHOLD = 0.30 # minimum 0.30 ATR hareketi gerekiyor
488
- threshold_passed = signal_strength >= THRESHOLD
489
 
490
- return pred_price, logret, signal_strength, threshold_passed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
491
 
492
 
493
  # ═════════════════════════════════════════��════════════════════════════════════
@@ -553,6 +510,20 @@ def signal_score(df: pd.DataFrame, direction: str) -> dict:
553
  return {'score': min(100, score), 'label': label, 'details': det}
554
 
555
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
556
  # ══════════════════════════════════════════════════════════════════════════════
557
  # CHART ENGINE β€” phosphor-green terminal aesthetic
558
  # ══════════════════════════════════════════════════════════════════════════════
@@ -573,7 +544,7 @@ def _ax_style(ax, title="", ylabel=""):
573
 
574
  def build_figure(df, pred_price, horizon, tp_sl,
575
  test_pred_abs, y_test_abs, best_alpha, mae,
576
- regime_val=0, sig_strength=0.0, thr_passed=True) -> plt.Figure:
577
 
578
  plt.rcParams.update({
579
  'font.family': 'monospace',
@@ -626,15 +597,6 @@ def build_figure(df, pred_price, horizon, tp_sl,
626
  (i - 0.25, min(o, c)), 0.5, abs(c - o) + 1e-7,
627
  boxstyle="square,pad=0", linewidth=0, facecolor=color, alpha=0.75, zorder=3))
628
 
629
- # ── Regime background shading ─────────────────────────────────────────────
630
- if 'Regime' in tail.columns:
631
- for i, (_, row) in enumerate(tail.iterrows()):
632
- rv = int(row.get('Regime', 0))
633
- if rv == 1: # trend
634
- ax_price.axvspan(i-0.5, i+0.5, alpha=0.06, color=ACID, zorder=0)
635
- elif rv == -1: # range
636
- ax_price.axvspan(i-0.5, i+0.5, alpha=0.06, color=ACID3, zorder=0)
637
-
638
  # Close line overlay (phosphor glow)
639
  ax_price.plot(idx, tail['Close'].values, lw=1.6, color=ACID, zorder=5, alpha=0.9, label='Close')
640
 
@@ -643,6 +605,18 @@ def build_figure(df, pred_price, horizon, tp_sl,
643
  ax_price.axhline(tp_sl['sl'], color=RED, lw=1.4, ls='--', alpha=0.9, zorder=6)
644
  ax_price.axhline(cp, color=TEXT, lw=0.7, ls='-', alpha=0.35, zorder=4)
645
 
 
 
 
 
 
 
 
 
 
 
 
 
646
  r = len(idx) * 0.018
647
  for val, lbl, col in [
648
  (tp_sl['tp'], f"TP {tp_sl['tp']:.5f} (+{tp_sl['tp_pips']:.0f}p)", GREEN),
@@ -663,14 +637,6 @@ def build_figure(df, pred_price, horizon, tp_sl,
663
  fontfamily='monospace',
664
  )
665
 
666
- # Threshold / signal strength label
667
- thr_color = GREEN if thr_passed else RED
668
- thr_text = f"{'βœ…' if thr_passed else 'β›”'} Sinyal GΓΌcΓΌ: {sig_strength:.2f}Γ— ATR"
669
- ax_price.text(0.01, 0.04, thr_text, transform=ax_price.transAxes,
670
- fontsize=7.5, color=thr_color, fontweight='bold',
671
- fontfamily='monospace', zorder=9,
672
- bbox=dict(facecolor=PANEL, edgecolor=thr_color, alpha=0.7, pad=3))
673
-
674
  ax_price.legend(fontsize=7, framealpha=0.1, labelcolor=SUBTEXT,
675
  ncol=3, loc='upper left')
676
  ax_price.yaxis.set_major_formatter(mticker.FormatStrFormatter('%.4f'))
@@ -751,7 +717,7 @@ def build_figure(df, pred_price, horizon, tp_sl,
751
  dir_color = GREEN if tp_sl['direction'] == 'LONG' else WARM
752
 
753
  fig.text(0.055, 0.97,
754
- "EUR/USD Β· ENSEMBLE FORECAST ENGINE Β· ULTRA-PREMIUM",
755
  fontsize=12, color=TEXT, fontweight='bold', va='top',
756
  fontfamily='monospace')
757
  fig.text(0.055, 0.955, dir_label,
@@ -786,30 +752,28 @@ def predict_eurusd(horizon: int = 1):
786
  if len(df) < 40:
787
  return "❌ Yetersiz veri (min 40 satΔ±r gerekli).", None
788
 
789
- # 3. Train Ensemble (Ridge + GBM) + backtest
790
- (ridge_pipe, gbm, scaler_gbm, atr_mean,
791
- test_pred_abs, y_test_abs, avail, best_alpha, mae,
792
- ridge_w, gbm_w) = train_ensemble(df, horizon)
793
-
794
- # 4. Predict next (ensemble)
795
- cp = float(df['Close'].iloc[-1])
796
- atr = float(df['ATR'].iloc[-1])
797
- pred_price, logret_val, sig_strength, thr_passed = predict_next_ensemble(
798
- ridge_pipe, gbm, scaler_gbm, atr_mean, df, avail, horizon, ridge_w, gbm_w)
799
 
800
- # 4b. Regime
801
- regime_val = int(df['Regime'].iloc[-1]) if 'Regime' in df.columns else 0
802
- regime_lbl = {1: "TREND β–Ά", 0: "NΓ–TR β—†", -1: "RANGE β—€"}.get(regime_val, "?")
803
 
804
  # 5. TP/SL
 
 
805
  tp_sl = compute_tp_sl(cp, pred_price, atr,
806
  atr_multiplier_sl=1.5, rr_ratio=2.0)
807
 
808
- # 6. Signal score
809
- sig = signal_score(df, tp_sl['direction'])
 
 
 
 
810
 
811
  # 7. Backtest metrics
812
  rmse = float(np.sqrt(np.mean((test_pred_abs - y_test_abs) ** 2)))
 
813
  close_vals = df['Close'].values
814
  n_test = len(test_pred_abs)
815
  entry_prices = close_vals[len(close_vals) - n_test - horizon :
@@ -842,19 +806,15 @@ def predict_eurusd(horizon: int = 1):
842
  W = 54
843
  sep = "═" * W
844
 
845
- thr_lbl = "βœ… AKTΔ°F SΔ°NYAL" if thr_passed else "β›” ZAYIF SΔ°NYAL (trade yok)"
846
  lines = [
847
  sep,
848
- f" EUR/USD Β· ENSEMBLE FORECAST ENGINE Β· ULTRA-PREMIUM",
849
  f" {datetime.now().strftime('%d %b %Y %H:%M:%S')}",
850
  sep,
851
  "",
852
  f" {'MEVCUT FΔ°YAT':<24}: {cp:.5f}",
853
- f" {f'+{horizon}G TAHMΔ°N (ENSEMBLE)':<24}: {pred_price:.5f} ({chg:+.3f}%)",
854
  f" {'YΓ–N':<24}: {tp_sl['direction']}",
855
- f" {'REJΔ°M':<24}: {regime_lbl}",
856
- f" {'SΔ°NYAL GÜCÜ (ATRΓ—)':<24}: {sig_strength:.2f}x β†’ {thr_lbl}",
857
- f" {'LOG-RETURN TAHMΔ°N':<24}: {logret_val*100:+.4f}%",
858
  f" {'OPTΔ°MAL ALPHA (RidgeCV)':<24}: {best_alpha:.6f}",
859
  "",
860
  f" {'─── RΔ°SK YΓ–NETΔ°MΔ° (ATR Γ— 1.5 | R:R 1:2)':<{W-2}}",
@@ -865,7 +825,17 @@ def predict_eurusd(horizon: int = 1):
865
  f" {'RΔ°SK %':<24}: {tp_sl['risk_pct']:.3f}%",
866
  f" {'ATR-14':<24}: {atr:.5f}",
867
  "",
868
- f" {'─── SΔ°NYAL GÜCÜ':<{W-2}}",
 
 
 
 
 
 
 
 
 
 
869
  f" {'SKOR':<24}: {sig['score']:.0f} / 100 β†’ {sig['label']}",
870
  ]
871
  for k, v in sig['details'].items():
@@ -890,8 +860,6 @@ def predict_eurusd(horizon: int = 1):
890
  f" {'RMSE':<24}: {rmse:.7f}",
891
  f" {'YΓΆn Doğruluğu':<24}: {hit:.1f}%",
892
  f" {'Feature SayΔ±sΔ±':<24}: {len(avail)}",
893
- f" {'Ensemble (Ridge/GBM)':<24}: {ridge_w*100:.0f}% / {gbm_w*100:.0f}%",
894
- f" {'Eşik (ATRΓ—0.30)':<24}: {'GeΓ§ti βœ…' if thr_passed else 'GEΓ‡EMEDΔ° β›”'}",
895
  "",
896
  sep,
897
  " ⚠ YalnΔ±zca eğitim amaΓ§lΔ±dΔ±r. Finansal tavsiye değildir.",
@@ -904,8 +872,7 @@ def predict_eurusd(horizon: int = 1):
904
  fig = build_figure(
905
  df, pred_price, horizon, tp_sl,
906
  test_pred_abs, y_test_abs, best_alpha, mae,
907
- regime_val=regime_val, sig_strength=sig_strength,
908
- thr_passed=thr_passed,
909
  )
910
 
911
  return text, fig
@@ -1022,13 +989,13 @@ HEADER = """
1022
  <div>
1023
  <h1 style="margin:0 0 4px;font-size:1.45rem;font-weight:900;
1024
  color:#E8EDF5;letter-spacing:.06em;font-family:monospace;">
1025
- EUR/USD &nbsp;Β·&nbsp; ENSEMBLE FORECAST ENGINE
1026
  <span style="color:#00FFB2;font-size:.85rem;margin-left:10px;
1027
  vertical-align:middle;letter-spacing:.12em;">ULTRA-PREMIUM</span>
1028
  </h1>
1029
  <p style="margin:0;color:#3A4A5E;font-size:.75rem;font-family:monospace;
1030
  letter-spacing:.04em;">
1031
- RIDGE+GBM ENSEMBLE Β· ATR-NORM TARGET Β· REGIME FILTER Β· SIGNAL THRESHOLD Β· 80+ FEATURES
1032
  </p>
1033
  </div>
1034
  </div>
@@ -1062,7 +1029,7 @@ HEADER = """
1062
  # GRADIO UI
1063
  # ══════════════════════════════════════════════════════════════════════════════
1064
 
1065
- with gr.Blocks(css=CSS, title="EUR/USD Ensemble Forecast Engine") as app:
1066
  gr.HTML(HEADER)
1067
 
1068
  with gr.Row(elem_classes="btn-row"):
 
1
  # ══════════════════════════════════════════════════════════════════════════════
2
+ # EUR/USD Β· RIDGE FORECAST ENGINE Β· Ultra-Premium Edition
3
+ # Model: Stacked Ridge Regression (L1+L2 optimised, polynomial features)
4
  # Forecast horizon: 1 or 2 days ahead
5
  # TP/SL: ATR-based risk management Β· Signal scoring Β· Backtest
6
  # Data: Yahoo Finance (yfinance)
 
15
  from sklearn.preprocessing import StandardScaler, PolynomialFeatures
16
  from sklearn.pipeline import Pipeline
17
  from sklearn.metrics import mean_absolute_error
 
 
18
  import matplotlib
19
  matplotlib.use('Agg')
20
  import matplotlib.pyplot as plt
 
96
  return 100 - (100 / (1 + gain / (loss + 1e-10)))
97
 
98
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  def create_features(data: pd.DataFrame) -> pd.DataFrame:
100
  """
101
  Premium feature set β€” 55+ engineered features designed to minimise MAE
 
218
  df['Volume_Ratio'] = 1.0
219
  df['OBV_Signal'] = 0.0
220
 
 
 
 
 
 
 
 
 
 
 
221
  # ── FIX 4: Direction Composite Signal (boosts directional accuracy) ─────
222
  # Encodes consensus of multiple indicators into a single direction score.
223
  # Positive β†’ bullish bias, Negative β†’ bearish bias.
 
299
  'Volume_Ratio', 'OBV_Signal',
300
  # Direction composite (FIX 4)
301
  'Direction_Score', 'Momentum_Consistency',
 
 
302
  ]
303
 
304
 
 
311
  def build_ridge_pipeline(alpha: float) -> Pipeline:
312
  """Ridge pipeline with degree-2 interactions (no bias terms in poly)."""
313
  return Pipeline([
314
+ ('scaler', StandardScaler()),
315
+ ('poly', PolynomialFeatures(degree=2, interaction_only=True,
316
+ include_bias=False)),
317
+ ('scaler2', StandardScaler(with_mean=False)), # re-scale after poly
318
+ ('ridge', Ridge(alpha=alpha, fit_intercept=True)),
319
  ])
320
 
321
 
322
  def select_best_alpha(X_tr, y_tr) -> float:
323
+ """
324
+ RidgeCV finds optimal alpha from a fine logarithmic grid.
325
+ Evaluated on 5-fold time-series split (no shuffle).
326
+ """
327
  alphas = np.logspace(-3, 4, 60)
328
  scaler = StandardScaler()
329
  Xs = scaler.fit_transform(X_tr)
 
332
  return float(rcv.alpha_)
333
 
334
 
335
+ def train_ridge(df: pd.DataFrame, horizon: int = 1):
 
 
 
 
336
  """
337
+ Trains the premium Ridge model.
338
+ Target: log-return over `horizon` days (direction-aware).
339
+ Returns pipeline, test predictions (absolute), y_test (absolute), avail_cols.
 
 
 
 
340
  """
341
  avail = [c for c in FEATURE_COLS if c in df.columns]
342
  X = df[avail].values
343
  close = df['Close'].values
 
344
 
345
+ # --- FIX 1: Use log-return as target (direction-aware, scale-invariant) ---
346
+ # log-return = log(close[t+horizon] / close[t])
347
+ # This directly encodes direction: positive = UP, negative = DOWN
348
  y_logret = np.log(close[horizon:] / close[:len(close) - horizon])
 
 
349
 
350
+ X = X[:len(y_logret)]
351
+ close_aligned = close[:len(y_logret)]
 
352
 
353
+ split = -15 # 15-day test window for robust backtest
354
+ X_train, X_test = X[:split], X[split:]
355
+ y_train, y_logret_test = y_logret[:split], y_logret[split:]
 
356
  close_test = close_aligned[split:]
 
357
 
358
+ # Find best alpha on training data only
359
  best_alpha = select_best_alpha(X_train, y_train)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
360
 
361
+ # Build & fit pipeline
362
+ pipe = build_ridge_pipeline(best_alpha)
363
+ pipe.fit(X_train, y_train)
 
364
 
365
+ # Test predictions β†’ convert log-return back to absolute price
366
+ logret_hat = pipe.predict(X_test)
367
+ pred_abs = close_test * np.exp(logret_hat)
368
+ y_test_abs = close_test * np.exp(y_logret_test)
 
369
 
370
+ mae = mean_absolute_error(y_test_abs, pred_abs)
371
 
372
+ return pipe, pred_abs, y_test_abs, avail, best_alpha, mae
 
 
373
 
374
 
375
+ def predict_next_ridge(pipe, df: pd.DataFrame, avail: list,
376
+ horizon: int = 1) -> float:
377
+ """Predict absolute close price horizon days ahead using log-return."""
378
+ cp = float(df['Close'].iloc[-1])
379
+ x = df[avail].iloc[-1].values.reshape(1, -1)
380
+ logret = float(pipe.predict(x)[0])
381
+ # Scale logret by horizon for multi-day forecasts
382
+ return cp * np.exp(logret * horizon)
 
 
 
 
 
383
 
 
 
384
 
385
+ # ══════════════════════════════════════════════════════════════════════════════
386
+ # FIBONACCI LEVELS (son swing ΓΌzerinden)
387
+ # ══════════════════════════════════════════════════════════════════════════════
388
 
389
+ FIB_RATIOS = [0.0, 0.236, 0.382, 0.500, 0.618, 0.786, 1.0]
390
+ FIB_COLORS = ['#FFB800', '#FF6B35', '#7B61FF', '#00D4FF', '#00FFB2', '#E8EDF5', '#FFB800']
391
 
392
+ def compute_fibonacci_levels(df: pd.DataFrame, lookback: int = 55) -> dict:
393
+ """Son `lookback` barda swing high/low bulur, Fibonacci retracement seviyeleri dΓΆner."""
394
+ recent = df.tail(lookback)
395
+ swing_high = float(recent['High'].max())
396
+ swing_low = float(recent['Low'].min())
397
+ diff = swing_high - swing_low
398
+ levels = {f"Fib {int(r*100)}%": swing_high - diff * r for r in FIB_RATIOS}
399
+ levels['_high'] = swing_high
400
+ levels['_low'] = swing_low
401
+ return levels
402
 
 
 
 
 
403
 
404
+ def fibonacci_direction_signal(cp: float, fib: dict) -> dict:
405
+ """
406
+ Fiyatın hangi Fibonacci bâlgesinde olduğunu tespit eder.
407
+ Destek/direnç yakınlığına gâre yân skoru (+bias) üretir.
408
+ """
409
+ high, low = fib['_high'], fib['_low']
410
+ diff = high - low
411
+ if diff < 1e-10:
412
+ return {'fib_score': 0, 'fib_zone': 'N/A', 'fib_bias': 'NΓ–TR', 'fib_nearest': 0.0}
413
+
414
+ # Normalize: fiyatΔ±n retracement iΓ§indeki yΓΌzdesi (0=low, 1=high)
415
+ pos = (cp - low) / diff # 0β†’1 arasΔ±
416
+
417
+ # Hangi seviyeye en yakΔ±n?
418
+ nearest_r = min(FIB_RATIOS, key=lambda r: abs(pos - (1 - r)))
419
+ nearest_price = high - diff * nearest_r
420
+ dist_pct = abs(cp - nearest_price) / diff * 100
421
+
422
+ # YΓΆn bias:
423
+ # Fiyat 0.618 altΔ±nda β†’ destek bΓΆlgesi β†’ LONG bias
424
+ # Fiyat 0.382 ΓΌstΓΌnde β†’ direnΓ§ bΓΆlgesi β†’ SHORT bias
425
+ # %50 bΓΆlgesi nΓΆtr
426
+ if pos < 0.382: # Fiyat altta, gΓΌΓ§lΓΌ destek
427
+ fib_score = +25
428
+ fib_bias = "LONG β–² (Destek)"
429
+ elif pos < 0.50: # Alt-orta bΓΆlge
430
+ fib_score = +12
431
+ fib_bias = "LONG β–² (ZayΔ±f Destek)"
432
+ elif pos > 0.618: # Fiyat ΓΌstte, gΓΌΓ§lΓΌ direnΓ§
433
+ fib_score = -25
434
+ fib_bias = "SHORT β–Ό (DirenΓ§)"
435
+ elif pos > 0.50: # Üst-orta bâlge
436
+ fib_score = -12
437
+ fib_bias = "SHORT β–Ό (ZayΔ±f DirenΓ§)"
438
+ else:
439
+ fib_score = 0
440
+ fib_bias = "NΓ–TR (%50)"
441
+
442
+ return {
443
+ 'fib_score': fib_score,
444
+ 'fib_zone': f"{pos*100:.1f}% (Fib {int(nearest_r*100)}%'e yakΔ±n, Ξ”{dist_pct:.1f}%)",
445
+ 'fib_bias': fib_bias,
446
+ 'fib_nearest': nearest_price,
447
+ }
448
 
449
 
450
  # ═════════════════════════════════════════��════════════════════════════════════
 
510
  return {'score': min(100, score), 'label': label, 'details': det}
511
 
512
 
513
+ def signal_score_with_fib(df: pd.DataFrame, direction: str, fib_signal: dict) -> dict:
514
+ """signal_score + Fibonacci yΓΆn skoru."""
515
+ base = signal_score(df, direction)
516
+ fs = fib_signal['fib_score']
517
+ aligned = (direction == "LONG" and fs > 0) or (direction == "SHORT" and fs < 0)
518
+ contrib = abs(fs) if aligned else -(abs(fs) // 2)
519
+ new_score = int(np.clip(base['score'] + contrib, 0, 100))
520
+ label = "GΓœΓ‡LÜ βœ…" if new_score >= 75 else ("ORTA ⚠️" if new_score >= 45 else "ZAYIF ❌")
521
+ base['details']['Fibonacci'] = f"{fib_signal['fib_bias']} β†’ {'+' if contrib >= 0 else ''}{contrib}p"
522
+ base['score'] = new_score
523
+ base['label'] = label
524
+ return base
525
+
526
+
527
  # ══════════════════════════════════════════════════════════════════════════════
528
  # CHART ENGINE β€” phosphor-green terminal aesthetic
529
  # ══════════════════════════════════════════════════════════════════════════════
 
544
 
545
  def build_figure(df, pred_price, horizon, tp_sl,
546
  test_pred_abs, y_test_abs, best_alpha, mae,
547
+ fib_levels: dict = None) -> plt.Figure:
548
 
549
  plt.rcParams.update({
550
  'font.family': 'monospace',
 
597
  (i - 0.25, min(o, c)), 0.5, abs(c - o) + 1e-7,
598
  boxstyle="square,pad=0", linewidth=0, facecolor=color, alpha=0.75, zorder=3))
599
 
 
 
 
 
 
 
 
 
 
600
  # Close line overlay (phosphor glow)
601
  ax_price.plot(idx, tail['Close'].values, lw=1.6, color=ACID, zorder=5, alpha=0.9, label='Close')
602
 
 
605
  ax_price.axhline(tp_sl['sl'], color=RED, lw=1.4, ls='--', alpha=0.9, zorder=6)
606
  ax_price.axhline(cp, color=TEXT, lw=0.7, ls='-', alpha=0.35, zorder=4)
607
 
608
+ # ── FIBONACCI levels ──────────────────────────────────────────────────────
609
+ if fib_levels:
610
+ fib_draw = [(r, c) for r, c in zip(FIB_RATIOS, FIB_COLORS)]
611
+ for ratio, col in fib_draw:
612
+ key = f"Fib {int(ratio*100)}%"
613
+ if key in fib_levels:
614
+ fval = fib_levels[key]
615
+ ax_price.axhline(fval, color=col, lw=0.7, ls=':', alpha=0.55, zorder=3)
616
+ ax_price.text(0.5, fval, f" {key} {fval:.5f}",
617
+ color=col, fontsize=6.5, va='bottom', alpha=0.75,
618
+ fontfamily='monospace', transform=ax_price.get_yaxis_transform())
619
+
620
  r = len(idx) * 0.018
621
  for val, lbl, col in [
622
  (tp_sl['tp'], f"TP {tp_sl['tp']:.5f} (+{tp_sl['tp_pips']:.0f}p)", GREEN),
 
637
  fontfamily='monospace',
638
  )
639
 
 
 
 
 
 
 
 
 
640
  ax_price.legend(fontsize=7, framealpha=0.1, labelcolor=SUBTEXT,
641
  ncol=3, loc='upper left')
642
  ax_price.yaxis.set_major_formatter(mticker.FormatStrFormatter('%.4f'))
 
717
  dir_color = GREEN if tp_sl['direction'] == 'LONG' else WARM
718
 
719
  fig.text(0.055, 0.97,
720
+ "EUR/USD Β· RIDGE FORECAST ENGINE Β· ULTRA-PREMIUM",
721
  fontsize=12, color=TEXT, fontweight='bold', va='top',
722
  fontfamily='monospace')
723
  fig.text(0.055, 0.955, dir_label,
 
752
  if len(df) < 40:
753
  return "❌ Yetersiz veri (min 40 satΔ±r gerekli).", None
754
 
755
+ # 3. Train Ridge + backtest
756
+ pipe, test_pred_abs, y_test_abs, avail, best_alpha, mae = train_ridge(df, horizon)
 
 
 
 
 
 
 
 
757
 
758
+ # 4. Predict next
759
+ pred_price = predict_next_ridge(pipe, df, avail, horizon)
 
760
 
761
  # 5. TP/SL
762
+ cp = float(df['Close'].iloc[-1])
763
+ atr = float(df['ATR'].iloc[-1])
764
  tp_sl = compute_tp_sl(cp, pred_price, atr,
765
  atr_multiplier_sl=1.5, rr_ratio=2.0)
766
 
767
+ # 5b. Fibonacci seviyeleri & yΓΆn sinyali
768
+ fib = compute_fibonacci_levels(df, lookback=55)
769
+ fib_signal = fibonacci_direction_signal(cp, fib)
770
+
771
+ # 6. Signal score (fibonacci dahil)
772
+ sig = signal_score_with_fib(df, tp_sl['direction'], fib_signal)
773
 
774
  # 7. Backtest metrics
775
  rmse = float(np.sqrt(np.mean((test_pred_abs - y_test_abs) ** 2)))
776
+ # FIX 3: Correct directional accuracy β€” compare pred vs entry price
777
  close_vals = df['Close'].values
778
  n_test = len(test_pred_abs)
779
  entry_prices = close_vals[len(close_vals) - n_test - horizon :
 
806
  W = 54
807
  sep = "═" * W
808
 
 
809
  lines = [
810
  sep,
811
+ f" EUR/USD Β· RIDGE FORECAST ENGINE Β· ULTRA-PREMIUM",
812
  f" {datetime.now().strftime('%d %b %Y %H:%M:%S')}",
813
  sep,
814
  "",
815
  f" {'MEVCUT FΔ°YAT':<24}: {cp:.5f}",
816
+ f" {f'+{horizon}G TAHMΔ°N (RIDGE)':<24}: {pred_price:.5f} ({chg:+.3f}%)",
817
  f" {'YΓ–N':<24}: {tp_sl['direction']}",
 
 
 
818
  f" {'OPTΔ°MAL ALPHA (RidgeCV)':<24}: {best_alpha:.6f}",
819
  "",
820
  f" {'─── RΔ°SK YΓ–NETΔ°MΔ° (ATR Γ— 1.5 | R:R 1:2)':<{W-2}}",
 
825
  f" {'RΔ°SK %':<24}: {tp_sl['risk_pct']:.3f}%",
826
  f" {'ATR-14':<24}: {atr:.5f}",
827
  "",
828
+ f" {'─── FΔ°BONACCΔ° SEVΔ°YELERΔ° (55 Bar)':<{W-2}}",
829
+ f" {'Swing High':<24}: {fib['_high']:.5f}",
830
+ f" {'Swing Low':<24}: {fib['_low']:.5f}",
831
+ f" {'Fiyat Pozisyonu':<24}: {fib_signal['fib_zone']}",
832
+ f" {'Fibonacci Bias':<24}: {fib_signal['fib_bias']}",
833
+ ] + [
834
+ f" {' Fib ' + k.split()[1]:<24}: {v:.5f}"
835
+ for k, v in fib.items() if k.startswith('Fib')
836
+ ] + [
837
+ "",
838
+ f" {'─── SΔ°NYAL GÜCÜ (Fibonacci Dahil)':<{W-2}}",
839
  f" {'SKOR':<24}: {sig['score']:.0f} / 100 β†’ {sig['label']}",
840
  ]
841
  for k, v in sig['details'].items():
 
860
  f" {'RMSE':<24}: {rmse:.7f}",
861
  f" {'YΓΆn Doğruluğu':<24}: {hit:.1f}%",
862
  f" {'Feature SayΔ±sΔ±':<24}: {len(avail)}",
 
 
863
  "",
864
  sep,
865
  " ⚠ YalnΔ±zca eğitim amaΓ§lΔ±dΔ±r. Finansal tavsiye değildir.",
 
872
  fig = build_figure(
873
  df, pred_price, horizon, tp_sl,
874
  test_pred_abs, y_test_abs, best_alpha, mae,
875
+ fib_levels=fib,
 
876
  )
877
 
878
  return text, fig
 
989
  <div>
990
  <h1 style="margin:0 0 4px;font-size:1.45rem;font-weight:900;
991
  color:#E8EDF5;letter-spacing:.06em;font-family:monospace;">
992
+ EUR/USD &nbsp;Β·&nbsp; RIDGE FORECAST ENGINE
993
  <span style="color:#00FFB2;font-size:.85rem;margin-left:10px;
994
  vertical-align:middle;letter-spacing:.12em;">ULTRA-PREMIUM</span>
995
  </h1>
996
  <p style="margin:0;color:#3A4A5E;font-size:.75rem;font-family:monospace;
997
  letter-spacing:.04em;">
998
+ STACKED RIDGE Β· POLYNOMIAL INTERACTIONS Β· RIDGECV ALPHA Β· 75+ FEATURES Β· ATR TP/SL
999
  </p>
1000
  </div>
1001
  </div>
 
1029
  # GRADIO UI
1030
  # ══════════════════════════════════════════════════════════════════════════════
1031
 
1032
+ with gr.Blocks(css=CSS, title="EUR/USD Ridge Forecast Engine") as app:
1033
  gr.HTML(HEADER)
1034
 
1035
  with gr.Row(elem_classes="btn-row"):