Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -95,27 +95,26 @@ os.makedirs(MODEL_DIR, exist_ok=True)
|
|
| 95 |
MODEL_PATH = os.path.join(MODEL_DIR, "gos_hawk_model.pkl")
|
| 96 |
|
| 97 |
MIN_TRAIN_SAMPLES = 150
|
| 98 |
-
REQUEST_TIMEOUT = 60
|
| 99 |
MAX_RETRIES = 3
|
| 100 |
RETRY_BACKOFF = 2
|
| 101 |
|
| 102 |
-
# Saatlik veri (24 saatlik hedef için daha fazla nokta → güvenilir model)
|
| 103 |
HISTORY_TIME_SCALE = 1
|
| 104 |
|
| 105 |
-
# Hedef ölçeği: log getiri * 10000 = baz puan
|
| 106 |
TARGET_SCALE = 10000.0
|
| 107 |
-
|
| 108 |
-
# Tahmin clip (log getiri, ±%15)
|
| 109 |
MAX_LOG_RETURN = 0.15
|
| 110 |
MIN_LOG_RETURN = -0.15
|
| 111 |
-
|
| 112 |
-
# Belirsizlik eşiği (log getiri)
|
| 113 |
LOW_CONFIDENCE_THRESHOLD = 0.01
|
| 114 |
-
|
| 115 |
-
# Zaman-tabanlı hedef toleransı (saat)
|
| 116 |
TARGET_TOLERANCE_HOURS = 12
|
| 117 |
|
| 118 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
FEATURE_COLS = [
|
| 120 |
"hour", "dayofweek", "is_weekend",
|
| 121 |
"lag_1h", "lag_3h", "lag_6h", "lag_12h", "lag_24h", "lag_48h",
|
|
@@ -139,7 +138,7 @@ PRICE_LIKE_COLS = [
|
|
| 139 |
|
| 140 |
def safe_request(url: str, params: Optional[dict] = None) -> Optional[list]:
|
| 141 |
headers = {
|
| 142 |
-
"User-Agent": "GOSHAWK-ALBION-MARKET-AI/1.
|
| 143 |
"Accept": "application/json",
|
| 144 |
}
|
| 145 |
for attempt in range(MAX_RETRIES):
|
|
@@ -191,7 +190,7 @@ def fetch_current_prices(item_ids: List[str], cities: List[str], qualities: str
|
|
| 191 |
|
| 192 |
def fetch_history(item_ids: List[str], cities: List[str], time_scale: int = 1, qualities: str = "1") -> pd.DataFrame:
|
| 193 |
all_records = []
|
| 194 |
-
chunk_size = 20
|
| 195 |
for i in range(0, len(item_ids), chunk_size):
|
| 196 |
chunk = item_ids[i:i+chunk_size]
|
| 197 |
url = f"{API_BASE}/stats/history/{','.join(chunk)}.json"
|
|
@@ -276,15 +275,6 @@ def compute_rsi(series: pd.Series, period: int = 14) -> pd.Series:
|
|
| 276 |
|
| 277 |
|
| 278 |
def build_features(df: pd.DataFrame) -> pd.DataFrame:
|
| 279 |
-
"""
|
| 280 |
-
Saatlik veride feature'lar:
|
| 281 |
-
- lag_1h..lag_48h: saatlik fiyat gecikmeleri
|
| 282 |
-
- chg_1h..chg_24h: log değişimleri
|
| 283 |
-
- ma_6h..ma_48h: hareketli ortalamalar
|
| 284 |
-
- rsi_14, volatility_24h
|
| 285 |
-
- volume_ratio, city_price_deviation
|
| 286 |
-
- target_return_24h: zaman-tabanlı (tolerance ile) 24 saat ileri log getiri × 10000
|
| 287 |
-
"""
|
| 288 |
if df.empty:
|
| 289 |
return df
|
| 290 |
|
|
@@ -298,7 +288,6 @@ def build_features(df: pd.DataFrame) -> pd.DataFrame:
|
|
| 298 |
|
| 299 |
group_cols = ["item_id", "city"]
|
| 300 |
|
| 301 |
-
# Saatlik lag'ler
|
| 302 |
for lag in [1, 3, 6, 12, 24, 48]:
|
| 303 |
df[f"lag_{lag}h"] = df.groupby(group_cols)["avg_price"].shift(lag)
|
| 304 |
|
|
@@ -325,17 +314,22 @@ def build_features(df: pd.DataFrame) -> pd.DataFrame:
|
|
| 325 |
city_avg = df.groupby(["item_id", "timestamp"])["avg_price"].transform("mean")
|
| 326 |
df["city_price_deviation"] = (df["avg_price"] - city_avg) / city_avg.replace(0, np.nan)
|
| 327 |
|
| 328 |
-
#
|
| 329 |
-
df =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 330 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 331 |
return df
|
| 332 |
|
| 333 |
|
| 334 |
def _compute_forward_target(df: pd.DataFrame) -> pd.DataFrame:
|
| 335 |
-
"""
|
| 336 |
-
Her (item_id, city) grubu için 24 saat ilerideki log fiyatını bulur.
|
| 337 |
-
shift(-N) yerine merge_asof ile zaman-tabanlı eşleştirme yapar (düzensiz aralıklara dayanıklı).
|
| 338 |
-
"""
|
| 339 |
if df.empty or "log_price" not in df.columns:
|
| 340 |
df["target_return_24h"] = np.nan
|
| 341 |
return df
|
|
@@ -394,12 +388,10 @@ def prepare_model_data(featured_df: pd.DataFrame) -> Tuple[pd.DataFrame, pd.Seri
|
|
| 394 |
|
| 395 |
df = df.dropna(subset=["target_return_24h", "lag_24h"]).copy()
|
| 396 |
|
| 397 |
-
# Fiyat-benzeri feature'ları log ölçeğine çevir
|
| 398 |
for col in PRICE_LIKE_COLS:
|
| 399 |
if col in df.columns:
|
| 400 |
df[col] = np.log1p(df[col].clip(lower=1.0))
|
| 401 |
|
| 402 |
-
# Uzun pencereleri doldur
|
| 403 |
if "lag_48h" in df.columns:
|
| 404 |
df["lag_48h"] = df["lag_48h"].fillna(df["lag_24h"])
|
| 405 |
if "ma_48h" in df.columns:
|
|
@@ -423,7 +415,6 @@ def prepare_model_data(featured_df: pd.DataFrame) -> Tuple[pd.DataFrame, pd.Seri
|
|
| 423 |
|
| 424 |
model_df = df.dropna(subset=FEATURE_COLS + ["target_return_24h"]).copy()
|
| 425 |
|
| 426 |
-
# Hedef clip (bp cinsinden ±1500)
|
| 427 |
target_min = MIN_LOG_RETURN * TARGET_SCALE
|
| 428 |
target_max = MAX_LOG_RETURN * TARGET_SCALE
|
| 429 |
model_df["target_return_24h"] = model_df["target_return_24h"].clip(target_min, target_max)
|
|
@@ -442,11 +433,10 @@ def train_global_model(X: pd.DataFrame, y: pd.Series) -> Optional[lgb.LGBMRegres
|
|
| 442 |
return None
|
| 443 |
|
| 444 |
y_log_std = (y / TARGET_SCALE).std()
|
| 445 |
-
|
| 446 |
-
print(f"[TANI] train_global_model: n={len(X)}, y_bp_std={y_bp_std:.4f}, y_log_std={y_log_std:.8f}")
|
| 447 |
|
| 448 |
if y_log_std < 1e-6:
|
| 449 |
-
print(f"⚠️ Hedef log std çok düşük: {y_log_std:.8f}
|
| 450 |
return None
|
| 451 |
|
| 452 |
split_idx = int(len(X) * 0.8)
|
|
@@ -485,8 +475,7 @@ def train_global_model(X: pd.DataFrame, y: pd.Series) -> Optional[lgb.LGBMRegres
|
|
| 485 |
)
|
| 486 |
|
| 487 |
val_preds = model.predict(X_val)
|
| 488 |
-
|
| 489 |
-
print(f"[TANI] val_pred_std={pred_std:.4f} bp")
|
| 490 |
|
| 491 |
return model
|
| 492 |
|
|
@@ -523,15 +512,14 @@ def predict_24h_returns(model: lgb.LGBMRegressor, latest_features: pd.DataFrame)
|
|
| 523 |
raw_preds_bp = model.predict(X_ready)
|
| 524 |
raw_preds_log = raw_preds_bp / TARGET_SCALE
|
| 525 |
|
| 526 |
-
pred_std = float(np.std(raw_preds_log))
|
| 527 |
-
if pred_std < 1e-5:
|
| 528 |
-
print(f"⚠️ UYARI: Tüm tahminler aynı (std={pred_std:.8f}).")
|
| 529 |
-
|
| 530 |
clipped_log = np.clip(raw_preds_log, MIN_LOG_RETURN, MAX_LOG_RETURN)
|
| 531 |
pred_return_pct = (np.exp(clipped_log) - 1) * 100
|
| 532 |
|
| 533 |
-
|
| 534 |
-
|
|
|
|
|
|
|
|
|
|
| 535 |
result["raw_log_pred"] = raw_preds_log
|
| 536 |
result["clipped_log_pred"] = clipped_log
|
| 537 |
result["predicted_log_return_24h"] = clipped_log
|
|
@@ -542,21 +530,117 @@ def predict_24h_returns(model: lgb.LGBMRegressor, latest_features: pd.DataFrame)
|
|
| 542 |
return result
|
| 543 |
|
| 544 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 545 |
# ---------------------------------------------------------------------------
|
| 546 |
# WALK-FORWARD BACKTEST
|
| 547 |
# ---------------------------------------------------------------------------
|
| 548 |
|
| 549 |
|
| 550 |
-
def walk_forward_backtest(featured_df: pd.DataFrame, n_splits: int =
|
| 551 |
if featured_df.empty:
|
| 552 |
return {"error": "Veri yok."}
|
| 553 |
|
| 554 |
df = featured_df.sort_values("timestamp").copy()
|
| 555 |
unique_times = df["timestamp"].sort_values().unique()
|
| 556 |
-
|
| 557 |
-
|
|
|
|
| 558 |
|
| 559 |
test_size = len(unique_times) // (n_splits + 1)
|
|
|
|
|
|
|
|
|
|
| 560 |
results = []
|
| 561 |
all_predictions = []
|
| 562 |
|
|
@@ -565,7 +649,7 @@ def walk_forward_backtest(featured_df: pd.DataFrame, n_splits: int = 3) -> Dict:
|
|
| 565 |
test_end_idx = test_start_idx + test_size
|
| 566 |
if test_end_idx > len(unique_times):
|
| 567 |
break
|
| 568 |
-
train_end_idx = test_start_idx -
|
| 569 |
if train_end_idx <= 0:
|
| 570 |
continue
|
| 571 |
|
|
@@ -575,19 +659,21 @@ def walk_forward_backtest(featured_df: pd.DataFrame, n_splits: int = 3) -> Dict:
|
|
| 575 |
train_df = df[df["timestamp"].isin(train_times)]
|
| 576 |
test_df = df[df["timestamp"].isin(test_times)]
|
| 577 |
|
| 578 |
-
if len(train_df) <
|
|
|
|
| 579 |
continue
|
| 580 |
|
| 581 |
X_train, y_train, _ = prepare_model_data(train_df)
|
| 582 |
X_test, y_test, meta_test = prepare_model_data(test_df)
|
| 583 |
|
| 584 |
-
if len(X_train) <
|
|
|
|
| 585 |
continue
|
| 586 |
|
| 587 |
model = lgb.LGBMRegressor(
|
| 588 |
objective="regression", metric="mae",
|
| 589 |
-
num_leaves=
|
| 590 |
-
learning_rate=0.05, n_estimators=
|
| 591 |
reg_lambda=0.1, verbose=-1, random_state=42, n_jobs=-1,
|
| 592 |
)
|
| 593 |
model.fit(X_train, y_train)
|
|
@@ -598,7 +684,7 @@ def walk_forward_backtest(featured_df: pd.DataFrame, n_splits: int = 3) -> Dict:
|
|
| 598 |
|
| 599 |
mae = mean_absolute_error(y_test_log, preds)
|
| 600 |
rmse = np.sqrt(mean_squared_error(y_test_log, preds))
|
| 601 |
-
direction_correct = np.mean((y_test_log > 0) == (preds > 0))
|
| 602 |
|
| 603 |
baseline_preds = np.zeros_like(y_test_log)
|
| 604 |
baseline_mae = mean_absolute_error(y_test_log, baseline_preds)
|
|
@@ -606,8 +692,8 @@ def walk_forward_backtest(featured_df: pd.DataFrame, n_splits: int = 3) -> Dict:
|
|
| 606 |
|
| 607 |
results.append({
|
| 608 |
"split": i + 1,
|
| 609 |
-
"train_samples": len(X_train),
|
| 610 |
-
"test_samples": len(X_test),
|
| 611 |
"mae": round(float(mae), 6),
|
| 612 |
"rmse": round(float(rmse), 6),
|
| 613 |
"direction_accuracy": round(float(direction_correct), 4),
|
|
@@ -620,9 +706,10 @@ def walk_forward_backtest(featured_df: pd.DataFrame, n_splits: int = 3) -> Dict:
|
|
| 620 |
test_meta["predicted_return"] = preds
|
| 621 |
test_meta["actual_return"] = y_test_log.values
|
| 622 |
all_predictions.append(test_meta)
|
|
|
|
| 623 |
|
| 624 |
if not results:
|
| 625 |
-
return {"error": "Backtest tamamlanamadı."}
|
| 626 |
|
| 627 |
if all_predictions:
|
| 628 |
sim_df = pd.concat(all_predictions)
|
|
@@ -738,16 +825,13 @@ class DataPipeline:
|
|
| 738 |
if n_with_target < MIN_TRAIN_SAMPLES:
|
| 739 |
self.status_message = (
|
| 740 |
f"Yetersiz hedefli örnek: {n_with_target} < {MIN_TRAIN_SAMPLES}. "
|
| 741 |
-
f"Toplam: {diag.get('n_total', 0)}, grup: {diag.get('n_groups', 0)}.
|
| 742 |
-
f"24 saatlik ileri veri bulunamadı — API çok seyrek olabilir."
|
| 743 |
)
|
| 744 |
return False
|
| 745 |
|
| 746 |
if std_log < 1e-6:
|
| 747 |
self.status_message = (
|
| 748 |
-
f"Hedef varyansı çok düşük (std_log={std_log:.8f}
|
| 749 |
-
f"std_bp={diag.get('target_std_bp', 0):.4f}). Model öğrenemez. "
|
| 750 |
-
f"API'den daha fazla veri çekmeyi deneyin."
|
| 751 |
)
|
| 752 |
return False
|
| 753 |
|
|
@@ -764,7 +848,7 @@ class DataPipeline:
|
|
| 764 |
sample_preds = self.model.predict(X.head(500))
|
| 765 |
pred_std = float(np.std(sample_preds))
|
| 766 |
if pred_std < 1.0:
|
| 767 |
-
self.status_message = f"⚠️ Model zayıf öğrendi (pred_std={pred_std:.2f} bp).
|
| 768 |
else:
|
| 769 |
self.status_message = f"✓ Model eğitildi ({len(X)} örnek, pred_std={pred_std:.1f} bp)."
|
| 770 |
|
|
@@ -810,6 +894,11 @@ class DataPipeline:
|
|
| 810 |
total_cost = SETUP_FEE_RATE + TRANSACTION_TAX_RATE
|
| 811 |
preds["net_return_pct"] = preds["predicted_return_pct"] - (total_cost * 100)
|
| 812 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 813 |
return preds
|
| 814 |
|
| 815 |
def get_backtest(self):
|
|
@@ -849,6 +938,7 @@ def get_dashboard_metrics():
|
|
| 849 |
"Ortalama Fiyat": f"{df['avg_price'].mean():.0f} silver",
|
| 850 |
"Model Durumu": "Eğitildi" if pipeline.model else "Eğitilmedi",
|
| 851 |
"Tahmin Sınırı": f"±%{MAX_LOG_RETURN * 100:.0f}",
|
|
|
|
| 852 |
"Son Güncelleme": pipeline.last_update.strftime("%Y-%m-%d %H:%M") if pipeline.last_update else "—",
|
| 853 |
}
|
| 854 |
return pd.DataFrame(list(metrics.items()), columns=["Metrik", "Değer"])
|
|
@@ -874,10 +964,13 @@ def get_analysis_table():
|
|
| 874 |
latest["Mevcut Fiyat"] = latest["avg_price"].round(0)
|
| 875 |
latest["Hacim"] = latest["item_count"].fillna(0).astype(int)
|
| 876 |
latest["Spread"] = (latest["sell_price_min"] - latest["buy_price_max"]).round(0)
|
|
|
|
|
|
|
|
|
|
| 877 |
latest["Likidite"] = pd.cut(latest["item_count"], bins=[-1, 10, 50, 200, np.inf],
|
| 878 |
labels=["Düşük", "Orta", "İyi", "Yüksek"]).astype(str)
|
| 879 |
|
| 880 |
-
return latest[["Ürün", "Şehir", "Mevcut Fiyat", "Hacim", "Spread", "Likidite"]].sort_values("Hacim", ascending=False).head(100)
|
| 881 |
|
| 882 |
|
| 883 |
def get_prediction_table():
|
|
@@ -892,18 +985,56 @@ def get_prediction_table():
|
|
| 892 |
preds["Tahmini Fiyat (24s)"] = preds["predicted_price_24h"].round(0)
|
| 893 |
preds["Tahmini Değişim %"] = preds["predicted_return_pct"].round(2)
|
| 894 |
preds["Güvenilirlik"] = (preds["confidence"] * 100).round(0).astype(int).astype(str) + "%"
|
| 895 |
-
preds["Spread"] = preds["spread"].round(0)
|
| 896 |
preds["Net Getiri % (maliyet sonrası)"] = preds["net_return_pct"].round(2)
|
| 897 |
preds["Likidite"] = pd.cut(preds["item_count"], bins=[-1, 10, 50, 200, np.inf],
|
| 898 |
labels=["Düşük", "Orta", "İyi", "Yüksek"]).astype(str)
|
| 899 |
-
preds["Not"] = np.where(preds["is_uncertain"], "⚠️ Belirsiz", "")
|
| 900 |
|
| 901 |
cols = ["Ürün", "Şehir", "Mevcut Fiyat", "Tahmini Fiyat (24s)",
|
| 902 |
-
"Tahmini Değişim %", "Güvenilirlik", "
|
| 903 |
-
"Net Getiri % (maliyet sonrası)", "
|
| 904 |
return preds[cols].sort_values("Tahmini Değişim %", ascending=False)
|
| 905 |
|
| 906 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 907 |
def get_arbitrage_table():
|
| 908 |
if pipeline.prices_df.empty:
|
| 909 |
return pd.DataFrame({"Bilgi": ["Anlık fiyat yok."]})
|
|
@@ -1023,6 +1154,7 @@ def refresh_data_ui():
|
|
| 1023 |
_safe(get_dashboard_metrics, pd.DataFrame()),
|
| 1024 |
_safe(get_analysis_table, pd.DataFrame()),
|
| 1025 |
_safe(get_prediction_table, pd.DataFrame()),
|
|
|
|
| 1026 |
_safe(get_arbitrage_table, pd.DataFrame()),
|
| 1027 |
_safe(get_model_performance_plot, go.Figure().update_layout(title="—", template="plotly_dark")),
|
| 1028 |
bt_table, bt_fig, sim_table,
|
|
@@ -1039,7 +1171,7 @@ def initial_load():
|
|
| 1039 |
|
| 1040 |
return (
|
| 1041 |
_safe(get_dashboard_metrics, pd.DataFrame({"Metrik": ["Durum"], "Değer": ["Hazır"]})),
|
| 1042 |
-
pd.DataFrame(), pd.DataFrame(), pd.DataFrame(),
|
| 1043 |
go.Figure().update_layout(title="Yenile butonuna basın", template="plotly_dark"),
|
| 1044 |
pd.DataFrame(),
|
| 1045 |
go.Figure().update_layout(title="Veri gerekli", template="plotly_dark"),
|
|
@@ -1059,11 +1191,10 @@ with gr.Blocks(title="GOSHAWK ALBION MARKET AI", theme=gr.themes.Soft(primary_hu
|
|
| 1059 |
"""
|
| 1060 |
# 🦅 GOSHAWK ALBION MARKET AI
|
| 1061 |
**Profesyonel Market Intelligence Dashboard** | Avrupa Sunucusu
|
| 1062 |
-
*LightGBM tabanlı 24 saatlik fiyat tahmini
|
| 1063 |
-
|
| 1064 |
-
**Tahmin sınırı:** ±%15 · **Hedef ölçeği:** baz puan · **Veri:** saatlik · **Hedef:** zaman-tabanlı (24h, tolerans ±12h)
|
| 1065 |
|
| 1066 |
-
**⚠️ Risk Uyarısı:** Bu uygulama yatırım tavsiyesi değildir. Model tahminleri belirsizlik içerir
|
|
|
|
| 1067 |
"""
|
| 1068 |
)
|
| 1069 |
|
|
@@ -1075,27 +1206,42 @@ with gr.Blocks(title="GOSHAWK ALBION MARKET AI", theme=gr.themes.Soft(primary_hu
|
|
| 1075 |
with gr.TabItem("📊 Dashboard"):
|
| 1076 |
dashboard_table = gr.Dataframe(label="Özet & Teşhis", interactive=False, wrap=True)
|
| 1077 |
gr.Markdown(
|
| 1078 |
-
"###
|
| 1079 |
-
"
|
| 1080 |
-
"
|
| 1081 |
-
"
|
| 1082 |
-
"
|
| 1083 |
-
"
|
| 1084 |
-
"
|
| 1085 |
-
"
|
| 1086 |
-
"
|
| 1087 |
-
"
|
| 1088 |
-
"- **
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1089 |
"### Riskler\n"
|
| 1090 |
-
"-
|
|
|
|
| 1091 |
)
|
| 1092 |
|
| 1093 |
-
with gr.TabItem("
|
| 1094 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1095 |
|
| 1096 |
-
with gr.TabItem("🔮
|
| 1097 |
-
prediction_table = gr.Dataframe(label="Tahminler", interactive=False)
|
| 1098 |
-
|
|
|
|
|
|
|
| 1099 |
|
| 1100 |
with gr.TabItem("🚚 Arbitraj"):
|
| 1101 |
arbitrage_table = gr.Dataframe(label="Arbitraj Fırsatları", interactive=False)
|
|
@@ -1106,13 +1252,16 @@ with gr.Blocks(title="GOSHAWK ALBION MARKET AI", theme=gr.themes.Soft(primary_hu
|
|
| 1106 |
with gr.TabItem("🧪 Backtest"):
|
| 1107 |
backtest_table = gr.Dataframe(label="Split Sonuçları", interactive=False)
|
| 1108 |
backtest_plot = gr.Plot(label="MAE Karşılaştırması")
|
| 1109 |
-
simulation_table = gr.Dataframe(label="Simülasyon", interactive=False)
|
| 1110 |
|
| 1111 |
with gr.TabItem("🗄️ Veri Yönetimi"):
|
| 1112 |
gr.Markdown(
|
| 1113 |
"**API:** `europe.albion-online-data.com/api/v2`\n"
|
| 1114 |
"**Time-scale:** 1h (saatlik)\n"
|
| 1115 |
-
"**Model:** `gos_hawk_data/models/gos_hawk_model.pkl`"
|
|
|
|
|
|
|
|
|
|
| 1116 |
)
|
| 1117 |
csv_files = gr.Dataframe(label="CSV Dosyaları", interactive=False,
|
| 1118 |
headers=["Dosya", "Boyut (KB)", "Son Değişiklik"])
|
|
@@ -1121,15 +1270,16 @@ with gr.Blocks(title="GOSHAWK ALBION MARKET AI", theme=gr.themes.Soft(primary_hu
|
|
| 1121 |
fn=refresh_data_ui,
|
| 1122 |
inputs=[],
|
| 1123 |
outputs=[status_text, dashboard_table, analysis_table, prediction_table,
|
| 1124 |
-
arbitrage_table, performance_plot, backtest_table,
|
| 1125 |
-
simulation_table, csv_files],
|
| 1126 |
)
|
| 1127 |
|
| 1128 |
demo.load(
|
| 1129 |
fn=initial_load,
|
| 1130 |
inputs=[],
|
| 1131 |
-
outputs=[dashboard_table, analysis_table, prediction_table,
|
| 1132 |
-
performance_plot, backtest_table, backtest_plot,
|
|
|
|
| 1133 |
)
|
| 1134 |
|
| 1135 |
|
|
|
|
| 95 |
MODEL_PATH = os.path.join(MODEL_DIR, "gos_hawk_model.pkl")
|
| 96 |
|
| 97 |
MIN_TRAIN_SAMPLES = 150
|
| 98 |
+
REQUEST_TIMEOUT = 60
|
| 99 |
MAX_RETRIES = 3
|
| 100 |
RETRY_BACKOFF = 2
|
| 101 |
|
|
|
|
| 102 |
HISTORY_TIME_SCALE = 1
|
| 103 |
|
|
|
|
| 104 |
TARGET_SCALE = 10000.0
|
|
|
|
|
|
|
| 105 |
MAX_LOG_RETURN = 0.15
|
| 106 |
MIN_LOG_RETURN = -0.15
|
|
|
|
|
|
|
| 107 |
LOW_CONFIDENCE_THRESHOLD = 0.01
|
|
|
|
|
|
|
| 108 |
TARGET_TOLERANCE_HOURS = 12
|
| 109 |
|
| 110 |
+
# DİP ALIM SİNYALİ PARAMETRELERİ
|
| 111 |
+
DIP_RSI_MAX = 40.0 # RSI bu değerin altındaysa "aşırı satım"
|
| 112 |
+
DIP_PERCENTILE_MAX = 0.25 # 7 günlük aralıkta alt %25'te mi?
|
| 113 |
+
DIP_MIN_VOLUME_RATIO = 1.0 # Hacim ortalamanın üstünde mi?
|
| 114 |
+
DIP_MIN_PREDICTED_PCT = -1.0 # Model tahmini bu kadar negatiften fazla olmalı
|
| 115 |
+
DIP_MAX_SPREAD_PCT = 0.15 # Spread / fiyat > %15 ise likit değil
|
| 116 |
+
DIP_MIN_LIQUIDITY = "Orta" # Düşük likidite = atla
|
| 117 |
+
|
| 118 |
FEATURE_COLS = [
|
| 119 |
"hour", "dayofweek", "is_weekend",
|
| 120 |
"lag_1h", "lag_3h", "lag_6h", "lag_12h", "lag_24h", "lag_48h",
|
|
|
|
| 138 |
|
| 139 |
def safe_request(url: str, params: Optional[dict] = None) -> Optional[list]:
|
| 140 |
headers = {
|
| 141 |
+
"User-Agent": "GOSHAWK-ALBION-MARKET-AI/1.6 (public-api-client)",
|
| 142 |
"Accept": "application/json",
|
| 143 |
}
|
| 144 |
for attempt in range(MAX_RETRIES):
|
|
|
|
| 190 |
|
| 191 |
def fetch_history(item_ids: List[str], cities: List[str], time_scale: int = 1, qualities: str = "1") -> pd.DataFrame:
|
| 192 |
all_records = []
|
| 193 |
+
chunk_size = 20
|
| 194 |
for i in range(0, len(item_ids), chunk_size):
|
| 195 |
chunk = item_ids[i:i+chunk_size]
|
| 196 |
url = f"{API_BASE}/stats/history/{','.join(chunk)}.json"
|
|
|
|
| 275 |
|
| 276 |
|
| 277 |
def build_features(df: pd.DataFrame) -> pd.DataFrame:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
if df.empty:
|
| 279 |
return df
|
| 280 |
|
|
|
|
| 288 |
|
| 289 |
group_cols = ["item_id", "city"]
|
| 290 |
|
|
|
|
| 291 |
for lag in [1, 3, 6, 12, 24, 48]:
|
| 292 |
df[f"lag_{lag}h"] = df.groupby(group_cols)["avg_price"].shift(lag)
|
| 293 |
|
|
|
|
| 314 |
city_avg = df.groupby(["item_id", "timestamp"])["avg_price"].transform("mean")
|
| 315 |
df["city_price_deviation"] = (df["avg_price"] - city_avg) / city_avg.replace(0, np.nan)
|
| 316 |
|
| 317 |
+
# 7 günlük (168h) fiyat aralığındaki yüzdelik konum
|
| 318 |
+
df["price_pct_7d"] = df.groupby(group_cols)["avg_price"].transform(
|
| 319 |
+
lambda x: x.rolling(168, min_periods=24).apply(
|
| 320 |
+
lambda arr: (arr[-1] - np.min(arr)) / (np.max(arr) - np.min(arr) + 1e-9),
|
| 321 |
+
raw=True
|
| 322 |
+
)
|
| 323 |
+
)
|
| 324 |
|
| 325 |
+
# Fiyat düşüş trendi (12h ve 24h değişimlerin negatif olması)
|
| 326 |
+
df["is_downtrend"] = ((df["chg_12h"] < 0) & (df["chg_24h"] < 0)).astype(int)
|
| 327 |
+
|
| 328 |
+
df = _compute_forward_target(df)
|
| 329 |
return df
|
| 330 |
|
| 331 |
|
| 332 |
def _compute_forward_target(df: pd.DataFrame) -> pd.DataFrame:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 333 |
if df.empty or "log_price" not in df.columns:
|
| 334 |
df["target_return_24h"] = np.nan
|
| 335 |
return df
|
|
|
|
| 388 |
|
| 389 |
df = df.dropna(subset=["target_return_24h", "lag_24h"]).copy()
|
| 390 |
|
|
|
|
| 391 |
for col in PRICE_LIKE_COLS:
|
| 392 |
if col in df.columns:
|
| 393 |
df[col] = np.log1p(df[col].clip(lower=1.0))
|
| 394 |
|
|
|
|
| 395 |
if "lag_48h" in df.columns:
|
| 396 |
df["lag_48h"] = df["lag_48h"].fillna(df["lag_24h"])
|
| 397 |
if "ma_48h" in df.columns:
|
|
|
|
| 415 |
|
| 416 |
model_df = df.dropna(subset=FEATURE_COLS + ["target_return_24h"]).copy()
|
| 417 |
|
|
|
|
| 418 |
target_min = MIN_LOG_RETURN * TARGET_SCALE
|
| 419 |
target_max = MAX_LOG_RETURN * TARGET_SCALE
|
| 420 |
model_df["target_return_24h"] = model_df["target_return_24h"].clip(target_min, target_max)
|
|
|
|
| 433 |
return None
|
| 434 |
|
| 435 |
y_log_std = (y / TARGET_SCALE).std()
|
| 436 |
+
print(f"[TANI] train: n={len(X)}, y_log_std={y_log_std:.8f}")
|
|
|
|
| 437 |
|
| 438 |
if y_log_std < 1e-6:
|
| 439 |
+
print(f"⚠️ Hedef log std çok düşük: {y_log_std:.8f}")
|
| 440 |
return None
|
| 441 |
|
| 442 |
split_idx = int(len(X) * 0.8)
|
|
|
|
| 475 |
)
|
| 476 |
|
| 477 |
val_preds = model.predict(X_val)
|
| 478 |
+
print(f"[TANI] val_pred_std={float(np.std(val_preds)):.4f} bp")
|
|
|
|
| 479 |
|
| 480 |
return model
|
| 481 |
|
|
|
|
| 512 |
raw_preds_bp = model.predict(X_ready)
|
| 513 |
raw_preds_log = raw_preds_bp / TARGET_SCALE
|
| 514 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 515 |
clipped_log = np.clip(raw_preds_log, MIN_LOG_RETURN, MAX_LOG_RETURN)
|
| 516 |
pred_return_pct = (np.exp(clipped_log) - 1) * 100
|
| 517 |
|
| 518 |
+
keep_cols = ["item_id", "city", "avg_price", "item_count",
|
| 519 |
+
"volatility_24h", "rsi_14", "volume_ratio",
|
| 520 |
+
"price_pct_7d", "is_downtrend", "chg_12h", "chg_24h"]
|
| 521 |
+
keep_cols = [c for c in keep_cols if c in X_pred.columns]
|
| 522 |
+
result = X_pred.loc[X_ready.index, keep_cols].copy()
|
| 523 |
result["raw_log_pred"] = raw_preds_log
|
| 524 |
result["clipped_log_pred"] = clipped_log
|
| 525 |
result["predicted_log_return_24h"] = clipped_log
|
|
|
|
| 530 |
return result
|
| 531 |
|
| 532 |
|
| 533 |
+
# ---------------------------------------------------------------------------
|
| 534 |
+
# DİP ALIM SİNYALİ
|
| 535 |
+
# ---------------------------------------------------------------------------
|
| 536 |
+
|
| 537 |
+
|
| 538 |
+
def compute_dip_signal(row: pd.Series) -> Tuple[str, str]:
|
| 539 |
+
"""
|
| 540 |
+
Kural tabanlı dip alım filtresi.
|
| 541 |
+
Returns: (signal_text, reason_text)
|
| 542 |
+
|
| 543 |
+
Sinyaller:
|
| 544 |
+
- 🟢 GÜÇLÜ AL: tüm koşullar sağlanıyor
|
| 545 |
+
- 🟡 ZAYIF AL: çoğu koşul sağlanıyor
|
| 546 |
+
- 🔵 İZLE: dip bölgesinde ama teyit yok
|
| 547 |
+
- ⚪ BEKLE: dip değil
|
| 548 |
+
- 🔴 ALMA: düşüş trendi, model de negatif
|
| 549 |
+
"""
|
| 550 |
+
reasons = []
|
| 551 |
+
passed = 0
|
| 552 |
+
total = 0
|
| 553 |
+
|
| 554 |
+
# 1. RSI aşırı satım
|
| 555 |
+
rsi = row.get("rsi_14", 50)
|
| 556 |
+
total += 1
|
| 557 |
+
if pd.notna(rsi) and rsi < DIP_RSI_MAX:
|
| 558 |
+
passed += 1
|
| 559 |
+
reasons.append(f"RSI={rsi:.0f}")
|
| 560 |
+
else:
|
| 561 |
+
reasons.append(f"RSI={rsi:.0f} (değil)")
|
| 562 |
+
|
| 563 |
+
# 2. 7 günlük fiyat percentile
|
| 564 |
+
pct = row.get("price_pct_7d", 0.5)
|
| 565 |
+
total += 1
|
| 566 |
+
if pd.notna(pct) and pct < DIP_PERCENTILE_MAX:
|
| 567 |
+
passed += 1
|
| 568 |
+
reasons.append(f"7g%={pct*100:.0f}")
|
| 569 |
+
else:
|
| 570 |
+
reasons.append(f"7g%={(pct*100 if pd.notna(pct) else 0):.0f} (yüksek)")
|
| 571 |
+
|
| 572 |
+
# 3. Hacim onayı (talep dönüyor)
|
| 573 |
+
vr = row.get("volume_ratio", 1.0)
|
| 574 |
+
total += 1
|
| 575 |
+
if pd.notna(vr) and vr >= DIP_MIN_VOLUME_RATIO:
|
| 576 |
+
passed += 1
|
| 577 |
+
reasons.append(f"Hacim×{vr:.1f}")
|
| 578 |
+
else:
|
| 579 |
+
reasons.append(f"Hacim×{(vr if pd.notna(vr) else 0):.1f} (zayıf)")
|
| 580 |
+
|
| 581 |
+
# 4. Model tahmini çok negatif değil
|
| 582 |
+
pred_pct = row.get("predicted_return_pct", 0)
|
| 583 |
+
total += 1
|
| 584 |
+
if pd.notna(pred_pct) and pred_pct >= DIP_MIN_PREDICTED_PCT:
|
| 585 |
+
passed += 1
|
| 586 |
+
reasons.append(f"Model={pred_pct:+.1f}%")
|
| 587 |
+
else:
|
| 588 |
+
reasons.append(f"Model={pred_pct:+.1f}% (düşüş)")
|
| 589 |
+
|
| 590 |
+
# 5. Likidite yeterli (spread kontrolü)
|
| 591 |
+
spread = row.get("spread", np.nan)
|
| 592 |
+
price = row.get("avg_price", 1)
|
| 593 |
+
total += 1
|
| 594 |
+
if pd.notna(spread) and price > 0 and abs(spread) / price < DIP_MAX_SPREAD_PCT:
|
| 595 |
+
passed += 1
|
| 596 |
+
reasons.append(f"Spread={abs(spread)/price*100:.1f}%")
|
| 597 |
+
else:
|
| 598 |
+
reasons.append(f"Spread geniş")
|
| 599 |
+
|
| 600 |
+
# 6. Aşırı düşüş trendi DEĞİL (bıçak tutma koruması)
|
| 601 |
+
is_down = row.get("is_downtrend", 0)
|
| 602 |
+
chg_24 = row.get("chg_24h", 0)
|
| 603 |
+
total += 1
|
| 604 |
+
if not is_down or (pd.notna(pred_pct) and pred_pct > 0.5):
|
| 605 |
+
passed += 1
|
| 606 |
+
reasons.append(f"Trend OK")
|
| 607 |
+
else:
|
| 608 |
+
reasons.append(f"↓ düşüş trendi")
|
| 609 |
+
|
| 610 |
+
# Sinyal kararı
|
| 611 |
+
if passed >= 5:
|
| 612 |
+
signal = "🟢 GÜÇLÜ AL"
|
| 613 |
+
elif passed == 4:
|
| 614 |
+
signal = "🟡 ZAYIF AL"
|
| 615 |
+
elif passed == 3 and pd.notna(rsi) and rsi < 45:
|
| 616 |
+
signal = "🔵 İZLE"
|
| 617 |
+
elif is_down and pd.notna(pred_pct) and pred_pct < -2:
|
| 618 |
+
signal = "🔴 ALMA"
|
| 619 |
+
else:
|
| 620 |
+
signal = "⚪ BEKLE"
|
| 621 |
+
|
| 622 |
+
return signal, " · ".join(reasons)
|
| 623 |
+
|
| 624 |
+
|
| 625 |
# ---------------------------------------------------------------------------
|
| 626 |
# WALK-FORWARD BACKTEST
|
| 627 |
# ---------------------------------------------------------------------------
|
| 628 |
|
| 629 |
|
| 630 |
+
def walk_forward_backtest(featured_df: pd.DataFrame, n_splits: int = 2) -> Dict:
|
| 631 |
if featured_df.empty:
|
| 632 |
return {"error": "Veri yok."}
|
| 633 |
|
| 634 |
df = featured_df.sort_values("timestamp").copy()
|
| 635 |
unique_times = df["timestamp"].sort_values().unique()
|
| 636 |
+
|
| 637 |
+
if len(unique_times) < 40:
|
| 638 |
+
return {"error": f"Yeterli zaman noktası yok ({len(unique_times)} < 40)."}
|
| 639 |
|
| 640 |
test_size = len(unique_times) // (n_splits + 1)
|
| 641 |
+
if test_size < 5:
|
| 642 |
+
return {"error": f"Test boyutu çok küçük ({test_size} < 5)."}
|
| 643 |
+
|
| 644 |
results = []
|
| 645 |
all_predictions = []
|
| 646 |
|
|
|
|
| 649 |
test_end_idx = test_start_idx + test_size
|
| 650 |
if test_end_idx > len(unique_times):
|
| 651 |
break
|
| 652 |
+
train_end_idx = test_start_idx - 24
|
| 653 |
if train_end_idx <= 0:
|
| 654 |
continue
|
| 655 |
|
|
|
|
| 659 |
train_df = df[df["timestamp"].isin(train_times)]
|
| 660 |
test_df = df[df["timestamp"].isin(test_times)]
|
| 661 |
|
| 662 |
+
if len(train_df) < 50 or len(test_df) < 5:
|
| 663 |
+
print(f"[BACKTEST] split {i+1}: train={len(train_df)}, test={len(test_df)} → atlandı")
|
| 664 |
continue
|
| 665 |
|
| 666 |
X_train, y_train, _ = prepare_model_data(train_df)
|
| 667 |
X_test, y_test, meta_test = prepare_model_data(test_df)
|
| 668 |
|
| 669 |
+
if len(X_train) < 30 or len(X_test) < 3:
|
| 670 |
+
print(f"[BACKTEST] split {i+1}: X_train={len(X_train)}, X_test={len(X_test)} → atlandı")
|
| 671 |
continue
|
| 672 |
|
| 673 |
model = lgb.LGBMRegressor(
|
| 674 |
objective="regression", metric="mae",
|
| 675 |
+
num_leaves=15, max_depth=6, min_child_samples=15,
|
| 676 |
+
learning_rate=0.05, n_estimators=200,
|
| 677 |
reg_lambda=0.1, verbose=-1, random_state=42, n_jobs=-1,
|
| 678 |
)
|
| 679 |
model.fit(X_train, y_train)
|
|
|
|
| 684 |
|
| 685 |
mae = mean_absolute_error(y_test_log, preds)
|
| 686 |
rmse = np.sqrt(mean_squared_error(y_test_log, preds))
|
| 687 |
+
direction_correct = float(np.mean((y_test_log > 0) == (preds > 0)))
|
| 688 |
|
| 689 |
baseline_preds = np.zeros_like(y_test_log)
|
| 690 |
baseline_mae = mean_absolute_error(y_test_log, baseline_preds)
|
|
|
|
| 692 |
|
| 693 |
results.append({
|
| 694 |
"split": i + 1,
|
| 695 |
+
"train_samples": int(len(X_train)),
|
| 696 |
+
"test_samples": int(len(X_test)),
|
| 697 |
"mae": round(float(mae), 6),
|
| 698 |
"rmse": round(float(rmse), 6),
|
| 699 |
"direction_accuracy": round(float(direction_correct), 4),
|
|
|
|
| 706 |
test_meta["predicted_return"] = preds
|
| 707 |
test_meta["actual_return"] = y_test_log.values
|
| 708 |
all_predictions.append(test_meta)
|
| 709 |
+
print(f"[BACKTEST] split {i+1}: MAE={mae:.6f}, baseline={baseline_mae:.6f}, dir_acc={direction_correct:.3f}")
|
| 710 |
|
| 711 |
if not results:
|
| 712 |
+
return {"error": "Backtest tamamlanamadı. Yeterli eğitim/test örneği oluşmadı."}
|
| 713 |
|
| 714 |
if all_predictions:
|
| 715 |
sim_df = pd.concat(all_predictions)
|
|
|
|
| 825 |
if n_with_target < MIN_TRAIN_SAMPLES:
|
| 826 |
self.status_message = (
|
| 827 |
f"Yetersiz hedefli örnek: {n_with_target} < {MIN_TRAIN_SAMPLES}. "
|
| 828 |
+
f"Toplam: {diag.get('n_total', 0)}, grup: {diag.get('n_groups', 0)}."
|
|
|
|
| 829 |
)
|
| 830 |
return False
|
| 831 |
|
| 832 |
if std_log < 1e-6:
|
| 833 |
self.status_message = (
|
| 834 |
+
f"Hedef varyansı çok düşük (std_log={std_log:.8f}). Model öğrenemez."
|
|
|
|
|
|
|
| 835 |
)
|
| 836 |
return False
|
| 837 |
|
|
|
|
| 848 |
sample_preds = self.model.predict(X.head(500))
|
| 849 |
pred_std = float(np.std(sample_preds))
|
| 850 |
if pred_std < 1.0:
|
| 851 |
+
self.status_message = f"⚠️ Model zayıf öğrendi (pred_std={pred_std:.2f} bp)."
|
| 852 |
else:
|
| 853 |
self.status_message = f"✓ Model eğitildi ({len(X)} örnek, pred_std={pred_std:.1f} bp)."
|
| 854 |
|
|
|
|
| 894 |
total_cost = SETUP_FEE_RATE + TRANSACTION_TAX_RATE
|
| 895 |
preds["net_return_pct"] = preds["predicted_return_pct"] - (total_cost * 100)
|
| 896 |
|
| 897 |
+
# Dip sinyali hesapla (her satır için)
|
| 898 |
+
dip_signals = preds.apply(compute_dip_signal, axis=1, result_type="expand")
|
| 899 |
+
preds["Dip Sinyali"] = dip_signals[0]
|
| 900 |
+
preds["Sinyal Detayı"] = dip_signals[1]
|
| 901 |
+
|
| 902 |
return preds
|
| 903 |
|
| 904 |
def get_backtest(self):
|
|
|
|
| 938 |
"Ortalama Fiyat": f"{df['avg_price'].mean():.0f} silver",
|
| 939 |
"Model Durumu": "Eğitildi" if pipeline.model else "Eğitilmedi",
|
| 940 |
"Tahmin Sınırı": f"±%{MAX_LOG_RETURN * 100:.0f}",
|
| 941 |
+
"Dip Alım Eşiği": f"RSI<{DIP_RSI_MAX:.0f}, 7g%<{DIP_PERCENTILE_MAX*100:.0f}%",
|
| 942 |
"Son Güncelleme": pipeline.last_update.strftime("%Y-%m-%d %H:%M") if pipeline.last_update else "—",
|
| 943 |
}
|
| 944 |
return pd.DataFrame(list(metrics.items()), columns=["Metrik", "Değer"])
|
|
|
|
| 964 |
latest["Mevcut Fiyat"] = latest["avg_price"].round(0)
|
| 965 |
latest["Hacim"] = latest["item_count"].fillna(0).astype(int)
|
| 966 |
latest["Spread"] = (latest["sell_price_min"] - latest["buy_price_max"]).round(0)
|
| 967 |
+
latest["RSI"] = latest["rsi_14"].round(0)
|
| 968 |
+
latest["7g Konum %"] = (latest["price_pct_7d"] * 100).round(0)
|
| 969 |
+
latest["Trend"] = np.where(latest["is_downtrend"] == 1, "↓ Düşüş", "→ Yatay/Yukarı")
|
| 970 |
latest["Likidite"] = pd.cut(latest["item_count"], bins=[-1, 10, 50, 200, np.inf],
|
| 971 |
labels=["Düşük", "Orta", "İyi", "Yüksek"]).astype(str)
|
| 972 |
|
| 973 |
+
return latest[["Ürün", "Şehir", "Mevcut Fiyat", "Hacim", "Spread", "RSI", "7g Konum %", "Trend", "Likidite"]].sort_values("Hacim", ascending=False).head(100)
|
| 974 |
|
| 975 |
|
| 976 |
def get_prediction_table():
|
|
|
|
| 985 |
preds["Tahmini Fiyat (24s)"] = preds["predicted_price_24h"].round(0)
|
| 986 |
preds["Tahmini Değişim %"] = preds["predicted_return_pct"].round(2)
|
| 987 |
preds["Güvenilirlik"] = (preds["confidence"] * 100).round(0).astype(int).astype(str) + "%"
|
|
|
|
| 988 |
preds["Net Getiri % (maliyet sonrası)"] = preds["net_return_pct"].round(2)
|
| 989 |
preds["Likidite"] = pd.cut(preds["item_count"], bins=[-1, 10, 50, 200, np.inf],
|
| 990 |
labels=["Düşük", "Orta", "İyi", "Yüksek"]).astype(str)
|
|
|
|
| 991 |
|
| 992 |
cols = ["Ürün", "Şehir", "Mevcut Fiyat", "Tahmini Fiyat (24s)",
|
| 993 |
+
"Tahmini Değişim %", "Güvenilirlik", "Likidite",
|
| 994 |
+
"Net Getiri % (maliyet sonrası)", "Dip Sinyali"]
|
| 995 |
return preds[cols].sort_values("Tahmini Değişim %", ascending=False)
|
| 996 |
|
| 997 |
|
| 998 |
+
def get_dip_alim_table():
|
| 999 |
+
"""Sadece DİP ALIM sinyali veren ürünler, sinyal gücüne göre sıralı."""
|
| 1000 |
+
preds = pipeline.get_latest_predictions()
|
| 1001 |
+
if preds.empty:
|
| 1002 |
+
return pd.DataFrame({"Bilgi": ["Tahmin üretilemedi."]})
|
| 1003 |
+
|
| 1004 |
+
# Sadece AL/İZLE sinyalleri
|
| 1005 |
+
dip_only = preds[preds["Dip Sinyali"].isin(
|
| 1006 |
+
["🟢 GÜÇLÜ AL", "🟡 ZAYIF AL", "🔵 İZLE"]
|
| 1007 |
+
)].copy()
|
| 1008 |
+
|
| 1009 |
+
if dip_only.empty:
|
| 1010 |
+
return pd.DataFrame({"Bilgi": [
|
| 1011 |
+
"Şu anda dip alım sinyali yok. Daha fazla veri veya farklı eşikler gerekebilir."
|
| 1012 |
+
]})
|
| 1013 |
+
|
| 1014 |
+
# Sıralama: sinyal gücü > güvenilirlik > tahmini değişim
|
| 1015 |
+
signal_order = {"🟢 GÜÇLÜ AL": 0, "🟡 ZAYIF AL": 1, "🔵 İZLE": 2}
|
| 1016 |
+
dip_only["_sig_rank"] = dip_only["Dip Sinyali"].map(signal_order)
|
| 1017 |
+
|
| 1018 |
+
dip_only["Ürün"] = dip_only["item_id"].apply(item_display_name)
|
| 1019 |
+
dip_only["Şehir"] = dip_only["city"]
|
| 1020 |
+
dip_only["Mevcut Fiyat"] = dip_only["avg_price"].round(0)
|
| 1021 |
+
dip_only["RSI"] = dip_only["rsi_14"].round(0)
|
| 1022 |
+
dip_only["7g Konum %"] = (dip_only["price_pct_7d"] * 100).round(0)
|
| 1023 |
+
dip_only["Trend"] = np.where(dip_only["is_downtrend"] == 1, "↓", "→")
|
| 1024 |
+
dip_only["Hacim×Ort"] = dip_only["volume_ratio"].round(1)
|
| 1025 |
+
dip_only["Tahmini Değişim %"] = dip_only["predicted_return_pct"].round(2)
|
| 1026 |
+
dip_only["Net Getiri %"] = dip_only["net_return_pct"].round(2)
|
| 1027 |
+
dip_only["Güvenilirlik"] = (dip_only["confidence"] * 100).round(0).astype(int).astype(str) + "%"
|
| 1028 |
+
dip_only["Spread"] = dip_only["spread"].round(0)
|
| 1029 |
+
|
| 1030 |
+
dip_only = dip_only.sort_values(["_sig_rank", "confidence"], ascending=[True, False])
|
| 1031 |
+
|
| 1032 |
+
cols = ["Dip Sinyali", "Ürün", "Şehir", "Mevcut Fiyat", "RSI", "7g Konum %",
|
| 1033 |
+
"Trend", "Hacim×Ort", "Tahmini Değişim %", "Net Getiri %",
|
| 1034 |
+
"Güvenilirlik", "Spread", "Sinyal Detayı"]
|
| 1035 |
+
return dip_only[cols].head(50)
|
| 1036 |
+
|
| 1037 |
+
|
| 1038 |
def get_arbitrage_table():
|
| 1039 |
if pipeline.prices_df.empty:
|
| 1040 |
return pd.DataFrame({"Bilgi": ["Anlık fiyat yok."]})
|
|
|
|
| 1154 |
_safe(get_dashboard_metrics, pd.DataFrame()),
|
| 1155 |
_safe(get_analysis_table, pd.DataFrame()),
|
| 1156 |
_safe(get_prediction_table, pd.DataFrame()),
|
| 1157 |
+
_safe(get_dip_alim_table, pd.DataFrame()),
|
| 1158 |
_safe(get_arbitrage_table, pd.DataFrame()),
|
| 1159 |
_safe(get_model_performance_plot, go.Figure().update_layout(title="—", template="plotly_dark")),
|
| 1160 |
bt_table, bt_fig, sim_table,
|
|
|
|
| 1171 |
|
| 1172 |
return (
|
| 1173 |
_safe(get_dashboard_metrics, pd.DataFrame({"Metrik": ["Durum"], "Değer": ["Hazır"]})),
|
| 1174 |
+
pd.DataFrame(), pd.DataFrame(), pd.DataFrame(), pd.DataFrame(),
|
| 1175 |
go.Figure().update_layout(title="Yenile butonuna basın", template="plotly_dark"),
|
| 1176 |
pd.DataFrame(),
|
| 1177 |
go.Figure().update_layout(title="Veri gerekli", template="plotly_dark"),
|
|
|
|
| 1191 |
"""
|
| 1192 |
# 🦅 GOSHAWK ALBION MARKET AI
|
| 1193 |
**Profesyonel Market Intelligence Dashboard** | Avrupa Sunucusu
|
| 1194 |
+
*LightGBM tabanlı 24 saatlik fiyat tahmini + kural-tabanlı dip alım sinyali.*
|
|
|
|
|
|
|
| 1195 |
|
| 1196 |
+
**⚠️ Risk Uyarısı:** Bu uygulama yatırım tavsiyesi değildir. Model tahminleri belirsizlik içerir.
|
| 1197 |
+
"Dip alım" sinyali **sadece** istatistiksel bir filtredir; fiyat daha da düşebilir.
|
| 1198 |
"""
|
| 1199 |
)
|
| 1200 |
|
|
|
|
| 1206 |
with gr.TabItem("📊 Dashboard"):
|
| 1207 |
dashboard_table = gr.Dataframe(label="Özet & Teşhis", interactive=False, wrap=True)
|
| 1208 |
gr.Markdown(
|
| 1209 |
+
"### Dip Alım Sinyali Mantığı\n"
|
| 1210 |
+
"**Fiyat düşerken almak** = riskli. Model bunu doğru şekilde **filtreler**:\n\n"
|
| 1211 |
+
"**6 koşuldan en az 4'ü sağlanırsa** AL sinyali:\n"
|
| 1212 |
+
"1. RSI < 40 (aşırı satım)\n"
|
| 1213 |
+
"2. 7 günlük aralıkta alt %25'te\n"
|
| 1214 |
+
"3. Hacim ortalamanın üstünde (talep dönüyor)\n"
|
| 1215 |
+
"4. Model tahmini çok negatif değil (> -1%)\n"
|
| 1216 |
+
"5. Spread dar (likit)\n"
|
| 1217 |
+
"6. Aşırı düşüş trendi DEĞİL (bıçak tutma koruması)\n\n"
|
| 1218 |
+
"### Sinyaller\n"
|
| 1219 |
+
"- 🟢 **GÜÇLÜ AL:** 5+ koşul\n"
|
| 1220 |
+
"- 🟡 **ZAYIF AL:** 4 koşul\n"
|
| 1221 |
+
"- 🔵 **İZLE:** 3 koşul + RSI<45\n"
|
| 1222 |
+
"- 🔴 **ALMA:** düşüş trendi + model negatif\n"
|
| 1223 |
+
"- ⚪ **BEKLE:** diğer\n\n"
|
| 1224 |
"### Riskler\n"
|
| 1225 |
+
"- Model tahminleri olasılıksaldır\n"
|
| 1226 |
+
"- Dip alım her zaman işe yaramaz, stop-loss kullanın"
|
| 1227 |
)
|
| 1228 |
|
| 1229 |
+
with gr.TabItem("🎯 DİP ALIM FIRSATLARI"):
|
| 1230 |
+
dip_alim_table = gr.Dataframe(label="Dip Alım Sinyalleri (Fiyat düşerken al)", interactive=False)
|
| 1231 |
+
gr.Markdown(
|
| 1232 |
+
"*Bu tablo **sadece** dip alım sinyali veren ürünleri gösterir. "
|
| 1233 |
+
"🟢 GÜÇLÜ AL en yüksek öncelikli. "
|
| 1234 |
+
"**Sinyal Detayı** sütunu hangi koşulların sağlandığını gösterir.*\n\n"
|
| 1235 |
+
"**⚠️ UYARI:** Fiyat düşerken almak, trend dönerse kârlıdır. "
|
| 1236 |
+
"Ama trend devam ederse zarar eder. "
|
| 1237 |
+
"Sadece **likit** ürünlerde ve **küçük pozisyonlarla** deneyin."
|
| 1238 |
+
)
|
| 1239 |
|
| 1240 |
+
with gr.TabItem("🔮 24s Tahmin"):
|
| 1241 |
+
prediction_table = gr.Dataframe(label="Tüm Tahminler + Dip Sinyali", interactive=False)
|
| 1242 |
+
|
| 1243 |
+
with gr.TabItem("📦 100 Ürün Analizi"):
|
| 1244 |
+
analysis_table = gr.Dataframe(label="Ürün Analizi (RSI, Trend, Konum)", interactive=False)
|
| 1245 |
|
| 1246 |
with gr.TabItem("🚚 Arbitraj"):
|
| 1247 |
arbitrage_table = gr.Dataframe(label="Arbitraj Fırsatları", interactive=False)
|
|
|
|
| 1252 |
with gr.TabItem("🧪 Backtest"):
|
| 1253 |
backtest_table = gr.Dataframe(label="Split Sonuçları", interactive=False)
|
| 1254 |
backtest_plot = gr.Plot(label="MAE Karşılaştırması")
|
| 1255 |
+
simulation_table = gr.Dataframe(label="Simülasyon (maliyetler sonrası)", interactive=False)
|
| 1256 |
|
| 1257 |
with gr.TabItem("🗄️ Veri Yönetimi"):
|
| 1258 |
gr.Markdown(
|
| 1259 |
"**API:** `europe.albion-online-data.com/api/v2`\n"
|
| 1260 |
"**Time-scale:** 1h (saatlik)\n"
|
| 1261 |
+
"**Model:** `gos_hawk_data/models/gos_hawk_model.pkl`\n\n"
|
| 1262 |
+
"**Dip Alım Parametreleri:** "
|
| 1263 |
+
f"RSI<{DIP_RSI_MAX}, 7g%<{DIP_PERCENTILE_MAX*100:.0f}%, "
|
| 1264 |
+
f"Hacim≥{DIP_MIN_VOLUME_RATIO}, Spread<%{DIP_MAX_SPREAD_PCT*100:.0f}"
|
| 1265 |
)
|
| 1266 |
csv_files = gr.Dataframe(label="CSV Dosyaları", interactive=False,
|
| 1267 |
headers=["Dosya", "Boyut (KB)", "Son Değişiklik"])
|
|
|
|
| 1270 |
fn=refresh_data_ui,
|
| 1271 |
inputs=[],
|
| 1272 |
outputs=[status_text, dashboard_table, analysis_table, prediction_table,
|
| 1273 |
+
dip_alim_table, arbitrage_table, performance_plot, backtest_table,
|
| 1274 |
+
backtest_plot, simulation_table, csv_files],
|
| 1275 |
)
|
| 1276 |
|
| 1277 |
demo.load(
|
| 1278 |
fn=initial_load,
|
| 1279 |
inputs=[],
|
| 1280 |
+
outputs=[dashboard_table, analysis_table, prediction_table, dip_alim_table,
|
| 1281 |
+
arbitrage_table, performance_plot, backtest_table, backtest_plot,
|
| 1282 |
+
simulation_table, csv_files],
|
| 1283 |
)
|
| 1284 |
|
| 1285 |
|