GoshawkVortexAI commited on
Commit
fdc3d5f
Β·
verified Β·
1 Parent(s): f197921

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +658 -586
app.py CHANGED
@@ -1,64 +1,61 @@
1
  # ══════════════════════════════════════════════════════════════════════════════
2
- # EUR/USD Β· ENSEMBLE FORECAST ENGINE Β· Premium Edition
3
- # 5 Model: Random Forest Β· Gradient Boosting Β· AdaBoost Β· Ridge Β· SVR
4
- # TP/SL hesaplama Β· ATR tabanlΔ± risk yΓΆnetimi Β· Backtest analizi
5
- # Veri: Yahoo Finance (yfinance)
 
6
  # ══════════════════════════════════════════════════════════════════════════════
7
 
8
  import gradio as gr
9
  import pandas as pd
10
  import numpy as np
11
- from datetime import datetime, timedelta
12
  import yfinance as yf
13
- from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor, AdaBoostRegressor
14
- from sklearn.linear_model import Ridge
15
- from sklearn.svm import SVR
16
- from sklearn.preprocessing import StandardScaler
17
  import matplotlib
18
  matplotlib.use('Agg')
19
  import matplotlib.pyplot as plt
20
  import matplotlib.patches as mpatches
21
  from matplotlib.gridspec import GridSpec
22
  import matplotlib.ticker as mticker
23
- from matplotlib.lines import Line2D
24
  import traceback
25
  import warnings
26
  warnings.filterwarnings('ignore')
27
 
28
  # ══════════════════════════════════════════════════════════════════════════════
29
- # RENK PALETΔ°
30
  # ══════════════════════════════════════════════════════════════════════════════
31
- BG = "#0A0E17"
32
- PANEL = "#0F1623"
33
- PANEL2 = "#111827"
34
- BORDER = "#1E293B"
35
- ACCENT = "#3B82F6"
36
- ACCENT2 = "#6366F1"
37
- GREEN = "#10B981"
38
- RED = "#EF4444"
39
- AMBER = "#F59E0B"
40
- PURPLE = "#8B5CF6"
41
- PINK = "#EC4899"
42
- MUTED = "#64748B"
43
- TEXT = "#F1F5F9"
44
- SUBTEXT = "#94A3B8"
45
-
46
- MODEL_COLORS = [ACCENT, PURPLE, GREEN, AMBER, PINK]
 
47
 
48
  # ══════════════════════════════════════════════════════════════════════════════
49
- # VERΔ° KATMANI
50
  # ══════════════════════════════════════════════════════════════════════════════
51
 
52
- def get_forex_data(symbol: str = "EURUSD=X", period: str = "150d") -> pd.DataFrame | None:
53
- """
54
- Yahoo Finance'ten gΓΌnlΓΌk OHLCV verisi indirir.
55
- 150 gΓΌnlΓΌk pencere β€” SMA-50 ve lag-7 iΓ§in yeterli.
56
- """
57
  try:
58
  raw = yf.download(symbol, period=period, interval="1d", progress=False, auto_adjust=True)
59
- if raw is None or len(raw) < 60:
60
  return None
61
- # MultiIndex sΓΌtunlarΔ±nΔ± dΓΌzelt
62
  if isinstance(raw.columns, pd.MultiIndex):
63
  raw.columns = raw.columns.get_level_values(0)
64
  return raw
@@ -67,757 +64,832 @@ def get_forex_data(symbol: str = "EURUSD=X", period: str = "150d") -> pd.DataFra
67
 
68
 
69
  def compute_atr(df: pd.DataFrame, period: int = 14) -> pd.Series:
70
- """True Range tabanlΔ± ATR hesabΔ±."""
71
- high = df['High']
72
- low = df['Low']
73
- prev_close = df['Close'].shift(1)
74
- tr = pd.concat([
75
- high - low,
76
- (high - prev_close).abs(),
77
- (low - prev_close).abs(),
78
- ], axis=1).max(axis=1)
79
  return tr.rolling(period).mean()
80
 
81
 
82
  def compute_stoch(df: pd.DataFrame, k: int = 14, d: int = 3):
83
- """Stochastic Oscillator %K ve %D."""
84
- low_min = df['Low'].rolling(k).min()
85
- high_max = df['High'].rolling(k).max()
86
- pct_k = 100 * (df['Close'] - low_min) / (high_max - low_min + 1e-10)
87
- pct_d = pct_k.rolling(d).mean()
88
- return pct_k, pct_d
89
 
90
 
91
  def compute_cci(df: pd.DataFrame, period: int = 20) -> pd.Series:
92
- """Commodity Channel Index."""
93
- tp = (df['High'] + df['Low'] + df['Close']) / 3
94
  sma = tp.rolling(period).mean()
95
  mad = tp.rolling(period).apply(lambda x: np.mean(np.abs(x - x.mean())), raw=True)
96
  return (tp - sma) / (0.015 * mad + 1e-10)
97
 
98
 
99
  def compute_williams_r(df: pd.DataFrame, period: int = 14) -> pd.Series:
100
- """Williams %R."""
101
- high_max = df['High'].rolling(period).max()
102
- low_min = df['Low'].rolling(period).min()
103
- return -100 * (high_max - df['Close']) / (high_max - low_min + 1e-10)
 
 
 
 
 
 
104
 
105
 
106
  def create_features(data: pd.DataFrame) -> pd.DataFrame:
107
  """
108
- 22 teknik gΓΆsterge + 7 lag feature ΓΌretir.
109
- Toplam: 29 input feature.
110
  """
111
  df = data.copy()
112
 
113
- # ── Temel getiri & mum ΓΆzellikleri ───────────────────────────────────────
114
- df['Returns'] = df['Close'].pct_change()
115
- df['Log_Returns'] = np.log(df['Close'] / df['Close'].shift(1))
116
- df['High_Low'] = df['High'] - df['Low']
117
- df['Close_Open'] = df['Close'] - df['Open']
118
- df['Body_Ratio'] = df['Close_Open'].abs() / (df['High_Low'] + 1e-10)
119
- df['Upper_Shadow'] = df['High'] - df[['Close', 'Open']].max(axis=1)
120
- df['Lower_Shadow'] = df[['Close', 'Open']].min(axis=1) - df['Low']
121
-
122
- # ── Hareketli ortalamalar ─────────────────────────────────────────────────
123
- for w in [5, 10, 20, 50]:
 
124
  df[f'SMA_{w}'] = df['Close'].rolling(w).mean()
125
  df[f'EMA_{w}'] = df['Close'].ewm(span=w, adjust=False).mean()
126
 
127
- # SMA mesafe oranlarΔ±
128
- df['Price_SMA5_Ratio'] = df['Close'] / df['SMA_5']
129
- df['Price_SMA20_Ratio'] = df['Close'] / df['SMA_20']
130
- df['SMA5_SMA20_Cross'] = df['SMA_5'] - df['SMA_20']
 
 
 
 
 
131
 
132
- # ── RSI ───────────────────────────────────────────────────────────────────
133
- delta = df['Close'].diff()
134
- gain = delta.where(delta > 0, 0.0).rolling(14).mean()
135
- loss = (-delta.where(delta < 0, 0.0)).rolling(14).mean()
136
- rs = gain / (loss + 1e-10)
137
- df['RSI'] = 100 - (100 / (1 + rs))
138
 
139
- # ── MACD ──────────────────────────────────────────────────────────────────
 
 
 
 
 
 
140
  ema12 = df['Close'].ewm(span=12, adjust=False).mean()
141
  ema26 = df['Close'].ewm(span=26, adjust=False).mean()
142
  df['MACD'] = ema12 - ema26
143
  df['MACD_Signal'] = df['MACD'].ewm(span=9, adjust=False).mean()
144
  df['MACD_Hist'] = df['MACD'] - df['MACD_Signal']
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
 
146
- # ── Bollinger BantlarΔ± ────────────────────────────────────────────────────
147
- df['BB_middle'] = df['Close'].rolling(20).mean()
148
- df['BB_std'] = df['Close'].rolling(20).std()
149
- df['BB_upper'] = df['BB_middle'] + df['BB_std'] * 2
150
- df['BB_lower'] = df['BB_middle'] - df['BB_std'] * 2
151
- df['BB_width'] = (df['BB_upper'] - df['BB_lower']) / (df['BB_middle'] + 1e-10)
152
- df['BB_pct'] = (df['Close'] - df['BB_lower']) / (df['BB_upper'] - df['BB_lower'] + 1e-10)
153
-
154
- # ── ATR ───────────────────────────────────────────────��───────────────────
155
- df['ATR'] = compute_atr(df, 14)
156
- df['ATR_Pct'] = df['ATR'] / df['Close']
157
-
158
- # ── Stochastic ────────────────────────────────────────────────────────────
159
- df['Stoch_K'], df['Stoch_D'] = compute_stoch(df)
160
-
161
- # ── CCI ───────────────────────────────────────────────────────────────────
162
- df['CCI'] = compute_cci(df, 20)
163
-
164
- # ── Williams %R ───────────────────────────────────────────────────────────
165
- df['WilliamsR'] = compute_williams_r(df, 14)
166
-
167
- # ── Volatilite ────────────────────────────────────────────────────────────
168
- df['Volatility_5'] = df['Returns'].rolling(5).std()
169
- df['Volatility_20'] = df['Returns'].rolling(20).std()
170
-
171
- # ── Volume (varsa) ────────────────────────────────────────────────────────
172
- if 'Volume' in df.columns:
173
- df['Volume_MA'] = df['Volume'].rolling(10).mean()
174
- df['Volume_Ratio'] = df['Volume'] / (df['Volume_MA'] + 1e-10)
175
  else:
176
- df['Volume_MA'] = 0.0
177
  df['Volume_Ratio'] = 1.0
178
-
179
- # ── Lag features ──────────────────────────────────────────────────────────
180
- for i in range(1, 8):
181
- df[f'Lag_{i}'] = df['Close'].shift(i)
182
 
183
  return df.dropna()
184
 
185
 
186
  # ══════════════════════════════════════════════════════════════════════════════
187
- # FEATURE KOLONLARI
188
  # ══════════════════════════════════════════════════════════════════════════════
189
  FEATURE_COLS = [
190
- # Temel
191
  'Returns', 'Log_Returns', 'High_Low', 'Close_Open',
192
- 'Body_Ratio', 'Upper_Shadow', 'Lower_Shadow',
193
- # SMA / EMA
194
- 'SMA_5', 'SMA_10', 'SMA_20', 'SMA_50',
195
- 'EMA_5', 'EMA_10', 'EMA_20',
196
- 'Price_SMA5_Ratio', 'Price_SMA20_Ratio', 'SMA5_SMA20_Cross',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  # Momentum
198
- 'RSI', 'MACD', 'MACD_Signal', 'MACD_Hist',
199
- 'Stoch_K', 'Stoch_D', 'CCI', 'WilliamsR',
200
- # Volatilite / BB
201
- 'BB_middle', 'BB_upper', 'BB_lower', 'BB_width', 'BB_pct',
202
- 'ATR', 'ATR_Pct',
203
- 'Volatility_5', 'Volatility_20',
 
 
204
  # Volume
205
- 'Volume_Ratio',
206
- # Lag
207
- 'Lag_1', 'Lag_2', 'Lag_3', 'Lag_4', 'Lag_5', 'Lag_6', 'Lag_7',
208
  ]
209
 
 
210
  # ══════════════════════════════════════════════════════════════════════════════
211
- # MODEL KATMANI
 
 
212
  # ══════════════════════════════════════════════════════════════════════════════
213
 
214
- def build_models() -> dict:
215
- """Her çağrıda taze model ârnekleri dândürür (paralel eğitim güvenliği)."""
216
- return {
217
- 'Random Forest': RandomForestRegressor(
218
- n_estimators=300, max_depth=12,
219
- min_samples_leaf=3, random_state=42, n_jobs=-1),
220
- 'Gradient Boosting': GradientBoostingRegressor(
221
- n_estimators=300, learning_rate=0.04,
222
- max_depth=5, subsample=0.8, random_state=42),
223
- 'AdaBoost': AdaBoostRegressor(
224
- n_estimators=200, learning_rate=0.08, random_state=42),
225
- 'Ridge': Ridge(alpha=0.3),
226
- 'SVR': SVR(kernel='rbf', C=500, gamma='scale', epsilon=0.00005),
227
- }
228
-
229
-
230
- def train_and_evaluate(df: pd.DataFrame):
231
  """
232
- Modelleri eğitir; son 10 günü test seti olarak kullanır.
233
- Hedef: yarınki kapanış - bugünkü kapanış (fiyat değişimi / delta).
234
- Bu sayede Ridge dahil tΓΌm modeller mutlak fiyata yakΔ±nsamak yerine
235
- gerçek yân ve büyüklük sinyali âğrenir.
236
- Dânüş: trained_models, scaler, test_preds (mutlak fiyat), y_test (mutlak fiyat), avail
 
 
 
 
 
 
 
 
237
  """
238
  avail = [c for c in FEATURE_COLS if c in df.columns]
239
  X = df[avail].values
240
  close = df['Close'].values
241
 
242
- # Hedef: bir sonraki günün fiyat değişimi (delta)
243
- y_delta = np.diff(close) # uzunluk = N-1
244
- X = X[:-1] # son satΔ±rΔ± at (yarΔ±nΔ± bilinmiyor)
245
- close_aligned = close[:-1] # delta ile hizalı kapanışlar
 
 
 
 
246
 
247
- split = -10
248
- X_train, X_test = X[:split], X[split:]
249
- y_train, y_delta_test = y_delta[:split], y_delta[split:]
250
  close_test = close_aligned[split:]
251
 
252
- scaler = StandardScaler()
253
- X_tr_sc = scaler.fit_transform(X_train)
254
- X_te_sc = scaler.transform(X_test)
255
 
256
- models = build_models()
257
- trained = {}
258
- test_preds = {} # grafik/metrik uyumu iΓ§in mutlak fiyat olarak sakla
259
 
260
- for name, m in models.items():
261
- m.fit(X_tr_sc, y_train)
262
- delta_pred = m.predict(X_te_sc)
263
- test_preds[name] = close_test + delta_pred # delta β†’ mutlak fiyat
264
- trained[name] = m
265
 
266
- y_test = close_test + y_delta_test # gerΓ§ek mutlak fiyat (backtest iΓ§in)
267
 
268
- return trained, scaler, test_preds, y_test, avail
269
 
270
 
271
- def predict_next(trained: dict, scaler, df: pd.DataFrame, avail: list):
272
- """
273
- Son satΔ±rΔ±n feature'larΔ±ndan ertesi gΓΌn tahmini ΓΌretir.
274
- Model delta (değişim) tahmin eder; mevcut kapanışa eklenerek
275
- mutlak fiyata dânüştürülür.
276
- """
277
  cp = float(df['Close'].iloc[-1])
278
  x = df[avail].iloc[-1].values.reshape(1, -1)
279
- xs = scaler.transform(x)
280
- preds = {n: cp + float(m.predict(xs)[0]) for n, m in trained.items()}
281
- ensemble = float(np.mean(list(preds.values())))
282
- return preds, ensemble
283
 
284
 
285
  # ══════════════════════════════════════════════════════════════════════════════
286
- # TP / SL HESAPLAMA (ATR tabanlΔ± risk yΓΆnetimi)
287
  # ══════════════════════════════════════════════════════════════════════════════
288
 
289
- def compute_tp_sl(
290
- current_price: float,
291
- ensemble_pred: float,
292
- atr: float,
293
- atr_multiplier_sl: float = 1.5,
294
- rr_ratio: float = 2.0,
295
- ) -> dict:
296
- """
297
- ATR tabanlΔ± TP / SL hesaplar.
298
 
299
- YΓΆn: ensemble > current β†’ LONG | ensemble < current β†’ SHORT
 
 
 
300
 
301
- SL = current Β± (ATR Γ— atr_multiplier_sl)
302
- TP = current Β± (|SL mesafesi| Γ— rr_ratio)
 
 
 
303
 
304
- Dânüş: direction, entry, sl, tp, sl_pips, tp_pips, rr, risk_pct
305
- """
306
- direction = "LONG" if ensemble_pred > current_price else "SHORT"
307
- sl_dist = atr * atr_multiplier_sl
308
- tp_dist = sl_dist * rr_ratio
309
 
310
- if direction == "LONG":
311
- sl = current_price - sl_dist
312
- tp = current_price + tp_dist
313
- else:
314
- sl = current_price + sl_dist
315
- tp = current_price - tp_dist
316
-
317
- pip_size = 0.0001 # EUR/USD standart pip
318
- sl_pips = sl_dist / pip_size
319
- tp_pips = tp_dist / pip_size
320
- risk_pct = (sl_dist / current_price) * 100
321
-
322
- return {
323
- 'direction' : direction,
324
- 'entry' : current_price,
325
- 'sl' : sl,
326
- 'tp' : tp,
327
- 'sl_pips' : sl_pips,
328
- 'tp_pips' : tp_pips,
329
- 'rr' : rr_ratio,
330
- 'risk_pct' : risk_pct,
331
- 'atr' : atr,
332
- }
333
-
334
-
335
- def compute_signal_score(df: pd.DataFrame, direction: str) -> dict:
336
- """
337
- RSI Β· MACD Β· Stoch Β· CCI Β· BB pozisyonuna bakarak
338
- sinyal gΓΌcΓΌnΓΌ 0–100 arasΔ±nda puanlar.
339
- """
340
- row = df.iloc[-1]
341
- score = 0
342
- details = {}
343
 
344
- rsi = float(row['RSI'])
345
  if direction == "LONG":
346
- if rsi < 70:
347
- pts = max(0, (70 - rsi) / 70 * 25)
348
- score += pts
349
- details['RSI'] = f"{rsi:.1f} β€” boğa bΓΆlgesi (+{pts:.0f}p)"
350
- else:
351
- details['RSI'] = f"{rsi:.1f} β€” aşırΔ± alΔ±m (0p)"
352
  else:
353
- if rsi > 30:
354
- pts = max(0, (rsi - 30) / 70 * 25)
355
- score += pts
356
- details['RSI'] = f"{rsi:.1f} β€” ayΔ± bΓΆlgesi (+{pts:.0f}p)"
357
- else:
358
- details['RSI'] = f"{rsi:.1f} β€” aşırΔ± satΔ±m (0p)"
359
-
360
- macd = float(row['MACD'])
361
- mhist = float(row['MACD_Hist'])
362
  if direction == "LONG":
363
  pts = 20 if (macd > 0 and mhist > 0) else (10 if macd > 0 else 0)
364
  else:
365
  pts = 20 if (macd < 0 and mhist < 0) else (10 if macd < 0 else 0)
366
- score += pts
367
- details['MACD'] = f"macd={macd:.5f} hist={mhist:.5f} (+{pts}p)"
368
 
369
- stk = float(row['Stoch_K'])
370
- std = float(row['Stoch_D'])
371
  if direction == "LONG":
372
  pts = 20 if (stk < 80 and stk > std) else (10 if stk < 80 else 0)
373
  else:
374
  pts = 20 if (stk > 20 and stk < std) else (10 if stk > 20 else 0)
375
- score += pts
376
- details['Stoch'] = f"K={stk:.1f} D={std:.1f} (+{pts}p)"
377
 
378
- cci = float(row['CCI'])
379
- if direction == "LONG":
380
- pts = 20 if cci > -100 else 5
381
- else:
382
- pts = 20 if cci < 100 else 5
383
- score += pts
384
- details['CCI'] = f"{cci:.1f} (+{pts}p)"
385
 
386
- bb_pct = float(row['BB_pct'])
387
- if direction == "LONG":
388
- pts = 15 if bb_pct < 0.5 else 5
389
- else:
390
- pts = 15 if bb_pct > 0.5 else 5
391
- score += pts
392
- details['BB%'] = f"{bb_pct:.2f} (+{pts}p)"
393
 
394
- label = ("GΓœΓ‡LÜ βœ…" if score >= 75
395
- else "ORTA ⚠" if score >= 45
396
- else "ZAYIF ❌")
397
-
398
- return {'score': min(100, score), 'label': label, 'details': details}
399
 
400
 
401
  # ══════════════════════════════════════════════════════════════════════════════
402
- # GRAFΔ°K MOTORU
403
  # ══════════════════════════════════════════════════════════════════════════════
404
 
405
- def _set_ax_style(ax, title: str = "", ylabel: str = ""):
406
  ax.set_facecolor(PANEL)
407
  ax.tick_params(colors=SUBTEXT, labelsize=7)
408
  for spine in ax.spines.values():
409
- spine.set_edgecolor(BORDER)
410
- ax.grid(True, color=BORDER, linewidth=0.4, alpha=0.6)
 
411
  if title:
412
- ax.set_title(title, fontsize=8.5, color=TEXT, pad=7, loc='left', fontweight='bold')
 
413
  if ylabel:
414
- ax.set_ylabel(ylabel, fontsize=7, color=SUBTEXT)
415
 
416
 
417
- def build_figure(
418
- df: pd.DataFrame,
419
- next_preds: dict,
420
- ensemble_pred: float,
421
- test_preds: dict,
422
- y_test: np.ndarray,
423
- tp_sl: dict,
424
- ) -> plt.Figure:
425
 
426
  plt.rcParams.update({
427
- 'font.family' : 'monospace',
428
- 'text.color' : TEXT,
429
- 'figure.facecolor': BG,
430
- 'axes.facecolor' : PANEL,
431
- 'axes.edgecolor' : BORDER,
432
- 'grid.color' : BORDER,
433
- 'grid.linewidth' : 0.4,
434
  })
435
 
436
- fig = plt.figure(figsize=(16, 15))
437
- gs = GridSpec(4, 2, figure=fig, hspace=0.52, wspace=0.32,
438
- left=0.06, right=0.97, top=0.94, bottom=0.04)
 
439
 
440
- ax_price = fig.add_subplot(gs[0, :]) # Fiyat + BB + TP/SL
441
- ax_rsi = fig.add_subplot(gs[1, 0]) # RSI
442
- ax_macd = fig.add_subplot(gs[1, 1]) # MACD
443
- ax_stoch = fig.add_subplot(gs[2, 0]) # Stochastic
444
- ax_cci = fig.add_subplot(gs[2, 1]) # CCI
445
- ax_models = fig.add_subplot(gs[3, :]) # Model karşılaştırması
446
 
447
- tail = df.tail(50)
448
  idx = np.arange(len(tail))
449
  cp = float(df['Close'].iloc[-1])
450
-
451
- # ── PANEL 1: Fiyat + BB + TP/SL ─────────────────────────────────────────
452
- _set_ax_style(ax_price, 'EUR/USD Β· Son 50 GΓΌn + Sonraki GΓΌn Tahmini (TP/SL)', 'Fiyat')
453
- ax_price.fill_between(idx,
454
- tail['BB_lower'].values, tail['BB_upper'].values,
455
- alpha=0.07, color=ACCENT)
456
- ax_price.plot(idx, tail['BB_upper'].values, lw=0.6, color=ACCENT, alpha=0.35, ls='--')
457
- ax_price.plot(idx, tail['BB_lower'].values, lw=0.6, color=ACCENT, alpha=0.35, ls='--')
458
- ax_price.plot(idx, tail['BB_middle'].values, lw=0.7, color=ACCENT, alpha=0.5, ls=':')
459
- ax_price.plot(idx, tail['Close'].values, lw=1.8, color=TEXT, label='Kapanış', zorder=5)
460
- ax_price.plot(idx, tail['SMA_20'].values, lw=1.0, color=AMBER, alpha=0.8, label='SMA 20', ls='--')
461
- ax_price.plot(idx, tail['SMA_50'].values, lw=1.0, color=MUTED, alpha=0.7, label='SMA 50', ls=':')
462
-
463
- # TP / SL yatay Γ§izgiler
464
- last_x = len(idx) - 1
465
- ax_price.axhline(y=tp_sl['tp'], color=GREEN, lw=1.3, ls='--', alpha=0.9, zorder=4)
466
- ax_price.axhline(y=tp_sl['sl'], color=RED, lw=1.3, ls='--', alpha=0.9, zorder=4)
467
- ax_price.axhline(y=cp, color=TEXT, lw=0.7, ls='-', alpha=0.4, zorder=3)
468
-
469
- r_margin = len(idx) * 0.02
470
- ax_price.text(last_x + r_margin, tp_sl['tp'],
471
- f" TP {tp_sl['tp']:.5f} (+{tp_sl['tp_pips']:.0f}p)",
472
- color=GREEN, fontsize=7.5, va='center', fontweight='bold')
473
- ax_price.text(last_x + r_margin, tp_sl['sl'],
474
- f" SL {tp_sl['sl']:.5f} (-{tp_sl['sl_pips']:.0f}p)",
475
- color=RED, fontsize=7.5, va='center', fontweight='bold')
476
- ax_price.text(last_x + r_margin, cp,
477
- f" GİRİŞ {cp:.5f}",
478
- color=TEXT, fontsize=7.5, va='center')
479
-
480
- # Ensemble ok
 
 
 
 
 
 
 
 
 
 
 
 
 
481
  ax_price.annotate(
482
- f"β–Ί ENS {ensemble_pred:.5f}",
483
- xy=(last_x, cp),
484
- xytext=(last_x + len(idx) * 0.04, ensemble_pred),
485
- arrowprops=dict(arrowstyle='->', color=ACCENT2, lw=1.4, connectionstyle='arc3,rad=0.2'),
486
- color=ACCENT2, fontsize=8, fontweight='bold', zorder=6,
 
 
487
  )
488
 
489
- ax_price.legend(fontsize=7, framealpha=0.15, labelcolor=SUBTEXT, ncol=5, loc='upper left')
 
490
  ax_price.yaxis.set_major_formatter(mticker.FormatStrFormatter('%.4f'))
491
 
492
- # ── PANEL 2: RSI ─────────────────────────────────────────────────────────
493
- _set_ax_style(ax_rsi, 'RSI (14)', 'RSI')
494
- rsi_v = tail['RSI'].values
495
- ax_rsi.plot(idx, rsi_v, lw=1.4, color=ACCENT)
496
- ax_rsi.fill_between(idx, rsi_v, 70, where=(rsi_v >= 70), alpha=0.18, color=RED)
497
- ax_rsi.fill_between(idx, rsi_v, 30, where=(rsi_v <= 30), alpha=0.18, color=GREEN)
498
- ax_rsi.fill_between(idx, rsi_v, 50, where=((rsi_v > 50) & (rsi_v < 70)), alpha=0.07, color=GREEN)
499
- ax_rsi.fill_between(idx, rsi_v, 50, where=((rsi_v < 50) & (rsi_v > 30)), alpha=0.07, color=RED)
500
- for lvl, lbl, c in [(70, 'OB', RED), (50, '50', MUTED), (30, 'OS', GREEN)]:
501
- ax_rsi.axhline(lvl, color=c, lw=0.7, ls='--', alpha=0.5)
502
- ax_rsi.text(len(idx), lvl, f' {lbl}', color=c, fontsize=6.5, va='center')
503
  ax_rsi.set_ylim(0, 100)
 
504
 
505
- # ── PANEL 3: MACD ─────────────────────────────────────────────────────────
506
- _set_ax_style(ax_macd, 'MACD (12 / 26 / 9)')
507
  macd_v = tail['MACD'].values
508
  sig_v = tail['MACD_Signal'].values
509
  hist_v = macd_v - sig_v
510
- hcolors = [GREEN if v >= 0 else RED for v in hist_v]
511
- ax_macd.bar(idx, hist_v, color=hcolors, alpha=0.45, width=0.75, label='Histogram')
512
- ax_macd.plot(idx, macd_v, lw=1.3, color=ACCENT, label='MACD')
513
- ax_macd.plot(idx, sig_v, lw=1.1, color=AMBER, label='Signal', ls='--')
514
- ax_macd.axhline(0, color=MUTED, lw=0.7)
515
- ax_macd.legend(fontsize=6.5, framealpha=0.1, labelcolor=SUBTEXT, ncol=3)
516
-
517
- # ── PANEL 4: Stochastic ───────────────────────────────────────────────────
518
- _set_ax_style(ax_stoch, 'Stochastic (14,3)', '%')
519
- stk_v = tail['Stoch_K'].values
520
- std_v = tail['Stoch_D'].values
521
- ax_stoch.plot(idx, stk_v, lw=1.3, color=ACCENT, label='%K')
522
- ax_stoch.plot(idx, std_v, lw=1.0, color=AMBER, label='%D', ls='--')
523
- ax_stoch.fill_between(idx, stk_v, 80, where=(stk_v >= 80), alpha=0.12, color=RED)
524
- ax_stoch.fill_between(idx, stk_v, 20, where=(stk_v <= 20), alpha=0.12, color=GREEN)
 
 
 
 
525
  for lvl, c in [(80, RED), (20, GREEN)]:
526
- ax_stoch.axhline(lvl, color=c, lw=0.7, ls='--', alpha=0.5)
527
  ax_stoch.set_ylim(-5, 105)
528
- ax_stoch.legend(fontsize=6.5, framealpha=0.1, labelcolor=SUBTEXT, ncol=2)
529
-
530
- # ── PANEL 5: CCI ──────────────────────────────────────────────────────────
531
- _set_ax_style(ax_cci, 'CCI (20)')
532
- cci_v = tail['CCI'].values
533
- ax_cci.plot(idx, cci_v, lw=1.3, color=PURPLE)
534
- ax_cci.fill_between(idx, cci_v, 100, where=(cci_v >= 100), alpha=0.12, color=RED)
535
- ax_cci.fill_between(idx, cci_v, -100, where=(cci_v <= -100), alpha=0.12, color=GREEN)
 
 
536
  for lvl, c in [(100, RED), (0, MUTED), (-100, GREEN)]:
537
- ax_cci.axhline(lvl, color=c, lw=0.7, ls='--', alpha=0.5)
538
-
539
- # ── PANEL 6: Model karşılaştΔ±rmasΔ± ────────────────────────────────────────
540
- _set_ax_style(ax_models, '5 Model Tahmini vs Giriş Fiyatı · (ATR TP/SL dahil)', 'Fiyat')
541
- names = list(next_preds.keys())
542
- values = [next_preds[n] for n in names]
543
- maes = [float(np.mean(np.abs(test_preds[n] - y_test))) for n in names]
544
- x_pos = np.arange(len(names))
545
-
546
- bcolors = [GREEN if v > cp else RED for v in values]
547
- bars = ax_models.bar(x_pos, values, color=bcolors, alpha=0.70,
548
- width=0.55, zorder=3, edgecolor=BORDER, linewidth=0.5)
549
-
550
- ax_models.axhline(y=cp, color=TEXT, lw=1.4, ls='--', alpha=0.7,
551
- label=f'Giriş {cp:.5f}', zorder=4)
552
- ax_models.axhline(y=ensemble_pred, color=ACCENT2, lw=1.6, ls='-', alpha=0.9,
553
- label=f'Ensemble {ensemble_pred:.5f}', zorder=4)
554
- ax_models.axhline(y=tp_sl['tp'], color=GREEN, lw=1.0, ls=':', alpha=0.7,
555
- label=f"TP {tp_sl['tp']:.5f}", zorder=3)
556
- ax_models.axhline(y=tp_sl['sl'], color=RED, lw=1.0, ls=':', alpha=0.7,
557
- label=f"SL {tp_sl['sl']:.5f}", zorder=3)
558
-
559
- for bar, val, mae in zip(bars, values, maes):
560
- pct = (val - cp) / cp * 100
561
- sign = '+' if pct >= 0 else ''
562
- label = f"{sign}{pct:.3f}%\nMAE {mae:.5f}"
563
- y_off = 0.00003 if val >= cp else -0.00006
564
- va = 'bottom' if val >= cp else 'top'
565
- ax_models.text(bar.get_x() + bar.get_width() / 2,
566
- val + y_off, label,
567
- ha='center', va=va, fontsize=7, color=TEXT, fontweight='bold')
568
-
569
- ax_models.set_xticks(x_pos)
570
- ax_models.set_xticklabels(names, fontsize=8.5, color=TEXT)
571
- ax_models.legend(fontsize=7.5, framealpha=0.15, labelcolor=SUBTEXT, ncol=4)
572
- ax_models.yaxis.set_major_formatter(mticker.FormatStrFormatter('%.4f'))
573
-
574
- # ── Ana başlΔ±k ────────────────────────────────────────────────────────────
575
- direction_label = f"{'β–² LONG' if tp_sl['direction'] == 'LONG' else 'β–Ό SHORT'} Β· " \
576
- f"TP +{tp_sl['tp_pips']:.0f}p Β· SL -{tp_sl['sl_pips']:.0f}p Β· " \
577
- f"R:R 1:{tp_sl['rr']}"
578
- dir_color = GREEN if tp_sl['direction'] == 'LONG' else RED
579
-
580
- fig.text(0.06, 0.97, "EUR/USD Β· ENSEMBLE FORECAST ENGINE",
581
- fontsize=13, color=TEXT, fontweight='bold', va='top')
582
- fig.text(0.06, 0.955, direction_label,
583
- fontsize=9, color=dir_color, va='top')
584
- fig.text(0.97, 0.97, datetime.now().strftime('%d %b %Y %H:%M'),
585
- fontsize=8, color=SUBTEXT, va='top', ha='right')
586
 
587
  return fig
588
 
589
 
590
  # ══════════════════════════════════════════════════════════════════════════════
591
- # ANA TAHMΔ°N FONKSΔ°YONU
592
  # ══════════════════════════════════════════════════════════════════════════════
593
 
594
- def predict_eurusd():
595
  try:
596
- # 1. Veri
597
  data = get_forex_data()
598
  if data is None:
599
  return "❌ Veri indirilemedi. Δ°nternet bağlantΔ±nΔ±zΔ± kontrol edin.", None
600
 
601
- # 2. Feature mühendisliği
602
  df = create_features(data)
603
- if len(df) < 20:
604
- return "❌ Yeterli veri yok (min 20 satır gerekli).", None
605
 
606
- # 3. Eğitim & backtest
607
- trained, scaler, test_preds, y_test, avail = train_and_evaluate(df)
608
 
609
- # 4. Tahmin
610
- next_preds, ensemble = predict_next(trained, scaler, df, avail)
611
 
612
- # 5. TP / SL
613
  cp = float(df['Close'].iloc[-1])
614
  atr = float(df['ATR'].iloc[-1])
615
- tp_sl = compute_tp_sl(
616
- current_price = cp,
617
- ensemble_pred = ensemble,
618
- atr = atr,
619
- atr_multiplier_sl = 1.5,
620
- rr_ratio = 2.0,
621
- )
622
-
623
- # 6. Sinyal gΓΌcΓΌ
624
- sig = compute_signal_score(df, tp_sl['direction'])
625
-
626
- # 7. Backtest metrikleri
627
- ens_test = np.mean([test_preds[n] for n in test_preds], axis=0)
628
- ens_mae = float(np.mean(np.abs(ens_test - y_test)))
629
- ens_rmse = float(np.sqrt(np.mean((ens_test - y_test) ** 2)))
630
- hit_arr = np.sign(ens_test[1:] - ens_test[:-1]) == np.sign(np.diff(y_test))
631
- hit_rate = float(np.mean(hit_arr)) * 100
632
-
633
- # 8. Teknik durum
634
- rsi_val = float(df['RSI'].iloc[-1])
635
- macd_val = float(df['MACD'].iloc[-1])
636
- mhist_val = float(df['MACD_Hist'].iloc[-1])
637
- stk_val = float(df['Stoch_K'].iloc[-1])
638
- std_val = float(df['Stoch_D'].iloc[-1])
639
- cci_val = float(df['CCI'].iloc[-1])
640
- wr_val = float(df['WilliamsR'].iloc[-1])
641
- bb_pct_val = float(df['BB_pct'].iloc[-1])
642
- vol20_val = float(df['Volatility_20'].iloc[-1]) * 100
643
- atr_pct = float(df['ATR_Pct'].iloc[-1]) * 100
644
-
645
- rsi_lbl = "AşırΔ± AlΔ±m ⚠" if rsi_val > 70 else ("AşırΔ± SatΔ±m ⚠" if rsi_val < 30 else "NΓΆtr βœ“")
646
- macd_lbl = "Boğa β–²" if macd_val > 0 else "AyΔ± β–Ό"
647
- stk_lbl = "%K > %D β–²" if stk_val > std_val else "%K < %D β–Ό"
648
- ens_chg = (ensemble - cp) / cp * 100
649
-
650
- # ── Rapor metni ───────────────────────────────────────────────────────
651
- W = 52 # satır genişliği
652
- sep = "━" * W
653
- sep2 = "─" * W
654
 
655
  lines = [
656
  sep,
657
- f" EUR/USD Β· ENSEMBLE FORECAST ENGINE",
658
  f" {datetime.now().strftime('%d %b %Y %H:%M:%S')}",
659
  sep,
660
  "",
661
- f" {'MEVCUT FΔ°YAT':<22}: {cp:.5f}",
662
- f" {'ENSEMBLE TAHMΔ°N':<22}: {ensemble:.5f} ({ens_chg:+.3f}%)",
663
- f" {'YΓ–N':<22}: {tp_sl['direction']}",
 
664
  "",
665
- f" {'─── RΔ°SK YΓ–NETΔ°MΔ° (ATR Γ— 1.5 / R:R 1:2)':<{W-2}}",
666
- f" {'GİRİŞ':<22}: {tp_sl['entry']:.5f}",
667
- f" {'TAKE PROFIT (TP)':<22}: {tp_sl['tp']:.5f} (+{tp_sl['tp_pips']:.0f} pip)",
668
- f" {'STOP LOSS (SL)':<22}: {tp_sl['sl']:.5f} (-{tp_sl['sl_pips']:.0f} pip)",
669
- f" {'R:R ORANI':<22}: 1 : {tp_sl['rr']:.1f}",
670
- f" {'RISK (%)':<22}: {tp_sl['risk_pct']:.3f}%",
671
- f" {'ATR (14)':<22}: {atr:.5f} ({atr_pct:.3f}%)",
672
  "",
673
  f" {'─── SΔ°NYAL GÜCÜ':<{W-2}}",
674
- f" {'SKOR':<22}: {sig['score']:.0f} / 100 β†’ {sig['label']}",
675
  ]
676
-
677
- for ind, detail in sig['details'].items():
678
- lines.append(f" {ind:<20}: {detail}")
679
 
680
  lines += [
681
  "",
682
  f" {'─── TEKNΔ°K GΓ–STERGELER':<{W-2}}",
683
- f" {'RSI (14)':<22}: {rsi_val:.2f} β†’ {rsi_lbl}",
684
- f" {'MACD':<22}: {macd_val:.5f} β†’ {macd_lbl}",
685
- f" {'MACD Histogram':<22}: {mhist_val:.5f}",
686
- f" {'Stochastic K/D':<22}: {stk_val:.1f} / {std_val:.1f} β†’ {stk_lbl}",
687
- f" {'CCI (20)':<22}: {cci_val:.1f}",
688
- f" {'Williams %R':<22}: {wr_val:.1f}",
689
- f" {'BB Pozisyonu (%)':<22}: {bb_pct_val:.3f}",
690
- f" {'Volatilite (20g Οƒ)':<22}: {vol20_val:.4f}%",
 
 
691
  "",
692
- f" {'─── MODEL TAHMΔ°NLERΔ°':<{W-2}}",
693
- ]
694
-
695
- for name, val in next_preds.items():
696
- chg = (val - cp) / cp * 100
697
- mae = float(np.mean(np.abs(test_preds[name] - y_test)))
698
- rmse = float(np.sqrt(np.mean((test_preds[name] - y_test) ** 2)))
699
- ar = "β–²" if chg > 0 else "β–Ό"
700
- lines.append(
701
- f" {name:<22}: {val:.5f} ({chg:+.3f}%) {ar}"
702
- f" MAE={mae:.5f} RMSE={rmse:.5f}"
703
- )
704
-
705
- lines += [
706
- "",
707
- f" {'─── BACKTEST (Son 10 GΓΌn)':<{W-2}}",
708
- f" {'Ensemble MAE':<22}: {ens_mae:.6f}",
709
- f" {'Ensemble RMSE':<22}: {ens_rmse:.6f}",
710
- f" {'Yân Doğruluğu':<22}: {hit_rate:.1f}%",
711
  "",
712
  sep,
713
- f" ⚠ Yalnızca eğitim amaçlıdır. Finansal tavsiye değildir.",
714
  sep,
715
  ]
716
 
717
- output_text = "\n".join(lines)
718
 
719
- # 9. Grafik
720
- fig = build_figure(df, next_preds, ensemble, test_preds, y_test, tp_sl)
 
 
 
721
 
722
- return output_text, fig
723
 
724
  except Exception:
725
  return f"❌ Hata:\n{traceback.format_exc()}", None
726
 
727
 
 
 
 
 
728
  # ══════════════════════════════════════════════════════════════════════════════
729
- # CSS & HTML
730
  # ══════════════════════════════════════════════════════════════════════════════
731
 
732
  CSS = """
733
- * { box-sizing: border-box; }
 
734
  body, .gradio-container {
735
- background: #0A0E17 !important;
736
- font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace !important;
737
- color: #F1F5F9 !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
738
  }
739
- button.lg.primary {
740
- background: linear-gradient(135deg, #1D4ED8 0%, #4F46E5 100%) !important;
741
- border: none !important;
742
- color: #fff !important;
743
- font-weight: 700 !important;
744
- letter-spacing: .06em !important;
745
- padding: 14px 36px !important;
746
- border-radius: 6px !important;
747
- font-size: 1rem !important;
748
- transition: filter .2s !important;
749
  }
750
- button.lg.primary:hover { filter: brightness(1.15) !important; }
 
751
  .gr-textbox textarea, textarea {
752
- background: #0F1623 !important;
753
- color: #94A3B8 !important;
754
- border: 1px solid #1E293B !important;
755
  font-family: 'JetBrains Mono', monospace !important;
756
- font-size: 12px !important;
757
- line-height: 1.8 !important;
758
- border-radius: 6px !important;
 
759
  }
760
- label span { color: #64748B !important; font-size: .78rem !important; }
761
- .gr-panel, .gr-box { background: #111827 !important; border: 1px solid #1E293B !important; }
 
762
  footer { display: none !important; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
763
  """
764
 
765
  HEADER = """
766
- <div style="
767
- background: linear-gradient(160deg,#0A0E17 0%,#0F1623 60%,#111827 100%);
768
- border: 1px solid #1E293B;
769
- border-radius: 10px;
770
- padding: 28px 32px 22px;
771
- margin-bottom: 12px;
 
772
  ">
773
- <div style="display:flex;align-items:center;gap:16px;margin-bottom:12px;">
774
- <span style="font-size:2.4rem;line-height:1;">πŸ’Ή</span>
 
 
 
 
 
 
 
 
775
  <div>
776
- <h1 style="margin:0;font-size:1.6rem;font-weight:900;color:#F1F5F9;letter-spacing:.05em;">
777
- EUR/USD &nbsp;Β·&nbsp; ENSEMBLE FORECAST ENGINE
 
 
 
778
  </h1>
779
- <p style="margin:5px 0 0;color:#64748B;font-size:.8rem;font-family:monospace;">
780
- Random Forest &nbsp;Β·&nbsp; Gradient Boosting &nbsp;Β·&nbsp;
781
- AdaBoost &nbsp;Β·&nbsp; Ridge &nbsp;Β·&nbsp; SVR
782
  </p>
783
  </div>
784
  </div>
785
- <div style="display:flex;gap:10px;flex-wrap:wrap;margin-top:4px;">
786
- <span style="background:#1E293B;border-radius:4px;padding:5px 14px;font-size:.76rem;color:#94A3B8;">πŸ“‘ Yahoo Finance 150g</span>
787
- <span style="background:#1E293B;border-radius:4px;padding:5px 14px;font-size:.76rem;color:#94A3B8;">πŸ“ 42 Feature</span>
788
- <span style="background:#1E293B;border-radius:4px;padding:5px 14px;font-size:.76rem;color:#94A3B8;">πŸ§ͺ 10-GΓΌn Backtest</span>
789
- <span style="background:#1E293B;border-radius:4px;padding:5px 14px;font-size:.76rem;color:#94A3B8;">🎯 ATR TP/SL</span>
790
- <span style="background:#1E293B;border-radius:4px;padding:5px 14px;font-size:.76rem;color:#94A3B8;">πŸ“Š 5 Teknik OsilatΓΆr</span>
791
- <span style="background:#1E293B;border-radius:4px;padding:5px 14px;font-size:.76rem;color:#94A3B8;">⚑ Tek Tıkla Güncelle</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
792
  </div>
793
  </div>
794
  """
795
 
796
  # ══════════════════════════════════════════════════════════════════════════════
797
- # GRADIO ARAYÜZÜ
798
  # ══════════════════════════════════════════════════════════════════════════════
799
 
800
- with gr.Blocks(css=CSS, title="EUR/USD Ensemble Forecast Engine") as app:
801
  gr.HTML(HEADER)
802
 
803
- with gr.Row():
804
- predict_btn = gr.Button("⟳ TAHMİN YAP / YENİLE", variant="primary", scale=1)
 
 
 
 
 
805
 
806
  with gr.Row():
807
- with gr.Column(scale=1, min_width=380):
808
- output_text = gr.Textbox(
809
- label="RAPOR",
810
- lines=38,
811
- interactive=False,
812
- )
813
  with gr.Column(scale=2):
814
- output_plot = gr.Plot(label="GRAFΔ°KLER")
815
 
816
- predict_btn.click(
817
- fn=predict_eurusd,
818
- inputs=[],
819
- outputs=[output_text, output_plot],
820
- )
821
 
822
  if __name__ == "__main__":
823
  app.launch()
 
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)
7
  # ══════════════════════════════════════════════════════════════════════════════
8
 
9
  import gradio as gr
10
  import pandas as pd
11
  import numpy as np
12
+ from datetime import datetime
13
  import yfinance as yf
14
+ 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
  import matplotlib
19
  matplotlib.use('Agg')
20
  import matplotlib.pyplot as plt
21
  import matplotlib.patches as mpatches
22
  from matplotlib.gridspec import GridSpec
23
  import matplotlib.ticker as mticker
24
+ from matplotlib.colors import LinearSegmentedColormap
25
  import traceback
26
  import warnings
27
  warnings.filterwarnings('ignore')
28
 
29
  # ══════════════════════════════════════════════════════════════════════════════
30
+ # DESIGN TOKENS (Obsidian / Phosphor palette)
31
  # ══════════════════════════════════════════════════════════════════════════════
32
+ BG = "#080C12"
33
+ PANEL = "#0C1119"
34
+ PANEL2 = "#10161F"
35
+ BORDER = "#1A2233"
36
+ BORDER2 = "#243044"
37
+
38
+ ACID = "#00FFB2" # phosphor-green accent β€” signature element
39
+ ACID2 = "#00D4FF" # cyan secondary
40
+ ACID3 = "#7B61FF" # violet tertiary
41
+ WARM = "#FF6B35" # orange for SHORT signals
42
+ WARN = "#FFB800" # amber for warnings
43
+ RED = "#FF3D5A"
44
+ GREEN = "#00E88F"
45
+ MUTED = "#3A4A5E"
46
+ TEXT = "#E8EDF5"
47
+ SUBTEXT = "#6B7E99"
48
+ DIM = "#3A4A5E"
49
 
50
  # ══════════════════════════════════════════════════════════════════════════════
51
+ # DATA LAYER
52
  # ══════════════════════════════════════════════════════════════════════════════
53
 
54
+ def get_forex_data(symbol: str = "EURUSD=X", period: str = "200d") -> pd.DataFrame | None:
 
 
 
 
55
  try:
56
  raw = yf.download(symbol, period=period, interval="1d", progress=False, auto_adjust=True)
57
+ if raw is None or len(raw) < 80:
58
  return None
 
59
  if isinstance(raw.columns, pd.MultiIndex):
60
  raw.columns = raw.columns.get_level_values(0)
61
  return raw
 
64
 
65
 
66
  def compute_atr(df: pd.DataFrame, period: int = 14) -> pd.Series:
67
+ high, low, prev_close = df['High'], df['Low'], df['Close'].shift(1)
68
+ tr = pd.concat([(high - low), (high - prev_close).abs(), (low - prev_close).abs()], axis=1).max(axis=1)
 
 
 
 
 
 
 
69
  return tr.rolling(period).mean()
70
 
71
 
72
  def compute_stoch(df: pd.DataFrame, k: int = 14, d: int = 3):
73
+ lmin = df['Low'].rolling(k).min()
74
+ hmax = df['High'].rolling(k).max()
75
+ pct_k = 100 * (df['Close'] - lmin) / (hmax - lmin + 1e-10)
76
+ return pct_k, pct_k.rolling(d).mean()
 
 
77
 
78
 
79
  def compute_cci(df: pd.DataFrame, period: int = 20) -> pd.Series:
80
+ tp = (df['High'] + df['Low'] + df['Close']) / 3
 
81
  sma = tp.rolling(period).mean()
82
  mad = tp.rolling(period).apply(lambda x: np.mean(np.abs(x - x.mean())), raw=True)
83
  return (tp - sma) / (0.015 * mad + 1e-10)
84
 
85
 
86
  def compute_williams_r(df: pd.DataFrame, period: int = 14) -> pd.Series:
87
+ hmax = df['High'].rolling(period).max()
88
+ lmin = df['Low'].rolling(period).min()
89
+ return -100 * (hmax - df['Close']) / (hmax - lmin + 1e-10)
90
+
91
+
92
+ def compute_rsi(series: pd.Series, period: int = 14) -> pd.Series:
93
+ delta = series.diff()
94
+ gain = delta.where(delta > 0, 0.0).rolling(period).mean()
95
+ loss = (-delta.where(delta < 0, 0.0)).rolling(period).mean()
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
102
+ for Ridge regression via diverse, low-noise signal construction.
103
  """
104
  df = data.copy()
105
 
106
+ # ── Price action ─────────────────────────────────────────────────────────
107
+ df['Returns'] = df['Close'].pct_change()
108
+ df['Log_Returns'] = np.log(df['Close'] / df['Close'].shift(1))
109
+ df['High_Low'] = df['High'] - df['Low']
110
+ df['Close_Open'] = df['Close'] - df['Open']
111
+ df['Body_Ratio'] = df['Close_Open'].abs() / (df['High_Low'] + 1e-10)
112
+ df['Upper_Shadow'] = df['High'] - df[['Close', 'Open']].max(axis=1)
113
+ df['Lower_Shadow'] = df[['Close', 'Open']].min(axis=1) - df['Low']
114
+ df['HL_to_Close'] = df['High_Low'] / (df['Close'] + 1e-10)
115
+
116
+ # ── Moving averages & crosses ─────────────────────────────────────────────
117
+ for w in [3, 5, 8, 13, 21, 34, 55]: # Fibonacci periods
118
  df[f'SMA_{w}'] = df['Close'].rolling(w).mean()
119
  df[f'EMA_{w}'] = df['Close'].ewm(span=w, adjust=False).mean()
120
 
121
+ df['Price_SMA3_Ratio'] = df['Close'] / (df['SMA_3'] + 1e-10)
122
+ df['Price_SMA8_Ratio'] = df['Close'] / (df['SMA_8'] + 1e-10)
123
+ df['Price_SMA21_Ratio'] = df['Close'] / (df['SMA_21'] + 1e-10)
124
+ df['Price_SMA55_Ratio'] = df['Close'] / (df['SMA_55'] + 1e-10)
125
+
126
+ df['SMA3_SMA21_Cross'] = df['SMA_3'] - df['SMA_21']
127
+ df['SMA8_SMA34_Cross'] = df['SMA_8'] - df['SMA_34']
128
+ df['EMA3_EMA21_Cross'] = df['EMA_3'] - df['EMA_21']
129
+ df['EMA8_EMA55_Cross'] = df['EMA_8'] - df['EMA_55']
130
 
131
+ # Price distance from key MA levels (mean-reversion signal)
132
+ df['Dist_SMA21'] = (df['Close'] - df['SMA_21']) / (df['SMA_21'] + 1e-10)
133
+ df['Dist_SMA55'] = (df['Close'] - df['SMA_55']) / (df['SMA_55'] + 1e-10)
 
 
 
134
 
135
+ # ── RSI multi-period ──────────────────────────────────────────────────────
136
+ for p in [7, 14, 21]:
137
+ df[f'RSI_{p}'] = compute_rsi(df['Close'], p)
138
+
139
+ df['RSI_Divergence'] = df['RSI_7'] - df['RSI_21']
140
+
141
+ # ── MACD variants ─────────────────────────────────────────────────────────
142
  ema12 = df['Close'].ewm(span=12, adjust=False).mean()
143
  ema26 = df['Close'].ewm(span=26, adjust=False).mean()
144
  df['MACD'] = ema12 - ema26
145
  df['MACD_Signal'] = df['MACD'].ewm(span=9, adjust=False).mean()
146
  df['MACD_Hist'] = df['MACD'] - df['MACD_Signal']
147
+ df['MACD_Norm'] = df['MACD'] / (df['Close'] + 1e-10) # normalised
148
+
149
+ # Short MACD (5/13/3) for faster signals
150
+ ema5 = df['Close'].ewm(span=5, adjust=False).mean()
151
+ ema13 = df['Close'].ewm(span=13, adjust=False).mean()
152
+ df['MACD_Fast'] = ema5 - ema13
153
+ df['MACD_Fast_Sig'] = df['MACD_Fast'].ewm(span=3, adjust=False).mean()
154
+ df['MACD_Fast_Hist']= df['MACD_Fast'] - df['MACD_Fast_Sig']
155
+
156
+ # ── Bollinger Bands (multi-period) ────────────────────────────────────────
157
+ for w in [20, 10]:
158
+ mid = df['Close'].rolling(w).mean()
159
+ std = df['Close'].rolling(w).std()
160
+ df[f'BB_upper_{w}'] = mid + std * 2
161
+ df[f'BB_lower_{w}'] = mid - std * 2
162
+ df[f'BB_width_{w}'] = (df[f'BB_upper_{w}'] - df[f'BB_lower_{w}']) / (mid + 1e-10)
163
+ df[f'BB_pct_{w}'] = (df['Close'] - df[f'BB_lower_{w}']) / (
164
+ df[f'BB_upper_{w}'] - df[f'BB_lower_{w}'] + 1e-10)
165
+
166
+ # ── ATR & volatility ──────────────────────────────────────────────────────
167
+ df['ATR'] = compute_atr(df, 14)
168
+ df['ATR_7'] = compute_atr(df, 7)
169
+ df['ATR_Pct'] = df['ATR'] / (df['Close'] + 1e-10)
170
+ df['ATR_Ratio'] = df['ATR_7'] / (df['ATR'] + 1e-10) # short/long vol ratio
171
+
172
+ df['Vol_5'] = df['Returns'].rolling(5).std()
173
+ df['Vol_10'] = df['Returns'].rolling(10).std()
174
+ df['Vol_20'] = df['Returns'].rolling(20).std()
175
+ df['Vol_Ratio'] = df['Vol_5'] / (df['Vol_20'] + 1e-10)
176
+
177
+ # ── Stochastic + CCI + Williams ───────────────────────────────────────────
178
+ df['Stoch_K'], df['Stoch_D'] = compute_stoch(df, 14, 3)
179
+ df['Stoch_K_5'], df['Stoch_D_5'] = compute_stoch(df, 5, 3)
180
+ df['Stoch_Cross'] = df['Stoch_K'] - df['Stoch_D']
181
+ df['CCI'] = compute_cci(df, 20)
182
+ df['CCI_10'] = compute_cci(df, 10)
183
+ df['WilliamsR'] = compute_williams_r(df, 14)
184
+ df['WilliamsR_7'] = compute_williams_r(df, 7)
185
+
186
+ # ── Momentum (Rate of Change) ─────────────────────────────────────────────
187
+ for p in [3, 5, 10, 20]:
188
+ df[f'ROC_{p}'] = df['Close'].pct_change(p)
189
+
190
+ # ── Trend strength ────────────────────────────────────────────────────────
191
+ df['ADX_Proxy'] = (df['Returns'].rolling(14).std() /
192
+ df['Returns'].rolling(14).mean().abs().clip(lower=1e-10))
193
+
194
+ # ── Lag features (close price) ────────────────────────────────────────────
195
+ for i in range(1, 11):
196
+ df[f'Lag_{i}'] = df['Close'].shift(i)
197
 
198
+ # ── Lag returns ───────────────────────────────────────────────────────────
199
+ for i in range(1, 6):
200
+ df[f'LagRet_{i}'] = df['Returns'].shift(i)
201
+
202
+ # ── Rolling statistics ────────────────────────────────────────────────────
203
+ df['Close_Min5'] = df['Close'].rolling(5).min()
204
+ df['Close_Max5'] = df['Close'].rolling(5).max()
205
+ df['Close_Min20'] = df['Close'].rolling(20).min()
206
+ df['Close_Max20'] = df['Close'].rolling(20).max()
207
+ df['Range_Pct5'] = (df['Close'] - df['Close_Min5']) / (df['Close_Max5'] - df['Close_Min5'] + 1e-10)
208
+ df['Range_Pct20'] = (df['Close'] - df['Close_Min20']) / (df['Close_Max20'] - df['Close_Min20'] + 1e-10)
209
+
210
+ # ── Volume ────────────────────────────────────────────────────────────────
211
+ if 'Volume' in df.columns and df['Volume'].sum() > 0:
212
+ df['Vol_MA10'] = df['Volume'].rolling(10).mean()
213
+ df['Volume_Ratio']= df['Volume'] / (df['Vol_MA10'] + 1e-10)
214
+ df['OBV'] = (np.sign(df['Returns']) * df['Volume']).cumsum()
215
+ df['OBV_MA'] = df['OBV'].rolling(10).mean()
216
+ df['OBV_Signal'] = (df['OBV'] - df['OBV_MA']) / (df['OBV_MA'].abs() + 1e-10)
 
 
 
 
 
 
 
 
 
 
217
  else:
 
218
  df['Volume_Ratio'] = 1.0
219
+ df['OBV_Signal'] = 0.0
 
 
 
220
 
221
  return df.dropna()
222
 
223
 
224
  # ══════════════════════════════════════════════════════════════════════════════
225
+ # FEATURE COLUMNS
226
  # ══════════════════════════════════════════════════════════════════════════════
227
  FEATURE_COLS = [
228
+ # Price action
229
  'Returns', 'Log_Returns', 'High_Low', 'Close_Open',
230
+ 'Body_Ratio', 'Upper_Shadow', 'Lower_Shadow', 'HL_to_Close',
231
+ # Moving averages
232
+ 'SMA_3', 'SMA_5', 'SMA_8', 'SMA_13', 'SMA_21', 'SMA_34', 'SMA_55',
233
+ 'EMA_3', 'EMA_5', 'EMA_8', 'EMA_13', 'EMA_21', 'EMA_34', 'EMA_55',
234
+ 'Price_SMA3_Ratio', 'Price_SMA8_Ratio', 'Price_SMA21_Ratio', 'Price_SMA55_Ratio',
235
+ 'SMA3_SMA21_Cross', 'SMA8_SMA34_Cross', 'EMA3_EMA21_Cross', 'EMA8_EMA55_Cross',
236
+ 'Dist_SMA21', 'Dist_SMA55',
237
+ # RSI
238
+ 'RSI_7', 'RSI_14', 'RSI_21', 'RSI_Divergence',
239
+ # MACD
240
+ 'MACD', 'MACD_Signal', 'MACD_Hist', 'MACD_Norm',
241
+ 'MACD_Fast', 'MACD_Fast_Sig', 'MACD_Fast_Hist',
242
+ # Bollinger
243
+ 'BB_width_20', 'BB_pct_20', 'BB_width_10', 'BB_pct_10',
244
+ # ATR & volatility
245
+ 'ATR', 'ATR_7', 'ATR_Pct', 'ATR_Ratio',
246
+ 'Vol_5', 'Vol_10', 'Vol_20', 'Vol_Ratio',
247
+ # Oscillators
248
+ 'Stoch_K', 'Stoch_D', 'Stoch_K_5', 'Stoch_D_5', 'Stoch_Cross',
249
+ 'CCI', 'CCI_10', 'WilliamsR', 'WilliamsR_7',
250
  # Momentum
251
+ 'ROC_3', 'ROC_5', 'ROC_10', 'ROC_20', 'ADX_Proxy',
252
+ # Lag close
253
+ 'Lag_1', 'Lag_2', 'Lag_3', 'Lag_4', 'Lag_5',
254
+ 'Lag_6', 'Lag_7', 'Lag_8', 'Lag_9', 'Lag_10',
255
+ # Lag returns
256
+ 'LagRet_1', 'LagRet_2', 'LagRet_3', 'LagRet_4', 'LagRet_5',
257
+ # Range
258
+ 'Range_Pct5', 'Range_Pct20',
259
  # Volume
260
+ 'Volume_Ratio', 'OBV_Signal',
 
 
261
  ]
262
 
263
+
264
  # ══════════════════════════════════════════════════════════════════════════════
265
+ # RIDGE MODEL LAYER
266
+ # Strategy: stacked multi-target Ridge with optimised alpha via RidgeCV
267
+ # + second-order interaction features for non-linear capture
268
  # ══════════════════════════════════════════════════════════════════════════════
269
 
270
+ def build_ridge_pipeline(alpha: float) -> Pipeline:
271
+ """Ridge pipeline with degree-2 interactions (no bias terms in poly)."""
272
+ return Pipeline([
273
+ ('scaler', StandardScaler()),
274
+ ('poly', PolynomialFeatures(degree=2, interaction_only=True,
275
+ include_bias=False)),
276
+ ('scaler2', StandardScaler(with_mean=False)), # re-scale after poly
277
+ ('ridge', Ridge(alpha=alpha, fit_intercept=True)),
278
+ ])
279
+
280
+
281
+ def select_best_alpha(X_tr, y_tr) -> float:
282
+ """
283
+ RidgeCV finds optimal alpha from a fine logarithmic grid.
284
+ Evaluated on 5-fold time-series split (no shuffle).
 
 
285
  """
286
+ alphas = np.logspace(-3, 4, 60)
287
+ scaler = StandardScaler()
288
+ Xs = scaler.fit_transform(X_tr)
289
+ rcv = RidgeCV(alphas=alphas, cv=5, scoring='neg_mean_absolute_error')
290
+ rcv.fit(Xs, y_tr)
291
+ return float(rcv.alpha_)
292
+
293
+
294
+ def train_ridge(df: pd.DataFrame, horizon: int = 1):
295
+ """
296
+ Trains the premium Ridge model.
297
+ Target: price delta after `horizon` days.
298
+ Returns pipeline, test predictions (absolute), y_test (absolute), avail_cols.
299
  """
300
  avail = [c for c in FEATURE_COLS if c in df.columns]
301
  X = df[avail].values
302
  close = df['Close'].values
303
 
304
+ # Build delta target aligned to horizon
305
+ y_delta = np.zeros(len(close) - horizon)
306
+ for h in range(1, horizon + 1):
307
+ y_delta += close[h:len(close) - horizon + h] - close[:len(close) - horizon]
308
+ y_delta /= horizon # average delta per day (smoother target)
309
+
310
+ X = X[:len(y_delta)]
311
+ close_aligned = close[:len(y_delta)]
312
 
313
+ split = -15 # 15-day test window for robust backtest
314
+ X_train, X_test = X[:split], X[split:]
315
+ y_train, y_delta_test = y_delta[:split], y_delta[split:]
316
  close_test = close_aligned[split:]
317
 
318
+ # Find best alpha on training data only
319
+ best_alpha = select_best_alpha(X_train, y_train)
 
320
 
321
+ # Build & fit pipeline
322
+ pipe = build_ridge_pipeline(best_alpha)
323
+ pipe.fit(X_train, y_train)
324
 
325
+ # Test predictions β†’ absolute price
326
+ delta_hat = pipe.predict(X_test)
327
+ pred_abs = close_test + delta_hat
328
+ y_test_abs = close_test + y_delta_test
 
329
 
330
+ mae = mean_absolute_error(y_test_abs, pred_abs)
331
 
332
+ return pipe, pred_abs, y_test_abs, avail, best_alpha, mae
333
 
334
 
335
+ def predict_next_ridge(pipe, df: pd.DataFrame, avail: list,
336
+ horizon: int = 1) -> float:
337
+ """Predict absolute close price horizon days ahead."""
 
 
 
338
  cp = float(df['Close'].iloc[-1])
339
  x = df[avail].iloc[-1].values.reshape(1, -1)
340
+ delta = float(pipe.predict(x)[0])
341
+ return cp + delta * horizon
 
 
342
 
343
 
344
  # ══════════════════════════════════════════════════════════════════════════════
345
+ # TP / SL (ATR-based)
346
  # ══════════════════════════════════════════════════════════════════════════════
347
 
348
+ def compute_tp_sl(current_price, pred_price, atr,
349
+ atr_multiplier_sl=1.5, rr_ratio=2.0) -> dict:
350
+ direction = "LONG" if pred_price > current_price else "SHORT"
351
+ sl_dist = atr * atr_multiplier_sl
352
+ tp_dist = sl_dist * rr_ratio
353
+ pip = 0.0001
 
 
 
354
 
355
+ if direction == "LONG":
356
+ sl, tp = current_price - sl_dist, current_price + tp_dist
357
+ else:
358
+ sl, tp = current_price + sl_dist, current_price - tp_dist
359
 
360
+ return dict(
361
+ direction=direction, entry=current_price, sl=sl, tp=tp,
362
+ sl_pips=sl_dist/pip, tp_pips=tp_dist/pip,
363
+ rr=rr_ratio, risk_pct=(sl_dist/current_price)*100, atr=atr,
364
+ )
365
 
 
 
 
 
 
366
 
367
+ def signal_score(df: pd.DataFrame, direction: str) -> dict:
368
+ row = df.iloc[-1]
369
+ score = 0
370
+ det = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
371
 
372
+ rsi = float(row.get('RSI_14', 50))
373
  if direction == "LONG":
374
+ pts = max(0, (70 - rsi) / 70 * 25) if rsi < 70 else 0
 
 
 
 
 
375
  else:
376
+ pts = max(0, (rsi - 30) / 70 * 25) if rsi > 30 else 0
377
+ score += pts; det['RSI-14'] = f"{rsi:.1f} (+{pts:.0f}p)"
378
+
379
+ macd = float(row.get('MACD', 0))
380
+ mhist = float(row.get('MACD_Hist', 0))
 
 
 
 
381
  if direction == "LONG":
382
  pts = 20 if (macd > 0 and mhist > 0) else (10 if macd > 0 else 0)
383
  else:
384
  pts = 20 if (macd < 0 and mhist < 0) else (10 if macd < 0 else 0)
385
+ score += pts; det['MACD'] = f"{macd:.5f} hist={mhist:.5f} (+{pts}p)"
 
386
 
387
+ stk = float(row.get('Stoch_K', 50))
388
+ std = float(row.get('Stoch_D', 50))
389
  if direction == "LONG":
390
  pts = 20 if (stk < 80 and stk > std) else (10 if stk < 80 else 0)
391
  else:
392
  pts = 20 if (stk > 20 and stk < std) else (10 if stk > 20 else 0)
393
+ score += pts; det['Stoch'] = f"K={stk:.1f} D={std:.1f} (+{pts}p)"
 
394
 
395
+ cci = float(row.get('CCI', 0))
396
+ pts = 20 if (direction=="LONG" and cci > -100) or (direction=="SHORT" and cci < 100) else 5
397
+ score += pts; det['CCI'] = f"{cci:.1f} (+{pts}p)"
 
 
 
 
398
 
399
+ bb = float(row.get('BB_pct_20', 0.5))
400
+ pts = 15 if (direction=="LONG" and bb < 0.5) or (direction=="SHORT" and bb > 0.5) else 5
401
+ score += pts; det['BB%'] = f"{bb:.3f} (+{pts}p)"
 
 
 
 
402
 
403
+ label = "GΓœΓ‡LÜ βœ…" if score >= 75 else ("ORTA ⚠️" if score >= 45 else "ZAYIF ❌")
404
+ return {'score': min(100, score), 'label': label, 'details': det}
 
 
 
405
 
406
 
407
  # ══════════════════════════════════════════════════════════════════════════════
408
+ # CHART ENGINE β€” phosphor-green terminal aesthetic
409
  # ══════════════════════════════════════════════════════════════════════════════
410
 
411
+ def _ax_style(ax, title="", ylabel=""):
412
  ax.set_facecolor(PANEL)
413
  ax.tick_params(colors=SUBTEXT, labelsize=7)
414
  for spine in ax.spines.values():
415
+ spine.set_edgecolor(BORDER2)
416
+ spine.set_linewidth(0.6)
417
+ ax.grid(True, color=BORDER, linewidth=0.35, alpha=0.7, linestyle=':')
418
  if title:
419
+ ax.set_title(title, fontsize=8, color=TEXT, pad=6, loc='left',
420
+ fontfamily='monospace', fontweight='bold')
421
  if ylabel:
422
+ ax.set_ylabel(ylabel, fontsize=6.5, color=SUBTEXT, fontfamily='monospace')
423
 
424
 
425
+ def build_figure(df, pred_price, horizon, tp_sl,
426
+ test_pred_abs, y_test_abs, best_alpha, mae) -> plt.Figure:
 
 
 
 
 
 
427
 
428
  plt.rcParams.update({
429
+ 'font.family': 'monospace',
430
+ 'text.color': TEXT,
431
+ 'figure.facecolor': BG,
432
+ 'axes.facecolor': PANEL,
433
+ 'axes.edgecolor': BORDER2,
434
+ 'grid.color': BORDER,
435
+ 'grid.linewidth': 0.35,
436
  })
437
 
438
+ fig = plt.figure(figsize=(17, 16))
439
+ gs = GridSpec(5, 2, figure=fig,
440
+ hspace=0.55, wspace=0.30,
441
+ left=0.055, right=0.97, top=0.93, bottom=0.04)
442
 
443
+ ax_price = fig.add_subplot(gs[0:2, :]) # tall price chart
444
+ ax_rsi = fig.add_subplot(gs[2, 0])
445
+ ax_macd = fig.add_subplot(gs[2, 1])
446
+ ax_stoch = fig.add_subplot(gs[3, 0])
447
+ ax_cci = fig.add_subplot(gs[3, 1])
448
+ ax_back = fig.add_subplot(gs[4, :]) # backtest accuracy
449
 
450
+ tail = df.tail(60)
451
  idx = np.arange(len(tail))
452
  cp = float(df['Close'].iloc[-1])
453
+ last = len(idx) - 1
454
+
455
+ # ── PRICE + BB + TP/SL ───────────────────────────────────────────────────
456
+ _ax_style(ax_price,
457
+ f'EUR/USD Β· 60-Day Close + Ridge Forecast (Horizon: +{horizon}D)',
458
+ 'Price')
459
+
460
+ # BB bands (glow effect)
461
+ for alpha_fill, mul in [(0.04, 2.5), (0.07, 2.0), (0.10, 1.0)]:
462
+ mid = tail['SMA_21'].values
463
+ std_ = df['Close'].rolling(20).std().tail(60).values
464
+ ax_price.fill_between(idx, mid - std_*mul, mid + std_*mul,
465
+ alpha=alpha_fill, color=ACID2, zorder=1)
466
+
467
+ ax_price.plot(idx, tail['SMA_21'].values, lw=0.8, color=ACID2, alpha=0.5, ls='--', zorder=2)
468
+ ax_price.plot(idx, tail['SMA_55'].values, lw=0.8, color=MUTED, alpha=0.6, ls=':', zorder=2)
469
+
470
+ # Candlestick-style OHLC bars
471
+ for i, (_, row) in enumerate(tail.iterrows()):
472
+ o, h, l, c = float(row['Open']), float(row['High']), float(row['Low']), float(row['Close'])
473
+ color = GREEN if c >= o else WARM
474
+ ax_price.plot([i, i], [l, h], color=color, lw=0.6, alpha=0.5, zorder=3)
475
+ ax_price.add_patch(mpatches.FancyBboxPatch(
476
+ (i - 0.25, min(o, c)), 0.5, abs(c - o) + 1e-7,
477
+ boxstyle="square,pad=0", linewidth=0, facecolor=color, alpha=0.75, zorder=3))
478
+
479
+ # Close line overlay (phosphor glow)
480
+ ax_price.plot(idx, tail['Close'].values, lw=1.6, color=ACID, zorder=5, alpha=0.9, label='Close')
481
+
482
+ # TP / SL / Entry lines
483
+ ax_price.axhline(tp_sl['tp'], color=GREEN, lw=1.4, ls='--', alpha=0.9, zorder=6)
484
+ ax_price.axhline(tp_sl['sl'], color=RED, lw=1.4, ls='--', alpha=0.9, zorder=6)
485
+ ax_price.axhline(cp, color=TEXT, lw=0.7, ls='-', alpha=0.35, zorder=4)
486
+
487
+ r = len(idx) * 0.018
488
+ for val, lbl, col in [
489
+ (tp_sl['tp'], f"TP {tp_sl['tp']:.5f} (+{tp_sl['tp_pips']:.0f}p)", GREEN),
490
+ (tp_sl['sl'], f"SL {tp_sl['sl']:.5f} (-{tp_sl['sl_pips']:.0f}p)", RED),
491
+ (cp, f"ENTRY {cp:.5f}", TEXT),
492
+ ]:
493
+ ax_price.text(last + r, val, f" {lbl}", color=col,
494
+ fontsize=7.5, va='center', fontweight='bold', zorder=7)
495
+
496
+ # Forecast arrow
497
  ax_price.annotate(
498
+ f"β–Ά +{horizon}D {pred_price:.5f}",
499
+ xy=(last, cp),
500
+ xytext=(last + len(idx)*0.05, pred_price),
501
+ arrowprops=dict(arrowstyle='->', color=ACID, lw=1.6,
502
+ connectionstyle='arc3,rad=0.25'),
503
+ color=ACID, fontsize=9, fontweight='bold', zorder=8,
504
+ fontfamily='monospace',
505
  )
506
 
507
+ ax_price.legend(fontsize=7, framealpha=0.1, labelcolor=SUBTEXT,
508
+ ncol=3, loc='upper left')
509
  ax_price.yaxis.set_major_formatter(mticker.FormatStrFormatter('%.4f'))
510
 
511
+ # ── RSI ──────────────────────────────────────────────────────────────────
512
+ _ax_style(ax_rsi, 'RSI-14 / RSI-7', 'RSI')
513
+ r14 = tail['RSI_14'].values
514
+ r7 = tail['RSI_7'].values
515
+ ax_rsi.plot(idx, r14, lw=1.3, color=ACID, label='RSI-14', zorder=4)
516
+ ax_rsi.plot(idx, r7, lw=1.0, color=ACID3, label='RSI-7', zorder=3, alpha=0.7)
517
+ ax_rsi.fill_between(idx, r14, 70, where=(r14 >= 70), alpha=0.18, color=RED)
518
+ ax_rsi.fill_between(idx, r14, 30, where=(r14 <= 30), alpha=0.18, color=GREEN)
519
+ for lvl, c, lbl in [(70, RED, 'OB'), (50, MUTED, '50'), (30, GREEN, 'OS')]:
520
+ ax_rsi.axhline(lvl, color=c, lw=0.6, ls='--', alpha=0.5)
521
+ ax_rsi.text(len(idx), lvl, f' {lbl}', color=c, fontsize=6, va='center')
522
  ax_rsi.set_ylim(0, 100)
523
+ ax_rsi.legend(fontsize=6.5, framealpha=0.1, labelcolor=SUBTEXT, ncol=2)
524
 
525
+ # ── MACD ─────────────────────────────────────────────────────────────────
526
+ _ax_style(ax_macd, 'MACD Β· Fast (5/13) + Standard (12/26)')
527
  macd_v = tail['MACD'].values
528
  sig_v = tail['MACD_Signal'].values
529
  hist_v = macd_v - sig_v
530
+ hcol = [GREEN if v >= 0 else RED for v in hist_v]
531
+ ax_macd.bar(idx, hist_v, color=hcol, alpha=0.45, width=0.7, label='Hist')
532
+ ax_macd.plot(idx, macd_v, lw=1.2, color=ACID, label='MACD')
533
+ ax_macd.plot(idx, sig_v, lw=1.0, color=WARN, label='Signal', ls='--', alpha=0.8)
534
+ ax_macd.plot(idx, tail['MACD_Fast'].values, lw=0.8, color=ACID3,
535
+ label='Fast', ls=':', alpha=0.7)
536
+ ax_macd.axhline(0, color=MUTED, lw=0.6)
537
+ ax_macd.legend(fontsize=6, framealpha=0.1, labelcolor=SUBTEXT, ncol=4)
538
+
539
+ # ── STOCHASTIC ────────────────────────────────────────────────────────────
540
+ _ax_style(ax_stoch, 'Stochastic (14,3) / (5,3)', '%')
541
+ sk = tail['Stoch_K'].values
542
+ sd = tail['Stoch_D'].values
543
+ ax_stoch.plot(idx, sk, lw=1.2, color=ACID, label='%K-14')
544
+ ax_stoch.plot(idx, sd, lw=1.0, color=WARN, label='%D-14', ls='--', alpha=0.8)
545
+ ax_stoch.plot(idx, tail['Stoch_K_5'].values, lw=0.7, color=ACID3,
546
+ label='%K-5', ls=':', alpha=0.7)
547
+ ax_stoch.fill_between(idx, sk, 80, where=(sk >= 80), alpha=0.12, color=RED)
548
+ ax_stoch.fill_between(idx, sk, 20, where=(sk <= 20), alpha=0.12, color=GREEN)
549
  for lvl, c in [(80, RED), (20, GREEN)]:
550
+ ax_stoch.axhline(lvl, color=c, lw=0.6, ls='--', alpha=0.5)
551
  ax_stoch.set_ylim(-5, 105)
552
+ ax_stoch.legend(fontsize=6.5, framealpha=0.1, labelcolor=SUBTEXT, ncol=3)
553
+
554
+ # ── CCI ───────────────────────────────────────────────────────────────────
555
+ _ax_style(ax_cci, 'CCI-20 / CCI-10')
556
+ cci20 = tail['CCI'].values
557
+ cci10 = tail['CCI_10'].values
558
+ ax_cci.plot(idx, cci20, lw=1.2, color=ACID3, label='CCI-20')
559
+ ax_cci.plot(idx, cci10, lw=0.8, color=ACID2, label='CCI-10', alpha=0.7, ls='--')
560
+ ax_cci.fill_between(idx, cci20, 100, where=(cci20 >= 100), alpha=0.12, color=RED)
561
+ ax_cci.fill_between(idx, cci20, -100, where=(cci20 <= -100), alpha=0.12, color=GREEN)
562
  for lvl, c in [(100, RED), (0, MUTED), (-100, GREEN)]:
563
+ ax_cci.axhline(lvl, color=c, lw=0.6, ls='--', alpha=0.5)
564
+ ax_cci.legend(fontsize=6.5, framealpha=0.1, labelcolor=SUBTEXT, ncol=2)
565
+
566
+ # ── BACKTEST ACCURACY ─────────────────────────────────────────────────────
567
+ _ax_style(ax_back, f'Ridge Backtest Β· 15-Day Window Β· Ξ±={best_alpha:.4f} Β· MAE={mae:.6f}', 'Price')
568
+ bidx = np.arange(len(y_test_abs))
569
+ ax_back.plot(bidx, y_test_abs, lw=1.5, color=TEXT, label='Actual', zorder=5)
570
+ ax_back.plot(bidx, test_pred_abs, lw=1.5, color=ACID, label='Ridge Pred', zorder=5, ls='--')
571
+ ax_back.fill_between(bidx, y_test_abs, test_pred_abs, alpha=0.12, color=ACID2)
572
+ for i in range(len(bidx)):
573
+ err = abs(test_pred_abs[i] - y_test_abs[i])
574
+ ax_back.plot([i, i], [y_test_abs[i], test_pred_abs[i]],
575
+ color=ACID2, lw=0.8, alpha=0.5, zorder=3)
576
+ ax_back.legend(fontsize=7.5, framealpha=0.12, labelcolor=SUBTEXT, ncol=2)
577
+ ax_back.yaxis.set_major_formatter(mticker.FormatStrFormatter('%.4f'))
578
+
579
+ # ── Header ────────────────────────────────────────────────────────────────
580
+ dir_label = (f"{'β–² LONG' if tp_sl['direction']=='LONG' else 'β–Ό SHORT'}"
581
+ f" Β· TP +{tp_sl['tp_pips']:.0f}p Β· SL -{tp_sl['sl_pips']:.0f}p"
582
+ f" Β· R:R 1:{tp_sl['rr']:.1f}"
583
+ f" Β· +{horizon}D forecast")
584
+ dir_color = GREEN if tp_sl['direction'] == 'LONG' else WARM
585
+
586
+ fig.text(0.055, 0.97,
587
+ "EUR/USD Β· RIDGE FORECAST ENGINE Β· ULTRA-PREMIUM",
588
+ fontsize=12, color=TEXT, fontweight='bold', va='top',
589
+ fontfamily='monospace')
590
+ fig.text(0.055, 0.955, dir_label,
591
+ fontsize=8.5, color=dir_color, va='top', fontfamily='monospace')
592
+ fig.text(0.97, 0.97,
593
+ datetime.now().strftime('%d %b %Y %H:%M'),
594
+ fontsize=8, color=SUBTEXT, va='top', ha='right',
595
+ fontfamily='monospace')
596
+
597
+ # Acid-green scan-line signature (subtle)
598
+ for frac in np.linspace(0.04, 0.93, 28):
599
+ fig.add_artist(plt.Line2D(
600
+ [0, 1], [frac, frac], transform=fig.transFigure,
601
+ color=ACID, alpha=0.007, linewidth=0.4, zorder=0))
 
 
 
 
 
 
 
 
 
 
602
 
603
  return fig
604
 
605
 
606
  # ══════════════════════════════════════════════════════════════════════════════
607
+ # MAIN PREDICT FUNCTION
608
  # ══════════════════════════════════════════════════════════════════════════════
609
 
610
+ def predict_eurusd(horizon: int = 1):
611
  try:
612
+ # 1. Data
613
  data = get_forex_data()
614
  if data is None:
615
  return "❌ Veri indirilemedi. Δ°nternet bağlantΔ±nΔ±zΔ± kontrol edin.", None
616
 
617
+ # 2. Feature engineering
618
  df = create_features(data)
619
+ if len(df) < 40:
620
+ return "❌ Yetersiz veri (min 40 satır gerekli).", None
621
 
622
+ # 3. Train Ridge + backtest
623
+ pipe, test_pred_abs, y_test_abs, avail, best_alpha, mae = train_ridge(df, horizon)
624
 
625
+ # 4. Predict next
626
+ pred_price = predict_next_ridge(pipe, df, avail, horizon)
627
 
628
+ # 5. TP/SL
629
  cp = float(df['Close'].iloc[-1])
630
  atr = float(df['ATR'].iloc[-1])
631
+ tp_sl = compute_tp_sl(cp, pred_price, atr,
632
+ atr_multiplier_sl=1.5, rr_ratio=2.0)
633
+
634
+ # 6. Signal score
635
+ sig = signal_score(df, tp_sl['direction'])
636
+
637
+ # 7. Backtest metrics
638
+ rmse = float(np.sqrt(np.mean((test_pred_abs - y_test_abs) ** 2)))
639
+ hit = float(np.mean(
640
+ np.sign(np.diff(test_pred_abs)) == np.sign(np.diff(y_test_abs))
641
+ )) * 100
642
+
643
+ # 8. Indicators
644
+ rsi = float(df['RSI_14'].iloc[-1])
645
+ macd = float(df['MACD'].iloc[-1])
646
+ mh = float(df['MACD_Hist'].iloc[-1])
647
+ stk = float(df['Stoch_K'].iloc[-1])
648
+ std = float(df['Stoch_D'].iloc[-1])
649
+ cci = float(df['CCI'].iloc[-1])
650
+ wr = float(df['WilliamsR'].iloc[-1])
651
+ bb = float(df['BB_pct_20'].iloc[-1])
652
+ vr = float(df['Vol_20'].iloc[-1]) * 100
653
+ roc5 = float(df['ROC_5'].iloc[-1]) * 100
654
+ chg = (pred_price - cp) / cp * 100
655
+
656
+ rsi_lbl = "AşırΔ± AlΔ±m ⚠️" if rsi > 70 else ("AşırΔ± SatΔ±m ⚠️" if rsi < 30 else "NΓΆtr βœ“")
657
+ macd_lbl = "Boğa β–²" if macd > 0 else "AyΔ± β–Ό"
658
+
659
+ W = 54
660
+ sep = "═" * W
 
 
 
 
 
 
 
 
 
661
 
662
  lines = [
663
  sep,
664
+ f" EUR/USD Β· RIDGE FORECAST ENGINE Β· ULTRA-PREMIUM",
665
  f" {datetime.now().strftime('%d %b %Y %H:%M:%S')}",
666
  sep,
667
  "",
668
+ f" {'MEVCUT FΔ°YAT':<24}: {cp:.5f}",
669
+ f" {f'+{horizon}G TAHMΔ°N (RIDGE)':<24}: {pred_price:.5f} ({chg:+.3f}%)",
670
+ f" {'YΓ–N':<24}: {tp_sl['direction']}",
671
+ f" {'OPTΔ°MAL ALPHA (RidgeCV)':<24}: {best_alpha:.6f}",
672
  "",
673
+ f" {'─── RΔ°SK YΓ–NETΔ°MΔ° (ATR Γ— 1.5 | R:R 1:2)':<{W-2}}",
674
+ f" {'GİRİŞ':<24}: {tp_sl['entry']:.5f}",
675
+ f" {'TAKE PROFIT':<24}: {tp_sl['tp']:.5f} (+{tp_sl['tp_pips']:.0f} pip)",
676
+ f" {'STOP LOSS':<24}: {tp_sl['sl']:.5f} (-{tp_sl['sl_pips']:.0f} pip)",
677
+ f" {'R:R':<24}: 1 : {tp_sl['rr']:.1f}",
678
+ f" {'RΔ°SK %':<24}: {tp_sl['risk_pct']:.3f}%",
679
+ f" {'ATR-14':<24}: {atr:.5f}",
680
  "",
681
  f" {'─── SΔ°NYAL GÜCÜ':<{W-2}}",
682
+ f" {'SKOR':<24}: {sig['score']:.0f} / 100 β†’ {sig['label']}",
683
  ]
684
+ for k, v in sig['details'].items():
685
+ lines.append(f" {k:<22}: {v}")
 
686
 
687
  lines += [
688
  "",
689
  f" {'─── TEKNΔ°K GΓ–STERGELER':<{W-2}}",
690
+ f" {'RSI-14':<24}: {rsi:.2f} β†’ {rsi_lbl}",
691
+ f" {'RSI-7':<24}: {float(df['RSI_7'].iloc[-1]):.2f}",
692
+ f" {'MACD':<24}: {macd:.5f} β†’ {macd_lbl}",
693
+ f" {'MACD Histogram':<24}: {mh:.5f}",
694
+ f" {'Stoch K/D':<24}: {stk:.1f} / {std:.1f}",
695
+ f" {'CCI-20':<24}: {cci:.1f}",
696
+ f" {'Williams %R':<24}: {wr:.1f}",
697
+ f" {'BB% (20)':<24}: {bb:.3f}",
698
+ f" {'ROC-5 (%)':<24}: {roc5:.4f}%",
699
+ f" {'Volatilite Οƒ20':<24}: {vr:.4f}%",
700
  "",
701
+ f" {'─── BACKTEST (Son 15 GΓΌn)':<{W-2}}",
702
+ f" {'MAE':<24}: {mae:.7f}",
703
+ f" {'RMSE':<24}: {rmse:.7f}",
704
+ f" {'Yân Doğruluğu':<24}: {hit:.1f}%",
705
+ f" {'Feature SayΔ±sΔ±':<24}: {len(avail)}",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
706
  "",
707
  sep,
708
+ " ⚠ Yalnızca eğitim amaçlıdır. Finansal tavsiye değildir.",
709
  sep,
710
  ]
711
 
712
+ text = "\n".join(lines)
713
 
714
+ # 9. Figure
715
+ fig = build_figure(
716
+ df, pred_price, horizon, tp_sl,
717
+ test_pred_abs, y_test_abs, best_alpha, mae,
718
+ )
719
 
720
+ return text, fig
721
 
722
  except Exception:
723
  return f"❌ Hata:\n{traceback.format_exc()}", None
724
 
725
 
726
+ def run_1d(): return predict_eurusd(horizon=1)
727
+ def run_2d(): return predict_eurusd(horizon=2)
728
+
729
+
730
  # ══════════════════════════════════════════════════════════════════════════════
731
+ # CSS
732
  # ══════════════════════════════════════════════════════════════════════════════
733
 
734
  CSS = """
735
+ *, *::before, *::after { box-sizing: border-box; }
736
+
737
  body, .gradio-container {
738
+ background: #080C12 !important;
739
+ font-family: 'JetBrains Mono', 'Cascadia Code', 'Fira Code', monospace !important;
740
+ color: #E8EDF5 !important;
741
+ }
742
+
743
+ /* Buttons */
744
+ .btn-row button {
745
+ background: transparent !important;
746
+ border: 1px solid #1A2233 !important;
747
+ color: #6B7E99 !important;
748
+ font-family: 'JetBrains Mono', monospace !important;
749
+ font-size: 0.85rem !important;
750
+ font-weight: 600 !important;
751
+ letter-spacing: 0.08em !important;
752
+ padding: 13px 28px !important;
753
+ border-radius: 4px !important;
754
+ transition: all 0.18s ease !important;
755
+ }
756
+
757
+ .btn-1d button {
758
+ background: linear-gradient(135deg, #003D28 0%, #00261A 100%) !important;
759
+ border: 1px solid #00FFB2 !important;
760
+ color: #00FFB2 !important;
761
+ box-shadow: 0 0 18px rgba(0,255,178,0.12) !important;
762
+ }
763
+ .btn-1d button:hover {
764
+ box-shadow: 0 0 30px rgba(0,255,178,0.25) !important;
765
+ filter: brightness(1.1) !important;
766
  }
767
+
768
+ .btn-2d button {
769
+ background: linear-gradient(135deg, #1A1040 0%, #100A30 100%) !important;
770
+ border: 1px solid #7B61FF !important;
771
+ color: #7B61FF !important;
772
+ box-shadow: 0 0 18px rgba(123,97,255,0.12) !important;
773
+ }
774
+ .btn-2d button:hover {
775
+ box-shadow: 0 0 30px rgba(123,97,255,0.25) !important;
776
+ filter: brightness(1.1) !important;
777
  }
778
+
779
+ /* Textbox */
780
  .gr-textbox textarea, textarea {
781
+ background: #0C1119 !important;
782
+ color: #00FFB2 !important;
783
+ border: 1px solid #1A2233 !important;
784
  font-family: 'JetBrains Mono', monospace !important;
785
+ font-size: 11.5px !important;
786
+ line-height: 1.85 !important;
787
+ border-radius: 4px !important;
788
+ caret-color: #00FFB2 !important;
789
  }
790
+
791
+ label span { color: #3A4A5E !important; font-size: .75rem !important; }
792
+ .gr-panel, .gr-box { background: #0C1119 !important; border: 1px solid #1A2233 !important; }
793
  footer { display: none !important; }
794
+
795
+ /* Scan-line overlay on header */
796
+ .header-wrap::after {
797
+ content: '';
798
+ position: absolute; inset: 0;
799
+ background: repeating-linear-gradient(
800
+ 0deg,
801
+ transparent,
802
+ transparent 3px,
803
+ rgba(0,255,178,0.012) 3px,
804
+ rgba(0,255,178,0.012) 4px
805
+ );
806
+ pointer-events: none;
807
+ border-radius: 8px;
808
+ }
809
+ .header-wrap { position: relative; }
810
  """
811
 
812
  HEADER = """
813
+ <div class="header-wrap" style="
814
+ background: linear-gradient(155deg, #080C12 0%, #0C1119 50%, #080C12 100%);
815
+ border: 1px solid #1A2233;
816
+ border-top: 2px solid #00FFB2;
817
+ border-radius: 8px;
818
+ padding: 26px 32px 20px;
819
+ margin-bottom: 14px;
820
  ">
821
+ <div style="display:flex;align-items:flex-start;gap:18px;">
822
+ <div style="flex-shrink:0;margin-top:3px;">
823
+ <svg width="40" height="40" viewBox="0 0 40 40" fill="none">
824
+ <rect width="40" height="40" rx="6" fill="#003D28"/>
825
+ <polyline points="6,28 13,18 20,22 27,12 34,16"
826
+ stroke="#00FFB2" stroke-width="2.5" fill="none"
827
+ stroke-linecap="round" stroke-linejoin="round"/>
828
+ <circle cx="27" cy="12" r="2.5" fill="#00FFB2"/>
829
+ </svg>
830
+ </div>
831
  <div>
832
+ <h1 style="margin:0 0 4px;font-size:1.45rem;font-weight:900;
833
+ color:#E8EDF5;letter-spacing:.06em;font-family:monospace;">
834
+ EUR/USD &nbsp;Β·&nbsp; RIDGE FORECAST ENGINE
835
+ <span style="color:#00FFB2;font-size:.85rem;margin-left:10px;
836
+ vertical-align:middle;letter-spacing:.12em;">ULTRA-PREMIUM</span>
837
  </h1>
838
+ <p style="margin:0;color:#3A4A5E;font-size:.75rem;font-family:monospace;
839
+ letter-spacing:.04em;">
840
+ STACKED RIDGE Β· POLYNOMIAL INTERACTIONS Β· RIDGECV ALPHA Β· 75+ FEATURES Β· ATR TP/SL
841
  </p>
842
  </div>
843
  </div>
844
+ <div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:16px;">
845
+ <span style="background:#0C1119;border:1px solid #1A2233;border-radius:3px;
846
+ padding:4px 12px;font-size:.72rem;color:#6B7E99;font-family:monospace;">
847
+ πŸ“‘ Yahoo Finance 200g</span>
848
+ <span style="background:#0C1119;border:1px solid #1A2233;border-radius:3px;
849
+ padding:4px 12px;font-size:.72rem;color:#6B7E99;font-family:monospace;">
850
+ 🧬 75+ Features</span>
851
+ <span style="background:#0C1119;border:1px solid #1A2233;border-radius:3px;
852
+ padding:4px 12px;font-size:.72rem;color:#6B7E99;font-family:monospace;">
853
+ πŸ”¬ Poly-2 Interactions</span>
854
+ <span style="background:#0C1119;border:1px solid #1A2233;border-radius:3px;
855
+ padding:4px 12px;font-size:.72rem;color:#6B7E99;font-family:monospace;">
856
+ ⚑ RidgeCV Auto-α</span>
857
+ <span style="background:#0C1119;border:1px solid #1A2233;border-radius:3px;
858
+ padding:4px 12px;font-size:.72rem;color:#6B7E99;font-family:monospace;">
859
+ 🎯 ATR TP/SL</span>
860
+ <span style="background:#0C1119;border:1px solid #1A2233;border-radius:3px;
861
+ padding:4px 12px;font-size:.72rem;color:#6B7E99;font-family:monospace;">
862
+ πŸ“… 1D / 2D Horizon</span>
863
+ <span style="background:#0C1119;border:1px solid #1A2233;border-radius:3px;
864
+ padding:4px 12px;font-size:.72rem;color:#6B7E99;font-family:monospace;">
865
+ πŸ§ͺ 15-Day Backtest</span>
866
  </div>
867
  </div>
868
  """
869
 
870
  # ══════════════════════════════════════════════════════════════════════════════
871
+ # GRADIO UI
872
  # ══════════════════════════════════════════════════════════════════════════════
873
 
874
+ with gr.Blocks(css=CSS, title="EUR/USD Ridge Forecast Engine") as app:
875
  gr.HTML(HEADER)
876
 
877
+ with gr.Row(elem_classes="btn-row"):
878
+ with gr.Column(scale=1, elem_classes="btn-1d"):
879
+ btn_1d = gr.Button("⟳ +1 GÜN TAHMİN", variant="primary")
880
+ with gr.Column(scale=1, elem_classes="btn-2d"):
881
+ btn_2d = gr.Button("⟳ +2 GÜN TAHMİN", variant="primary")
882
+ with gr.Column(scale=3):
883
+ pass # spacer
884
 
885
  with gr.Row():
886
+ with gr.Column(scale=1, min_width=400):
887
+ out_text = gr.Textbox(label="RAPOR", lines=42, interactive=False)
 
 
 
 
888
  with gr.Column(scale=2):
889
+ out_plot = gr.Plot(label="GRAFΔ°KLER")
890
 
891
+ btn_1d.click(fn=run_1d, inputs=[], outputs=[out_text, out_plot])
892
+ btn_2d.click(fn=run_2d, inputs=[], outputs=[out_text, out_plot])
 
 
 
893
 
894
  if __name__ == "__main__":
895
  app.launch()