Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -2,10 +2,6 @@
|
|
| 2 |
# GOSHAWK ALBION MARKET AI
|
| 3 |
# Tek dosyalık, çalıştırılabilir Market Intelligence uygulaması.
|
| 4 |
# Gerekli paketler: gradio, lightgbm, pandas, numpy, plotly, requests, scikit-learn, joblib
|
| 5 |
-
#
|
| 6 |
-
# NOT: Bu uygulama Albion Online Data Project (AODP) public API'sini kullanır.
|
| 7 |
-
# Varsayılan sunucu: Avrupa (europe).
|
| 8 |
-
# API dokümantasyonu: https://www.albion-online-data.com/
|
| 9 |
|
| 10 |
import os
|
| 11 |
import time
|
|
@@ -37,9 +33,6 @@ warnings.filterwarnings("ignore")
|
|
| 37 |
# ---------------------------------------------------------------------------
|
| 38 |
|
| 39 |
API_BASE = "https://europe.albion-online-data.com/api/v2"
|
| 40 |
-
# Alternatifler:
|
| 41 |
-
# API_BASE = "https://west.albion-online-data.com/api/v2" # Americas
|
| 42 |
-
# API_BASE = "https://east.albion-online-data.com/api/v2" # Asia
|
| 43 |
|
| 44 |
CITIES = [
|
| 45 |
"Bridgewatch",
|
|
@@ -108,16 +101,29 @@ MODEL_PATH = os.path.join(MODEL_DIR, "gos_hawk_model.pkl")
|
|
| 108 |
|
| 109 |
PURGE_HOURS = 24
|
| 110 |
EMBARGO_HOURS = 24
|
| 111 |
-
# Eşik düşürüldü: public API'den gelen gerçek veri hacmi genellikle sınırlıdır
|
| 112 |
MIN_TRAIN_SAMPLES = 150
|
| 113 |
|
| 114 |
REQUEST_TIMEOUT = 30
|
| 115 |
MAX_RETRIES = 3
|
| 116 |
RETRY_BACKOFF = 2
|
| 117 |
|
| 118 |
-
# Public API zaman ölçeği (saat): 6 → 6 saatlik bucket → 4× daha fazla veri noktası
|
| 119 |
HISTORY_TIME_SCALE = 6
|
| 120 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
# ---------------------------------------------------------------------------
|
| 122 |
# PUBLIC API ÇAĞRILARI
|
| 123 |
# ---------------------------------------------------------------------------
|
|
@@ -125,7 +131,7 @@ HISTORY_TIME_SCALE = 6
|
|
| 125 |
|
| 126 |
def safe_request(url: str, params: Optional[dict] = None) -> Optional[list]:
|
| 127 |
headers = {
|
| 128 |
-
"User-Agent": "GOSHAWK-ALBION-MARKET-AI/1.
|
| 129 |
"Accept": "application/json",
|
| 130 |
}
|
| 131 |
for attempt in range(MAX_RETRIES):
|
|
@@ -181,16 +187,7 @@ def fetch_current_prices(item_ids: List[str], cities: List[str], qualities: str
|
|
| 181 |
return df
|
| 182 |
|
| 183 |
|
| 184 |
-
def fetch_history(
|
| 185 |
-
item_ids: List[str],
|
| 186 |
-
cities: List[str],
|
| 187 |
-
time_scale: int = 6,
|
| 188 |
-
qualities: str = "1",
|
| 189 |
-
) -> pd.DataFrame:
|
| 190 |
-
"""
|
| 191 |
-
AODP /stats/history/ endpoint'inden tarihsel ortalama fiyatları çeker.
|
| 192 |
-
time_scale=6 → 6 saatlik bucket → 24h'ye göre 4 kat daha fazla nokta.
|
| 193 |
-
"""
|
| 194 |
all_records = []
|
| 195 |
chunk_size = 30
|
| 196 |
for i in range(0, len(item_ids), chunk_size):
|
|
@@ -198,11 +195,7 @@ def fetch_history(
|
|
| 198 |
items_str = ",".join(chunk)
|
| 199 |
locations_str = ",".join(cities)
|
| 200 |
url = f"{API_BASE}/stats/history/{items_str}.json"
|
| 201 |
-
params = {
|
| 202 |
-
"locations": locations_str,
|
| 203 |
-
"qualities": qualities,
|
| 204 |
-
"time-scale": time_scale,
|
| 205 |
-
}
|
| 206 |
data = safe_request(url, params)
|
| 207 |
if data:
|
| 208 |
for entry in data:
|
|
@@ -304,10 +297,6 @@ def build_features(df: pd.DataFrame) -> pd.DataFrame:
|
|
| 304 |
|
| 305 |
group_cols = ["item_id", "city"]
|
| 306 |
|
| 307 |
-
# time_scale=6 ile saatlik değil 6-saatlik bucket kullanıyoruz.
|
| 308 |
-
# 1 birim = 6 saat. Bu yüzden lag/chg/ma pencerelerini "bucket" cinsinden
|
| 309 |
-
# ifade ediyoruz: 1h→1 bucket, 24h→4 bucket, 48h→8 bucket.
|
| 310 |
-
# Yine de isimleri saat cinsinden bırakıyoruz (anlamlılık için).
|
| 311 |
for lag in [1, 3, 6, 12, 24, 48]:
|
| 312 |
df[f"lag_{lag}h"] = df.groupby(group_cols)["avg_price"].shift(lag)
|
| 313 |
|
|
@@ -315,7 +304,6 @@ def build_features(df: pd.DataFrame) -> pd.DataFrame:
|
|
| 315 |
for chg in [1, 6, 12, 24]:
|
| 316 |
df[f"chg_{chg}h"] = df.groupby(group_cols)["log_price"].shift(chg)
|
| 317 |
|
| 318 |
-
# min_periods yumuşatıldı: daha az NaN, daha çok eğitim örneği
|
| 319 |
for window in [6, 12, 24, 48]:
|
| 320 |
min_p = max(3, window // 4)
|
| 321 |
df[f"ma_{window}h"] = df.groupby(group_cols)["avg_price"].transform(
|
|
@@ -336,7 +324,8 @@ def build_features(df: pd.DataFrame) -> pd.DataFrame:
|
|
| 336 |
city_avg = df.groupby(["item_id", "timestamp"])["avg_price"].transform("mean")
|
| 337 |
df["city_price_deviation"] = (df["avg_price"] - city_avg) / city_avg.replace(0, np.nan)
|
| 338 |
|
| 339 |
-
|
|
|
|
| 340 |
|
| 341 |
return df
|
| 342 |
|
|
@@ -358,10 +347,6 @@ FEATURE_COLS = [
|
|
| 358 |
|
| 359 |
|
| 360 |
def prepare_model_data(featured_df: pd.DataFrame) -> Tuple[pd.DataFrame, pd.Series, pd.DataFrame]:
|
| 361 |
-
"""
|
| 362 |
-
Eğitim verisini haz��rlar. 48h lag/ma NaN ise daha kısa pencereden doldurur
|
| 363 |
-
ki örnek sayısı düşmesin.
|
| 364 |
-
"""
|
| 365 |
if featured_df.empty:
|
| 366 |
return pd.DataFrame(), pd.Series(dtype=float), pd.DataFrame()
|
| 367 |
|
|
@@ -369,10 +354,8 @@ def prepare_model_data(featured_df: pd.DataFrame) -> Tuple[pd.DataFrame, pd.Seri
|
|
| 369 |
df["item_code"] = df["item_id"].astype("category").cat.codes
|
| 370 |
df["city_code"] = df["city"].astype("category").cat.codes
|
| 371 |
|
| 372 |
-
# Target ve temel lag'ler zorunlu
|
| 373 |
df = df.dropna(subset=["target_return_24h", "lag_1h", "lag_24h"]).copy()
|
| 374 |
|
| 375 |
-
# Uzun pencereleri kısa pencereden doldur
|
| 376 |
if "lag_48h" in df.columns:
|
| 377 |
df["lag_48h"] = df["lag_48h"].fillna(df["lag_24h"])
|
| 378 |
if "ma_48h" in df.columns:
|
|
@@ -380,13 +363,13 @@ def prepare_model_data(featured_df: pd.DataFrame) -> Tuple[pd.DataFrame, pd.Seri
|
|
| 380 |
if "ma_24h" in df.columns:
|
| 381 |
df["ma_24h"] = df["ma_24h"].fillna(df["lag_24h"])
|
| 382 |
if "ma_12h" in df.columns:
|
| 383 |
-
df["ma_12h"] = df["ma_12h"].fillna(df
|
| 384 |
if "ma_6h" in df.columns:
|
| 385 |
-
df["ma_6h"] = df["ma_6h"].fillna(df
|
| 386 |
if "lag_3h" in df.columns:
|
| 387 |
-
df["lag_3h"] = df["lag_3h"].fillna(df
|
| 388 |
if "lag_6h" in df.columns:
|
| 389 |
-
df["lag_6h"] = df["lag_6h"].fillna(df
|
| 390 |
if "lag_12h" in df.columns:
|
| 391 |
df["lag_12h"] = df["lag_12h"].fillna(df["lag_24h"])
|
| 392 |
|
|
@@ -400,13 +383,15 @@ def prepare_model_data(featured_df: pd.DataFrame) -> Tuple[pd.DataFrame, pd.Seri
|
|
| 400 |
df["volume_ratio"] = df["volume_ratio"].fillna(1.0)
|
| 401 |
df["city_price_deviation"] = df["city_price_deviation"].fillna(0.0)
|
| 402 |
|
| 403 |
-
# Kalan NaN'ları temizle
|
| 404 |
model_df = df.dropna(subset=FEATURE_COLS + ["target_return_24h"]).copy()
|
| 405 |
|
|
|
|
| 406 |
if len(model_df) > 100:
|
| 407 |
-
lower = model_df["target_return_24h"].quantile(
|
| 408 |
-
upper = model_df["target_return_24h"].quantile(
|
| 409 |
-
model_df["target_return_24h"] = model_df["target_return_24h"].clip(
|
|
|
|
|
|
|
| 410 |
|
| 411 |
X = model_df[FEATURE_COLS].astype("float64")
|
| 412 |
y = model_df["target_return_24h"].astype("float64")
|
|
@@ -424,23 +409,28 @@ def train_global_model(X: pd.DataFrame, y: pd.Series) -> Optional[lgb.LGBMRegres
|
|
| 424 |
X_train, X_val = X.iloc[:split_idx], X.iloc[split_idx:]
|
| 425 |
y_train, y_val = y.iloc[:split_idx], y.iloc[split_idx:]
|
| 426 |
|
|
|
|
| 427 |
model = lgb.LGBMRegressor(
|
| 428 |
objective="regression",
|
| 429 |
metric="mae",
|
| 430 |
boosting_type="gbdt",
|
| 431 |
-
num_leaves=
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
|
|
|
|
|
|
| 436 |
bagging_freq=5,
|
|
|
|
|
|
|
| 437 |
verbose=-1,
|
| 438 |
random_state=42,
|
| 439 |
n_jobs=-1,
|
| 440 |
)
|
| 441 |
|
| 442 |
callbacks = [
|
| 443 |
-
lgb.early_stopping(stopping_rounds=
|
| 444 |
lgb.log_evaluation(period=0),
|
| 445 |
]
|
| 446 |
|
|
@@ -455,6 +445,12 @@ def train_global_model(X: pd.DataFrame, y: pd.Series) -> Optional[lgb.LGBMRegres
|
|
| 455 |
|
| 456 |
|
| 457 |
def predict_24h_returns(model: lgb.LGBMRegressor, latest_features: pd.DataFrame) -> pd.DataFrame:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 458 |
if latest_features.empty or model is None:
|
| 459 |
return pd.DataFrame()
|
| 460 |
|
|
@@ -467,11 +463,23 @@ def predict_24h_returns(model: lgb.LGBMRegressor, latest_features: pd.DataFrame)
|
|
| 467 |
if X_pred.empty:
|
| 468 |
return pd.DataFrame()
|
| 469 |
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 475 |
|
| 476 |
return result
|
| 477 |
|
|
@@ -496,12 +504,12 @@ def walk_forward_backtest(featured_df: pd.DataFrame, n_splits: int = 3) -> Dict:
|
|
| 496 |
all_predictions = []
|
| 497 |
|
| 498 |
for i in range(n_splits):
|
| 499 |
-
test_start_idx = test_size * (i + 1) + 4
|
| 500 |
test_end_idx = test_start_idx + test_size
|
| 501 |
if test_end_idx > len(unique_times):
|
| 502 |
break
|
| 503 |
|
| 504 |
-
train_end_idx = test_start_idx - 8
|
| 505 |
if train_end_idx <= 0:
|
| 506 |
continue
|
| 507 |
|
|
@@ -523,16 +531,21 @@ def walk_forward_backtest(featured_df: pd.DataFrame, n_splits: int = 3) -> Dict:
|
|
| 523 |
model = lgb.LGBMRegressor(
|
| 524 |
objective="regression",
|
| 525 |
metric="mae",
|
| 526 |
-
num_leaves=
|
| 527 |
-
|
|
|
|
|
|
|
| 528 |
n_estimators=300,
|
|
|
|
|
|
|
| 529 |
verbose=-1,
|
| 530 |
random_state=42,
|
| 531 |
n_jobs=-1,
|
| 532 |
)
|
| 533 |
model.fit(X_train, y_train)
|
| 534 |
|
| 535 |
-
|
|
|
|
| 536 |
|
| 537 |
mae = mean_absolute_error(y_test, preds)
|
| 538 |
rmse = np.sqrt(mean_squared_error(y_test, preds))
|
|
@@ -566,7 +579,7 @@ def walk_forward_backtest(featured_df: pd.DataFrame, n_splits: int = 3) -> Dict:
|
|
| 566 |
sim_df = pd.concat(all_predictions)
|
| 567 |
total_cost_rate = SETUP_FEE_RATE + TRANSACTION_TAX_RATE
|
| 568 |
sim_df["net_return"] = sim_df["actual_return"] - total_cost_rate
|
| 569 |
-
sim_df["trade_signal"] = sim_df["predicted_return"] >
|
| 570 |
sim_trades = sim_df[sim_df["trade_signal"]].copy()
|
| 571 |
|
| 572 |
if len(sim_trades) > 0:
|
|
@@ -606,7 +619,6 @@ class DataPipeline:
|
|
| 606 |
self._backtest_cache = None
|
| 607 |
self.data_source = "PUBLIC_API_EUROPE"
|
| 608 |
|
| 609 |
-
# Diskte kayıtlı model varsa yükle
|
| 610 |
if _HAS_JOBLIB and os.path.exists(MODEL_PATH):
|
| 611 |
try:
|
| 612 |
self.model = joblib.load(MODEL_PATH)
|
|
@@ -661,7 +673,7 @@ class DataPipeline:
|
|
| 661 |
self.status_message = (
|
| 662 |
f"Yetersiz örnek: {len(X)} (min {MIN_TRAIN_SAMPLES}). "
|
| 663 |
f"Toplam tarihsel satır: {len(self.featured_df)}. "
|
| 664 |
-
f"
|
| 665 |
)
|
| 666 |
return False
|
| 667 |
|
|
@@ -670,17 +682,17 @@ class DataPipeline:
|
|
| 670 |
self.status_message = "Model eğitimi başarısız."
|
| 671 |
return False
|
| 672 |
|
| 673 |
-
# Kaydet
|
| 674 |
if _HAS_JOBLIB:
|
| 675 |
try:
|
| 676 |
joblib.dump(self.model, MODEL_PATH)
|
| 677 |
self.status_message = (
|
| 678 |
-
f"Model eğitildi ve kaydedildi ({len(X)} örnek).
|
|
|
|
| 679 |
)
|
| 680 |
except Exception as e:
|
| 681 |
self.status_message = f"Model eğitildi ancak kaydedilemedi: {e}"
|
| 682 |
else:
|
| 683 |
-
self.status_message = f"Model eğitildi ({len(X)} örnek).
|
| 684 |
|
| 685 |
self.last_update = dt.datetime.now()
|
| 686 |
return True
|
|
@@ -693,7 +705,6 @@ class DataPipeline:
|
|
| 693 |
latest["item_code"] = latest["item_id"].astype("category").cat.codes
|
| 694 |
latest["city_code"] = latest["city"].astype("category").cat.codes
|
| 695 |
|
| 696 |
-
# En son satırlar için feature NaN olabilir → kısmi doldur
|
| 697 |
for col in FEATURE_COLS:
|
| 698 |
if col not in latest.columns:
|
| 699 |
latest[col] = 0.0
|
|
@@ -709,7 +720,15 @@ class DataPipeline:
|
|
| 709 |
else:
|
| 710 |
preds["spread"] = np.nan
|
| 711 |
|
| 712 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 713 |
total_cost = SETUP_FEE_RATE + TRANSACTION_TAX_RATE
|
| 714 |
preds["net_return_pct"] = preds["predicted_return_pct"] - (total_cost * 100)
|
| 715 |
|
|
@@ -751,6 +770,7 @@ def get_dashboard_metrics():
|
|
| 751 |
"Ortalama Fiyat": f"{df['avg_price'].mean():.0f} silver",
|
| 752 |
"Ortalama Hacim": f"{df['item_count'].mean():.1f}",
|
| 753 |
"Model Durumu": "Eğitildi" if pipeline.model else "Eğitilmedi",
|
|
|
|
| 754 |
"Son Güncelleme": pipeline.last_update.strftime("%Y-%m-%d %H:%M") if pipeline.last_update else "—",
|
| 755 |
}
|
| 756 |
return pd.DataFrame(list(metrics.items()), columns=["Metrik", "Değer"])
|
|
@@ -807,11 +827,13 @@ def get_prediction_table():
|
|
| 807 |
bins=[-1, 10, 50, 200, np.inf],
|
| 808 |
labels=["Düşük", "Orta", "İyi", "Yüksek"]
|
| 809 |
).astype(str)
|
|
|
|
|
|
|
| 810 |
|
| 811 |
cols = [
|
| 812 |
"Ürün", "Şehir", "Mevcut Fiyat", "Tahmini Fiyat (24s)",
|
| 813 |
"Tahmini Değişim %", "Güvenilirlik", "Spread", "Likidite",
|
| 814 |
-
"Net Getiri % (maliyet sonrası)"
|
| 815 |
]
|
| 816 |
return preds[cols].sort_values("Tahmini Değişim %", ascending=False)
|
| 817 |
|
|
@@ -1029,7 +1051,7 @@ with gr.Blocks(title="GOSHAWK ALBION MARKET AI", theme=gr.themes.Soft(primary_hu
|
|
| 1029 |
*LightGBM tabanlı 24 saatlik fiyat tahmini ve arbitraj analizi.*
|
| 1030 |
|
| 1031 |
**Veri kaynağı:** Public Albion Online Data Project API (`europe.albion-online-data.com`)
|
| 1032 |
-
**
|
| 1033 |
|
| 1034 |
**⚠️ Risk Uyarısı:** Bu uygulama yatırım tavsiyesi değildir. Model tahminleri belirsizlik içerir ve kesin kazanç garantisi sunmaz. Geçmiş performans gelecek sonuçların göstergesi değildir.
|
| 1035 |
"""
|
|
@@ -1044,18 +1066,13 @@ with gr.Blocks(title="GOSHAWK ALBION MARKET AI", theme=gr.themes.Soft(primary_hu
|
|
| 1044 |
with gr.TabItem("📊 Dashboard"):
|
| 1045 |
dashboard_table = gr.Dataframe(label="Özet Metrikler", interactive=False, wrap=True)
|
| 1046 |
gr.Markdown(
|
| 1047 |
-
"###
|
| 1048 |
-
"
|
| 1049 |
-
"
|
| 1050 |
-
"
|
| 1051 |
-
"
|
| 1052 |
-
"###
|
| 1053 |
-
"- `time-scale=6` → 6 saatlik bucket kullanılır, 24h'ye göre 4× daha fazla nokta.\n"
|
| 1054 |
-
"- `MIN_TRAIN_SAMPLES=150` → düşük hacimli ürünlerde de model eğitilebilir.\n"
|
| 1055 |
-
"- Hâlâ 'Yetersiz örnek' alıyorsanız `HISTORY_TIME_SCALE=1` (saatlik) deneyin.\n\n"
|
| 1056 |
-
"### Riskler ve Belirsizlikler\n"
|
| 1057 |
"- Model tahminleri **olasılıksaldır**, kesin değildir.\n"
|
| 1058 |
-
"- Albion Online piyasası oyuncu davranışı, güncellemeler ve sezon değişimlerinden etkilenir.\n"
|
| 1059 |
"- Backtest sonuçları geçmiş performansı yansıtır; gelecek performansı garanti etmez.\n"
|
| 1060 |
"- Spread ve likidite düşükse gerçekleşme fiyatı tahminden sapabilir.\n"
|
| 1061 |
)
|
|
@@ -1066,9 +1083,8 @@ with gr.Blocks(title="GOSHAWK ALBION MARKET AI", theme=gr.themes.Soft(primary_hu
|
|
| 1066 |
with gr.TabItem("🔮 24 Saatlik Fiyat Tahmini"):
|
| 1067 |
prediction_table = gr.Dataframe(label="Tahminler (24 saatlik log getiri modeli)", interactive=False)
|
| 1068 |
gr.Markdown(
|
| 1069 |
-
"*
|
| 1070 |
-
"
|
| 1071 |
-
"Spread ve likidite düşükse işlem maliyeti artabilir. "
|
| 1072 |
"Bu tahminler garanti değildir.*"
|
| 1073 |
)
|
| 1074 |
|
|
@@ -1076,7 +1092,7 @@ with gr.Blocks(title="GOSHAWK ALBION MARKET AI", theme=gr.themes.Soft(primary_hu
|
|
| 1076 |
arbitrage_table = gr.Dataframe(label="Arbitraj Fırsatları (işlem maliyetleri dahil)", interactive=False)
|
| 1077 |
gr.Markdown(
|
| 1078 |
"*Net kâr; alışta %2.5 setup fee, satışta %2.5 setup + %8 transaction tax düşülerek hesaplanmıştır. "
|
| 1079 |
-
"Seyahat maliyeti ve zaman riski dahil değildir.
|
| 1080 |
)
|
| 1081 |
|
| 1082 |
with gr.TabItem("📈 Model Performansı"):
|
|
@@ -1088,9 +1104,8 @@ with gr.Blocks(title="GOSHAWK ALBION MARKET AI", theme=gr.themes.Soft(primary_hu
|
|
| 1088 |
simulation_table = gr.Dataframe(label="İşlem Simülasyonu (maliyetler sonrası)", interactive=False)
|
| 1089 |
gr.Markdown(
|
| 1090 |
"*Walk-forward backtest kronolojik split kullanır; purge ve embargo uygulanır. "
|
| 1091 |
-
"Baseline
|
| 1092 |
-
"
|
| 1093 |
-
"Gerçek işlemlerde slippage ve rekabet nedeniyle sonuçlar farklı olabilir.*"
|
| 1094 |
)
|
| 1095 |
|
| 1096 |
with gr.TabItem("🗄️ Veri Yönetimi"):
|
|
@@ -1098,7 +1113,6 @@ with gr.Blocks(title="GOSHAWK ALBION MARKET AI", theme=gr.themes.Soft(primary_hu
|
|
| 1098 |
"Veriler `gos_hawk_data/` dizininde CSV olarak saklanır.\n\n"
|
| 1099 |
"**API Kaynağı:** Public AODP API — `europe.albion-online-data.com/api/v2`\n"
|
| 1100 |
"**Endpoint'ler:** `/stats/prices/` (anlık), `/stats/history/` (tarihsel)\n"
|
| 1101 |
-
"**Rate-limit:** dakikada ~60 istek (public)\n"
|
| 1102 |
"**Model dosyası:** `gos_hawk_data/models/gos_hawk_model.pkl`\n"
|
| 1103 |
)
|
| 1104 |
csv_files = gr.Dataframe(
|
|
|
|
| 2 |
# GOSHAWK ALBION MARKET AI
|
| 3 |
# Tek dosyalık, çalıştırılabilir Market Intelligence uygulaması.
|
| 4 |
# Gerekli paketler: gradio, lightgbm, pandas, numpy, plotly, requests, scikit-learn, joblib
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
import os
|
| 7 |
import time
|
|
|
|
| 33 |
# ---------------------------------------------------------------------------
|
| 34 |
|
| 35 |
API_BASE = "https://europe.albion-online-data.com/api/v2"
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
CITIES = [
|
| 38 |
"Bridgewatch",
|
|
|
|
| 101 |
|
| 102 |
PURGE_HOURS = 24
|
| 103 |
EMBARGO_HOURS = 24
|
|
|
|
| 104 |
MIN_TRAIN_SAMPLES = 150
|
| 105 |
|
| 106 |
REQUEST_TIMEOUT = 30
|
| 107 |
MAX_RETRIES = 3
|
| 108 |
RETRY_BACKOFF = 2
|
| 109 |
|
|
|
|
| 110 |
HISTORY_TIME_SCALE = 6
|
| 111 |
|
| 112 |
+
# ---------------------------------------------------------------------------
|
| 113 |
+
# TAHMİN SINIRLARI (aşırı tahminleri engelle)
|
| 114 |
+
# ---------------------------------------------------------------------------
|
| 115 |
+
|
| 116 |
+
# 24h'lik gerçekçi max log getiri aralığı (yaklaşık ±%15)
|
| 117 |
+
MAX_LOG_RETURN = 0.15
|
| 118 |
+
MIN_LOG_RETURN = -0.15
|
| 119 |
+
|
| 120 |
+
# Eğitim target clip (log uzayında)
|
| 121 |
+
TARGET_CLIP_LOWER_Q = 0.02
|
| 122 |
+
TARGET_CLIP_UPPER_Q = 0.98
|
| 123 |
+
|
| 124 |
+
# Güven eşiği: bu log getirinin altındaki tahminler "belirsiz" sayılır
|
| 125 |
+
LOW_CONFIDENCE_THRESHOLD = 0.02
|
| 126 |
+
|
| 127 |
# ---------------------------------------------------------------------------
|
| 128 |
# PUBLIC API ÇAĞRILARI
|
| 129 |
# ---------------------------------------------------------------------------
|
|
|
|
| 131 |
|
| 132 |
def safe_request(url: str, params: Optional[dict] = None) -> Optional[list]:
|
| 133 |
headers = {
|
| 134 |
+
"User-Agent": "GOSHAWK-ALBION-MARKET-AI/1.2 (public-api-client)",
|
| 135 |
"Accept": "application/json",
|
| 136 |
}
|
| 137 |
for attempt in range(MAX_RETRIES):
|
|
|
|
| 187 |
return df
|
| 188 |
|
| 189 |
|
| 190 |
+
def fetch_history(item_ids: List[str], cities: List[str], time_scale: int = 6, qualities: str = "1") -> pd.DataFrame:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
all_records = []
|
| 192 |
chunk_size = 30
|
| 193 |
for i in range(0, len(item_ids), chunk_size):
|
|
|
|
| 195 |
items_str = ",".join(chunk)
|
| 196 |
locations_str = ",".join(cities)
|
| 197 |
url = f"{API_BASE}/stats/history/{items_str}.json"
|
| 198 |
+
params = {"locations": locations_str, "qualities": qualities, "time-scale": time_scale}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
data = safe_request(url, params)
|
| 200 |
if data:
|
| 201 |
for entry in data:
|
|
|
|
| 297 |
|
| 298 |
group_cols = ["item_id", "city"]
|
| 299 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 300 |
for lag in [1, 3, 6, 12, 24, 48]:
|
| 301 |
df[f"lag_{lag}h"] = df.groupby(group_cols)["avg_price"].shift(lag)
|
| 302 |
|
|
|
|
| 304 |
for chg in [1, 6, 12, 24]:
|
| 305 |
df[f"chg_{chg}h"] = df.groupby(group_cols)["log_price"].shift(chg)
|
| 306 |
|
|
|
|
| 307 |
for window in [6, 12, 24, 48]:
|
| 308 |
min_p = max(3, window // 4)
|
| 309 |
df[f"ma_{window}h"] = df.groupby(group_cols)["avg_price"].transform(
|
|
|
|
| 324 |
city_avg = df.groupby(["item_id", "timestamp"])["avg_price"].transform("mean")
|
| 325 |
df["city_price_deviation"] = (df["avg_price"] - city_avg) / city_avg.replace(0, np.nan)
|
| 326 |
|
| 327 |
+
# Hedef: 4 bucket sonraki log getiri (time_scale=6h → 4*6=24h)
|
| 328 |
+
df["target_return_24h"] = df.groupby(group_cols)["log_price"].shift(-4)
|
| 329 |
|
| 330 |
return df
|
| 331 |
|
|
|
|
| 347 |
|
| 348 |
|
| 349 |
def prepare_model_data(featured_df: pd.DataFrame) -> Tuple[pd.DataFrame, pd.Series, pd.DataFrame]:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 350 |
if featured_df.empty:
|
| 351 |
return pd.DataFrame(), pd.Series(dtype=float), pd.DataFrame()
|
| 352 |
|
|
|
|
| 354 |
df["item_code"] = df["item_id"].astype("category").cat.codes
|
| 355 |
df["city_code"] = df["city"].astype("category").cat.codes
|
| 356 |
|
|
|
|
| 357 |
df = df.dropna(subset=["target_return_24h", "lag_1h", "lag_24h"]).copy()
|
| 358 |
|
|
|
|
| 359 |
if "lag_48h" in df.columns:
|
| 360 |
df["lag_48h"] = df["lag_48h"].fillna(df["lag_24h"])
|
| 361 |
if "ma_48h" in df.columns:
|
|
|
|
| 363 |
if "ma_24h" in df.columns:
|
| 364 |
df["ma_24h"] = df["ma_24h"].fillna(df["lag_24h"])
|
| 365 |
if "ma_12h" in df.columns:
|
| 366 |
+
df["ma_12h"] = df["ma_12h"].fillna(df.get("lag_12h", df["lag_24h"]))
|
| 367 |
if "ma_6h" in df.columns:
|
| 368 |
+
df["ma_6h"] = df["ma_6h"].fillna(df.get("lag_6h", df["lag_24h"]))
|
| 369 |
if "lag_3h" in df.columns:
|
| 370 |
+
df["lag_3h"] = df["lag_3h"].fillna(df.get("lag_6h", df["lag_24h"]))
|
| 371 |
if "lag_6h" in df.columns:
|
| 372 |
+
df["lag_6h"] = df["lag_6h"].fillna(df.get("lag_12h", df["lag_24h"]))
|
| 373 |
if "lag_12h" in df.columns:
|
| 374 |
df["lag_12h"] = df["lag_12h"].fillna(df["lag_24h"])
|
| 375 |
|
|
|
|
| 383 |
df["volume_ratio"] = df["volume_ratio"].fillna(1.0)
|
| 384 |
df["city_price_deviation"] = df["city_price_deviation"].fillna(0.0)
|
| 385 |
|
|
|
|
| 386 |
model_df = df.dropna(subset=FEATURE_COLS + ["target_return_24h"]).copy()
|
| 387 |
|
| 388 |
+
# KRİTİK DÜZELTME: target clip çok daha dar
|
| 389 |
if len(model_df) > 100:
|
| 390 |
+
lower = model_df["target_return_24h"].quantile(TARGET_CLIP_LOWER_Q)
|
| 391 |
+
upper = model_df["target_return_24h"].quantile(TARGET_CLIP_UPPER_Q)
|
| 392 |
+
model_df["target_return_24h"] = model_df["target_return_24h"].clip(
|
| 393 |
+
max(lower, MIN_LOG_RETURN), min(upper, MAX_LOG_RETURN)
|
| 394 |
+
)
|
| 395 |
|
| 396 |
X = model_df[FEATURE_COLS].astype("float64")
|
| 397 |
y = model_df["target_return_24h"].astype("float64")
|
|
|
|
| 409 |
X_train, X_val = X.iloc[:split_idx], X.iloc[split_idx:]
|
| 410 |
y_train, y_val = y.iloc[:split_idx], y.iloc[split_idx:]
|
| 411 |
|
| 412 |
+
# KRİTİK DÜZELTME: daha güçlü regülarizasyon → overfit önleme
|
| 413 |
model = lgb.LGBMRegressor(
|
| 414 |
objective="regression",
|
| 415 |
metric="mae",
|
| 416 |
boosting_type="gbdt",
|
| 417 |
+
num_leaves=15, # 31 → 15
|
| 418 |
+
max_depth=6, # eklendi
|
| 419 |
+
min_child_samples=50, # eklendi
|
| 420 |
+
learning_rate=0.03, # 0.05 → 0.03
|
| 421 |
+
n_estimators=400,
|
| 422 |
+
feature_fraction=0.7,
|
| 423 |
+
bagging_fraction=0.7,
|
| 424 |
bagging_freq=5,
|
| 425 |
+
reg_alpha=0.1, # L1
|
| 426 |
+
reg_lambda=1.0, # L2
|
| 427 |
verbose=-1,
|
| 428 |
random_state=42,
|
| 429 |
n_jobs=-1,
|
| 430 |
)
|
| 431 |
|
| 432 |
callbacks = [
|
| 433 |
+
lgb.early_stopping(stopping_rounds=30, verbose=False),
|
| 434 |
lgb.log_evaluation(period=0),
|
| 435 |
]
|
| 436 |
|
|
|
|
| 445 |
|
| 446 |
|
| 447 |
def predict_24h_returns(model: lgb.LGBMRegressor, latest_features: pd.DataFrame) -> pd.DataFrame:
|
| 448 |
+
"""
|
| 449 |
+
KRİTİK DÜZELTME:
|
| 450 |
+
1. Ham tahmin clip'lenir (aşırı değerleri engelle).
|
| 451 |
+
2. Tahminin mutlak değeri çok küçükse "belirsiz" işaretlenir.
|
| 452 |
+
3. exp() güvenli aralıkta uygulanır.
|
| 453 |
+
"""
|
| 454 |
if latest_features.empty or model is None:
|
| 455 |
return pd.DataFrame()
|
| 456 |
|
|
|
|
| 463 |
if X_pred.empty:
|
| 464 |
return pd.DataFrame()
|
| 465 |
|
| 466 |
+
raw_preds = model.predict(X_pred)
|
| 467 |
+
|
| 468 |
+
# Ham tahmini güvenli aralığa clip'le
|
| 469 |
+
clipped_preds = np.clip(raw_preds, MIN_LOG_RETURN, MAX_LOG_RETURN)
|
| 470 |
+
|
| 471 |
+
# Yüzdeye çevir
|
| 472 |
+
pred_return_pct = (np.exp(clipped_preds) - 1) * 100
|
| 473 |
+
|
| 474 |
+
result = latest_features.loc[X_pred.index, ["item_id", "city", "avg_price", "item_count", "volatility_24h", "rsi_14", "volume_ratio"]].copy()
|
| 475 |
+
result["raw_log_pred"] = raw_preds
|
| 476 |
+
result["clipped_log_pred"] = clipped_preds
|
| 477 |
+
result["predicted_log_return_24h"] = clipped_preds
|
| 478 |
+
result["predicted_return_pct"] = pred_return_pct
|
| 479 |
+
result["predicted_price_24h"] = result["avg_price"] * np.exp(clipped_preds)
|
| 480 |
+
|
| 481 |
+
# Belirsizlik göstergesi: |tahmin| < eşik ise "belirsiz"
|
| 482 |
+
result["is_uncertain"] = np.abs(clipped_preds) < LOW_CONFIDENCE_THRESHOLD
|
| 483 |
|
| 484 |
return result
|
| 485 |
|
|
|
|
| 504 |
all_predictions = []
|
| 505 |
|
| 506 |
for i in range(n_splits):
|
| 507 |
+
test_start_idx = test_size * (i + 1) + 4
|
| 508 |
test_end_idx = test_start_idx + test_size
|
| 509 |
if test_end_idx > len(unique_times):
|
| 510 |
break
|
| 511 |
|
| 512 |
+
train_end_idx = test_start_idx - 8
|
| 513 |
if train_end_idx <= 0:
|
| 514 |
continue
|
| 515 |
|
|
|
|
| 531 |
model = lgb.LGBMRegressor(
|
| 532 |
objective="regression",
|
| 533 |
metric="mae",
|
| 534 |
+
num_leaves=15,
|
| 535 |
+
max_depth=6,
|
| 536 |
+
min_child_samples=50,
|
| 537 |
+
learning_rate=0.03,
|
| 538 |
n_estimators=300,
|
| 539 |
+
reg_alpha=0.1,
|
| 540 |
+
reg_lambda=1.0,
|
| 541 |
verbose=-1,
|
| 542 |
random_state=42,
|
| 543 |
n_jobs=-1,
|
| 544 |
)
|
| 545 |
model.fit(X_train, y_train)
|
| 546 |
|
| 547 |
+
raw_preds = model.predict(X_test)
|
| 548 |
+
preds = np.clip(raw_preds, MIN_LOG_RETURN, MAX_LOG_RETURN)
|
| 549 |
|
| 550 |
mae = mean_absolute_error(y_test, preds)
|
| 551 |
rmse = np.sqrt(mean_squared_error(y_test, preds))
|
|
|
|
| 579 |
sim_df = pd.concat(all_predictions)
|
| 580 |
total_cost_rate = SETUP_FEE_RATE + TRANSACTION_TAX_RATE
|
| 581 |
sim_df["net_return"] = sim_df["actual_return"] - total_cost_rate
|
| 582 |
+
sim_df["trade_signal"] = sim_df["predicted_return"] > LOW_CONFIDENCE_THRESHOLD
|
| 583 |
sim_trades = sim_df[sim_df["trade_signal"]].copy()
|
| 584 |
|
| 585 |
if len(sim_trades) > 0:
|
|
|
|
| 619 |
self._backtest_cache = None
|
| 620 |
self.data_source = "PUBLIC_API_EUROPE"
|
| 621 |
|
|
|
|
| 622 |
if _HAS_JOBLIB and os.path.exists(MODEL_PATH):
|
| 623 |
try:
|
| 624 |
self.model = joblib.load(MODEL_PATH)
|
|
|
|
| 673 |
self.status_message = (
|
| 674 |
f"Yetersiz örnek: {len(X)} (min {MIN_TRAIN_SAMPLES}). "
|
| 675 |
f"Toplam tarihsel satır: {len(self.featured_df)}. "
|
| 676 |
+
f"HISTORY_TIME_SCALE=1 deneyin."
|
| 677 |
)
|
| 678 |
return False
|
| 679 |
|
|
|
|
| 682 |
self.status_message = "Model eğitimi başarısız."
|
| 683 |
return False
|
| 684 |
|
|
|
|
| 685 |
if _HAS_JOBLIB:
|
| 686 |
try:
|
| 687 |
joblib.dump(self.model, MODEL_PATH)
|
| 688 |
self.status_message = (
|
| 689 |
+
f"Model eğitildi ve kaydedildi ({len(X)} örnek). "
|
| 690 |
+
f"Tahminler ±%15 aralığına sınırlandırıldı."
|
| 691 |
)
|
| 692 |
except Exception as e:
|
| 693 |
self.status_message = f"Model eğitildi ancak kaydedilemedi: {e}"
|
| 694 |
else:
|
| 695 |
+
self.status_message = f"Model eğitildi ({len(X)} örnek). Tahminler ±%15 aralığına sınırlandırıldı."
|
| 696 |
|
| 697 |
self.last_update = dt.datetime.now()
|
| 698 |
return True
|
|
|
|
| 705 |
latest["item_code"] = latest["item_id"].astype("category").cat.codes
|
| 706 |
latest["city_code"] = latest["city"].astype("category").cat.codes
|
| 707 |
|
|
|
|
| 708 |
for col in FEATURE_COLS:
|
| 709 |
if col not in latest.columns:
|
| 710 |
latest[col] = 0.0
|
|
|
|
| 720 |
else:
|
| 721 |
preds["spread"] = np.nan
|
| 722 |
|
| 723 |
+
# Gelişmi�� güvenilirlik: volatilite + RSI aşırı bölge + volume
|
| 724 |
+
vol_score = (1.0 - preds["volatility_24h"].clip(0, 0.2) / 0.2).fillna(0.5)
|
| 725 |
+
rsi_penalty = ((preds["rsi_14"] - 50).abs() / 50).clip(0, 1).fillna(0.5) * 0.2
|
| 726 |
+
vol_ratio_score = preds["volume_ratio"].clip(0, 2).fillna(1.0) / 2
|
| 727 |
+
base_conf = vol_score * 0.6 + vol_ratio_score * 0.4 - rsi_penalty
|
| 728 |
+
# Belirsiz tahminleri cezalandır
|
| 729 |
+
certainty_penalty = preds["is_uncertain"].astype(float) * 0.4
|
| 730 |
+
preds["confidence"] = (base_conf - certainty_penalty).clip(0, 1).fillna(0.3)
|
| 731 |
+
|
| 732 |
total_cost = SETUP_FEE_RATE + TRANSACTION_TAX_RATE
|
| 733 |
preds["net_return_pct"] = preds["predicted_return_pct"] - (total_cost * 100)
|
| 734 |
|
|
|
|
| 770 |
"Ortalama Fiyat": f"{df['avg_price'].mean():.0f} silver",
|
| 771 |
"Ortalama Hacim": f"{df['item_count'].mean():.1f}",
|
| 772 |
"Model Durumu": "Eğitildi" if pipeline.model else "Eğitilmedi",
|
| 773 |
+
"Tahmin Sınırı": f"±%{MAX_LOG_RETURN * 100:.0f}",
|
| 774 |
"Son Güncelleme": pipeline.last_update.strftime("%Y-%m-%d %H:%M") if pipeline.last_update else "—",
|
| 775 |
}
|
| 776 |
return pd.DataFrame(list(metrics.items()), columns=["Metrik", "Değer"])
|
|
|
|
| 827 |
bins=[-1, 10, 50, 200, np.inf],
|
| 828 |
labels=["Düşük", "Orta", "İyi", "Yüksek"]
|
| 829 |
).astype(str)
|
| 830 |
+
# Belirsiz tahminleri işaretle
|
| 831 |
+
preds["Not"] = np.where(preds["is_uncertain"], "⚠️ Belirsiz", "")
|
| 832 |
|
| 833 |
cols = [
|
| 834 |
"Ürün", "Şehir", "Mevcut Fiyat", "Tahmini Fiyat (24s)",
|
| 835 |
"Tahmini Değişim %", "Güvenilirlik", "Spread", "Likidite",
|
| 836 |
+
"Net Getiri % (maliyet sonrası)", "Not"
|
| 837 |
]
|
| 838 |
return preds[cols].sort_values("Tahmini Değişim %", ascending=False)
|
| 839 |
|
|
|
|
| 1051 |
*LightGBM tabanlı 24 saatlik fiyat tahmini ve arbitraj analizi.*
|
| 1052 |
|
| 1053 |
**Veri kaynağı:** Public Albion Online Data Project API (`europe.albion-online-data.com`)
|
| 1054 |
+
**Tahmin sınırı:** ±%15 (aşırı tahminler engellendi)
|
| 1055 |
|
| 1056 |
**⚠️ Risk Uyarısı:** Bu uygulama yatırım tavsiyesi değildir. Model tahminleri belirsizlik içerir ve kesin kazanç garantisi sunmaz. Geçmiş performans gelecek sonuçların göstergesi değildir.
|
| 1057 |
"""
|
|
|
|
| 1066 |
with gr.TabItem("📊 Dashboard"):
|
| 1067 |
dashboard_table = gr.Dataframe(label="Özet Metrikler", interactive=False, wrap=True)
|
| 1068 |
gr.Markdown(
|
| 1069 |
+
"### Tahmin Kalibrasyonu\n"
|
| 1070 |
+
"- Model log getiri tahminleri **±0.15 log birimi** (yaklaşık ±%16) ile sınırlandırılmıştır.\n"
|
| 1071 |
+
"- Bu aralığın dışındaki ham tahminler `clip`lenir.\n"
|
| 1072 |
+
"- Tahmin mutlak değeri < 0.02 ise **belirsiz** olarak işaretlenir.\n"
|
| 1073 |
+
"- Güvenilirlik; volatilite, RSI aşırı bölge ve hacim oranına göre hesaplanır.\n\n"
|
| 1074 |
+
"### Riskler\n"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1075 |
"- Model tahminleri **olasılıksaldır**, kesin değildir.\n"
|
|
|
|
| 1076 |
"- Backtest sonuçları geçmiş performansı yansıtır; gelecek performansı garanti etmez.\n"
|
| 1077 |
"- Spread ve likidite düşükse gerçekleşme fiyatı tahminden sapabilir.\n"
|
| 1078 |
)
|
|
|
|
| 1083 |
with gr.TabItem("🔮 24 Saatlik Fiyat Tahmini"):
|
| 1084 |
prediction_table = gr.Dataframe(label="Tahminler (24 saatlik log getiri modeli)", interactive=False)
|
| 1085 |
gr.Markdown(
|
| 1086 |
+
"*Net getiri %2.5 setup + %8 vergi düşülerek hesaplanır. "
|
| 1087 |
+
"**Not** sütunundaki ⚠️ Belirsiz işareti, modelin sinyal üretmediği durumları gösterir. "
|
|
|
|
| 1088 |
"Bu tahminler garanti değildir.*"
|
| 1089 |
)
|
| 1090 |
|
|
|
|
| 1092 |
arbitrage_table = gr.Dataframe(label="Arbitraj Fırsatları (işlem maliyetleri dahil)", interactive=False)
|
| 1093 |
gr.Markdown(
|
| 1094 |
"*Net kâr; alışta %2.5 setup fee, satışta %2.5 setup + %8 transaction tax düşülerek hesaplanmıştır. "
|
| 1095 |
+
"Seyahat maliyeti ve zaman riski dahil değildir.*"
|
| 1096 |
)
|
| 1097 |
|
| 1098 |
with gr.TabItem("📈 Model Performansı"):
|
|
|
|
| 1104 |
simulation_table = gr.Dataframe(label="İşlem Simülasyonu (maliyetler sonrası)", interactive=False)
|
| 1105 |
gr.Markdown(
|
| 1106 |
"*Walk-forward backtest kronolojik split kullanır; purge ve embargo uygulanır. "
|
| 1107 |
+
"Baseline 'fiyat değişmez' varsayımıdır. Simülasyon yalnızca modelin güven eşiğinin üzerinde "
|
| 1108 |
+
"pozitif getiri öngördüğü işlemleri içerir.*"
|
|
|
|
| 1109 |
)
|
| 1110 |
|
| 1111 |
with gr.TabItem("🗄️ Veri Yönetimi"):
|
|
|
|
| 1113 |
"Veriler `gos_hawk_data/` dizininde CSV olarak saklanır.\n\n"
|
| 1114 |
"**API Kaynağı:** Public AODP API — `europe.albion-online-data.com/api/v2`\n"
|
| 1115 |
"**Endpoint'ler:** `/stats/prices/` (anlık), `/stats/history/` (tarihsel)\n"
|
|
|
|
| 1116 |
"**Model dosyası:** `gos_hawk_data/models/gos_hawk_model.pkl`\n"
|
| 1117 |
)
|
| 1118 |
csv_files = gr.Dataframe(
|