Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2 |
-
# EUR/USD Β·
|
| 3 |
-
# Model:
|
| 4 |
# Forecast horizon: 1 or 2 days ahead
|
| 5 |
# TP/SL: ATR-based risk management Β· Signal scoring Β· Backtest
|
| 6 |
# Data: Yahoo Finance (yfinance)
|
|
@@ -15,6 +15,8 @@ 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
|
|
@@ -96,6 +98,43 @@ def compute_rsi(series: pd.Series, period: int = 14) -> pd.Series:
|
|
| 96 |
return 100 - (100 / (1 + gain / (loss + 1e-10)))
|
| 97 |
|
| 98 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
def create_features(data: pd.DataFrame) -> pd.DataFrame:
|
| 100 |
"""
|
| 101 |
Premium feature set β 55+ engineered features designed to minimise MAE
|
|
@@ -218,6 +257,16 @@ def create_features(data: pd.DataFrame) -> pd.DataFrame:
|
|
| 218 |
df['Volume_Ratio'] = 1.0
|
| 219 |
df['OBV_Signal'] = 0.0
|
| 220 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
# ββ FIX 4: Direction Composite Signal (boosts directional accuracy) βββββ
|
| 222 |
# Encodes consensus of multiple indicators into a single direction score.
|
| 223 |
# Positive β bullish bias, Negative β bearish bias.
|
|
@@ -299,6 +348,8 @@ FEATURE_COLS = [
|
|
| 299 |
'Volume_Ratio', 'OBV_Signal',
|
| 300 |
# Direction composite (FIX 4)
|
| 301 |
'Direction_Score', 'Momentum_Consistency',
|
|
|
|
|
|
|
| 302 |
]
|
| 303 |
|
| 304 |
|
|
@@ -311,19 +362,15 @@ FEATURE_COLS = [
|
|
| 311 |
def build_ridge_pipeline(alpha: float) -> Pipeline:
|
| 312 |
"""Ridge pipeline with degree-2 interactions (no bias terms in poly)."""
|
| 313 |
return Pipeline([
|
| 314 |
-
('scaler',
|
| 315 |
-
('poly',
|
| 316 |
-
|
| 317 |
-
('scaler2', StandardScaler(with_mean=False)),
|
| 318 |
-
('ridge',
|
| 319 |
])
|
| 320 |
|
| 321 |
|
| 322 |
def select_best_alpha(X_tr, y_tr) -> float:
|
| 323 |
-
"""
|
| 324 |
-
RidgeCV finds optimal alpha from a fine logarithmic grid.
|
| 325 |
-
Evaluated on 5-fold time-series split (no shuffle).
|
| 326 |
-
"""
|
| 327 |
alphas = np.logspace(-3, 4, 60)
|
| 328 |
scaler = StandardScaler()
|
| 329 |
Xs = scaler.fit_transform(X_tr)
|
|
@@ -332,54 +379,115 @@ def select_best_alpha(X_tr, y_tr) -> float:
|
|
| 332 |
return float(rcv.alpha_)
|
| 333 |
|
| 334 |
|
| 335 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
"""
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 340 |
"""
|
| 341 |
avail = [c for c in FEATURE_COLS if c in df.columns]
|
| 342 |
X = df[avail].values
|
| 343 |
close = df['Close'].values
|
|
|
|
| 344 |
|
| 345 |
-
#
|
| 346 |
-
# log-return = log(close[t+horizon] / close[t])
|
| 347 |
-
# This directly encodes direction: positive = UP, negative = DOWN
|
| 348 |
y_logret = np.log(close[horizon:] / close[:len(close) - horizon])
|
|
|
|
|
|
|
| 349 |
|
| 350 |
-
X = X[:len(
|
| 351 |
-
close_aligned = close[:len(
|
|
|
|
| 352 |
|
| 353 |
-
split = -15
|
| 354 |
-
X_train, X_test = X[:split],
|
| 355 |
-
y_train,
|
|
|
|
| 356 |
close_test = close_aligned[split:]
|
|
|
|
| 357 |
|
| 358 |
-
#
|
| 359 |
best_alpha = select_best_alpha(X_train, y_train)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 360 |
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 364 |
|
| 365 |
-
#
|
| 366 |
-
|
| 367 |
-
pred_abs = close_test * np.exp(logret_hat)
|
| 368 |
-
y_test_abs = close_test * np.exp(y_logret_test)
|
| 369 |
|
| 370 |
-
|
|
|
|
|
|
|
| 371 |
|
| 372 |
-
|
| 373 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
logret
|
| 381 |
-
# Scale logret by horizon for multi-day forecasts
|
| 382 |
-
return cp * np.exp(logret * horizon)
|
| 383 |
|
| 384 |
|
| 385 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -464,7 +572,8 @@ def _ax_style(ax, title="", ylabel=""):
|
|
| 464 |
|
| 465 |
|
| 466 |
def build_figure(df, pred_price, horizon, tp_sl,
|
| 467 |
-
test_pred_abs, y_test_abs, best_alpha, mae
|
|
|
|
| 468 |
|
| 469 |
plt.rcParams.update({
|
| 470 |
'font.family': 'monospace',
|
|
@@ -517,6 +626,15 @@ def build_figure(df, pred_price, horizon, tp_sl,
|
|
| 517 |
(i - 0.25, min(o, c)), 0.5, abs(c - o) + 1e-7,
|
| 518 |
boxstyle="square,pad=0", linewidth=0, facecolor=color, alpha=0.75, zorder=3))
|
| 519 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 520 |
# Close line overlay (phosphor glow)
|
| 521 |
ax_price.plot(idx, tail['Close'].values, lw=1.6, color=ACID, zorder=5, alpha=0.9, label='Close')
|
| 522 |
|
|
@@ -545,6 +663,14 @@ def build_figure(df, pred_price, horizon, tp_sl,
|
|
| 545 |
fontfamily='monospace',
|
| 546 |
)
|
| 547 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 548 |
ax_price.legend(fontsize=7, framealpha=0.1, labelcolor=SUBTEXT,
|
| 549 |
ncol=3, loc='upper left')
|
| 550 |
ax_price.yaxis.set_major_formatter(mticker.FormatStrFormatter('%.4f'))
|
|
@@ -625,7 +751,7 @@ def build_figure(df, pred_price, horizon, tp_sl,
|
|
| 625 |
dir_color = GREEN if tp_sl['direction'] == 'LONG' else WARM
|
| 626 |
|
| 627 |
fig.text(0.055, 0.97,
|
| 628 |
-
"EUR/USD Β·
|
| 629 |
fontsize=12, color=TEXT, fontweight='bold', va='top',
|
| 630 |
fontfamily='monospace')
|
| 631 |
fig.text(0.055, 0.955, dir_label,
|
|
@@ -660,15 +786,22 @@ def predict_eurusd(horizon: int = 1):
|
|
| 660 |
if len(df) < 40:
|
| 661 |
return "β Yetersiz veri (min 40 satΔ±r gerekli).", None
|
| 662 |
|
| 663 |
-
# 3. Train Ridge + backtest
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
pred_price = predict_next_ridge(pipe, df, avail, horizon)
|
| 668 |
|
| 669 |
-
#
|
| 670 |
cp = float(df['Close'].iloc[-1])
|
| 671 |
atr = float(df['ATR'].iloc[-1])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 672 |
tp_sl = compute_tp_sl(cp, pred_price, atr,
|
| 673 |
atr_multiplier_sl=1.5, rr_ratio=2.0)
|
| 674 |
|
|
@@ -677,7 +810,6 @@ def predict_eurusd(horizon: int = 1):
|
|
| 677 |
|
| 678 |
# 7. Backtest metrics
|
| 679 |
rmse = float(np.sqrt(np.mean((test_pred_abs - y_test_abs) ** 2)))
|
| 680 |
-
# FIX 3: Correct directional accuracy β compare pred vs entry price
|
| 681 |
close_vals = df['Close'].values
|
| 682 |
n_test = len(test_pred_abs)
|
| 683 |
entry_prices = close_vals[len(close_vals) - n_test - horizon :
|
|
@@ -710,15 +842,19 @@ def predict_eurusd(horizon: int = 1):
|
|
| 710 |
W = 54
|
| 711 |
sep = "β" * W
|
| 712 |
|
|
|
|
| 713 |
lines = [
|
| 714 |
sep,
|
| 715 |
-
f" EUR/USD Β·
|
| 716 |
f" {datetime.now().strftime('%d %b %Y %H:%M:%S')}",
|
| 717 |
sep,
|
| 718 |
"",
|
| 719 |
f" {'MEVCUT FΔ°YAT':<24}: {cp:.5f}",
|
| 720 |
-
f" {f'+{horizon}G TAHMΔ°N (
|
| 721 |
f" {'YΓN':<24}: {tp_sl['direction']}",
|
|
|
|
|
|
|
|
|
|
| 722 |
f" {'OPTΔ°MAL ALPHA (RidgeCV)':<24}: {best_alpha:.6f}",
|
| 723 |
"",
|
| 724 |
f" {'βββ RΔ°SK YΓNETΔ°MΔ° (ATR Γ 1.5 | R:R 1:2)':<{W-2}}",
|
|
@@ -754,6 +890,8 @@ def predict_eurusd(horizon: int = 1):
|
|
| 754 |
f" {'RMSE':<24}: {rmse:.7f}",
|
| 755 |
f" {'YΓΆn DoΔruluΔu':<24}: {hit:.1f}%",
|
| 756 |
f" {'Feature SayΔ±sΔ±':<24}: {len(avail)}",
|
|
|
|
|
|
|
| 757 |
"",
|
| 758 |
sep,
|
| 759 |
" β YalnΔ±zca eΔitim amaΓ§lΔ±dΔ±r. Finansal tavsiye deΔildir.",
|
|
@@ -766,6 +904,8 @@ def predict_eurusd(horizon: int = 1):
|
|
| 766 |
fig = build_figure(
|
| 767 |
df, pred_price, horizon, tp_sl,
|
| 768 |
test_pred_abs, y_test_abs, best_alpha, mae,
|
|
|
|
|
|
|
| 769 |
)
|
| 770 |
|
| 771 |
return text, fig
|
|
@@ -882,13 +1022,13 @@ HEADER = """
|
|
| 882 |
<div>
|
| 883 |
<h1 style="margin:0 0 4px;font-size:1.45rem;font-weight:900;
|
| 884 |
color:#E8EDF5;letter-spacing:.06em;font-family:monospace;">
|
| 885 |
-
EUR/USD Β·
|
| 886 |
<span style="color:#00FFB2;font-size:.85rem;margin-left:10px;
|
| 887 |
vertical-align:middle;letter-spacing:.12em;">ULTRA-PREMIUM</span>
|
| 888 |
</h1>
|
| 889 |
<p style="margin:0;color:#3A4A5E;font-size:.75rem;font-family:monospace;
|
| 890 |
letter-spacing:.04em;">
|
| 891 |
-
|
| 892 |
</p>
|
| 893 |
</div>
|
| 894 |
</div>
|
|
@@ -922,7 +1062,7 @@ HEADER = """
|
|
| 922 |
# GRADIO UI
|
| 923 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 924 |
|
| 925 |
-
with gr.Blocks(css=CSS, title="EUR/USD
|
| 926 |
gr.HTML(HEADER)
|
| 927 |
|
| 928 |
with gr.Row(elem_classes="btn-row"):
|
|
|
|
| 1 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2 |
+
# EUR/USD Β· ENSEMBLE FORECAST ENGINE Β· Ultra-Premium Edition v2
|
| 3 |
+
# Model: Ensemble Ridge + GBM Β· ATR-normalised target Β· Regime filter Β· Threshold
|
| 4 |
# Forecast horizon: 1 or 2 days ahead
|
| 5 |
# TP/SL: ATR-based risk management Β· Signal scoring Β· Backtest
|
| 6 |
# Data: Yahoo Finance (yfinance)
|
|
|
|
| 15 |
from sklearn.preprocessing import StandardScaler, PolynomialFeatures
|
| 16 |
from sklearn.pipeline import Pipeline
|
| 17 |
from sklearn.metrics import mean_absolute_error
|
| 18 |
+
from sklearn.ensemble import GradientBoostingRegressor
|
| 19 |
+
from sklearn.model_selection import TimeSeriesSplit
|
| 20 |
import matplotlib
|
| 21 |
matplotlib.use('Agg')
|
| 22 |
import matplotlib.pyplot as plt
|
|
|
|
| 98 |
return 100 - (100 / (1 + gain / (loss + 1e-10)))
|
| 99 |
|
| 100 |
|
| 101 |
+
def detect_regime(df: pd.DataFrame) -> pd.Series:
|
| 102 |
+
"""
|
| 103 |
+
Market regime detection:
|
| 104 |
+
+1 = TREND (directional, EMA spread wide, ADX-proxy high)
|
| 105 |
+
0 = NEUTRAL
|
| 106 |
+
-1 = RANGE (mean-reverting, BB squeeze, flat MAs)
|
| 107 |
+
"""
|
| 108 |
+
close = df['Close']
|
| 109 |
+
ema8 = close.ewm(span=8, adjust=False).mean()
|
| 110 |
+
ema21 = close.ewm(span=21, adjust=False).mean()
|
| 111 |
+
ema55 = close.ewm(span=55, adjust=False).mean()
|
| 112 |
+
|
| 113 |
+
# Trend strength: normalised EMA spread
|
| 114 |
+
spread = (ema8 - ema55).abs() / (close + 1e-10)
|
| 115 |
+
spread_z = (spread - spread.rolling(40).mean()) / (spread.rolling(40).std() + 1e-10)
|
| 116 |
+
|
| 117 |
+
# Volatility regime: BB width z-score
|
| 118 |
+
mid = close.rolling(20).mean()
|
| 119 |
+
bb_w = (close.rolling(20).std() * 4) / (mid + 1e-10)
|
| 120 |
+
bb_z = (bb_w - bb_w.rolling(40).mean()) / (bb_w.rolling(40).std() + 1e-10)
|
| 121 |
+
|
| 122 |
+
regime = pd.Series(0, index=df.index)
|
| 123 |
+
regime[spread_z > 0.5] = 1 # trending
|
| 124 |
+
regime[bb_z < -0.5] = -1 # range / squeeze
|
| 125 |
+
return regime
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def normalise_target_by_atr(y_logret: np.ndarray, atr_vals: np.ndarray) -> np.ndarray:
|
| 129 |
+
"""
|
| 130 |
+
Divide log-returns by ATR to make the target volatility-scaled.
|
| 131 |
+
Model learns to predict 'how many ATRs will price move', not raw pips.
|
| 132 |
+
This prevents near-zero predictions when volatility is high.
|
| 133 |
+
"""
|
| 134 |
+
atr_norm = atr_vals / (atr_vals.mean() + 1e-10) # relative ATR
|
| 135 |
+
return y_logret / (atr_norm + 1e-10)
|
| 136 |
+
|
| 137 |
+
|
| 138 |
def create_features(data: pd.DataFrame) -> pd.DataFrame:
|
| 139 |
"""
|
| 140 |
Premium feature set β 55+ engineered features designed to minimise MAE
|
|
|
|
| 257 |
df['Volume_Ratio'] = 1.0
|
| 258 |
df['OBV_Signal'] = 0.0
|
| 259 |
|
| 260 |
+
# ββ Regime features ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 261 |
+
df['Regime'] = detect_regime(df)
|
| 262 |
+
df['Regime_Trend'] = (df['Regime'] == 1).astype(float)
|
| 263 |
+
df['Regime_Range'] = (df['Regime'] == -1).astype(float)
|
| 264 |
+
|
| 265 |
+
# BB squeeze: width below 20th percentile of last 60 days
|
| 266 |
+
bb20 = df['BB_width_20'] if 'BB_width_20' in df.columns else (
|
| 267 |
+
df['Close'].rolling(20).std() * 4 / (df['Close'].rolling(20).mean() + 1e-10))
|
| 268 |
+
df['BB_Squeeze'] = (bb20 < bb20.rolling(60).quantile(0.20)).astype(float)
|
| 269 |
+
|
| 270 |
# ββ FIX 4: Direction Composite Signal (boosts directional accuracy) βββββ
|
| 271 |
# Encodes consensus of multiple indicators into a single direction score.
|
| 272 |
# Positive β bullish bias, Negative β bearish bias.
|
|
|
|
| 348 |
'Volume_Ratio', 'OBV_Signal',
|
| 349 |
# Direction composite (FIX 4)
|
| 350 |
'Direction_Score', 'Momentum_Consistency',
|
| 351 |
+
# Regime
|
| 352 |
+
'Regime', 'Regime_Trend', 'Regime_Range', 'BB_Squeeze',
|
| 353 |
]
|
| 354 |
|
| 355 |
|
|
|
|
| 362 |
def build_ridge_pipeline(alpha: float) -> Pipeline:
|
| 363 |
"""Ridge pipeline with degree-2 interactions (no bias terms in poly)."""
|
| 364 |
return Pipeline([
|
| 365 |
+
('scaler', StandardScaler()),
|
| 366 |
+
('poly', PolynomialFeatures(degree=2, interaction_only=True,
|
| 367 |
+
include_bias=False)),
|
| 368 |
+
('scaler2', StandardScaler(with_mean=False)),
|
| 369 |
+
('ridge', Ridge(alpha=alpha, fit_intercept=True)),
|
| 370 |
])
|
| 371 |
|
| 372 |
|
| 373 |
def select_best_alpha(X_tr, y_tr) -> float:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
alphas = np.logspace(-3, 4, 60)
|
| 375 |
scaler = StandardScaler()
|
| 376 |
Xs = scaler.fit_transform(X_tr)
|
|
|
|
| 379 |
return float(rcv.alpha_)
|
| 380 |
|
| 381 |
|
| 382 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 383 |
+
# ENSEMBLE MODEL: Ridge + Gradient Boosting
|
| 384 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 385 |
+
|
| 386 |
+
def train_ensemble(df: pd.DataFrame, horizon: int = 1):
|
| 387 |
"""
|
| 388 |
+
Four upgrades over baseline:
|
| 389 |
+
A) ATR-normalised log-return target β prevents near-zero predictions
|
| 390 |
+
B) Regime-conditioned training β model sees market context
|
| 391 |
+
C) GBM secondary model β captures nonlinear patterns
|
| 392 |
+
D) Ensemble blend (Ridge 55% + GBM 45%)
|
| 393 |
+
Final signal passes through a threshold filter: if predicted move
|
| 394 |
+
is < 0.3Γ ATR the signal is tagged WEAK (no trade recommended).
|
| 395 |
"""
|
| 396 |
avail = [c for c in FEATURE_COLS if c in df.columns]
|
| 397 |
X = df[avail].values
|
| 398 |
close = df['Close'].values
|
| 399 |
+
atr = df['ATR'].values if 'ATR' in df.columns else np.full(len(close), 1e-4)
|
| 400 |
|
| 401 |
+
# ββ A. ATR-normalised log-return target βββββββββββββββββββββββββββββββββββ
|
|
|
|
|
|
|
| 402 |
y_logret = np.log(close[horizon:] / close[:len(close) - horizon])
|
| 403 |
+
atr_aligned = atr[:len(y_logret)]
|
| 404 |
+
y_norm = normalise_target_by_atr(y_logret, atr_aligned) # scaled target
|
| 405 |
|
| 406 |
+
X = X[:len(y_norm)]
|
| 407 |
+
close_aligned = close[:len(y_norm)]
|
| 408 |
+
atr_aligned = atr_aligned[:len(y_norm)]
|
| 409 |
|
| 410 |
+
split = -15
|
| 411 |
+
X_train, X_test = X[:split], X[split:]
|
| 412 |
+
y_train, y_norm_test = y_norm[:split], y_norm[split:]
|
| 413 |
+
y_raw_test = y_logret[len(y_logret)+split:]
|
| 414 |
close_test = close_aligned[split:]
|
| 415 |
+
atr_test = atr_aligned[split:]
|
| 416 |
|
| 417 |
+
# ββ B. Ridge on full training set ββββββββββββββββββββββββββββββββββββββββ
|
| 418 |
best_alpha = select_best_alpha(X_train, y_train)
|
| 419 |
+
ridge_pipe = build_ridge_pipeline(best_alpha)
|
| 420 |
+
ridge_pipe.fit(X_train, y_train)
|
| 421 |
+
ridge_pred_norm = ridge_pipe.predict(X_test)
|
| 422 |
+
|
| 423 |
+
# ββ C. GBM on residuals (boosting approach) βββββββββββββββββββββββββββββββ
|
| 424 |
+
# Fit GBM on training residuals so it learns what Ridge misses
|
| 425 |
+
ridge_train_pred = ridge_pipe.predict(X_train)
|
| 426 |
+
residuals_train = y_train - ridge_train_pred
|
| 427 |
+
|
| 428 |
+
scaler_gbm = StandardScaler()
|
| 429 |
+
X_tr_sc = scaler_gbm.fit_transform(X_train)
|
| 430 |
+
X_te_sc = scaler_gbm.transform(X_test)
|
| 431 |
+
|
| 432 |
+
gbm = GradientBoostingRegressor(
|
| 433 |
+
n_estimators=120, max_depth=3, learning_rate=0.05,
|
| 434 |
+
subsample=0.8, min_samples_leaf=5, random_state=42,
|
| 435 |
+
)
|
| 436 |
+
gbm.fit(X_tr_sc, residuals_train)
|
| 437 |
+
gbm_correction = gbm.predict(X_te_sc)
|
| 438 |
+
|
| 439 |
+
# ββ D. Ensemble blend ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 440 |
+
RIDGE_W = 0.55
|
| 441 |
+
GBM_W = 0.45
|
| 442 |
+
ensemble_norm = ridge_pred_norm * RIDGE_W + (ridge_pred_norm + gbm_correction) * GBM_W
|
| 443 |
+
|
| 444 |
+
# Convert normalised prediction back to log-return, then to price
|
| 445 |
+
atr_norm_test = atr_test / (atr_aligned.mean() + 1e-10)
|
| 446 |
+
logret_hat = ensemble_norm * atr_norm_test
|
| 447 |
+
pred_abs = close_test * np.exp(logret_hat)
|
| 448 |
+
y_test_abs = close_test * np.exp(y_raw_test)
|
| 449 |
+
|
| 450 |
+
mae = mean_absolute_error(y_test_abs, pred_abs)
|
| 451 |
|
| 452 |
+
return (ridge_pipe, gbm, scaler_gbm, atr_aligned.mean(),
|
| 453 |
+
pred_abs, y_test_abs, avail, best_alpha, mae,
|
| 454 |
+
RIDGE_W, GBM_W)
|
| 455 |
+
|
| 456 |
+
|
| 457 |
+
def predict_next_ensemble(ridge_pipe, gbm, scaler_gbm, atr_mean,
|
| 458 |
+
df: pd.DataFrame, avail: list,
|
| 459 |
+
horizon: int = 1,
|
| 460 |
+
ridge_w: float = 0.55,
|
| 461 |
+
gbm_w: float = 0.45) -> tuple:
|
| 462 |
+
"""
|
| 463 |
+
Returns (pred_price, logret, signal_strength, threshold_passed).
|
| 464 |
+
signal_strength = |predicted move| / ATR (how many ATRs away)
|
| 465 |
+
threshold_passed = True if signal_strength >= 0.30
|
| 466 |
+
"""
|
| 467 |
+
cp = float(df['Close'].iloc[-1])
|
| 468 |
+
atr = float(df['ATR'].iloc[-1]) if 'ATR' in df.columns else 1e-4
|
| 469 |
+
x = df[avail].iloc[-1].values.reshape(1, -1)
|
| 470 |
|
| 471 |
+
# Ridge prediction (normalised)
|
| 472 |
+
ridge_norm = float(ridge_pipe.predict(x)[0])
|
|
|
|
|
|
|
| 473 |
|
| 474 |
+
# GBM correction on residual
|
| 475 |
+
x_sc = scaler_gbm.transform(x)
|
| 476 |
+
gbm_corr = float(gbm.predict(x_sc)[0])
|
| 477 |
|
| 478 |
+
ensemble_norm = ridge_norm * ridge_w + (ridge_norm + gbm_corr) * gbm_w
|
| 479 |
|
| 480 |
+
# Denormalise
|
| 481 |
+
atr_norm = atr / (atr_mean + 1e-10)
|
| 482 |
+
logret = ensemble_norm * atr_norm * horizon
|
| 483 |
+
pred_price = cp * np.exp(logret)
|
| 484 |
|
| 485 |
+
# ββ Sinyal eΕiΔi βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 486 |
+
signal_strength = abs(pred_price - cp) / (atr + 1e-10)
|
| 487 |
+
THRESHOLD = 0.30 # minimum 0.30 ATR hareketi gerekiyor
|
| 488 |
+
threshold_passed = signal_strength >= THRESHOLD
|
| 489 |
+
|
| 490 |
+
return pred_price, logret, signal_strength, threshold_passed
|
|
|
|
|
|
|
| 491 |
|
| 492 |
|
| 493 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 572 |
|
| 573 |
|
| 574 |
def build_figure(df, pred_price, horizon, tp_sl,
|
| 575 |
+
test_pred_abs, y_test_abs, best_alpha, mae,
|
| 576 |
+
regime_val=0, sig_strength=0.0, thr_passed=True) -> plt.Figure:
|
| 577 |
|
| 578 |
plt.rcParams.update({
|
| 579 |
'font.family': 'monospace',
|
|
|
|
| 626 |
(i - 0.25, min(o, c)), 0.5, abs(c - o) + 1e-7,
|
| 627 |
boxstyle="square,pad=0", linewidth=0, facecolor=color, alpha=0.75, zorder=3))
|
| 628 |
|
| 629 |
+
# ββ Regime background shading βββββββββββββββββββββββββββββββββββββββββββββ
|
| 630 |
+
if 'Regime' in tail.columns:
|
| 631 |
+
for i, (_, row) in enumerate(tail.iterrows()):
|
| 632 |
+
rv = int(row.get('Regime', 0))
|
| 633 |
+
if rv == 1: # trend
|
| 634 |
+
ax_price.axvspan(i-0.5, i+0.5, alpha=0.06, color=ACID, zorder=0)
|
| 635 |
+
elif rv == -1: # range
|
| 636 |
+
ax_price.axvspan(i-0.5, i+0.5, alpha=0.06, color=ACID3, zorder=0)
|
| 637 |
+
|
| 638 |
# Close line overlay (phosphor glow)
|
| 639 |
ax_price.plot(idx, tail['Close'].values, lw=1.6, color=ACID, zorder=5, alpha=0.9, label='Close')
|
| 640 |
|
|
|
|
| 663 |
fontfamily='monospace',
|
| 664 |
)
|
| 665 |
|
| 666 |
+
# Threshold / signal strength label
|
| 667 |
+
thr_color = GREEN if thr_passed else RED
|
| 668 |
+
thr_text = f"{'β
' if thr_passed else 'β'} Sinyal GΓΌcΓΌ: {sig_strength:.2f}Γ ATR"
|
| 669 |
+
ax_price.text(0.01, 0.04, thr_text, transform=ax_price.transAxes,
|
| 670 |
+
fontsize=7.5, color=thr_color, fontweight='bold',
|
| 671 |
+
fontfamily='monospace', zorder=9,
|
| 672 |
+
bbox=dict(facecolor=PANEL, edgecolor=thr_color, alpha=0.7, pad=3))
|
| 673 |
+
|
| 674 |
ax_price.legend(fontsize=7, framealpha=0.1, labelcolor=SUBTEXT,
|
| 675 |
ncol=3, loc='upper left')
|
| 676 |
ax_price.yaxis.set_major_formatter(mticker.FormatStrFormatter('%.4f'))
|
|
|
|
| 751 |
dir_color = GREEN if tp_sl['direction'] == 'LONG' else WARM
|
| 752 |
|
| 753 |
fig.text(0.055, 0.97,
|
| 754 |
+
"EUR/USD Β· ENSEMBLE FORECAST ENGINE Β· ULTRA-PREMIUM",
|
| 755 |
fontsize=12, color=TEXT, fontweight='bold', va='top',
|
| 756 |
fontfamily='monospace')
|
| 757 |
fig.text(0.055, 0.955, dir_label,
|
|
|
|
| 786 |
if len(df) < 40:
|
| 787 |
return "β Yetersiz veri (min 40 satΔ±r gerekli).", None
|
| 788 |
|
| 789 |
+
# 3. Train Ensemble (Ridge + GBM) + backtest
|
| 790 |
+
(ridge_pipe, gbm, scaler_gbm, atr_mean,
|
| 791 |
+
test_pred_abs, y_test_abs, avail, best_alpha, mae,
|
| 792 |
+
ridge_w, gbm_w) = train_ensemble(df, horizon)
|
|
|
|
| 793 |
|
| 794 |
+
# 4. Predict next (ensemble)
|
| 795 |
cp = float(df['Close'].iloc[-1])
|
| 796 |
atr = float(df['ATR'].iloc[-1])
|
| 797 |
+
pred_price, logret_val, sig_strength, thr_passed = predict_next_ensemble(
|
| 798 |
+
ridge_pipe, gbm, scaler_gbm, atr_mean, df, avail, horizon, ridge_w, gbm_w)
|
| 799 |
+
|
| 800 |
+
# 4b. Regime
|
| 801 |
+
regime_val = int(df['Regime'].iloc[-1]) if 'Regime' in df.columns else 0
|
| 802 |
+
regime_lbl = {1: "TREND βΆ", 0: "NΓTR β", -1: "RANGE β"}.get(regime_val, "?")
|
| 803 |
+
|
| 804 |
+
# 5. TP/SL
|
| 805 |
tp_sl = compute_tp_sl(cp, pred_price, atr,
|
| 806 |
atr_multiplier_sl=1.5, rr_ratio=2.0)
|
| 807 |
|
|
|
|
| 810 |
|
| 811 |
# 7. Backtest metrics
|
| 812 |
rmse = float(np.sqrt(np.mean((test_pred_abs - y_test_abs) ** 2)))
|
|
|
|
| 813 |
close_vals = df['Close'].values
|
| 814 |
n_test = len(test_pred_abs)
|
| 815 |
entry_prices = close_vals[len(close_vals) - n_test - horizon :
|
|
|
|
| 842 |
W = 54
|
| 843 |
sep = "β" * W
|
| 844 |
|
| 845 |
+
thr_lbl = "β
AKTΔ°F SΔ°NYAL" if thr_passed else "β ZAYIF SΔ°NYAL (trade yok)"
|
| 846 |
lines = [
|
| 847 |
sep,
|
| 848 |
+
f" EUR/USD Β· ENSEMBLE FORECAST ENGINE Β· ULTRA-PREMIUM",
|
| 849 |
f" {datetime.now().strftime('%d %b %Y %H:%M:%S')}",
|
| 850 |
sep,
|
| 851 |
"",
|
| 852 |
f" {'MEVCUT FΔ°YAT':<24}: {cp:.5f}",
|
| 853 |
+
f" {f'+{horizon}G TAHMΔ°N (ENSEMBLE)':<24}: {pred_price:.5f} ({chg:+.3f}%)",
|
| 854 |
f" {'YΓN':<24}: {tp_sl['direction']}",
|
| 855 |
+
f" {'REJΔ°M':<24}: {regime_lbl}",
|
| 856 |
+
f" {'SΔ°NYAL GΓCΓ (ATRΓ)':<24}: {sig_strength:.2f}x β {thr_lbl}",
|
| 857 |
+
f" {'LOG-RETURN TAHMΔ°N':<24}: {logret_val*100:+.4f}%",
|
| 858 |
f" {'OPTΔ°MAL ALPHA (RidgeCV)':<24}: {best_alpha:.6f}",
|
| 859 |
"",
|
| 860 |
f" {'βββ RΔ°SK YΓNETΔ°MΔ° (ATR Γ 1.5 | R:R 1:2)':<{W-2}}",
|
|
|
|
| 890 |
f" {'RMSE':<24}: {rmse:.7f}",
|
| 891 |
f" {'YΓΆn DoΔruluΔu':<24}: {hit:.1f}%",
|
| 892 |
f" {'Feature SayΔ±sΔ±':<24}: {len(avail)}",
|
| 893 |
+
f" {'Ensemble (Ridge/GBM)':<24}: {ridge_w*100:.0f}% / {gbm_w*100:.0f}%",
|
| 894 |
+
f" {'EΕik (ATRΓ0.30)':<24}: {'GeΓ§ti β
' if thr_passed else 'GEΓEMEDΔ° β'}",
|
| 895 |
"",
|
| 896 |
sep,
|
| 897 |
" β YalnΔ±zca eΔitim amaΓ§lΔ±dΔ±r. Finansal tavsiye deΔildir.",
|
|
|
|
| 904 |
fig = build_figure(
|
| 905 |
df, pred_price, horizon, tp_sl,
|
| 906 |
test_pred_abs, y_test_abs, best_alpha, mae,
|
| 907 |
+
regime_val=regime_val, sig_strength=sig_strength,
|
| 908 |
+
thr_passed=thr_passed,
|
| 909 |
)
|
| 910 |
|
| 911 |
return text, fig
|
|
|
|
| 1022 |
<div>
|
| 1023 |
<h1 style="margin:0 0 4px;font-size:1.45rem;font-weight:900;
|
| 1024 |
color:#E8EDF5;letter-spacing:.06em;font-family:monospace;">
|
| 1025 |
+
EUR/USD Β· ENSEMBLE FORECAST ENGINE
|
| 1026 |
<span style="color:#00FFB2;font-size:.85rem;margin-left:10px;
|
| 1027 |
vertical-align:middle;letter-spacing:.12em;">ULTRA-PREMIUM</span>
|
| 1028 |
</h1>
|
| 1029 |
<p style="margin:0;color:#3A4A5E;font-size:.75rem;font-family:monospace;
|
| 1030 |
letter-spacing:.04em;">
|
| 1031 |
+
RIDGE+GBM ENSEMBLE Β· ATR-NORM TARGET Β· REGIME FILTER Β· SIGNAL THRESHOLD Β· 80+ FEATURES
|
| 1032 |
</p>
|
| 1033 |
</div>
|
| 1034 |
</div>
|
|
|
|
| 1062 |
# GRADIO UI
|
| 1063 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1064 |
|
| 1065 |
+
with gr.Blocks(css=CSS, title="EUR/USD Ensemble Forecast Engine") as app:
|
| 1066 |
gr.HTML(HEADER)
|
| 1067 |
|
| 1068 |
with gr.Row(elem_classes="btn-row"):
|