GoshawkVortexAI commited on
Commit
7b737c1
·
verified ·
1 Parent(s): a12c7e5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +299 -942
app.py CHANGED
@@ -1,984 +1,341 @@
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
62
- except Exception:
63
- return None
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
- # ── 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.
224
- direction_score = pd.Series(0.0, index=df.index)
225
-
226
- # RSI signal: oversold=bullish, overbought=bearish
227
- if 'RSI_14' in df.columns:
228
- direction_score += (50 - df['RSI_14']) / 50 # +1 at RSI=0, -1 at RSI=100
229
-
230
- # MACD histogram direction
231
- if 'MACD_Hist' in df.columns:
232
- direction_score += np.sign(df['MACD_Hist'])
233
-
234
- # Stoch cross: K above D = bullish
235
- if 'Stoch_Cross' in df.columns:
236
- direction_score += np.sign(df['Stoch_Cross'])
237
-
238
- # Price vs SMA21
239
- if 'Dist_SMA21' in df.columns:
240
- direction_score += np.sign(df['Dist_SMA21'])
241
-
242
- # EMA cross
243
- if 'EMA3_EMA21_Cross' in df.columns:
244
- direction_score += np.sign(df['EMA3_EMA21_Cross'])
245
-
246
- # ROC momentum
247
- if 'ROC_5' in df.columns:
248
- direction_score += np.sign(df['ROC_5'])
249
-
250
- df['Direction_Score'] = direction_score / 6.0 # normalise to [-1, +1]
251
-
252
- # Momentum consistency: are recent returns all same sign?
253
- if 'LagRet_1' in df.columns and 'LagRet_2' in df.columns and 'LagRet_3' in df.columns:
254
- df['Momentum_Consistency'] = (
255
- np.sign(df['LagRet_1']) +
256
- np.sign(df['LagRet_2']) +
257
- np.sign(df['LagRet_3'])
258
- ) / 3.0
259
-
260
- # ── Horizon-aware features (2-day dynamics) ───────────────────────────────
261
- # These explicitly capture 2-day structure to boost multi-day accuracy.
262
- df['LagRet_1_2sum'] = df['LagRet_1'] + df['LagRet_2'] # 2-day return sum
263
- df['LagRet_1_2diff'] = df['LagRet_1'] - df['LagRet_2'] # acceleration
264
- df['Vol_2'] = df['Returns'].rolling(2).std() # 2-day volatility
265
- df['ROC_2'] = df['Close'].pct_change(2) # 2-day ROC
266
- df['SMA3_slope'] = df['SMA_3'].diff(2) # short MA 2-day slope
267
- df['EMA3_slope'] = df['EMA_3'].diff(2) # short EMA 2-day slope
268
- df['ATR_delta'] = df['ATR'].diff(1) # ATR change (vol expansion/contraction)
269
- df['RSI14_delta'] = df['RSI_14'].diff(2) # RSI momentum 2-day
270
- df['MACD_Hist_delta']= df['MACD_Hist'].diff(2) # MACD histogram acceleration
271
- df['BB_pct20_delta'] = df['BB_pct_20'].diff(2) # BB position change
272
-
273
- return df.dropna()
274
-
275
-
276
- # ══════════════════════════════════════════════════════════════════════════════
277
- # FEATURE COLUMNS
278
- # ══════════════════════════════════════════════════════════════════════════════
279
- FEATURE_COLS = [
280
- # Price action
281
- 'Returns', 'Log_Returns', 'High_Low', 'Close_Open',
282
- 'Body_Ratio', 'Upper_Shadow', 'Lower_Shadow', 'HL_to_Close',
283
- # Moving averages
284
- 'SMA_3', 'SMA_5', 'SMA_8', 'SMA_13', 'SMA_21', 'SMA_34', 'SMA_55',
285
- 'EMA_3', 'EMA_5', 'EMA_8', 'EMA_13', 'EMA_21', 'EMA_34', 'EMA_55',
286
- 'Price_SMA3_Ratio', 'Price_SMA8_Ratio', 'Price_SMA21_Ratio', 'Price_SMA55_Ratio',
287
- 'SMA3_SMA21_Cross', 'SMA8_SMA34_Cross', 'EMA3_EMA21_Cross', 'EMA8_EMA55_Cross',
288
- 'Dist_SMA21', 'Dist_SMA55',
289
- # RSI
290
- 'RSI_7', 'RSI_14', 'RSI_21', 'RSI_Divergence',
291
- # MACD
292
- 'MACD', 'MACD_Signal', 'MACD_Hist', 'MACD_Norm',
293
- 'MACD_Fast', 'MACD_Fast_Sig', 'MACD_Fast_Hist',
294
- # Bollinger
295
- 'BB_width_20', 'BB_pct_20', 'BB_width_10', 'BB_pct_10',
296
- # ATR & volatility
297
- 'ATR', 'ATR_7', 'ATR_Pct', 'ATR_Ratio',
298
- 'Vol_5', 'Vol_10', 'Vol_20', 'Vol_Ratio',
299
- # Oscillators
300
- 'Stoch_K', 'Stoch_D', 'Stoch_K_5', 'Stoch_D_5', 'Stoch_Cross',
301
- 'CCI', 'CCI_10', 'WilliamsR', 'WilliamsR_7',
302
- # Momentum
303
- 'ROC_3', 'ROC_5', 'ROC_10', 'ROC_20', 'ADX_Proxy',
304
- # Lag close
305
- 'Lag_1', 'Lag_2', 'Lag_3', 'Lag_4', 'Lag_5',
306
- 'Lag_6', 'Lag_7', 'Lag_8', 'Lag_9', 'Lag_10',
307
- # Lag returns
308
- 'LagRet_1', 'LagRet_2', 'LagRet_3', 'LagRet_4', 'LagRet_5',
309
- # Range
310
- 'Range_Pct5', 'Range_Pct20',
311
- # Volume
312
- 'Volume_Ratio', 'OBV_Signal',
313
- # Direction composite (FIX 4)
314
- 'Direction_Score', 'Momentum_Consistency',
315
- # Horizon-aware (2-day dynamics)
316
- 'LagRet_1_2sum', 'LagRet_1_2diff', 'Vol_2', 'ROC_2',
317
- 'SMA3_slope', 'EMA3_slope', 'ATR_delta',
318
- 'RSI14_delta', 'MACD_Hist_delta', 'BB_pct20_delta',
319
- ]
320
-
321
-
322
- # ══════════════════════════════════════════════════════════════════════════════
323
- # RIDGE MODEL LAYER
324
- # Strategy: stacked multi-target Ridge with optimised alpha via RidgeCV
325
- # + second-order interaction features for non-linear capture
326
- # ══════════════════════════════════════════════════════════════════════════════
327
-
328
- def build_ridge_pipeline(alpha: float) -> Pipeline:
329
- """Ridge pipeline with degree-2 interactions (no bias terms in poly)."""
330
- return Pipeline([
331
- ('scaler', StandardScaler()),
332
- ('poly', PolynomialFeatures(degree=2, interaction_only=True,
333
- include_bias=False)),
334
- ('scaler2', StandardScaler(with_mean=False)), # re-scale after poly
335
- ('ridge', Ridge(alpha=alpha, fit_intercept=True)),
336
- ])
337
-
338
-
339
- def select_best_alpha(X_tr, y_tr) -> float:
340
- """
341
- RidgeCV finds optimal alpha from a fine logarithmic grid.
342
- FIXED: alpha is selected on poly-expanded features (same space as the
343
- actual Ridge model), not on raw features. This eliminates alpha
344
- scale mismatch that degraded 2-day accuracy.
345
- """
346
- sc1 = StandardScaler()
347
- poly = PolynomialFeatures(degree=2, interaction_only=True, include_bias=False)
348
- sc2 = StandardScaler(with_mean=False)
349
- Xp = sc2.fit_transform(poly.fit_transform(sc1.fit_transform(X_tr)))
350
 
351
- # After poly expansion the feature count is ~n*(n-1)/2 — ridge needs
352
- # higher alphas to stay regularised at that scale.
353
- alphas = np.logspace(0, 6, 60)
354
- rcv = RidgeCV(alphas=alphas, cv=5, scoring='neg_mean_absolute_error')
355
- rcv.fit(Xp, y_tr)
356
- return float(rcv.alpha_)
 
357
 
 
 
 
 
 
358
 
359
- def train_ridge(df: pd.DataFrame, horizon: int = 1):
360
- """
361
- Trains the premium Ridge model.
362
- Target: log-return over `horizon` days (direction-aware).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
363
 
364
- FIXES applied:
365
- - Log-return target correctly encodes h-day return (not 1-day)
366
- - Residual bias correction: calibrates systematic over/under-shoot
367
- - Returns pipeline, test predictions (absolute), y_test (absolute), avail_cols.
368
- """
369
- avail = [c for c in FEATURE_COLS if c in df.columns]
370
- X = df[avail].values
371
- close = df['Close'].values
372
 
373
- # h-day log-return — model learns this directly, no horizon scaling needed
374
- y_logret = np.log(close[horizon:] / close[:len(close) - horizon])
375
 
376
- X = X[:len(y_logret)]
377
- close_aligned = close[:len(y_logret)]
378
 
379
- split = -15 # 15-day test window for robust backtest
380
- X_train, X_test = X[:split], X[split:]
381
- y_train, y_logret_test = y_logret[:split], y_logret[split:]
382
- close_test = close_aligned[split:]
 
 
383
 
384
- # Find best alpha on poly-expanded training data
385
- best_alpha = select_best_alpha(X_train, y_train)
386
 
387
- # Build & fit pipeline
388
- pipe = build_ridge_pipeline(best_alpha)
389
- pipe.fit(X_train, y_train)
390
 
391
- # Test predictions → convert log-return back to absolute price
392
- logret_hat = pipe.predict(X_test)
 
 
393
 
394
- # Residual bias correction: remove mean systematic bias from log-return predictions
395
- # This improves directional accuracy without leaking future info (computed on train residuals)
396
- train_preds = pipe.predict(X_train)
397
- residual_bias = float(np.mean(y_train - train_preds)) # mean error on train set
398
- logret_hat_corrected = logret_hat + residual_bias
399
 
400
- pred_abs = close_test * np.exp(logret_hat_corrected)
401
- y_test_abs = close_test * np.exp(y_logret_test)
402
 
403
- mae = mean_absolute_error(y_test_abs, pred_abs)
 
404
 
405
- return pipe, pred_abs, y_test_abs, avail, best_alpha, mae, residual_bias
406
 
 
407
 
408
- def predict_next_ridge(pipe, df: pd.DataFrame, avail: list,
409
- horizon: int = 1, residual_bias: float = 0.0) -> float:
410
- """
411
- Predict absolute close price horizon days ahead using log-return.
412
 
413
- FIXED: Removed `* horizon` scaling — model already learns h-day log-return.
414
- Applies residual_bias correction from training set for calibrated prediction.
 
 
 
 
 
 
 
 
 
 
415
  """
416
- cp = float(df['Close'].iloc[-1])
417
- x = df[avail].iloc[-1].values.reshape(1, -1)
418
- logret = float(pipe.predict(x)[0]) + residual_bias
419
- return cp * np.exp(logret)
420
 
 
 
421
 
422
- # ══════════════════════════════════════════════════════════════════════════════
423
- # TP / SL (ATR-based)
424
- # ══════════════════════════════════════════════════════════════════════════════
425
-
426
- def compute_tp_sl(current_price, pred_price, atr,
427
- atr_multiplier_sl=1.5, rr_ratio=2.0) -> dict:
428
- direction = "LONG" if pred_price > current_price else "SHORT"
429
- sl_dist = atr * atr_multiplier_sl
430
- tp_dist = sl_dist * rr_ratio
431
- pip = 0.0001
432
-
433
- if direction == "LONG":
434
- sl, tp = current_price - sl_dist, current_price + tp_dist
435
- else:
436
- sl, tp = current_price + sl_dist, current_price - tp_dist
437
-
438
- return dict(
439
- direction=direction, entry=current_price, sl=sl, tp=tp,
440
- sl_pips=sl_dist/pip, tp_pips=tp_dist/pip,
441
- rr=rr_ratio, risk_pct=(sl_dist/current_price)*100, atr=atr,
442
  )
443
 
444
-
445
- def signal_score(df: pd.DataFrame, direction: str) -> dict:
446
- row = df.iloc[-1]
447
- score = 0
448
- det = {}
449
-
450
- rsi = float(row.get('RSI_14', 50))
451
- if direction == "LONG":
452
- pts = max(0, (70 - rsi) / 70 * 25) if rsi < 70 else 0
453
- else:
454
- pts = max(0, (rsi - 30) / 70 * 25) if rsi > 30 else 0
455
- score += pts; det['RSI-14'] = f"{rsi:.1f} (+{pts:.0f}p)"
456
-
457
- macd = float(row.get('MACD', 0))
458
- mhist = float(row.get('MACD_Hist', 0))
459
- if direction == "LONG":
460
- pts = 20 if (macd > 0 and mhist > 0) else (10 if macd > 0 else 0)
461
- else:
462
- pts = 20 if (macd < 0 and mhist < 0) else (10 if macd < 0 else 0)
463
- score += pts; det['MACD'] = f"{macd:.5f} hist={mhist:.5f} (+{pts}p)"
464
-
465
- stk = float(row.get('Stoch_K', 50))
466
- std = float(row.get('Stoch_D', 50))
467
- if direction == "LONG":
468
- pts = 20 if (stk < 80 and stk > std) else (10 if stk < 80 else 0)
469
- else:
470
- pts = 20 if (stk > 20 and stk < std) else (10 if stk > 20 else 0)
471
- score += pts; det['Stoch'] = f"K={stk:.1f} D={std:.1f} (+{pts}p)"
472
-
473
- cci = float(row.get('CCI', 0))
474
- pts = 20 if (direction=="LONG" and cci > -100) or (direction=="SHORT" and cci < 100) else 5
475
- score += pts; det['CCI'] = f"{cci:.1f} (+{pts}p)"
476
-
477
- bb = float(row.get('BB_pct_20', 0.5))
478
- pts = 15 if (direction=="LONG" and bb < 0.5) or (direction=="SHORT" and bb > 0.5) else 5
479
- score += pts; det['BB%'] = f"{bb:.3f} (+{pts}p)"
480
-
481
- label = "GÜÇLÜ ✅" if score >= 75 else ("ORTA ⚠️" if score >= 45 else "ZAYIF ❌")
482
- return {'score': min(100, score), 'label': label, 'details': det}
483
-
484
-
485
- # ══════════════════════════════════════════════════════════════════════════════
486
- # CHART ENGINE — phosphor-green terminal aesthetic
487
- # ══════════════════════════════════════════════════════════════════════════════
488
-
489
- def _ax_style(ax, title="", ylabel=""):
490
- ax.set_facecolor(PANEL)
491
- ax.tick_params(colors=SUBTEXT, labelsize=7)
492
- for spine in ax.spines.values():
493
- spine.set_edgecolor(BORDER2)
494
- spine.set_linewidth(0.6)
495
- ax.grid(True, color=BORDER, linewidth=0.35, alpha=0.7, linestyle=':')
496
- if title:
497
- ax.set_title(title, fontsize=8, color=TEXT, pad=6, loc='left',
498
- fontfamily='monospace', fontweight='bold')
499
- if ylabel:
500
- ax.set_ylabel(ylabel, fontsize=6.5, color=SUBTEXT, fontfamily='monospace')
501
-
502
-
503
- def build_figure(df, pred_price, horizon, tp_sl,
504
- test_pred_abs, y_test_abs, best_alpha, mae) -> plt.Figure:
505
-
506
- plt.rcParams.update({
507
- 'font.family': 'monospace',
508
- 'text.color': TEXT,
509
- 'figure.facecolor': BG,
510
- 'axes.facecolor': PANEL,
511
- 'axes.edgecolor': BORDER2,
512
- 'grid.color': BORDER,
513
- 'grid.linewidth': 0.35,
514
- })
515
-
516
- fig = plt.figure(figsize=(17, 16))
517
- gs = GridSpec(5, 2, figure=fig,
518
- hspace=0.55, wspace=0.30,
519
- left=0.055, right=0.97, top=0.93, bottom=0.04)
520
-
521
- ax_price = fig.add_subplot(gs[0:2, :]) # tall price chart
522
- ax_rsi = fig.add_subplot(gs[2, 0])
523
- ax_macd = fig.add_subplot(gs[2, 1])
524
- ax_stoch = fig.add_subplot(gs[3, 0])
525
- ax_cci = fig.add_subplot(gs[3, 1])
526
- ax_back = fig.add_subplot(gs[4, :]) # backtest accuracy
527
-
528
- tail = df.tail(60)
529
- idx = np.arange(len(tail))
530
- cp = float(df['Close'].iloc[-1])
531
- last = len(idx) - 1
532
-
533
- # ── PRICE + BB + TP/SL ───────────────────────────────────────────────────
534
- _ax_style(ax_price,
535
- f'EUR/USD · 60-Day Close + Ridge Forecast (Horizon: +{horizon}D)',
536
- 'Price')
537
-
538
- # BB bands (glow effect)
539
- for alpha_fill, mul in [(0.04, 2.5), (0.07, 2.0), (0.10, 1.0)]:
540
- mid = tail['SMA_21'].values
541
- std_ = df['Close'].rolling(20).std().tail(60).values
542
- ax_price.fill_between(idx, mid - std_*mul, mid + std_*mul,
543
- alpha=alpha_fill, color=ACID2, zorder=1)
544
-
545
- ax_price.plot(idx, tail['SMA_21'].values, lw=0.8, color=ACID2, alpha=0.5, ls='--', zorder=2)
546
- ax_price.plot(idx, tail['SMA_55'].values, lw=0.8, color=MUTED, alpha=0.6, ls=':', zorder=2)
547
-
548
- # Candlestick-style OHLC bars
549
- for i, (_, row) in enumerate(tail.iterrows()):
550
- o, h, l, c = float(row['Open']), float(row['High']), float(row['Low']), float(row['Close'])
551
- color = GREEN if c >= o else WARM
552
- ax_price.plot([i, i], [l, h], color=color, lw=0.6, alpha=0.5, zorder=3)
553
- ax_price.add_patch(mpatches.FancyBboxPatch(
554
- (i - 0.25, min(o, c)), 0.5, abs(c - o) + 1e-7,
555
- boxstyle="square,pad=0", linewidth=0, facecolor=color, alpha=0.75, zorder=3))
556
-
557
- # Close line overlay (phosphor glow)
558
- ax_price.plot(idx, tail['Close'].values, lw=1.6, color=ACID, zorder=5, alpha=0.9, label='Close')
559
-
560
- # TP / SL / Entry lines
561
- ax_price.axhline(tp_sl['tp'], color=GREEN, lw=1.4, ls='--', alpha=0.9, zorder=6)
562
- ax_price.axhline(tp_sl['sl'], color=RED, lw=1.4, ls='--', alpha=0.9, zorder=6)
563
- ax_price.axhline(cp, color=TEXT, lw=0.7, ls='-', alpha=0.35, zorder=4)
564
-
565
- r = len(idx) * 0.018
566
- for val, lbl, col in [
567
- (tp_sl['tp'], f"TP {tp_sl['tp']:.5f} (+{tp_sl['tp_pips']:.0f}p)", GREEN),
568
- (tp_sl['sl'], f"SL {tp_sl['sl']:.5f} (-{tp_sl['sl_pips']:.0f}p)", RED),
569
- (cp, f"ENTRY {cp:.5f}", TEXT),
570
- ]:
571
- ax_price.text(last + r, val, f" {lbl}", color=col,
572
- fontsize=7.5, va='center', fontweight='bold', zorder=7)
573
-
574
- # Forecast arrow
575
- ax_price.annotate(
576
- f"▶ +{horizon}D {pred_price:.5f}",
577
- xy=(last, cp),
578
- xytext=(last + len(idx)*0.05, pred_price),
579
- arrowprops=dict(arrowstyle='->', color=ACID, lw=1.6,
580
- connectionstyle='arc3,rad=0.25'),
581
- color=ACID, fontsize=9, fontweight='bold', zorder=8,
582
- fontfamily='monospace',
583
  )
584
 
585
- ax_price.legend(fontsize=7, framealpha=0.1, labelcolor=SUBTEXT,
586
- ncol=3, loc='upper left')
587
- ax_price.yaxis.set_major_formatter(mticker.FormatStrFormatter('%.4f'))
588
-
589
- # ── RSI ──────────────────────────────────────────────────────────────────
590
- _ax_style(ax_rsi, 'RSI-14 / RSI-7', 'RSI')
591
- r14 = tail['RSI_14'].values
592
- r7 = tail['RSI_7'].values
593
- ax_rsi.plot(idx, r14, lw=1.3, color=ACID, label='RSI-14', zorder=4)
594
- ax_rsi.plot(idx, r7, lw=1.0, color=ACID3, label='RSI-7', zorder=3, alpha=0.7)
595
- ax_rsi.fill_between(idx, r14, 70, where=(r14 >= 70), alpha=0.18, color=RED)
596
- ax_rsi.fill_between(idx, r14, 30, where=(r14 <= 30), alpha=0.18, color=GREEN)
597
- for lvl, c, lbl in [(70, RED, 'OB'), (50, MUTED, '50'), (30, GREEN, 'OS')]:
598
- ax_rsi.axhline(lvl, color=c, lw=0.6, ls='--', alpha=0.5)
599
- ax_rsi.text(len(idx), lvl, f' {lbl}', color=c, fontsize=6, va='center')
600
- ax_rsi.set_ylim(0, 100)
601
- ax_rsi.legend(fontsize=6.5, framealpha=0.1, labelcolor=SUBTEXT, ncol=2)
602
-
603
- # ── MACD ─────────────────────────────────────────────────────────────────
604
- _ax_style(ax_macd, 'MACD · Fast (5/13) + Standard (12/26)')
605
- macd_v = tail['MACD'].values
606
- sig_v = tail['MACD_Signal'].values
607
- hist_v = macd_v - sig_v
608
- hcol = [GREEN if v >= 0 else RED for v in hist_v]
609
- ax_macd.bar(idx, hist_v, color=hcol, alpha=0.45, width=0.7, label='Hist')
610
- ax_macd.plot(idx, macd_v, lw=1.2, color=ACID, label='MACD')
611
- ax_macd.plot(idx, sig_v, lw=1.0, color=WARN, label='Signal', ls='--', alpha=0.8)
612
- ax_macd.plot(idx, tail['MACD_Fast'].values, lw=0.8, color=ACID3,
613
- label='Fast', ls=':', alpha=0.7)
614
- ax_macd.axhline(0, color=MUTED, lw=0.6)
615
- ax_macd.legend(fontsize=6, framealpha=0.1, labelcolor=SUBTEXT, ncol=4)
616
-
617
- # ── STOCHASTIC ────────────────────────────────────────────────────────────
618
- _ax_style(ax_stoch, 'Stochastic (14,3) / (5,3)', '%')
619
- sk = tail['Stoch_K'].values
620
- sd = tail['Stoch_D'].values
621
- ax_stoch.plot(idx, sk, lw=1.2, color=ACID, label='%K-14')
622
- ax_stoch.plot(idx, sd, lw=1.0, color=WARN, label='%D-14', ls='--', alpha=0.8)
623
- ax_stoch.plot(idx, tail['Stoch_K_5'].values, lw=0.7, color=ACID3,
624
- label='%K-5', ls=':', alpha=0.7)
625
- ax_stoch.fill_between(idx, sk, 80, where=(sk >= 80), alpha=0.12, color=RED)
626
- ax_stoch.fill_between(idx, sk, 20, where=(sk <= 20), alpha=0.12, color=GREEN)
627
- for lvl, c in [(80, RED), (20, GREEN)]:
628
- ax_stoch.axhline(lvl, color=c, lw=0.6, ls='--', alpha=0.5)
629
- ax_stoch.set_ylim(-5, 105)
630
- ax_stoch.legend(fontsize=6.5, framealpha=0.1, labelcolor=SUBTEXT, ncol=3)
631
-
632
- # ── CCI ───────────────────────────────────────────────────────────────────
633
- _ax_style(ax_cci, 'CCI-20 / CCI-10')
634
- cci20 = tail['CCI'].values
635
- cci10 = tail['CCI_10'].values
636
- ax_cci.plot(idx, cci20, lw=1.2, color=ACID3, label='CCI-20')
637
- ax_cci.plot(idx, cci10, lw=0.8, color=ACID2, label='CCI-10', alpha=0.7, ls='--')
638
- ax_cci.fill_between(idx, cci20, 100, where=(cci20 >= 100), alpha=0.12, color=RED)
639
- ax_cci.fill_between(idx, cci20, -100, where=(cci20 <= -100), alpha=0.12, color=GREEN)
640
- for lvl, c in [(100, RED), (0, MUTED), (-100, GREEN)]:
641
- ax_cci.axhline(lvl, color=c, lw=0.6, ls='--', alpha=0.5)
642
- ax_cci.legend(fontsize=6.5, framealpha=0.1, labelcolor=SUBTEXT, ncol=2)
643
-
644
- # ── BACKTEST ACCURACY ─────────────────────────────────────────────────────
645
- _ax_style(ax_back, f'Ridge Backtest · 15-Day Window · α={best_alpha:.4f} · MAE={mae:.6f}', 'Price')
646
- bidx = np.arange(len(y_test_abs))
647
- ax_back.plot(bidx, y_test_abs, lw=1.5, color=TEXT, label='Actual', zorder=5)
648
- ax_back.plot(bidx, test_pred_abs, lw=1.5, color=ACID, label='Ridge Pred', zorder=5, ls='--')
649
- ax_back.fill_between(bidx, y_test_abs, test_pred_abs, alpha=0.12, color=ACID2)
650
- for i in range(len(bidx)):
651
- err = abs(test_pred_abs[i] - y_test_abs[i])
652
- ax_back.plot([i, i], [y_test_abs[i], test_pred_abs[i]],
653
- color=ACID2, lw=0.8, alpha=0.5, zorder=3)
654
- ax_back.legend(fontsize=7.5, framealpha=0.12, labelcolor=SUBTEXT, ncol=2)
655
- ax_back.yaxis.set_major_formatter(mticker.FormatStrFormatter('%.4f'))
656
-
657
- # ── Header ────────────────────────────────────────────────────────────────
658
- dir_label = (f"{'▲ LONG' if tp_sl['direction']=='LONG' else '▼ SHORT'}"
659
- f" · TP +{tp_sl['tp_pips']:.0f}p · SL -{tp_sl['sl_pips']:.0f}p"
660
- f" · R:R 1:{tp_sl['rr']:.1f}"
661
- f" · +{horizon}D forecast")
662
- dir_color = GREEN if tp_sl['direction'] == 'LONG' else WARM
663
-
664
- fig.text(0.055, 0.97,
665
- "EUR/USD · RIDGE FORECAST ENGINE · ULTRA-PREMIUM",
666
- fontsize=12, color=TEXT, fontweight='bold', va='top',
667
- fontfamily='monospace')
668
- fig.text(0.055, 0.955, dir_label,
669
- fontsize=8.5, color=dir_color, va='top', fontfamily='monospace')
670
- fig.text(0.97, 0.97,
671
- datetime.now().strftime('%d %b %Y %H:%M'),
672
- fontsize=8, color=SUBTEXT, va='top', ha='right',
673
- fontfamily='monospace')
674
-
675
- # Acid-green scan-line signature (subtle)
676
- for frac in np.linspace(0.04, 0.93, 28):
677
- fig.add_artist(plt.Line2D(
678
- [0, 1], [frac, frac], transform=fig.transFigure,
679
- color=ACID, alpha=0.007, linewidth=0.4, zorder=0))
680
-
681
- return fig
682
-
683
-
684
- # ══════════════════════════════════════════════════════════════════════════════
685
- # MAIN PREDICT FUNCTION
686
- # ══════════════════════════════════════════════════════════════════════════════
687
-
688
- def predict_eurusd(horizon: int = 1):
689
- try:
690
- # 1. Data
691
- data = get_forex_data()
692
- if data is None:
693
- return "❌ Veri indirilemedi. İnternet bağlantınızı kontrol edin.", None
694
-
695
- # 2. Feature engineering
696
- df = create_features(data)
697
- if len(df) < 40:
698
- return "❌ Yetersiz veri (min 40 satır gerekli).", None
699
-
700
- # 3. Train Ridge + backtest
701
- pipe, test_pred_abs, y_test_abs, avail, best_alpha, mae, residual_bias = train_ridge(df, horizon)
702
-
703
- # 4. Predict next
704
- pred_price = predict_next_ridge(pipe, df, avail, horizon, residual_bias)
705
-
706
- # 5. TP/SL
707
- cp = float(df['Close'].iloc[-1])
708
- atr = float(df['ATR'].iloc[-1])
709
- tp_sl = compute_tp_sl(cp, pred_price, atr,
710
- atr_multiplier_sl=1.5, rr_ratio=2.0)
711
-
712
- # 6. Signal score
713
- sig = signal_score(df, tp_sl['direction'])
714
-
715
- # 7. Backtest metrics
716
- rmse = float(np.sqrt(np.mean((test_pred_abs - y_test_abs) ** 2)))
717
- # FIX 3: Correct directional accuracy — compare pred vs entry price
718
- close_vals = df['Close'].values
719
- n_test = len(test_pred_abs)
720
- entry_prices = close_vals[len(close_vals) - n_test - horizon :
721
- len(close_vals) - horizon]
722
- if len(entry_prices) == n_test:
723
- pred_dirs = np.sign(test_pred_abs - entry_prices)
724
- actual_dirs = np.sign(y_test_abs - entry_prices)
725
- hit = float(np.mean(pred_dirs == actual_dirs)) * 100
726
- else:
727
- hit = float(np.mean(
728
- np.sign(np.diff(test_pred_abs)) == np.sign(np.diff(y_test_abs))
729
- )) * 100
730
-
731
- # 8. Indicators
732
- rsi = float(df['RSI_14'].iloc[-1])
733
- macd = float(df['MACD'].iloc[-1])
734
- mh = float(df['MACD_Hist'].iloc[-1])
735
- stk = float(df['Stoch_K'].iloc[-1])
736
- std = float(df['Stoch_D'].iloc[-1])
737
- cci = float(df['CCI'].iloc[-1])
738
- wr = float(df['WilliamsR'].iloc[-1])
739
- bb = float(df['BB_pct_20'].iloc[-1])
740
- vr = float(df['Vol_20'].iloc[-1]) * 100
741
- roc5 = float(df['ROC_5'].iloc[-1]) * 100
742
- chg = (pred_price - cp) / cp * 100
743
-
744
- rsi_lbl = "Aşırı Alım ⚠️" if rsi > 70 else ("Aşırı Satım ⚠️" if rsi < 30 else "Nötr ✓")
745
- macd_lbl = "Boğa ▲" if macd > 0 else "Ayı ▼"
746
-
747
- W = 54
748
- sep = "═" * W
749
-
750
- lines = [
751
- sep,
752
- f" EUR/USD · RIDGE FORECAST ENGINE · ULTRA-PREMIUM",
753
- f" {datetime.now().strftime('%d %b %Y %H:%M:%S')}",
754
- sep,
755
- "",
756
- f" {'MEVCUT FİYAT':<24}: {cp:.5f}",
757
- f" {f'+{horizon}G TAHMİN (RIDGE)':<24}: {pred_price:.5f} ({chg:+.3f}%)",
758
- f" {'YÖN':<24}: {tp_sl['direction']}",
759
- f" {'OPTİMAL ALPHA (RidgeCV)':<24}: {best_alpha:.6f}",
760
- f" {'BIAS DÜZELTME (log-ret)':<24}: {residual_bias:+.7f}",
761
- "",
762
- f" {'─── RİSK YÖNETİMİ (ATR × 1.5 | R:R 1:2)':<{W-2}}",
763
- f" {'GİRİŞ':<24}: {tp_sl['entry']:.5f}",
764
- f" {'TAKE PROFIT':<24}: {tp_sl['tp']:.5f} (+{tp_sl['tp_pips']:.0f} pip)",
765
- f" {'STOP LOSS':<24}: {tp_sl['sl']:.5f} (-{tp_sl['sl_pips']:.0f} pip)",
766
- f" {'R:R':<24}: 1 : {tp_sl['rr']:.1f}",
767
- f" {'RİSK %':<24}: {tp_sl['risk_pct']:.3f}%",
768
- f" {'ATR-14':<24}: {atr:.5f}",
769
- "",
770
- f" {'─── SİNYAL GÜCÜ':<{W-2}}",
771
- f" {'SKOR':<24}: {sig['score']:.0f} / 100 → {sig['label']}",
772
- ]
773
- for k, v in sig['details'].items():
774
- lines.append(f" {k:<22}: {v}")
775
-
776
- lines += [
777
- "",
778
- f" {'─── TEKNİK GÖSTERGELER':<{W-2}}",
779
- f" {'RSI-14':<24}: {rsi:.2f} → {rsi_lbl}",
780
- f" {'RSI-7':<24}: {float(df['RSI_7'].iloc[-1]):.2f}",
781
- f" {'MACD':<24}: {macd:.5f} → {macd_lbl}",
782
- f" {'MACD Histogram':<24}: {mh:.5f}",
783
- f" {'Stoch K/D':<24}: {stk:.1f} / {std:.1f}",
784
- f" {'CCI-20':<24}: {cci:.1f}",
785
- f" {'Williams %R':<24}: {wr:.1f}",
786
- f" {'BB% (20)':<24}: {bb:.3f}",
787
- f" {'ROC-5 (%)':<24}: {roc5:.4f}%",
788
- f" {'Volatilite σ20':<24}: {vr:.4f}%",
789
- "",
790
- f" {'─── BACKTEST (Son 15 Gün)':<{W-2}}",
791
- f" {'MAE':<24}: {mae:.7f}",
792
- f" {'RMSE':<24}: {rmse:.7f}",
793
- f" {'Yön Doğruluğu':<24}: {hit:.1f}%",
794
- f" {'Feature Sayısı':<24}: {len(avail)}",
795
- "",
796
- sep,
797
- " ⚠ Yalnızca eğitim amaçlıdır. Finansal tavsiye değildir.",
798
- sep,
799
- ]
800
-
801
- text = "\n".join(lines)
802
-
803
- # 9. Figure
804
- fig = build_figure(
805
- df, pred_price, horizon, tp_sl,
806
- test_pred_abs, y_test_abs, best_alpha, mae,
807
- )
808
-
809
- return text, fig
810
-
811
- except Exception:
812
- return f"❌ Hata:\n{traceback.format_exc()}", None
813
-
814
-
815
- def run_1d(): return predict_eurusd(horizon=1)
816
- def run_2d(): return predict_eurusd(horizon=2)
817
-
818
-
819
- # ══════════════════════════════════════════════════════════════════════════════
820
- # CSS
821
- # ══════════════════════════════════════════════════════════════════════════════
822
-
823
- CSS = """
824
- *, *::before, *::after { box-sizing: border-box; }
825
-
826
- body, .gradio-container {
827
- background: #080C12 !important;
828
- font-family: 'JetBrains Mono', 'Cascadia Code', 'Fira Code', monospace !important;
829
- color: #E8EDF5 !important;
830
- }
831
-
832
- /* Buttons */
833
- .btn-row button {
834
- background: transparent !important;
835
- border: 1px solid #1A2233 !important;
836
- color: #6B7E99 !important;
837
- font-family: 'JetBrains Mono', monospace !important;
838
- font-size: 0.85rem !important;
839
- font-weight: 600 !important;
840
- letter-spacing: 0.08em !important;
841
- padding: 13px 28px !important;
842
- border-radius: 4px !important;
843
- transition: all 0.18s ease !important;
844
- }
845
-
846
- .btn-1d button {
847
- background: linear-gradient(135deg, #003D28 0%, #00261A 100%) !important;
848
- border: 1px solid #00FFB2 !important;
849
- color: #00FFB2 !important;
850
- box-shadow: 0 0 18px rgba(0,255,178,0.12) !important;
851
- }
852
- .btn-1d button:hover {
853
- box-shadow: 0 0 30px rgba(0,255,178,0.25) !important;
854
- filter: brightness(1.1) !important;
855
- }
856
-
857
- .btn-2d button {
858
- background: linear-gradient(135deg, #1A1040 0%, #100A30 100%) !important;
859
- border: 1px solid #7B61FF !important;
860
- color: #7B61FF !important;
861
- box-shadow: 0 0 18px rgba(123,97,255,0.12) !important;
862
- }
863
- .btn-2d button:hover {
864
- box-shadow: 0 0 30px rgba(123,97,255,0.25) !important;
865
- filter: brightness(1.1) !important;
866
- }
867
-
868
- /* Textbox */
869
- .gr-textbox textarea, textarea {
870
- background: #0C1119 !important;
871
- color: #00FFB2 !important;
872
- border: 1px solid #1A2233 !important;
873
- font-family: 'JetBrains Mono', monospace !important;
874
- font-size: 11.5px !important;
875
- line-height: 1.85 !important;
876
- border-radius: 4px !important;
877
- caret-color: #00FFB2 !important;
878
- }
879
-
880
- label span { color: #3A4A5E !important; font-size: .75rem !important; }
881
- .gr-panel, .gr-box { background: #0C1119 !important; border: 1px solid #1A2233 !important; }
882
- footer { display: none !important; }
883
-
884
- /* Scan-line overlay on header */
885
- .header-wrap::after {
886
- content: '';
887
- position: absolute; inset: 0;
888
- background: repeating-linear-gradient(
889
- 0deg,
890
- transparent,
891
- transparent 3px,
892
- rgba(0,255,178,0.012) 3px,
893
- rgba(0,255,178,0.012) 4px
894
- );
895
- pointer-events: none;
896
- border-radius: 8px;
897
- }
898
- .header-wrap { position: relative; }
899
- """
900
-
901
- HEADER = """
902
- <div class="header-wrap" style="
903
- background: linear-gradient(155deg, #080C12 0%, #0C1119 50%, #080C12 100%);
904
- border: 1px solid #1A2233;
905
- border-top: 2px solid #00FFB2;
906
- border-radius: 8px;
907
- padding: 26px 32px 20px;
908
- margin-bottom: 14px;
909
- ">
910
- <div style="display:flex;align-items:flex-start;gap:18px;">
911
- <div style="flex-shrink:0;margin-top:3px;">
912
- <svg width="40" height="40" viewBox="0 0 40 40" fill="none">
913
- <rect width="40" height="40" rx="6" fill="#003D28"/>
914
- <polyline points="6,28 13,18 20,22 27,12 34,16"
915
- stroke="#00FFB2" stroke-width="2.5" fill="none"
916
- stroke-linecap="round" stroke-linejoin="round"/>
917
- <circle cx="27" cy="12" r="2.5" fill="#00FFB2"/>
918
- </svg>
919
- </div>
920
- <div>
921
- <h1 style="margin:0 0 4px;font-size:1.45rem;font-weight:900;
922
- color:#E8EDF5;letter-spacing:.06em;font-family:monospace;">
923
- EUR/USD &nbsp;·&nbsp; RIDGE FORECAST ENGINE
924
- <span style="color:#00FFB2;font-size:.85rem;margin-left:10px;
925
- vertical-align:middle;letter-spacing:.12em;">ULTRA-PREMIUM</span>
926
- </h1>
927
- <p style="margin:0;color:#3A4A5E;font-size:.75rem;font-family:monospace;
928
- letter-spacing:.04em;">
929
- STACKED RIDGE · POLYNOMIAL INTERACTIONS · RIDGECV ALPHA · 75+ FEATURES · ATR TP/SL
930
- </p>
931
- </div>
932
- </div>
933
- <div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:16px;">
934
- <span style="background:#0C1119;border:1px solid #1A2233;border-radius:3px;
935
- padding:4px 12px;font-size:.72rem;color:#6B7E99;font-family:monospace;">
936
- 📡 Yahoo Finance 200g</span>
937
- <span style="background:#0C1119;border:1px solid #1A2233;border-radius:3px;
938
- padding:4px 12px;font-size:.72rem;color:#6B7E99;font-family:monospace;">
939
- 🧬 75+ Features</span>
940
- <span style="background:#0C1119;border:1px solid #1A2233;border-radius:3px;
941
- padding:4px 12px;font-size:.72rem;color:#6B7E99;font-family:monospace;">
942
- 🔬 Poly-2 Interactions</span>
943
- <span style="background:#0C1119;border:1px solid #1A2233;border-radius:3px;
944
- padding:4px 12px;font-size:.72rem;color:#6B7E99;font-family:monospace;">
945
- ⚡ RidgeCV Auto-α</span>
946
- <span style="background:#0C1119;border:1px solid #1A2233;border-radius:3px;
947
- padding:4px 12px;font-size:.72rem;color:#6B7E99;font-family:monospace;">
948
- 🎯 ATR TP/SL</span>
949
- <span style="background:#0C1119;border:1px solid #1A2233;border-radius:3px;
950
- padding:4px 12px;font-size:.72rem;color:#6B7E99;font-family:monospace;">
951
- 📅 1D / 2D Horizon</span>
952
- <span style="background:#0C1119;border:1px solid #1A2233;border-radius:3px;
953
- padding:4px 12px;font-size:.72rem;color:#6B7E99;font-family:monospace;">
954
- 🧪 15-Day Backtest</span>
955
- </div>
956
- </div>
957
- """
958
-
959
- # ══════════════════════════════════════════════════════════════════════════════
960
- # GRADIO UI
961
- # ══════════════════════════════════════════════════════════════════════════════
962
-
963
- with gr.Blocks(css=CSS, title="EUR/USD Ridge Forecast Engine") as app:
964
- gr.HTML(HEADER)
965
-
966
- with gr.Row(elem_classes="btn-row"):
967
- with gr.Column(scale=1, elem_classes="btn-1d"):
968
- btn_1d = gr.Button("⟳ +1 GÜN TAHMİN", variant="primary")
969
- with gr.Column(scale=1, elem_classes="btn-2d"):
970
- btn_2d = gr.Button("⟳ +2 GÜN TAHMİN", variant="primary")
971
- with gr.Column(scale=3):
972
- pass # spacer
973
-
974
- with gr.Row():
975
- with gr.Column(scale=1, min_width=400):
976
- out_text = gr.Textbox(label="RAPOR", lines=42, interactive=False)
977
- with gr.Column(scale=2):
978
- out_plot = gr.Plot(label="GRAFİKLER")
979
-
980
- btn_1d.click(fn=run_1d, inputs=[], outputs=[out_text, out_plot])
981
- btn_2d.click(fn=run_2d, inputs=[], outputs=[out_text, out_plot])
982
 
983
  if __name__ == "__main__":
984
- app.launch()
 
1
+ """
2
+ EURUSD Pattern AI - yfinance + Gradio
3
+ Candlestick pattern tespiti ve AI yorumu
4
+ """
 
 
 
5
 
6
  import gradio as gr
7
+ import yfinance as yf
8
  import pandas as pd
9
  import numpy as np
10
+ import plotly.graph_objects as go
11
+ from datetime import datetime, timedelta
12
+
13
+ # ─── PATTERN TESPİT FONKSİYONLARI ───────────────────────────────────────────
14
+
15
+ def detect_patterns(df):
16
+ """Temel candlestick patternleri tespit eder."""
17
+ patterns = []
18
+ df = df.copy()
19
+
20
+ o = df["Open"].values
21
+ h = df["High"].values
22
+ l = df["Low"].values
23
+ c = df["Close"].values
24
+ dates = df.index
25
+
26
+ for i in range(2, len(df)):
27
+ found = []
28
+
29
+ body = abs(c[i] - o[i])
30
+ body_prev = abs(c[i-1] - o[i-1])
31
+ rng = h[i] - l[i]
32
+ upper_wick = h[i] - max(c[i], o[i])
33
+ lower_wick = min(c[i], o[i]) - l[i]
34
+
35
+ # Doji
36
+ if body < rng * 0.1:
37
+ found.append("🔵 Doji")
38
+
39
+ # Hammer
40
+ if (lower_wick > body * 2 and upper_wick < body * 0.5
41
+ and c[i-1] < o[i-1]):
42
+ found.append("🟢 Hammer (Bullish)")
43
+
44
+ # Shooting Star
45
+ if (upper_wick > body * 2 and lower_wick < body * 0.5
46
+ and c[i-1] > o[i-1]):
47
+ found.append("🔴 Shooting Star (Bearish)")
48
+
49
+ # Bullish Engulfing
50
+ if (c[i-1] < o[i-1] and c[i] > o[i]
51
+ and c[i] > o[i-1] and o[i] < c[i-1]):
52
+ found.append("🟢 Bullish Engulfing")
53
+
54
+ # Bearish Engulfing
55
+ if (c[i-1] > o[i-1] and c[i] < o[i]
56
+ and c[i] < o[i-1] and o[i] > c[i-1]):
57
+ found.append("🔴 Bearish Engulfing")
58
+
59
+ # Morning Star
60
+ if (i >= 2 and c[i-2] < o[i-2]
61
+ and body_prev < abs(c[i-2] - o[i-2]) * 0.3
62
+ and c[i] > o[i]
63
+ and c[i] > (c[i-2] + o[i-2]) / 2):
64
+ found.append("🟢 Morning Star")
65
+
66
+ # Evening Star
67
+ if (i >= 2 and c[i-2] > o[i-2]
68
+ and body_prev < abs(c[i-2] - o[i-2]) * 0.3
69
+ and c[i] < o[i]
70
+ and c[i] < (c[i-2] + o[i-2]) / 2):
71
+ found.append("🔴 Evening Star")
72
+
73
+ # Three White Soldiers
74
+ if (i >= 2
75
+ and c[i] > o[i] and c[i-1] > o[i-1] and c[i-2] > o[i-2]
76
+ and c[i] > c[i-1] > c[i-2]):
77
+ found.append("🟢 Three White Soldiers")
78
+
79
+ # Three Black Crows
80
+ if (i >= 2
81
+ and c[i] < o[i] and c[i-1] < o[i-1] and c[i-2] < o[i-2]
82
+ and c[i] < c[i-1] < c[i-2]):
83
+ found.append("🔴 Three Black Crows")
84
+
85
+ # Spinning Top
86
+ if (body < rng * 0.3 and upper_wick > body and lower_wick > body):
87
+ found.append("🔵 Spinning Top")
88
+
89
+ if found:
90
+ patterns.append({
91
+ "Tarih": dates[i].strftime("%Y-%m-%d %H:%M") if hasattr(dates[i], "hour") else str(dates[i])[:10],
92
+ "Pattern": ", ".join(found),
93
+ "Close": round(c[i], 5),
94
+ "Yön": "↑ Bullish" if any("Bullish" in p or "Hammer" in p or "Soldier" in p or "Morning" in p for p in found)
95
+ else ("↓ Bearish" if any("Bearish" in p or "Star" in p or "Crow" in p or "Evening" in p for p in found)
96
+ else "↔ Nötr"),
97
+ })
98
+
99
+ return patterns[-30:] if len(patterns) > 30 else patterns # son 30 pattern
100
+
101
+
102
+ def calculate_indicators(df):
103
+ """RSI, EMA, Bollinger hesaplar."""
104
+ df = df.copy()
105
+
106
+ # EMA
107
+ df["EMA20"] = df["Close"].ewm(span=20).mean()
108
+ df["EMA50"] = df["Close"].ewm(span=50).mean()
109
 
110
+ # RSI
111
+ delta = df["Close"].diff()
112
+ gain = delta.clip(lower=0).rolling(14).mean()
113
+ loss = (-delta.clip(upper=0)).rolling(14).mean()
114
+ rs = gain / loss
115
+ df["RSI"] = 100 - (100 / (1 + rs))
116
 
117
+ # Bollinger
118
+ df["BB_mid"] = df["Close"].rolling(20).mean()
119
+ std = df["Close"].rolling(20).std()
120
+ df["BB_upper"] = df["BB_mid"] + 2 * std
121
+ df["BB_lower"] = df["BB_mid"] - 2 * std
122
 
123
+ # MACD
124
+ ema12 = df["Close"].ewm(span=12).mean()
125
+ ema26 = df["Close"].ewm(span=26).mean()
126
+ df["MACD"] = ema12 - ema26
127
+ df["Signal"] = df["MACD"].ewm(span=9).mean()
128
 
129
+ return df
130
 
 
 
 
 
 
131
 
132
+ def build_chart(df, patterns):
133
+ """Plotly candlestick + indikatör grafiği oluşturur."""
134
+ fig = go.Figure()
135
 
136
+ # Candlestick
137
+ fig.add_trace(go.Candlestick(
138
+ x=df.index, open=df["Open"], high=df["High"],
139
+ low=df["Low"], close=df["Close"],
140
+ name="EUR/USD", increasing_line_color="#26a69a",
141
+ decreasing_line_color="#ef5350"
142
+ ))
143
 
144
+ # EMA
145
+ fig.add_trace(go.Scatter(x=df.index, y=df["EMA20"], name="EMA 20",
146
+ line=dict(color="#FFA726", width=1.2)))
147
+ fig.add_trace(go.Scatter(x=df.index, y=df["EMA50"], name="EMA 50",
148
+ line=dict(color="#AB47BC", width=1.2)))
149
 
150
+ # Bollinger
151
+ fig.add_trace(go.Scatter(x=df.index, y=df["BB_upper"], name="BB Üst",
152
+ line=dict(color="#607D8B", dash="dot", width=1)))
153
+ fig.add_trace(go.Scatter(x=df.index, y=df["BB_lower"], name="BB Alt",
154
+ line=dict(color="#607D8B", dash="dot", width=1),
155
+ fill="tonexty", fillcolor="rgba(96,125,139,0.07)"))
156
+
157
+ # Pattern işaretleri
158
+ for p in patterns[-10:]:
159
+ try:
160
+ dt = pd.to_datetime(p["Tarih"])
161
+ row = df[df.index.date == dt.date()] if hasattr(df.index[0], "date") else df[df.index == p["Tarih"]]
162
+ if not row.empty:
163
+ y_pos = row["High"].values[0] * 1.001
164
+ color = "#26a69a" if "↑" in p["Yön"] else ("#ef5350" if "↓" in p["Yön"] else "#FFA726")
165
+ fig.add_annotation(x=row.index[0], y=y_pos,
166
+ text=p["Pattern"].split(",")[0][2:],
167
+ showarrow=True, arrowhead=2,
168
+ font=dict(size=9, color=color),
169
+ arrowcolor=color)
170
+ except Exception:
171
+ pass
172
+
173
+ fig.update_layout(
174
+ title="EUR/USD – Candlestick + İndikatörler",
175
+ template="plotly_dark",
176
+ xaxis_rangeslider_visible=False,
177
+ height=550,
178
+ legend=dict(orientation="h", y=1.02),
179
+ margin=dict(l=40, r=20, t=50, b=20),
180
+ font=dict(family="Inter, sans-serif"),
181
+ paper_bgcolor="#0f1117",
182
+ plot_bgcolor="#0f1117",
183
+ )
184
+ return fig
185
 
186
 
187
+ def ai_summary(df, patterns):
188
+ """Kural tabanlı AI yorumu üretir."""
189
+ if df is None or df.empty:
190
+ return "Veri yok."
191
+
192
+ latest = df.iloc[-1]
193
+ rsi = round(latest["RSI"], 1)
194
+ macd = latest["MACD"]
195
+ signal = latest["Signal"]
196
+ close = round(latest["Close"], 5)
197
+ ema20 = round(latest["EMA20"], 5)
198
+ ema50 = round(latest["EMA50"], 5)
199
+ bb_upper = round(latest["BB_upper"], 5)
200
+ bb_lower = round(latest["BB_lower"], 5)
201
+
202
+ lines = [f"### 🤖 AI Pattern & İndikatör Analizi\n"]
203
+ lines.append(f"**Son Fiyat:** {close} | **RSI(14):** {rsi}")
204
+ lines.append(f"**EMA20:** {ema20} | **EMA50:** {ema50}")
205
+ lines.append(f"**BB Üst:** {bb_upper} | **BB Alt:** {bb_lower}\n")
206
+
207
+ # Trend
208
+ if close > ema20 > ema50:
209
+ lines.append("📈 **Trend:** Güçlü Yükseliş – Fiyat her iki EMA'nın üzerinde.")
210
+ elif close < ema20 < ema50:
211
+ lines.append("📉 **Trend:** Güçlü Düşüş – Fiyat her iki EMA'nın altında.")
212
+ elif close > ema20:
213
+ lines.append("↗️ **Trend:** Kısa vadeli yükseliş, ancak EMA50 henüz kırılmadı.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
  else:
215
+ lines.append("↘️ **Trend:** Kısa vadeli zayıflık.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
 
217
+ # RSI
218
+ if rsi > 70:
219
+ lines.append(f"⚠️ **RSI {rsi}** – Aşırı alım bölgesi, düzeltme riski.")
220
+ elif rsi < 30:
221
+ lines.append(f"✅ **RSI {rsi}** – Aşırı satım bölgesi, toparlanma ihtimali.")
222
+ else:
223
+ lines.append(f"🔵 **RSI {rsi}** – Nötr bölgede.")
224
 
225
+ # MACD
226
+ if macd > signal:
227
+ lines.append("✅ **MACD** Sinyal üzerinde – Momentum pozitif.")
228
+ else:
229
+ lines.append("⚠️ **MACD** Sinyal altında – Momentum negatif.")
230
 
231
+ # Bollinger
232
+ if close > bb_upper:
233
+ lines.append("🔴 Fiyat **Bollinger Üst Bandı** dışında – Aşırı alım.")
234
+ elif close < bb_lower:
235
+ lines.append("🟢 Fiyat **Bollinger Alt Bandı** dışında – Aşırı satım.")
236
+
237
+ # Son patternler
238
+ if patterns:
239
+ last = patterns[-3:]
240
+ lines.append(f"\n**Son Tespit Edilen Patternler:**")
241
+ for p in reversed(last):
242
+ lines.append(f"- `{p['Tarih']}` → {p['Pattern']} {p['Yön']}")
243
+
244
+ # Genel yorum
245
+ bull_count = sum(1 for p in patterns[-10:] if "↑" in p.get("Yön", ""))
246
+ bear_count = sum(1 for p in patterns[-10:] if "↓" in p.get("Yön", ""))
247
+ lines.append(f"\n**Son 10 Pattern:** 🟢 {bull_count} Bullish | 🔴 {bear_count} Bearish")
248
+
249
+ if bull_count > bear_count and rsi < 65 and macd > signal:
250
+ lines.append("\n> 💡 **Genel Görünüm:** Birden fazla gösterge **yükseliş** yönünde hizalanmış.")
251
+ elif bear_count > bull_count and rsi > 40 and macd < signal:
252
+ lines.append("\n> 💡 **Genel Görünüm:** Birden fazla gösterge **düşüş** yönünde hizalanmış.")
253
+ else:
254
+ lines.append("\n> 💡 **Genel Görünüm:** Sinyaller karışık, dikkatli izlenebilir.")
255
 
256
+ lines.append("\n---\n*Bu analiz yatırım tavsiyesi değildir.*")
257
+ return "\n".join(lines)
 
 
 
 
 
 
258
 
 
 
259
 
260
+ # ─── ANA FONKSİYON ───────────────────────────────────────────────────────────
 
261
 
262
+ def analyze(period, interval):
263
+ """Veri çek, analiz et, grafik + tablo + özet döndür."""
264
+ try:
265
+ ticker = "EURUSD=X"
266
+ df = yf.download(ticker, period=period, interval=interval,
267
+ auto_adjust=True, progress=False)
268
 
269
+ if df.empty:
270
+ return None, pd.DataFrame(), "⚠️ Veri alınamadı. Dönem/interval kombinasyonu geçersiz olabilir."
271
 
272
+ # Çok seviyeli sütunları düzleştir
273
+ if isinstance(df.columns, pd.MultiIndex):
274
+ df.columns = df.columns.get_level_values(0)
275
 
276
+ df = calculate_indicators(df)
277
+ patterns = detect_patterns(df)
278
+ fig = build_chart(df, patterns)
279
+ summary = ai_summary(df, patterns)
280
 
281
+ pat_df = pd.DataFrame(patterns) if patterns else pd.DataFrame(
282
+ columns=["Tarih", "Pattern", "Close", "Yön"])
 
 
 
283
 
284
+ return fig, pat_df, summary
 
285
 
286
+ except Exception as e:
287
+ return None, pd.DataFrame(), f"❌ Hata: {str(e)}"
288
 
 
289
 
290
+ # ─── GRADIO ARAYÜZÜ ──────────────────────────────────────────────────────────
291
 
292
+ PERIOD_CHOICES = ["1d", "5d", "1mo", "3mo", "6mo", "1y"]
293
+ INTERVAL_CHOICES = ["15m", "30m", "1h", "4h", "1d"]
 
 
294
 
295
+ with gr.Blocks(
296
+ title="EUR/USD Pattern AI",
297
+ theme=gr.themes.Base(
298
+ primary_hue="teal",
299
+ neutral_hue="slate",
300
+ font=[gr.themes.GoogleFont("Inter"), "sans-serif"],
301
+ ),
302
+ css="""
303
+ body { background: #0f1117 !important; }
304
+ .gradio-container { max-width: 1100px !important; }
305
+ #title { text-align: center; padding: 16px 0 4px; }
306
+ #subtitle { text-align: center; color: #94a3b8; margin-bottom: 8px; font-size: 0.9em; }
307
  """
308
+ ) as demo:
 
 
 
309
 
310
+ gr.HTML("<h1 id='title'>📊 EUR/USD Pattern AI</h1>")
311
+ gr.HTML("<p id='subtitle'>yfinance • Candlestick Pattern Tespiti • Teknik İndikatörler</p>")
312
 
313
+ with gr.Row():
314
+ period_dd = gr.Dropdown(PERIOD_CHOICES, value="1mo", label="Dönem")
315
+ interval_dd = gr.Dropdown(INTERVAL_CHOICES, value="1d", label="Interval")
316
+ run_btn = gr.Button("🔍 Analiz Et", variant="primary", scale=1)
317
+
318
+ chart_out = gr.Plot(label="Grafik")
319
+ summary_out = gr.Markdown(label="AI Yorumu")
320
+ table_out = gr.Dataframe(
321
+ label="Tespit Edilen Patternler",
322
+ headers=["Tarih", "Pattern", "Close", "Yön"],
323
+ interactive=False,
324
+ wrap=True,
 
 
 
 
 
 
 
 
325
  )
326
 
327
+ run_btn.click(
328
+ fn=analyze,
329
+ inputs=[period_dd, interval_dd],
330
+ outputs=[chart_out, table_out, summary_out],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
331
  )
332
 
333
+ # Sayfa yüklenince otomatik çalıştır
334
+ demo.load(
335
+ fn=analyze,
336
+ inputs=[period_dd, interval_dd],
337
+ outputs=[chart_out, table_out, summary_out],
338
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
339
 
340
  if __name__ == "__main__":
341
+ demo.launch()