GoshawkVortexAI commited on
Commit
6bcb7d9
·
verified ·
1 Parent(s): 3ad1482

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +329 -244
app.py CHANGED
@@ -1,6 +1,6 @@
1
  # app.py
2
  # GOSHAWK ALBION MARKET AI
3
- # MAE iyileştirilmiş sürüm: sınıflandırma + overfit önleme + sadeleştirilmiş feature set
4
 
5
  import os
6
  import time
@@ -15,7 +15,7 @@ import plotly.graph_objects as go
15
  import plotly.express as px
16
 
17
  import lightgbm as lgb
18
- from sklearn.metrics import mean_absolute_error, mean_squared_error, accuracy_score, confusion_matrix
19
 
20
  import gradio as gr
21
 
@@ -38,6 +38,12 @@ CITIES = [
38
  "Martlock", "Thetford", "Caerleon",
39
  ]
40
 
 
 
 
 
 
 
41
  _ITEM_POOL = [
42
  "T4_BAG", "T5_BAG", "T6_BAG", "T7_BAG", "T8_BAG",
43
  "T4_CAPE", "T5_CAPE", "T6_CAPE", "T7_CAPE", "T8_CAPE",
@@ -84,6 +90,13 @@ def item_display_name(item_id: str) -> str:
84
  return item_id.replace("_", " ").title()
85
 
86
 
 
 
 
 
 
 
 
87
  SETUP_FEE_RATE = 0.025
88
  TRANSACTION_TAX_RATE = 0.08
89
  TOTAL_TRADE_COST = SETUP_FEE_RATE + TRANSACTION_TAX_RATE
@@ -104,12 +117,9 @@ RETRY_BACKOFF = 2
104
 
105
  HISTORY_TIME_SCALE = 1
106
 
107
- # Sınıflandırma sınırları (log getiri)
108
- CLASS_DOWN = -0.03 # -3% altı = AŞAĞI
109
- CLASS_UP = 0.03 # +3% üstü = YUKARI
110
- # Arada kalan = SABİT
111
 
112
- TARGET_SCALE = 10000.0
113
  TARGET_TOLERANCE_HOURS = 12
114
 
115
  # Dip alım
@@ -119,17 +129,19 @@ DIP_MIN_VOLUME_RATIO = 1.0
119
  DIP_MIN_PREDICTED_PCT = -1.0
120
  DIP_MAX_SPREAD_PCT = 0.15
121
 
122
- # Backtest
123
- MAX_POSITIONS = 5
124
 
125
- # SADELEŞTİRİLMİŞ feature set (13 güçlü özellik)
126
  FEATURE_COLS = [
127
  "hour", "dayofweek",
128
- "lag_24h_log", "chg_1h", "chg_6h", "chg_24h",
 
129
  "rsi_14", "volatility_24h",
130
  "volume_ratio",
131
- "ma_24h_ratio",
132
  "city_price_deviation",
 
133
  "item_code", "city_code",
134
  ]
135
 
@@ -139,7 +151,7 @@ FEATURE_COLS = [
139
 
140
 
141
  def safe_request(url: str, params: Optional[dict] = None) -> Optional[list]:
142
- headers = {"User-Agent": "GOSHAWK-ALBION-MARKET-AI/1.8", "Accept": "application/json"}
143
  for attempt in range(MAX_RETRIES):
144
  try:
145
  resp = requests.get(url, params=params, headers=headers, timeout=REQUEST_TIMEOUT)
@@ -162,16 +174,17 @@ def safe_request(url: str, params: Optional[dict] = None) -> Optional[list]:
162
  return None
163
 
164
 
165
- def fetch_current_prices(item_ids: List[str], cities: List[str], qualities: str = "1") -> pd.DataFrame:
 
166
  all_data = []
167
- for i in range(0, len(item_ids), 50):
168
- chunk = item_ids[i:i+50]
169
  url = f"{API_BASE}/stats/prices/{','.join(chunk)}.json"
170
  params = {"locations": ",".join(cities), "qualities": qualities}
171
  data = safe_request(url, params)
172
  if data:
173
  all_data.extend(data)
174
- time.sleep(0.3)
175
  if not all_data:
176
  return pd.DataFrame()
177
 
@@ -179,13 +192,16 @@ def fetch_current_prices(item_ids: List[str], cities: List[str], qualities: str
179
  for col in ["sell_price_min", "sell_price_max", "buy_price_min", "buy_price_max"]:
180
  if col in df.columns:
181
  df[col] = pd.to_numeric(df[col], errors="coerce").astype("float64")
 
 
182
  return df
183
 
184
 
185
- def fetch_history(item_ids: List[str], cities: List[str], time_scale: int = 1, qualities: str = "1") -> pd.DataFrame:
 
186
  all_records = []
187
- for i in range(0, len(item_ids), 20):
188
- chunk = item_ids[i:i+20]
189
  url = f"{API_BASE}/stats/history/{','.join(chunk)}.json"
190
  params = {"locations": ",".join(cities), "qualities": qualities, "time-scale": time_scale}
191
  data = safe_request(url, params)
@@ -193,15 +209,17 @@ def fetch_history(item_ids: List[str], cities: List[str], time_scale: int = 1, q
193
  for entry in data:
194
  item_id = entry.get("item_id")
195
  loc = entry.get("location")
 
196
  for point in entry.get("data", []):
197
  all_records.append({
198
  "item_id": item_id,
199
  "city": loc,
 
200
  "timestamp": point.get("timestamp"),
201
  "avg_price": point.get("avg_price"),
202
  "item_count": point.get("item_count", 0),
203
  })
204
- time.sleep(0.5)
205
  if not all_records:
206
  return pd.DataFrame()
207
 
@@ -209,6 +227,8 @@ def fetch_history(item_ids: List[str], cities: List[str], time_scale: int = 1, q
209
  df["timestamp"] = pd.to_datetime(df["timestamp"], errors="coerce")
210
  df["avg_price"] = pd.to_numeric(df["avg_price"], errors="coerce").astype("float64")
211
  df["item_count"] = pd.to_numeric(df["item_count"], errors="coerce").fillna(0).astype("float64")
 
 
212
  return df
213
 
214
 
@@ -227,11 +247,16 @@ def clean_market_data(df: pd.DataFrame) -> pd.DataFrame:
227
  df = df.copy()
228
  df["avg_price"] = pd.to_numeric(df["avg_price"], errors="coerce").astype("float64")
229
  df["item_count"] = pd.to_numeric(df["item_count"], errors="coerce").fillna(0).astype("float64")
 
 
 
 
230
  df = df[df["avg_price"] > 0].copy()
231
  df = df.dropna(subset=["timestamp"]).copy()
232
 
233
- for item in df["item_id"].unique():
234
- mask = df["item_id"] == item
 
235
  if mask.sum() < 10:
236
  continue
237
  lower = df.loc[mask, "avg_price"].quantile(0.005)
@@ -239,7 +264,7 @@ def clean_market_data(df: pd.DataFrame) -> pd.DataFrame:
239
  if pd.notna(lower) and pd.notna(upper) and upper > lower:
240
  df.loc[mask, "avg_price"] = df.loc[mask, "avg_price"].clip(lower, upper)
241
 
242
- return df.sort_values(["item_id", "city", "timestamp"]).reset_index(drop=True)
243
 
244
 
245
  def save_to_csv(df: pd.DataFrame, name: str) -> str:
@@ -268,31 +293,50 @@ def compute_rsi(series: pd.Series, period: int = 14) -> pd.Series:
268
 
269
 
270
  def build_features(df: pd.DataFrame) -> pd.DataFrame:
 
 
 
 
271
  if df.empty:
272
  return df
273
 
274
- df = df.sort_values(["item_id", "city", "timestamp"]).copy()
 
 
 
275
  df["avg_price"] = pd.to_numeric(df["avg_price"], errors="coerce").astype("float64")
276
  df["item_count"] = pd.to_numeric(df["item_count"], errors="coerce").fillna(0).astype("float64")
277
 
278
  df["hour"] = df["timestamp"].dt.hour
279
  df["dayofweek"] = df["timestamp"].dt.dayofweek
280
  df["is_weekend"] = (df["dayofweek"] >= 5).astype(int)
 
281
 
282
- group_cols = ["item_id", "city"]
 
283
 
284
  df["log_price"] = np.log(df["avg_price"].replace(0, np.nan))
 
 
285
  df["lag_24h_log"] = df.groupby(group_cols)["log_price"].shift(24)
 
 
286
 
287
- for chg in [1, 6, 24]:
 
288
  df[f"chg_{chg}h"] = df.groupby(group_cols)["log_price"].shift(chg)
289
 
290
- # ma_24h / lag_24h oranı (trend göstergesi)
291
  df["ma_24h_raw"] = df.groupby(group_cols)["avg_price"].transform(
292
  lambda x: x.rolling(24, min_periods=6).mean()
293
  )
294
  df["ma_24h_ratio"] = df["ma_24h_raw"] / df["avg_price"].replace(0, np.nan) - 1.0
295
 
 
 
 
 
 
296
  df["rsi_14"] = df.groupby(group_cols)["avg_price"].transform(lambda x: compute_rsi(x, 14))
297
  df["volatility_24h"] = df.groupby(group_cols)["log_price"].transform(
298
  lambda x: x.rolling(24, min_periods=6).std()
@@ -302,21 +346,66 @@ def build_features(df: pd.DataFrame) -> pd.DataFrame:
302
  )
303
  df["volume_ratio"] = df["item_count"] / df["volume_ma_24h"].replace(0, np.nan)
304
 
305
- city_avg = df.groupby(["item_id", "timestamp"])["avg_price"].transform("mean")
 
306
  df["city_price_deviation"] = (df["avg_price"] - city_avg) / city_avg.replace(0, np.nan)
307
 
 
308
  df["price_pct_7d"] = df.groupby(group_cols)["avg_price"].transform(
309
  lambda x: x.rolling(168, min_periods=24).apply(
310
  lambda arr: (arr[-1] - np.min(arr)) / (np.max(arr) - np.min(arr) + 1e-9),
311
  raw=True
312
  )
313
  )
 
314
  df["is_downtrend"] = ((df["chg_6h"] < 0) & (df["chg_24h"] < 0)).astype(int)
315
 
 
 
 
316
  df = _compute_forward_target(df)
317
  return df
318
 
319
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
320
  def _compute_forward_target(df: pd.DataFrame) -> pd.DataFrame:
321
  if df.empty or "log_price" not in df.columns:
322
  df["target_log_return_24h"] = np.nan
@@ -324,8 +413,9 @@ def _compute_forward_target(df: pd.DataFrame) -> pd.DataFrame:
324
  return df
325
 
326
  df["target_log_return_24h"] = np.nan
 
327
 
328
- for (item_id, city), g in df.groupby(["item_id", "city"]):
329
  if len(g) < 5:
330
  continue
331
 
@@ -355,17 +445,14 @@ def _compute_forward_target(df: pd.DataFrame) -> pd.DataFrame:
355
  target_log = merged["log_price_future"].values - merged["log_price_now"].values
356
  df.loc[merged["idx"].values, "target_log_return_24h"] = target_log
357
 
358
- # 3 sınıf: 0=AŞAĞI, 1=SABİT, 2=YUKARI
359
  t = df["target_log_return_24h"]
360
- df["target_class"] = np.where(t < CLASS_DOWN, 0,
361
- np.where(t > CLASS_UP, 2, 1))
362
  df.loc[t.isna(), "target_class"] = np.nan
363
-
364
  return df
365
 
366
 
367
  # ---------------------------------------------------------------------------
368
- # MODEL EĞİTİMİ — SINIFLANDIRMA
369
  # ---------------------------------------------------------------------------
370
 
371
 
@@ -376,120 +463,109 @@ def prepare_model_data(featured_df: pd.DataFrame) -> Tuple[pd.DataFrame, pd.Seri
376
  df = featured_df.copy()
377
  df["item_code"] = df["item_id"].astype("category").cat.codes
378
  df["city_code"] = df["city"].astype("category").cat.codes
 
 
379
 
380
  df = df.dropna(subset=["target_class", "lag_24h_log"]).copy()
381
  df["target_class"] = df["target_class"].astype(int)
382
 
383
- for col in ["chg_1h", "chg_6h", "chg_24h"]:
384
- df[col] = df[col].fillna(0.0)
 
 
 
 
 
385
 
386
  df["rsi_14"] = df["rsi_14"].fillna(50.0)
387
  df["volatility_24h"] = df["volatility_24h"].fillna(0.0)
388
  df["volume_ratio"] = df["volume_ratio"].fillna(1.0)
389
  df["ma_24h_ratio"] = df["ma_24h_ratio"].fillna(0.0)
 
390
  df["city_price_deviation"] = df["city_price_deviation"].fillna(0.0)
 
 
391
 
392
  model_df = df.dropna(subset=FEATURE_COLS + ["target_class"]).copy()
393
 
394
  X = model_df[FEATURE_COLS].astype("float64")
395
  y = model_df["target_class"]
396
- meta = model_df[["item_id", "city", "timestamp", "avg_price", "target_log_return_24h"]].copy()
397
-
398
  return X, y, meta
399
 
400
 
401
  def train_global_model(X: pd.DataFrame, y: pd.Series) -> Optional[lgb.LGBMClassifier]:
402
  if len(X) < MIN_TRAIN_SAMPLES:
403
- print(f"Yetersiz veri: {len(X)} < {MIN_TRAIN_SAMPLES}")
404
  return None
405
 
406
  class_counts = y.value_counts().to_dict()
407
- print(f"[TANI] Sınıf dağılımı: {class_counts}")
408
-
409
- if len(class_counts) < 3:
410
- print(f"⚠️ Bazı sınıflar eksik. Daha fazla veri gerekli.")
411
  return None
412
 
413
  split_idx = int(len(X) * 0.8)
414
  X_train, X_val = X.iloc[:split_idx], X.iloc[split_idx:]
415
  y_train, y_val = y.iloc[:split_idx], y.iloc[split_idx:]
416
 
417
- # Overfit önleyici parametreler
418
  model = lgb.LGBMClassifier(
419
- objective="multiclass",
420
- num_class=3,
421
- metric="multi_logloss",
422
  boosting_type="gbdt",
423
- num_leaves=15,
424
- max_depth=5,
425
- min_child_samples=50,
426
- learning_rate=0.03,
427
- n_estimators=200,
428
- feature_fraction=0.8,
429
- bagging_fraction=0.8,
430
- bagging_freq=5,
431
- reg_alpha=0.5,
432
- reg_lambda=1.0,
433
- verbose=-1,
434
- random_state=42,
435
- n_jobs=-1,
436
  )
437
 
438
- callbacks = [
439
- lgb.early_stopping(stopping_rounds=30, verbose=False),
440
- lgb.log_evaluation(period=0),
441
- ]
442
-
443
  model.fit(
444
  X_train, y_train,
445
  eval_set=[(X_val, y_val)],
446
- eval_metric="multi_logloss",
447
- callbacks=callbacks,
448
  )
449
 
450
  val_preds = model.predict(X_val)
451
  val_acc = accuracy_score(y_val, val_preds)
452
  print(f"[TANI] val_accuracy={val_acc:.4f}")
453
-
454
  return model
455
 
456
 
457
- def predict_24h_returns(model: lgb.LGBMClassifier, latest_features: pd.DataFrame) -> pd.DataFrame:
458
  if latest_features.empty or model is None:
459
  return pd.DataFrame()
460
 
461
  missing = [c for c in FEATURE_COLS if c not in latest_features.columns]
462
  if missing:
463
- print(f"Eksik özellikler: {missing}")
464
  return pd.DataFrame()
465
 
466
  X_pred = latest_features.copy()
467
-
468
- for col in ["chg_1h", "chg_6h", "chg_24h"]:
469
  if col in X_pred.columns:
470
  X_pred[col] = X_pred[col].fillna(0.0)
471
-
 
472
  X_pred["rsi_14"] = X_pred["rsi_14"].fillna(50.0)
473
  X_pred["volatility_24h"] = X_pred["volatility_24h"].fillna(0.0)
474
  X_pred["volume_ratio"] = X_pred["volume_ratio"].fillna(1.0)
475
  X_pred["ma_24h_ratio"] = X_pred["ma_24h_ratio"].fillna(0.0)
 
476
  X_pred["city_price_deviation"] = X_pred["city_price_deviation"].fillna(0.0)
 
 
477
 
478
  X_ready = X_pred[FEATURE_COLS].astype("float64").dropna()
479
  if X_ready.empty:
480
  return pd.DataFrame()
481
 
482
- probs = model.predict_proba(X_ready) # shape (n, 3)
483
  pred_class = np.argmax(probs, axis=1)
484
-
485
- # Beklenen log getiri (olasılık ağırlıklı)
486
  expected_log = probs[:, 0] * CLASS_DOWN + probs[:, 1] * 0.0 + probs[:, 2] * CLASS_UP
487
  clipped_log = np.clip(expected_log, CLASS_DOWN, CLASS_UP)
488
  pred_return_pct = (np.exp(clipped_log) - 1) * 100
489
 
490
- keep_cols = ["item_id", "city", "avg_price", "item_count",
491
- "volatility_24h", "rsi_14", "volume_ratio",
492
- "price_pct_7d", "is_downtrend", "chg_6h", "chg_24h"]
 
493
  keep_cols = [c for c in keep_cols if c in X_pred.columns]
494
  result = X_pred.loc[X_ready.index, keep_cols].copy()
495
  result["pred_class"] = pred_class
@@ -500,18 +576,16 @@ def predict_24h_returns(model: lgb.LGBMClassifier, latest_features: pd.DataFrame
500
  result["predicted_return_pct"] = pred_return_pct
501
  result["predicted_price_24h"] = result["avg_price"] * np.exp(clipped_log)
502
  result["confidence"] = probs.max(axis=1)
503
-
504
  return result
505
 
506
 
507
  # ---------------------------------------------------------------------------
508
- # DİP ALIM SİNYALİ
509
  # ---------------------------------------------------------------------------
510
 
511
 
512
  def compute_dip_signal(row: pd.Series) -> Tuple[str, str]:
513
- reasons = []
514
- passed = 0
515
 
516
  rsi = row.get("rsi_14", 50)
517
  if pd.notna(rsi) and rsi < DIP_RSI_MAX:
@@ -526,40 +600,39 @@ def compute_dip_signal(row: pd.Series) -> Tuple[str, str]:
526
  vr = row.get("volume_ratio", 1.0)
527
  if pd.notna(vr) and vr >= DIP_MIN_VOLUME_RATIO:
528
  passed += 1
529
- reasons.append(f"Hacim×{vr:.1f}")
530
 
531
  pred_pct = row.get("predicted_return_pct", 0)
532
  if pd.notna(pred_pct) and pred_pct >= DIP_MIN_PREDICTED_PCT:
533
  passed += 1
534
- reasons.append(f"Model={pred_pct:+.1f}%")
535
 
536
- spread = row.get("spread", np.nan)
537
- price = row.get("avg_price", 1)
538
- if pd.notna(spread) and price > 0 and abs(spread) / price < DIP_MAX_SPREAD_PCT:
539
  passed += 1
540
- reasons.append(f"Spread={abs(spread)/price*100:.1f}%")
541
 
542
  is_down = row.get("is_downtrend", 0)
543
  if not is_down or (pd.notna(pred_pct) and pred_pct > 2):
544
  passed += 1
545
- reasons.append("Trend OK")
546
 
547
  if passed >= 5:
548
- signal = "🟢 GÜÇLÜ AL"
549
  elif passed == 4:
550
- signal = "🟡 ZAYIF AL"
551
  elif passed == 3 and pd.notna(rsi) and rsi < 45:
552
- signal = "🔵 İZLE"
553
  elif is_down and pd.notna(pred_pct) and pred_pct < -2:
554
- signal = "🔴 ALMA"
555
  else:
556
- signal = "⚪ BEKLE"
557
 
558
- return signal, " · ".join(reasons) if reasons else "—"
559
 
560
 
561
  # ---------------------------------------------------------------------------
562
- # WALK-FORWARD BACKTEST
563
  # ---------------------------------------------------------------------------
564
 
565
 
@@ -575,10 +648,9 @@ def walk_forward_backtest(featured_df: pd.DataFrame, n_splits: int = 3) -> Dict:
575
 
576
  test_size = len(unique_times) // (n_splits + 1)
577
  if test_size < 5:
578
- return {"error": f"Test boyutu çok küçük ({test_size} < 5)."}
579
 
580
- results = []
581
- all_predictions = []
582
 
583
  for i in range(n_splits):
584
  test_start_idx = test_size * (i + 1) + 24
@@ -601,10 +673,7 @@ def walk_forward_backtest(featured_df: pd.DataFrame, n_splits: int = 3) -> Dict:
601
  X_train, y_train, _ = prepare_model_data(train_df)
602
  X_test, y_test, meta_test = prepare_model_data(test_df)
603
 
604
- if len(X_train) < 30 or len(X_test) < 3:
605
- continue
606
-
607
- if y_train.nunique() < 3:
608
  continue
609
 
610
  model = lgb.LGBMClassifier(
@@ -618,17 +687,14 @@ def walk_forward_backtest(featured_df: pd.DataFrame, n_splits: int = 3) -> Dict:
618
 
619
  preds_class = model.predict(X_test)
620
  probs = model.predict_proba(X_test)
621
- expected_log = probs[:, 0] * CLASS_DOWN + probs[:, 1] * 0.0 + probs[:, 2] * CLASS_UP
622
 
623
  accuracy = accuracy_score(y_test, preds_class)
624
 
625
- # Direction accuracy: sadece UP vs DOWN (flat hariç)
626
  y_dir = np.where(y_test == 2, 1, np.where(y_test == 0, 0, -1))
627
  p_dir = np.where(preds_class == 2, 1, np.where(preds_class == 0, 0, -1))
628
  mask = (y_dir >= 0) & (p_dir >= 0)
629
  direction_acc = float(np.mean(y_dir[mask] == p_dir[mask])) if mask.sum() > 0 else 0.5
630
 
631
- # Baseline: hep "flat" tahmin
632
  baseline_acc = float((y_test == 1).mean())
633
 
634
  results.append({
@@ -643,32 +709,33 @@ def walk_forward_backtest(featured_df: pd.DataFrame, n_splits: int = 3) -> Dict:
643
 
644
  test_meta = meta_test.copy()
645
  test_meta["predicted_class"] = preds_class
646
- test_meta["expected_log_return"] = expected_log
647
  all_predictions.append(test_meta)
648
 
649
  if not results:
650
  return {"error": "Backtest tamamlanamadı."}
651
 
652
- # Simülasyon: sadece UP tahmini + güçlü sinyalleri al
653
- simulation = {"total_trades": 0, "win_rate_pct": 0.0, "avg_net_return_pct": 0.0, "profitable_trades": 0}
654
  if all_predictions:
655
  sim_df = pd.concat(all_predictions).dropna(subset=["target_log_return_24h"])
656
- buy_signals = sim_df[sim_df["predicted_class"] == 2].copy()
657
- if len(buy_signals) > 0:
658
- buy_signals["net_return"] = buy_signals["target_log_return_24h"] - TOTAL_TRADE_COST
659
- win_rate = float((buy_signals["net_return"] > 0).mean() * 100)
660
  simulation = {
661
- "total_trades": int(len(buy_signals)),
662
- "win_rate_pct": round(win_rate, 2),
663
- "avg_net_return_pct": round(float(buy_signals["net_return"].mean() * 100), 4),
664
- "profitable_trades": int((buy_signals["net_return"] > 0).sum()),
 
665
  }
666
 
667
  return {"results": results, "simulation": simulation, "n_splits_completed": len(results)}
668
 
669
 
670
  # ---------------------------------------------------------------------------
671
- # VERİ HATTI
672
  # ---------------------------------------------------------------------------
673
 
674
 
@@ -679,7 +746,7 @@ class DataPipeline:
679
  self.featured_df = pd.DataFrame()
680
  self.model = None
681
  self.last_update = None
682
- self.status_message = "Veri yüklenmedi. 'Verileri Yenile' butonuna basın."
683
  self._backtest_cache = None
684
  self._diagnostics = {}
685
 
@@ -687,14 +754,14 @@ class DataPipeline:
687
  try:
688
  self.model = joblib.load(MODEL_PATH)
689
  self.status_message = "Kayıtlı model yüklendi."
690
- except Exception as e:
691
- print(f"Model yükleme hatası: {e}")
692
 
693
  def fetch_data(self, item_ids=None, cities=None):
694
  item_ids = item_ids or ITEM_IDS
695
  cities = cities or CITIES
696
 
697
- self.status_message = "Public API'den veri çekiliyor..."
698
  ok, msg = check_api_health()
699
  if not ok:
700
  self.status_message = f"API erişilemiyor: {msg}"
@@ -704,7 +771,7 @@ class DataPipeline:
704
  hist = fetch_history(item_ids, cities, time_scale=HISTORY_TIME_SCALE)
705
  prices = fetch_current_prices(item_ids, cities)
706
  except Exception as e:
707
- self.status_message = f"API bağlantı hatası: {e}"
708
  return
709
 
710
  if hist.empty:
@@ -713,12 +780,14 @@ class DataPipeline:
713
 
714
  self.history_df = clean_market_data(hist)
715
  self.prices_df = prices
716
- self.status_message = f"✓ {len(self.history_df)} satır tarihsel, {len(self.prices_df)} anlık."
 
717
 
718
  save_to_csv(self.history_df, "history")
719
  save_to_csv(self.prices_df, "prices")
720
 
721
  self.featured_df = build_features(self.history_df)
 
722
  self._backtest_cache = None
723
  self._update_diagnostics()
724
 
@@ -727,33 +796,29 @@ class DataPipeline:
727
  if df.empty:
728
  self._diagnostics = {}
729
  return
730
- n_total = len(df)
731
  n_with_target = int(df["target_class"].notna().sum()) if "target_class" in df.columns else 0
732
- class_dist = {}
733
- if n_with_target > 0:
734
- class_dist = df["target_class"].value_counts().to_dict()
735
  self._diagnostics = {
736
- "n_total": n_total,
737
  "n_with_target": n_with_target,
738
  "class_dist": class_dist,
 
739
  }
740
- print(f"[TANI] total={n_total}, hedefli={n_with_target}, sınıf={class_dist}")
741
 
742
  def train_model(self):
743
  if self.featured_df.empty:
744
- self.status_message = "Önce veri çekmelisiniz."
745
  return False
746
 
747
  diag = self._diagnostics or {}
748
- n_with_target = diag.get("n_with_target", 0)
749
-
750
- if n_with_target < MIN_TRAIN_SAMPLES:
751
- self.status_message = f"Yetersiz örnek: {n_with_target} < {MIN_TRAIN_SAMPLES}"
752
  return False
753
 
754
  class_dist = diag.get("class_dist", {})
755
  if len(class_dist) < 3 or min(class_dist.values()) < 10:
756
- self.status_message = f"Sınıf dağılımı dengesiz: {class_dist}. Daha fazla veri gerekli."
757
  return False
758
 
759
  X, y, _ = prepare_model_data(self.featured_df)
@@ -766,7 +831,7 @@ class DataPipeline:
766
  self.status_message = "Model eğitimi başarısız."
767
  return False
768
 
769
- self.status_message = f"✓ Model eğitildi (sınıflandırma, {len(X)} örnek)."
770
 
771
  if _HAS_JOBLIB:
772
  try:
@@ -781,32 +846,38 @@ class DataPipeline:
781
  if self.featured_df.empty or self.model is None:
782
  return pd.DataFrame()
783
 
784
- latest = self.featured_df.sort_values("timestamp").groupby(["item_id", "city"]).tail(1).copy()
 
 
 
785
  latest["item_code"] = latest["item_id"].astype("category").cat.codes
786
  latest["city_code"] = latest["city"].astype("category").cat.codes
 
 
787
 
788
  for col in FEATURE_COLS:
789
  if col not in latest.columns:
790
  latest[col] = 0.0
791
 
792
  preds = predict_24h_returns(self.model, latest)
793
-
794
- if not preds.empty and not self.prices_df.empty:
795
- price_info = self.prices_df[["item_id", "city", "sell_price_min", "buy_price_max"]].copy()
796
- price_info["sell_price_min"] = pd.to_numeric(price_info["sell_price_min"], errors="coerce").astype("float64")
797
- price_info["buy_price_max"] = pd.to_numeric(price_info["buy_price_max"], errors="coerce").astype("float64")
798
- preds = preds.merge(price_info, on=["item_id", "city"], how="left")
 
 
 
799
  preds["spread"] = preds["sell_price_min"] - preds["buy_price_max"]
800
  else:
801
  preds["spread"] = np.nan
802
 
803
- total_cost = SETUP_FEE_RATE + TRANSACTION_TAX_RATE
804
- preds["net_return_pct"] = preds["predicted_return_pct"] - (total_cost * 100)
805
 
806
  dip_signals = preds.apply(compute_dip_signal, axis=1, result_type="expand")
807
  preds["Dip Sinyali"] = dip_signals[0]
808
  preds["Sinyal Detayı"] = dip_signals[1]
809
-
810
  return preds
811
 
812
  def get_backtest(self):
@@ -832,18 +903,18 @@ def get_dashboard_metrics():
832
  })
833
 
834
  df = pipeline.featured_df
835
- class_dist = diag.get("class_dist", {})
836
  metrics = {
837
  "Veri Kaynağı": "Public AODP API (europe)",
838
- "Görev Tipi": "3-sınıf sınıflandırma (AŞAĞI/SABİT/YUKARI)",
 
839
  "Toplam Gözlem": len(df),
840
  "Hedefli Satır": diag.get("n_with_target", 0),
841
- "Sınıf AŞAĞI": class_dist.get(0, 0),
842
- "Sınıf SABİT": class_dist.get(1, 0),
843
- "Sınıf YUKARI": class_dist.get(2, 0),
844
  "Tarih Aralığı": f"{df['timestamp'].min().date()} - {df['timestamp'].max().date()}",
845
- "Model Durumu": "Eğitildi" if pipeline.model else "Eğitilmedi",
846
- "Sınıf Sınırları": f"±%{CLASS_UP*100:.0f}",
847
  "Son Güncelleme": pipeline.last_update.strftime("%Y-%m-%d %H:%M") if pipeline.last_update else "—",
848
  }
849
  return pd.DataFrame(list(metrics.items()), columns=["Metrik", "Değer"])
@@ -853,29 +924,35 @@ def get_analysis_table():
853
  if pipeline.featured_df.empty:
854
  return pd.DataFrame()
855
 
856
- latest = pipeline.featured_df.sort_values("timestamp").groupby(["item_id", "city"]).tail(1).copy()
 
 
857
 
858
  if not pipeline.prices_df.empty:
859
- price_info = pipeline.prices_df[["item_id", "city", "sell_price_min", "buy_price_max"]].copy()
860
- price_info["sell_price_min"] = pd.to_numeric(price_info["sell_price_min"], errors="coerce").astype("float64")
861
- price_info["buy_price_max"] = pd.to_numeric(price_info["buy_price_max"], errors="coerce").astype("float64")
862
- latest = latest.merge(price_info, on=["item_id", "city"], how="left")
 
863
  else:
864
  latest["sell_price_min"] = np.nan
865
  latest["buy_price_max"] = np.nan
866
 
867
  latest["Ürün"] = latest["item_id"].apply(item_display_name)
 
868
  latest["Şehir"] = latest["city"]
869
  latest["Mevcut Fiyat"] = latest["avg_price"].round(0)
870
  latest["Hacim"] = latest["item_count"].fillna(0).astype(int)
871
  latest["Spread"] = (latest["sell_price_min"] - latest["buy_price_max"]).round(0)
872
  latest["RSI"] = latest["rsi_14"].round(0)
873
- latest["7g Konum %"] = (latest["price_pct_7d"] * 100).round(0)
874
- latest["Trend"] = np.where(latest["is_downtrend"] == 1, "↓ Düşüş", "→ Yatay/Yukarı")
875
  latest["Likidite"] = pd.cut(latest["item_count"], bins=[-1, 10, 50, 200, np.inf],
876
  labels=["Düşük", "Orta", "İyi", "Yüksek"]).astype(str)
877
 
878
- return latest[["Ürün", "Şehir", "Mevcut Fiyat", "Hacim", "Spread", "RSI", "7g Konum %", "Trend", "Likidite"]].sort_values("Hacim", ascending=False).head(100)
 
 
879
 
880
 
881
  def get_prediction_table():
@@ -885,91 +962,96 @@ def get_prediction_table():
885
 
886
  preds = preds.copy()
887
  preds["Ürün"] = preds["item_id"].apply(item_display_name)
 
888
  preds["Şehir"] = preds["city"]
889
  preds["Mevcut Fiyat"] = preds["avg_price"].round(0)
890
- preds["Tahmin Sınıfı"] = preds["pred_class"].map({0: "AŞAĞI", 1: "SABİT", 2: "YUKARI"})
891
- preds["P(Aşağı)"] = (preds["prob_down"] * 100).round(0).astype(int).astype(str) + "%"
892
- preds["P(Sabit)"] = (preds["prob_flat"] * 100).round(0).astype(int).astype(str) + "%"
893
- preds["P(Yukarı)"] = (preds["prob_up"] * 100).round(0).astype(int).astype(str) + "%"
894
  preds["Güvenilirlik"] = (preds["confidence"] * 100).round(0).astype(int).astype(str) + "%"
895
  preds["Tahmini Değişim %"] = preds["predicted_return_pct"].round(2)
896
  preds["Net Getiri %"] = preds["net_return_pct"].round(2)
897
 
898
- cols = ["Ürün", "Şehir", "Mevcut Fiyat", "Tahmin Sınıfı", "P(Aşağı)", "P(Sabit)",
899
- "P(Yukarı)", "Güvenilirlik", "Tahmini Değişim %", "Net Getiri %", "Dip Sinyali"]
900
  return preds[cols].sort_values("Güvenilirlik", ascending=False)
901
 
902
 
903
  def get_dip_alim_table():
904
  preds = pipeline.get_latest_predictions()
905
  if preds.empty:
906
- return pd.DataFrame({"Bilgi": ["Tahmin üretilemedi."]})
907
-
908
- dip_only = preds[preds["Dip Sinyali"].isin(["🟢 GÜÇLÜ AL", "🟡 ZAYIF AL", "🔵 İZLE"])].copy()
909
 
910
- if dip_only.empty:
 
911
  return pd.DataFrame({"Bilgi": ["Şu anda dip alım sinyali yok."]})
912
 
913
- signal_order = {"🟢 GÜÇLÜ AL": 0, "🟡 ZAYIF AL": 1, "🔵 İZLE": 2}
914
- dip_only["_sig_rank"] = dip_only["Dip Sinyali"].map(signal_order)
915
 
916
- dip_only["Ürün"] = dip_only["item_id"].apply(item_display_name)
917
- dip_only["Şehir"] = dip_only["city"]
918
- dip_only["Mevcut Fiyat"] = dip_only["avg_price"].round(0)
919
- dip_only["RSI"] = dip_only["rsi_14"].round(0)
920
- dip_only["7g Konum %"] = (dip_only["price_pct_7d"] * 100).round(0)
921
- dip_only["Trend"] = np.where(dip_only["is_downtrend"] == 1, "↓", "→")
922
- dip_only["Hacim×Ort"] = dip_only["volume_ratio"].round(1)
923
- dip_only["Tahmin Sınıfı"] = dip_only["pred_class"].map({0: "AŞAĞI", 1: "SABİT", 2: "YUKARI"})
924
- dip_only["P(Yukarı)"] = (dip_only["prob_up"] * 100).round(0).astype(int).astype(str) + "%"
925
- dip_only["Net Getiri %"] = dip_only["net_return_pct"].round(2)
926
 
927
- dip_only = dip_only.sort_values(["_sig_rank", "confidence"], ascending=[True, False])
928
-
929
- cols = ["Dip Sinyali", "Ürün", "Şehir", "Mevcut Fiyat", "RSI", "7g Konum %",
930
- "Trend", "Hacim×Ort", "Tahmin Sınıfı", "P(Yukarı)", "Net Getiri %", "Sinyal Detayı"]
931
- return dip_only[cols].head(50)
932
 
933
 
934
  def get_arbitrage_table():
935
  if pipeline.prices_df.empty:
936
- return pd.DataFrame({"Bilgi": ["Anlık fiyat yok."]})
937
 
938
  prices = pipeline.prices_df.copy()
939
- prices["sell_price_min"] = pd.to_numeric(prices["sell_price_min"], errors="coerce").astype("float64")
940
- prices["buy_price_max"] = pd.to_numeric(prices["buy_price_max"], errors="coerce").astype("float64")
 
 
 
 
941
  prices = prices[prices["sell_price_min"] > 0]
942
  if prices.empty:
943
  return pd.DataFrame({"Bilgi": ["Geçerli fiyat yok."]})
944
 
945
- buy = prices.loc[prices.groupby("item_id")["sell_price_min"].idxmin()][["item_id", "city", "sell_price_min"]]
946
- buy.columns = ["item_id", "buy_city", "buy_price"]
947
- sell = prices.loc[prices.groupby("item_id")["buy_price_max"].idxmax()][["item_id", "city", "buy_price_max"]]
948
- sell.columns = ["item_id", "sell_city", "sell_price"]
 
 
 
 
949
 
950
- arb = buy.merge(sell, on="item_id")
951
  arb = arb[arb["sell_price"] > arb["buy_price"]].copy()
952
  if arb.empty:
953
- return pd.DataFrame({"Bilgi": ["Kârlı arbitraj yok."]})
954
 
955
- arb["brüt_kâr"] = arb["sell_price"] - arb["buy_price"]
956
- arb["toplam_maliyet"] = arb["buy_price"] * SETUP_FEE_RATE + arb["sell_price"] * (SETUP_FEE_RATE + TRANSACTION_TAX_RATE)
957
- arb["net_kâr"] = arb["brüt_kâr"] - arb["toplam_maliyet"]
958
- arb["kâr_marjı_%"] = (arb["net_kâr"] / arb["buy_price"]) * 100
959
- arb = arb[arb["net_kâr"] > 0].sort_values("kâr_marjı_%", ascending=False)
960
 
961
  if arb.empty:
962
- return pd.DataFrame({"Bilgi": ["Maliyetler sonrası kârlı arbitraj yok."]})
963
 
964
  arb["Ürün"] = arb["item_id"].apply(item_display_name)
 
965
  arb["Alış Şehri"] = arb["buy_city"]
966
  arb["Satış Şehri"] = arb["sell_city"]
967
- arb["Alış Fiyatı"] = arb["buy_price"].round(0)
968
- arb["Satış Fiyatı"] = arb["sell_price"].round(0)
969
  arb["Net Kâr"] = arb["net_kâr"].round(0)
970
- arb["Kâr Marjı %"] = arb["kâr_marjı_%"].round(1)
971
 
972
- return arb[["Ürün", "Alış Şehri", "Satış Şehri", "Alış Fiyatı", "Satış Fiyatı", "Net Kâr", "Kâr Marjı %"]].head(50)
 
973
 
974
 
975
  def get_model_performance_plot():
@@ -981,7 +1063,7 @@ def get_model_performance_plot():
981
  "Önem": pipeline.model.feature_importances_,
982
  }).sort_values("Önem", ascending=True).tail(20)
983
  fig = px.bar(df_imp, x="Önem", y="Özellik", orientation="h",
984
- title="Feature Importance (Top 20)", template="plotly_dark")
985
  fig.update_layout(height=600)
986
  return fig
987
  except Exception as e:
@@ -992,19 +1074,18 @@ def get_backtest_outputs():
992
  bt = pipeline.get_backtest()
993
  if "error" in bt:
994
  return (pd.DataFrame({"Bilgi": [bt["error"]]}),
995
- go.Figure().update_layout(title="Backtest hatası", template="plotly_dark"),
996
  pd.DataFrame())
997
 
998
- results_df = pd.DataFrame(bt["results"])
999
-
1000
  fig = go.Figure()
1001
- fig.add_trace(go.Bar(name="Model Accuracy", x=results_df["split"], y=results_df["accuracy"], marker_color="cyan"))
1002
- fig.add_trace(go.Bar(name="Baseline (Flat)", x=results_df["split"], y=results_df["baseline_accuracy"], marker_color="gray"))
1003
- fig.update_layout(title="Walk-Forward: Accuracy vs Baseline", xaxis_title="Split",
1004
  yaxis_title="Accuracy", barmode="group", template="plotly_dark")
1005
 
1006
- sim_df = pd.DataFrame([bt["simulation"]]) if bt.get("simulation") else pd.DataFrame()
1007
- return results_df, fig, sim_df
1008
 
1009
 
1010
  def list_csv_files() -> pd.DataFrame:
@@ -1038,7 +1119,7 @@ def refresh_data_ui():
1038
  bt_table, bt_fig, sim_table = get_backtest_outputs()
1039
  except Exception:
1040
  bt_table, sim_table = pd.DataFrame(), pd.DataFrame()
1041
- bt_fig = go.Figure().update_layout(title="Backtest hatası", template="plotly_dark")
1042
 
1043
  def _safe(fn, default):
1044
  try:
@@ -1069,7 +1150,7 @@ def initial_load():
1069
  return (
1070
  _safe(get_dashboard_metrics, pd.DataFrame({"Metrik": ["Durum"], "Değer": ["Hazır"]})),
1071
  pd.DataFrame(), pd.DataFrame(), pd.DataFrame(), pd.DataFrame(),
1072
- go.Figure().update_layout(title="Yenile butonuna basın", template="plotly_dark"),
1073
  pd.DataFrame(),
1074
  go.Figure().update_layout(title="Veri gerekli", template="plotly_dark"),
1075
  pd.DataFrame(),
@@ -1078,7 +1159,7 @@ def initial_load():
1078
 
1079
 
1080
  # ---------------------------------------------------------------------------
1081
- # GRADIO ARAYÜZÜ
1082
  # ---------------------------------------------------------------------------
1083
 
1084
 
@@ -1086,11 +1167,11 @@ with gr.Blocks(title="GOSHAWK ALBION MARKET AI", theme=gr.themes.Soft(primary_hu
1086
 
1087
  gr.Markdown(
1088
  """
1089
- # 🦅 GOSHAWK ALBION MARKET AI
1090
- **Sınıflandırma tabanlı sürüm** | Avrupa Sunucusu
1091
  *LightGBM 3-sınıf sınıflandırma: AŞAĞI / SABİT / YUKARI*
1092
 
1093
- **⚠️ Risk Uyarısı:** Bu uygulama yatırım tavsiyesi değildir. Model tahminleri belirsizlik içerir.
1094
  """
1095
  )
1096
 
@@ -1100,21 +1181,21 @@ with gr.Blocks(title="GOSHAWK ALBION MARKET AI", theme=gr.themes.Soft(primary_hu
1100
 
1101
  with gr.Tabs():
1102
  with gr.TabItem("📊 Dashboard"):
1103
- dashboard_table = gr.Dataframe(label="Özet & Teşhis", interactive=False, wrap=True)
1104
  gr.Markdown(
1105
- "### Sınıflandırma Görevi\n"
1106
- "MAE yerine **accuracy** kullanılır. Model 3 sınıf tahmin eder:\n"
1107
- f"- **AŞAĞI** (log < {CLASS_DOWN:.2f}, ≈ %{np.exp(CLASS_DOWN)*100-100:.0f}'den fazla düşüş)\n"
1108
- "- **SABİT** (arada)\n"
1109
- f"- **YUKARI** (log > {CLASS_UP:.2f}, ≈ %{np.exp(CLASS_UP)*100-100:.0f}'den fazla artış)\n\n"
1110
- "### Backtest Metrikleri\n"
1111
- "- **Accuracy:** Genel doğruluk (3 sınıf)\n"
1112
- "- **Direction Accuracy:** Sadece AŞAĞI vs YUKARI (SABİT hariç)\n"
1113
- "- **Baseline Accuracy:** Hep SABİT tahmin etmenin doğruluğu\n"
1114
- "- **Accuracy Lift:** Modelin baseline'dan farkı (%)\n\n"
1115
  "### Riskler\n"
1116
- "- Sınıf dağılımı dengesizse accuracy yanıltıcı olabilir\n"
1117
- "- Direction accuracy'ye bakmak daha güvenilir"
1118
  )
1119
 
1120
  with gr.TabItem("🎯 DİP ALIM"):
@@ -1127,22 +1208,26 @@ with gr.Blocks(title="GOSHAWK ALBION MARKET AI", theme=gr.themes.Soft(primary_hu
1127
  analysis_table = gr.Dataframe(label="Ürün Analizi", interactive=False)
1128
 
1129
  with gr.TabItem("🚚 Arbitraj"):
1130
- arbitrage_table = gr.Dataframe(label="Arbitraj", interactive=False)
 
 
 
 
1131
 
1132
  with gr.TabItem("📈 Model"):
1133
  performance_plot = gr.Plot(label="Feature Importance")
1134
 
1135
  with gr.TabItem("🧪 Backtest"):
1136
  backtest_table = gr.Dataframe(label="Split Sonuçları", interactive=False)
1137
- backtest_plot = gr.Plot(label="Accuracy Karşılaştırması")
1138
  simulation_table = gr.Dataframe(label="Simülasyon", interactive=False)
1139
 
1140
  with gr.TabItem("🗄️ Veri"):
1141
  gr.Markdown(
1142
- "**API:** `europe.albion-online-data.com/api/v2`\n"
1143
- "**Görev:** 3-sınıf sınıflandırma\n"
1144
- f"**Sınıf Sınırları:** ±%{CLASS_UP*100:.0f}\n"
1145
- f"**Maliyet:** %{(SETUP_FEE_RATE+TRANSACTION_TAX_RATE)*100:.1f}"
1146
  )
1147
  csv_files = gr.Dataframe(label="CSV Dosyaları", interactive=False,
1148
  headers=["Dosya", "Boyut (KB)", "Son Değişiklik"])
 
1
  # app.py
2
  # GOSHAWK ALBION MARKET AI
3
+ # Kalite ayrımı + uzun vadeli trend (7/14 gün) + alış-satış spread feature'ları
4
 
5
  import os
6
  import time
 
15
  import plotly.express as px
16
 
17
  import lightgbm as lgb
18
+ from sklearn.metrics import accuracy_score
19
 
20
  import gradio as gr
21
 
 
38
  "Martlock", "Thetford", "Caerleon",
39
  ]
40
 
41
+ # Kalite seviyeleri (1=Normal, 2=Good, 3=Outstanding, 4=Excellent, 5=Masterpiece)
42
+ QUALITIES = ["1", "2", "3", "4", "5"]
43
+
44
+ # Kalite kodları → insan-okunur
45
+ QUALITY_NAMES = {1: "Normal", 2: "Good", 3: "Outstanding", 4: "Excellent", 5: "Masterpiece"}
46
+
47
  _ITEM_POOL = [
48
  "T4_BAG", "T5_BAG", "T6_BAG", "T7_BAG", "T8_BAG",
49
  "T4_CAPE", "T5_CAPE", "T6_CAPE", "T7_CAPE", "T8_CAPE",
 
90
  return item_id.replace("_", " ").title()
91
 
92
 
93
+ def quality_name(q) -> str:
94
+ try:
95
+ return QUALITY_NAMES.get(int(q), f"Q{q}")
96
+ except Exception:
97
+ return f"Q{q}"
98
+
99
+
100
  SETUP_FEE_RATE = 0.025
101
  TRANSACTION_TAX_RATE = 0.08
102
  TOTAL_TRADE_COST = SETUP_FEE_RATE + TRANSACTION_TAX_RATE
 
117
 
118
  HISTORY_TIME_SCALE = 1
119
 
120
+ CLASS_DOWN = -0.03
121
+ CLASS_UP = 0.03
 
 
122
 
 
123
  TARGET_TOLERANCE_HOURS = 12
124
 
125
  # Dip alım
 
129
  DIP_MIN_PREDICTED_PCT = -1.0
130
  DIP_MAX_SPREAD_PCT = 0.15
131
 
132
+ # Uzun vadeli lag/chg pencereleri
133
+ LONG_TERM_WINDOWS = [24, 168, 336] # 1g, 7g, 14g
134
 
135
+ # Feature kolonları (spread + uzun vadeli dahil)
136
  FEATURE_COLS = [
137
  "hour", "dayofweek",
138
+ "lag_24h_log", "lag_168h_log", "lag_336h_log",
139
+ "chg_1h", "chg_6h", "chg_24h", "chg_168h", "chg_336h",
140
  "rsi_14", "volatility_24h",
141
  "volume_ratio",
142
+ "ma_24h_ratio", "ma_168h_ratio",
143
  "city_price_deviation",
144
+ "spread_pct", "buy_sell_ratio", "qual_code",
145
  "item_code", "city_code",
146
  ]
147
 
 
151
 
152
 
153
  def safe_request(url: str, params: Optional[dict] = None) -> Optional[list]:
154
+ headers = {"User-Agent": "GOSHAWK-ALBION-MARKET-AI/2.0", "Accept": "application/json"}
155
  for attempt in range(MAX_RETRIES):
156
  try:
157
  resp = requests.get(url, params=params, headers=headers, timeout=REQUEST_TIMEOUT)
 
174
  return None
175
 
176
 
177
+ def fetch_current_prices(item_ids: List[str], cities: List[str], qualities: str = "1,2,3,4,5") -> pd.DataFrame:
178
+ """Anlık fiyatlar — tüm kaliteler."""
179
  all_data = []
180
+ for i in range(0, len(item_ids), 40):
181
+ chunk = item_ids[i:i+40]
182
  url = f"{API_BASE}/stats/prices/{','.join(chunk)}.json"
183
  params = {"locations": ",".join(cities), "qualities": qualities}
184
  data = safe_request(url, params)
185
  if data:
186
  all_data.extend(data)
187
+ time.sleep(0.4)
188
  if not all_data:
189
  return pd.DataFrame()
190
 
 
192
  for col in ["sell_price_min", "sell_price_max", "buy_price_min", "buy_price_max"]:
193
  if col in df.columns:
194
  df[col] = pd.to_numeric(df[col], errors="coerce").astype("float64")
195
+ if "quality" in df.columns:
196
+ df["quality"] = pd.to_numeric(df["quality"], errors="coerce").fillna(1).astype(int)
197
  return df
198
 
199
 
200
+ def fetch_history(item_ids: List[str], cities: List[str], time_scale: int = 1, qualities: str = "1,2,3,4,5") -> pd.DataFrame:
201
+ """Tarihsel fiyatlar — tüm kaliteler."""
202
  all_records = []
203
+ for i in range(0, len(item_ids), 15):
204
+ chunk = item_ids[i:i+15]
205
  url = f"{API_BASE}/stats/history/{','.join(chunk)}.json"
206
  params = {"locations": ",".join(cities), "qualities": qualities, "time-scale": time_scale}
207
  data = safe_request(url, params)
 
209
  for entry in data:
210
  item_id = entry.get("item_id")
211
  loc = entry.get("location")
212
+ quality = entry.get("quality", 1)
213
  for point in entry.get("data", []):
214
  all_records.append({
215
  "item_id": item_id,
216
  "city": loc,
217
+ "quality": quality,
218
  "timestamp": point.get("timestamp"),
219
  "avg_price": point.get("avg_price"),
220
  "item_count": point.get("item_count", 0),
221
  })
222
+ time.sleep(0.6)
223
  if not all_records:
224
  return pd.DataFrame()
225
 
 
227
  df["timestamp"] = pd.to_datetime(df["timestamp"], errors="coerce")
228
  df["avg_price"] = pd.to_numeric(df["avg_price"], errors="coerce").astype("float64")
229
  df["item_count"] = pd.to_numeric(df["item_count"], errors="coerce").fillna(0).astype("float64")
230
+ if "quality" in df.columns:
231
+ df["quality"] = pd.to_numeric(df["quality"], errors="coerce").fillna(1).astype(int)
232
  return df
233
 
234
 
 
247
  df = df.copy()
248
  df["avg_price"] = pd.to_numeric(df["avg_price"], errors="coerce").astype("float64")
249
  df["item_count"] = pd.to_numeric(df["item_count"], errors="coerce").fillna(0).astype("float64")
250
+ if "quality" in df.columns:
251
+ df["quality"] = pd.to_numeric(df["quality"], errors="coerce").fillna(1).astype(int)
252
+ else:
253
+ df["quality"] = 1
254
  df = df[df["avg_price"] > 0].copy()
255
  df = df.dropna(subset=["timestamp"]).copy()
256
 
257
+ # Kalite bazlı temizlik (item+city+quality bazında)
258
+ for (item, qual) in df[["item_id", "quality"]].drop_duplicates().itertuples(index=False):
259
+ mask = (df["item_id"] == item) & (df["quality"] == qual)
260
  if mask.sum() < 10:
261
  continue
262
  lower = df.loc[mask, "avg_price"].quantile(0.005)
 
264
  if pd.notna(lower) and pd.notna(upper) and upper > lower:
265
  df.loc[mask, "avg_price"] = df.loc[mask, "avg_price"].clip(lower, upper)
266
 
267
+ return df.sort_values(["item_id", "city", "quality", "timestamp"]).reset_index(drop=True)
268
 
269
 
270
  def save_to_csv(df: pd.DataFrame, name: str) -> str:
 
293
 
294
 
295
  def build_features(df: pd.DataFrame) -> pd.DataFrame:
296
+ """
297
+ Kalite ayrımı + uzun vadeli trend + spread feature'ları.
298
+ group_cols = [item_id, city, quality] → kalite farkı artık karışmıyor.
299
+ """
300
  if df.empty:
301
  return df
302
 
303
+ if "quality" not in df.columns:
304
+ df["quality"] = 1
305
+
306
+ df = df.sort_values(["item_id", "city", "quality", "timestamp"]).copy()
307
  df["avg_price"] = pd.to_numeric(df["avg_price"], errors="coerce").astype("float64")
308
  df["item_count"] = pd.to_numeric(df["item_count"], errors="coerce").fillna(0).astype("float64")
309
 
310
  df["hour"] = df["timestamp"].dt.hour
311
  df["dayofweek"] = df["timestamp"].dt.dayofweek
312
  df["is_weekend"] = (df["dayofweek"] >= 5).astype(int)
313
+ df["qual_code"] = df["quality"].astype(int)
314
 
315
+ # KRİTİK: kalite artık grup içinde
316
+ group_cols = ["item_id", "city", "quality"]
317
 
318
  df["log_price"] = np.log(df["avg_price"].replace(0, np.nan))
319
+
320
+ # Uzun vadeli lag'ler
321
  df["lag_24h_log"] = df.groupby(group_cols)["log_price"].shift(24)
322
+ df["lag_168h_log"] = df.groupby(group_cols)["log_price"].shift(168)
323
+ df["lag_336h_log"] = df.groupby(group_cols)["log_price"].shift(336)
324
 
325
+ # Değişimler
326
+ for chg in [1, 6, 24, 168, 336]:
327
  df[f"chg_{chg}h"] = df.groupby(group_cols)["log_price"].shift(chg)
328
 
329
+ # MA oranları
330
  df["ma_24h_raw"] = df.groupby(group_cols)["avg_price"].transform(
331
  lambda x: x.rolling(24, min_periods=6).mean()
332
  )
333
  df["ma_24h_ratio"] = df["ma_24h_raw"] / df["avg_price"].replace(0, np.nan) - 1.0
334
 
335
+ df["ma_168h_raw"] = df.groupby(group_cols)["avg_price"].transform(
336
+ lambda x: x.rolling(168, min_periods=24).mean()
337
+ )
338
+ df["ma_168h_ratio"] = df["ma_168h_raw"] / df["avg_price"].replace(0, np.nan) - 1.0
339
+
340
  df["rsi_14"] = df.groupby(group_cols)["avg_price"].transform(lambda x: compute_rsi(x, 14))
341
  df["volatility_24h"] = df.groupby(group_cols)["log_price"].transform(
342
  lambda x: x.rolling(24, min_periods=6).std()
 
346
  )
347
  df["volume_ratio"] = df["item_count"] / df["volume_ma_24h"].replace(0, np.nan)
348
 
349
+ # Şehir fiyat farkı (aynı item+quality+timestamp içinde)
350
+ city_avg = df.groupby(["item_id", "quality", "timestamp"])["avg_price"].transform("mean")
351
  df["city_price_deviation"] = (df["avg_price"] - city_avg) / city_avg.replace(0, np.nan)
352
 
353
+ # 7g yüzdelik konum
354
  df["price_pct_7d"] = df.groupby(group_cols)["avg_price"].transform(
355
  lambda x: x.rolling(168, min_periods=24).apply(
356
  lambda arr: (arr[-1] - np.min(arr)) / (np.max(arr) - np.min(arr) + 1e-9),
357
  raw=True
358
  )
359
  )
360
+
361
  df["is_downtrend"] = ((df["chg_6h"] < 0) & (df["chg_24h"] < 0)).astype(int)
362
 
363
+ # SPREAD feature'ları — prices_df'ten merge edilecek
364
+ # Şimdilik NaN, sonra enrich_spread_features ile doldurulacak
365
+
366
  df = _compute_forward_target(df)
367
  return df
368
 
369
 
370
+ def enrich_spread_features(featured_df: pd.DataFrame, prices_df: pd.DataFrame) -> pd.DataFrame:
371
+ """
372
+ Anlık alış-satış spread'i feature olarak ekle.
373
+ prices_df'te kalite ayrımı varsa ona göre merge.
374
+ """
375
+ if featured_df.empty or prices_df.empty:
376
+ featured_df["spread_pct"] = 0.0
377
+ featured_df["buy_sell_ratio"] = 1.0
378
+ return featured_df
379
+
380
+ prices = prices_df.copy()
381
+ if "quality" not in prices.columns:
382
+ prices["quality"] = 1
383
+ prices["quality"] = pd.to_numeric(prices["quality"], errors="coerce").fillna(1).astype(int)
384
+
385
+ prices["sell_price_min"] = pd.to_numeric(prices["sell_price_min"], errors="coerce").astype("float64")
386
+ prices["buy_price_max"] = pd.to_numeric(prices["buy_price_max"], errors="coerce").astype("float64")
387
+
388
+ prices["spread_pct"] = np.where(
389
+ prices["buy_price_max"] > 0,
390
+ (prices["sell_price_min"] - prices["buy_price_max"]) / prices["buy_price_max"],
391
+ np.nan
392
+ )
393
+ prices["buy_sell_ratio"] = np.where(
394
+ prices["sell_price_min"] > 0,
395
+ prices["buy_price_max"] / prices["sell_price_min"],
396
+ np.nan
397
+ )
398
+
399
+ price_features = prices[["item_id", "city", "quality", "spread_pct", "buy_sell_ratio"]].drop_duplicates(
400
+ subset=["item_id", "city", "quality"]
401
+ )
402
+
403
+ result = featured_df.merge(price_features, on=["item_id", "city", "quality"], how="left")
404
+ result["spread_pct"] = result["spread_pct"].fillna(0.0)
405
+ result["buy_sell_ratio"] = result["buy_sell_ratio"].fillna(1.0)
406
+ return result
407
+
408
+
409
  def _compute_forward_target(df: pd.DataFrame) -> pd.DataFrame:
410
  if df.empty or "log_price" not in df.columns:
411
  df["target_log_return_24h"] = np.nan
 
413
  return df
414
 
415
  df["target_log_return_24h"] = np.nan
416
+ group_cols = ["item_id", "city", "quality"]
417
 
418
+ for keys, g in df.groupby(group_cols):
419
  if len(g) < 5:
420
  continue
421
 
 
445
  target_log = merged["log_price_future"].values - merged["log_price_now"].values
446
  df.loc[merged["idx"].values, "target_log_return_24h"] = target_log
447
 
 
448
  t = df["target_log_return_24h"]
449
+ df["target_class"] = np.where(t < CLASS_DOWN, 0, np.where(t > CLASS_UP, 2, 1))
 
450
  df.loc[t.isna(), "target_class"] = np.nan
 
451
  return df
452
 
453
 
454
  # ---------------------------------------------------------------------------
455
+ # MODEL
456
  # ---------------------------------------------------------------------------
457
 
458
 
 
463
  df = featured_df.copy()
464
  df["item_code"] = df["item_id"].astype("category").cat.codes
465
  df["city_code"] = df["city"].astype("category").cat.codes
466
+ if "qual_code" not in df.columns:
467
+ df["qual_code"] = df.get("quality", 1)
468
 
469
  df = df.dropna(subset=["target_class", "lag_24h_log"]).copy()
470
  df["target_class"] = df["target_class"].astype(int)
471
 
472
+ # Uzun pencereli feature'ları doldur (kısa pencereden)
473
+ for col in ["chg_1h", "chg_6h", "chg_24h", "chg_168h", "chg_336h"]:
474
+ if col in df.columns:
475
+ df[col] = df[col].fillna(0.0)
476
+
477
+ df["lag_168h_log"] = df["lag_168h_log"].fillna(df["lag_24h_log"])
478
+ df["lag_336h_log"] = df["lag_336h_log"].fillna(df["lag_168h_log"]).fillna(df["lag_24h_log"])
479
 
480
  df["rsi_14"] = df["rsi_14"].fillna(50.0)
481
  df["volatility_24h"] = df["volatility_24h"].fillna(0.0)
482
  df["volume_ratio"] = df["volume_ratio"].fillna(1.0)
483
  df["ma_24h_ratio"] = df["ma_24h_ratio"].fillna(0.0)
484
+ df["ma_168h_ratio"] = df["ma_168h_ratio"].fillna(df["ma_24h_ratio"]).fillna(0.0)
485
  df["city_price_deviation"] = df["city_price_deviation"].fillna(0.0)
486
+ df["spread_pct"] = df.get("spread_pct", pd.Series(0.0, index=df.index)).fillna(0.0)
487
+ df["buy_sell_ratio"] = df.get("buy_sell_ratio", pd.Series(1.0, index=df.index)).fillna(1.0)
488
 
489
  model_df = df.dropna(subset=FEATURE_COLS + ["target_class"]).copy()
490
 
491
  X = model_df[FEATURE_COLS].astype("float64")
492
  y = model_df["target_class"]
493
+ meta = model_df[["item_id", "city", "quality", "timestamp", "avg_price", "target_log_return_24h"]].copy()
 
494
  return X, y, meta
495
 
496
 
497
  def train_global_model(X: pd.DataFrame, y: pd.Series) -> Optional[lgb.LGBMClassifier]:
498
  if len(X) < MIN_TRAIN_SAMPLES:
 
499
  return None
500
 
501
  class_counts = y.value_counts().to_dict()
502
+ print(f"[TANI] Sınıf: {class_counts}")
503
+ if len(class_counts) < 3 or min(class_counts.values()) < 10:
 
 
504
  return None
505
 
506
  split_idx = int(len(X) * 0.8)
507
  X_train, X_val = X.iloc[:split_idx], X.iloc[split_idx:]
508
  y_train, y_val = y.iloc[:split_idx], y.iloc[split_idx:]
509
 
 
510
  model = lgb.LGBMClassifier(
511
+ objective="multiclass", num_class=3, metric="multi_logloss",
 
 
512
  boosting_type="gbdt",
513
+ num_leaves=15, max_depth=5, min_child_samples=50,
514
+ learning_rate=0.03, n_estimators=300,
515
+ feature_fraction=0.8, bagging_fraction=0.8, bagging_freq=5,
516
+ reg_alpha=0.5, reg_lambda=1.0,
517
+ verbose=-1, random_state=42, n_jobs=-1,
 
 
 
 
 
 
 
 
518
  )
519
 
 
 
 
 
 
520
  model.fit(
521
  X_train, y_train,
522
  eval_set=[(X_val, y_val)],
523
+ callbacks=[lgb.early_stopping(30, verbose=False), lgb.log_evaluation(0)],
 
524
  )
525
 
526
  val_preds = model.predict(X_val)
527
  val_acc = accuracy_score(y_val, val_preds)
528
  print(f"[TANI] val_accuracy={val_acc:.4f}")
 
529
  return model
530
 
531
 
532
+ def predict_24h_returns(model, latest_features: pd.DataFrame) -> pd.DataFrame:
533
  if latest_features.empty or model is None:
534
  return pd.DataFrame()
535
 
536
  missing = [c for c in FEATURE_COLS if c not in latest_features.columns]
537
  if missing:
 
538
  return pd.DataFrame()
539
 
540
  X_pred = latest_features.copy()
541
+ for col in ["chg_1h", "chg_6h", "chg_24h", "chg_168h", "chg_336h"]:
 
542
  if col in X_pred.columns:
543
  X_pred[col] = X_pred[col].fillna(0.0)
544
+ X_pred["lag_168h_log"] = X_pred["lag_168h_log"].fillna(X_pred["lag_24h_log"])
545
+ X_pred["lag_336h_log"] = X_pred["lag_336h_log"].fillna(X_pred["lag_168h_log"]).fillna(X_pred["lag_24h_log"])
546
  X_pred["rsi_14"] = X_pred["rsi_14"].fillna(50.0)
547
  X_pred["volatility_24h"] = X_pred["volatility_24h"].fillna(0.0)
548
  X_pred["volume_ratio"] = X_pred["volume_ratio"].fillna(1.0)
549
  X_pred["ma_24h_ratio"] = X_pred["ma_24h_ratio"].fillna(0.0)
550
+ X_pred["ma_168h_ratio"] = X_pred["ma_168h_ratio"].fillna(X_pred["ma_24h_ratio"]).fillna(0.0)
551
  X_pred["city_price_deviation"] = X_pred["city_price_deviation"].fillna(0.0)
552
+ X_pred["spread_pct"] = X_pred.get("spread_pct", pd.Series(0.0, index=X_pred.index)).fillna(0.0)
553
+ X_pred["buy_sell_ratio"] = X_pred.get("buy_sell_ratio", pd.Series(1.0, index=X_pred.index)).fillna(1.0)
554
 
555
  X_ready = X_pred[FEATURE_COLS].astype("float64").dropna()
556
  if X_ready.empty:
557
  return pd.DataFrame()
558
 
559
+ probs = model.predict_proba(X_ready)
560
  pred_class = np.argmax(probs, axis=1)
 
 
561
  expected_log = probs[:, 0] * CLASS_DOWN + probs[:, 1] * 0.0 + probs[:, 2] * CLASS_UP
562
  clipped_log = np.clip(expected_log, CLASS_DOWN, CLASS_UP)
563
  pred_return_pct = (np.exp(clipped_log) - 1) * 100
564
 
565
+ keep_cols = ["item_id", "city", "quality", "avg_price", "item_count",
566
+ "volatility_24h", "rsi_14", "volume_ratio", "price_pct_7d",
567
+ "is_downtrend", "chg_6h", "chg_24h", "chg_168h",
568
+ "spread_pct", "buy_sell_ratio"]
569
  keep_cols = [c for c in keep_cols if c in X_pred.columns]
570
  result = X_pred.loc[X_ready.index, keep_cols].copy()
571
  result["pred_class"] = pred_class
 
576
  result["predicted_return_pct"] = pred_return_pct
577
  result["predicted_price_24h"] = result["avg_price"] * np.exp(clipped_log)
578
  result["confidence"] = probs.max(axis=1)
 
579
  return result
580
 
581
 
582
  # ---------------------------------------------------------------------------
583
+ # DİP SİNYALİ
584
  # ---------------------------------------------------------------------------
585
 
586
 
587
  def compute_dip_signal(row: pd.Series) -> Tuple[str, str]:
588
+ reasons, passed = [], 0
 
589
 
590
  rsi = row.get("rsi_14", 50)
591
  if pd.notna(rsi) and rsi < DIP_RSI_MAX:
 
600
  vr = row.get("volume_ratio", 1.0)
601
  if pd.notna(vr) and vr >= DIP_MIN_VOLUME_RATIO:
602
  passed += 1
603
+ reasons.append(f"H×{vr:.1f}")
604
 
605
  pred_pct = row.get("predicted_return_pct", 0)
606
  if pd.notna(pred_pct) and pred_pct >= DIP_MIN_PREDICTED_PCT:
607
  passed += 1
608
+ reasons.append(f"M={pred_pct:+.1f}%")
609
 
610
+ spread_pct = row.get("spread_pct", np.nan)
611
+ if pd.notna(spread_pct) and abs(spread_pct) < DIP_MAX_SPREAD_PCT:
 
612
  passed += 1
613
+ reasons.append(f"Sp={spread_pct*100:.1f}%")
614
 
615
  is_down = row.get("is_downtrend", 0)
616
  if not is_down or (pd.notna(pred_pct) and pred_pct > 2):
617
  passed += 1
618
+ reasons.append("Trend✓")
619
 
620
  if passed >= 5:
621
+ sig = "🟢 GÜÇLÜ AL"
622
  elif passed == 4:
623
+ sig = "🟡 ZAYIF AL"
624
  elif passed == 3 and pd.notna(rsi) and rsi < 45:
625
+ sig = "🔵 İZLE"
626
  elif is_down and pd.notna(pred_pct) and pred_pct < -2:
627
+ sig = "🔴 ALMA"
628
  else:
629
+ sig = "⚪ BEKLE"
630
 
631
+ return sig, " · ".join(reasons) if reasons else "—"
632
 
633
 
634
  # ---------------------------------------------------------------------------
635
+ # BACKTEST
636
  # ---------------------------------------------------------------------------
637
 
638
 
 
648
 
649
  test_size = len(unique_times) // (n_splits + 1)
650
  if test_size < 5:
651
+ return {"error": "Test boyutu çok küçük."}
652
 
653
+ results, all_predictions = [], []
 
654
 
655
  for i in range(n_splits):
656
  test_start_idx = test_size * (i + 1) + 24
 
673
  X_train, y_train, _ = prepare_model_data(train_df)
674
  X_test, y_test, meta_test = prepare_model_data(test_df)
675
 
676
+ if len(X_train) < 30 or len(X_test) < 3 or y_train.nunique() < 3:
 
 
 
677
  continue
678
 
679
  model = lgb.LGBMClassifier(
 
687
 
688
  preds_class = model.predict(X_test)
689
  probs = model.predict_proba(X_test)
 
690
 
691
  accuracy = accuracy_score(y_test, preds_class)
692
 
 
693
  y_dir = np.where(y_test == 2, 1, np.where(y_test == 0, 0, -1))
694
  p_dir = np.where(preds_class == 2, 1, np.where(preds_class == 0, 0, -1))
695
  mask = (y_dir >= 0) & (p_dir >= 0)
696
  direction_acc = float(np.mean(y_dir[mask] == p_dir[mask])) if mask.sum() > 0 else 0.5
697
 
 
698
  baseline_acc = float((y_test == 1).mean())
699
 
700
  results.append({
 
709
 
710
  test_meta = meta_test.copy()
711
  test_meta["predicted_class"] = preds_class
712
+ test_meta["prob_up"] = probs[:, 2]
713
  all_predictions.append(test_meta)
714
 
715
  if not results:
716
  return {"error": "Backtest tamamlanamadı."}
717
 
718
+ simulation = {"total_trades": 0, "win_rate_pct": 0.0, "avg_net_return_pct": 0.0,
719
+ "profitable_trades": 0, "avg_confidence_pct": 0.0}
720
  if all_predictions:
721
  sim_df = pd.concat(all_predictions).dropna(subset=["target_log_return_24h"])
722
+ # Sadece YUKARI tahmini + yüksek güven
723
+ buys = sim_df[(sim_df["predicted_class"] == 2) & (sim_df["prob_up"] > 0.6)].copy()
724
+ if len(buys) > 0:
725
+ buys["net_return"] = buys["target_log_return_24h"] - TOTAL_TRADE_COST
726
  simulation = {
727
+ "total_trades": int(len(buys)),
728
+ "win_rate_pct": round(float((buys["net_return"] > 0).mean() * 100), 2),
729
+ "avg_net_return_pct": round(float(buys["net_return"].mean() * 100), 4),
730
+ "profitable_trades": int((buys["net_return"] > 0).sum()),
731
+ "avg_confidence_pct": round(float(buys["prob_up"].mean() * 100), 1),
732
  }
733
 
734
  return {"results": results, "simulation": simulation, "n_splits_completed": len(results)}
735
 
736
 
737
  # ---------------------------------------------------------------------------
738
+ # PIPELINE
739
  # ---------------------------------------------------------------------------
740
 
741
 
 
746
  self.featured_df = pd.DataFrame()
747
  self.model = None
748
  self.last_update = None
749
+ self.status_message = "Veri yüklenmedi."
750
  self._backtest_cache = None
751
  self._diagnostics = {}
752
 
 
754
  try:
755
  self.model = joblib.load(MODEL_PATH)
756
  self.status_message = "Kayıtlı model yüklendi."
757
+ except Exception:
758
+ pass
759
 
760
  def fetch_data(self, item_ids=None, cities=None):
761
  item_ids = item_ids or ITEM_IDS
762
  cities = cities or CITIES
763
 
764
+ self.status_message = "Public API'den veri çekiliyor (5 kalite)..."
765
  ok, msg = check_api_health()
766
  if not ok:
767
  self.status_message = f"API erişilemiyor: {msg}"
 
771
  hist = fetch_history(item_ids, cities, time_scale=HISTORY_TIME_SCALE)
772
  prices = fetch_current_prices(item_ids, cities)
773
  except Exception as e:
774
+ self.status_message = f"API hatası: {e}"
775
  return
776
 
777
  if hist.empty:
 
780
 
781
  self.history_df = clean_market_data(hist)
782
  self.prices_df = prices
783
+ n_qualities = self.history_df["quality"].nunique() if "quality" in self.history_df.columns else 1
784
+ self.status_message = f"✓ {len(self.history_df)} satır tarihsel ({n_qualities} kalite), {len(self.prices_df)} anlık."
785
 
786
  save_to_csv(self.history_df, "history")
787
  save_to_csv(self.prices_df, "prices")
788
 
789
  self.featured_df = build_features(self.history_df)
790
+ self.featured_df = enrich_spread_features(self.featured_df, self.prices_df)
791
  self._backtest_cache = None
792
  self._update_diagnostics()
793
 
 
796
  if df.empty:
797
  self._diagnostics = {}
798
  return
 
799
  n_with_target = int(df["target_class"].notna().sum()) if "target_class" in df.columns else 0
800
+ class_dist = df["target_class"].value_counts().to_dict() if n_with_target > 0 else {}
 
 
801
  self._diagnostics = {
802
+ "n_total": len(df),
803
  "n_with_target": n_with_target,
804
  "class_dist": class_dist,
805
+ "n_qualities": int(df["quality"].nunique()) if "quality" in df.columns else 1,
806
  }
807
+ print(f"[TANI] total={len(df)}, hedefli={n_with_target}, sınıf={class_dist}")
808
 
809
  def train_model(self):
810
  if self.featured_df.empty:
811
+ self.status_message = "Önce veri çekin."
812
  return False
813
 
814
  diag = self._diagnostics or {}
815
+ if diag.get("n_with_target", 0) < MIN_TRAIN_SAMPLES:
816
+ self.status_message = f"Yetersiz örnek: {diag.get('n_with_target', 0)}"
 
 
817
  return False
818
 
819
  class_dist = diag.get("class_dist", {})
820
  if len(class_dist) < 3 or min(class_dist.values()) < 10:
821
+ self.status_message = f"Sınıf dengesiz: {class_dist}"
822
  return False
823
 
824
  X, y, _ = prepare_model_data(self.featured_df)
 
831
  self.status_message = "Model eğitimi başarısız."
832
  return False
833
 
834
+ self.status_message = f"✓ Model eğitildi ({len(X)} örnek)."
835
 
836
  if _HAS_JOBLIB:
837
  try:
 
846
  if self.featured_df.empty or self.model is None:
847
  return pd.DataFrame()
848
 
849
+ latest = self.featured_df.sort_values("timestamp").groupby(
850
+ ["item_id", "city", "quality"]
851
+ ).tail(1).copy()
852
+
853
  latest["item_code"] = latest["item_id"].astype("category").cat.codes
854
  latest["city_code"] = latest["city"].astype("category").cat.codes
855
+ if "qual_code" not in latest.columns:
856
+ latest["qual_code"] = latest.get("quality", 1)
857
 
858
  for col in FEATURE_COLS:
859
  if col not in latest.columns:
860
  latest[col] = 0.0
861
 
862
  preds = predict_24h_returns(self.model, latest)
863
+ if preds.empty:
864
+ return preds
865
+
866
+ if not self.prices_df.empty:
867
+ price_info = self.prices_df[["item_id", "city", "quality",
868
+ "sell_price_min", "buy_price_max"]].copy()
869
+ for col in ["sell_price_min", "buy_price_max"]:
870
+ price_info[col] = pd.to_numeric(price_info[col], errors="coerce").astype("float64")
871
+ preds = preds.merge(price_info, on=["item_id", "city", "quality"], how="left")
872
  preds["spread"] = preds["sell_price_min"] - preds["buy_price_max"]
873
  else:
874
  preds["spread"] = np.nan
875
 
876
+ preds["net_return_pct"] = preds["predicted_return_pct"] - (TOTAL_TRADE_COST * 100)
 
877
 
878
  dip_signals = preds.apply(compute_dip_signal, axis=1, result_type="expand")
879
  preds["Dip Sinyali"] = dip_signals[0]
880
  preds["Sinyal Detayı"] = dip_signals[1]
 
881
  return preds
882
 
883
  def get_backtest(self):
 
903
  })
904
 
905
  df = pipeline.featured_df
906
+ cd = diag.get("class_dist", {})
907
  metrics = {
908
  "Veri Kaynağı": "Public AODP API (europe)",
909
+ "Görev": "3-sınıf sınıflandırma",
910
+ "Kalite Sayısı": diag.get("n_qualities", 1),
911
  "Toplam Gözlem": len(df),
912
  "Hedefli Satır": diag.get("n_with_target", 0),
913
+ "Sınıf AŞAĞI": cd.get(0, 0),
914
+ "Sınıf SABİT": cd.get(1, 0),
915
+ "Sınıf YUKARI": cd.get(2, 0),
916
  "Tarih Aralığı": f"{df['timestamp'].min().date()} - {df['timestamp'].max().date()}",
917
+ "Model": "Eğitildi" if pipeline.model else "Eğitilmedi",
 
918
  "Son Güncelleme": pipeline.last_update.strftime("%Y-%m-%d %H:%M") if pipeline.last_update else "—",
919
  }
920
  return pd.DataFrame(list(metrics.items()), columns=["Metrik", "Değer"])
 
924
  if pipeline.featured_df.empty:
925
  return pd.DataFrame()
926
 
927
+ latest = pipeline.featured_df.sort_values("timestamp").groupby(
928
+ ["item_id", "city", "quality"]
929
+ ).tail(1).copy()
930
 
931
  if not pipeline.prices_df.empty:
932
+ price_info = pipeline.prices_df[["item_id", "city", "quality",
933
+ "sell_price_min", "buy_price_max"]].copy()
934
+ for col in ["sell_price_min", "buy_price_max"]:
935
+ price_info[col] = pd.to_numeric(price_info[col], errors="coerce").astype("float64")
936
+ latest = latest.merge(price_info, on=["item_id", "city", "quality"], how="left")
937
  else:
938
  latest["sell_price_min"] = np.nan
939
  latest["buy_price_max"] = np.nan
940
 
941
  latest["Ürün"] = latest["item_id"].apply(item_display_name)
942
+ latest["Kalite"] = latest["quality"].apply(quality_name)
943
  latest["Şehir"] = latest["city"]
944
  latest["Mevcut Fiyat"] = latest["avg_price"].round(0)
945
  latest["Hacim"] = latest["item_count"].fillna(0).astype(int)
946
  latest["Spread"] = (latest["sell_price_min"] - latest["buy_price_max"]).round(0)
947
  latest["RSI"] = latest["rsi_14"].round(0)
948
+ latest["7g%"] = (latest["price_pct_7d"] * 100).round(0)
949
+ latest["Trend"] = np.where(latest["is_downtrend"] == 1, "↓", "→")
950
  latest["Likidite"] = pd.cut(latest["item_count"], bins=[-1, 10, 50, 200, np.inf],
951
  labels=["Düşük", "Orta", "İyi", "Yüksek"]).astype(str)
952
 
953
+ cols = ["Ürün", "Kalite", "Şehir", "Mevcut Fiyat", "Hacim", "Spread",
954
+ "RSI", "7g%", "Trend", "Likidite"]
955
+ return latest[cols].sort_values("Hacim", ascending=False).head(100)
956
 
957
 
958
  def get_prediction_table():
 
962
 
963
  preds = preds.copy()
964
  preds["Ürün"] = preds["item_id"].apply(item_display_name)
965
+ preds["Kalite"] = preds["quality"].apply(quality_name)
966
  preds["Şehir"] = preds["city"]
967
  preds["Mevcut Fiyat"] = preds["avg_price"].round(0)
968
+ preds["Tahmin"] = preds["pred_class"].map({0: "AŞAĞI", 1: "SABİT", 2: "YUKARI"})
969
+ preds["P(↑)"] = (preds["prob_up"] * 100).round(0).astype(int).astype(str) + "%"
 
 
970
  preds["Güvenilirlik"] = (preds["confidence"] * 100).round(0).astype(int).astype(str) + "%"
971
  preds["Tahmini Değişim %"] = preds["predicted_return_pct"].round(2)
972
  preds["Net Getiri %"] = preds["net_return_pct"].round(2)
973
 
974
+ cols = ["Ürün", "Kalite", "Şehir", "Mevcut Fiyat", "Tahmin",
975
+ "P(↑)", "Güvenilirlik", "Tahmini Değişim %", "Net Getiri %", "Dip Sinyali"]
976
  return preds[cols].sort_values("Güvenilirlik", ascending=False)
977
 
978
 
979
  def get_dip_alim_table():
980
  preds = pipeline.get_latest_predictions()
981
  if preds.empty:
982
+ return pd.DataFrame({"Bilgi": ["Tahmin yok."]})
 
 
983
 
984
+ dip = preds[preds["Dip Sinyali"].isin(["🟢 GÜÇLÜ AL", "🟡 ZAYIF AL", "🔵 İZLE"])].copy()
985
+ if dip.empty:
986
  return pd.DataFrame({"Bilgi": ["Şu anda dip alım sinyali yok."]})
987
 
988
+ order = {"🟢 GÜÇLÜ AL": 0, "🟡 ZAYIF AL": 1, "🔵 İZLE": 2}
989
+ dip["_r"] = dip["Dip Sinyali"].map(order)
990
 
991
+ dip["Ürün"] = dip["item_id"].apply(item_display_name)
992
+ dip["Kalite"] = dip["quality"].apply(quality_name)
993
+ dip["Şehir"] = dip["city"]
994
+ dip["Mevcut Fiyat"] = dip["avg_price"].round(0)
995
+ dip["RSI"] = dip["rsi_14"].round(0)
996
+ dip["7g%"] = (dip["price_pct_7d"] * 100).round(0)
997
+ dip["P(↑)"] = (dip["prob_up"] * 100).round(0).astype(int).astype(str) + "%"
998
+ dip["Net Getiri %"] = dip["net_return_pct"].round(2)
 
 
999
 
1000
+ dip = dip.sort_values(["_r", "confidence"], ascending=[True, False])
1001
+ cols = ["Dip Sinyali", "Ürün", "Kalite", "Şehir", "Mevcut Fiyat",
1002
+ "RSI", "7g%", "P(↑)", "Net Getiri %", "Sinyal Detayı"]
1003
+ return dip[cols].head(50)
 
1004
 
1005
 
1006
  def get_arbitrage_table():
1007
  if pipeline.prices_df.empty:
1008
+ return pd.DataFrame({"Bilgi": ["Fiyat yok."]})
1009
 
1010
  prices = pipeline.prices_df.copy()
1011
+ if "quality" not in prices.columns:
1012
+ prices["quality"] = 1
1013
+ prices["quality"] = pd.to_numeric(prices["quality"], errors="coerce").fillna(1).astype(int)
1014
+ for c in ["sell_price_min", "buy_price_max"]:
1015
+ prices[c] = pd.to_numeric(prices[c], errors="coerce").astype("float64")
1016
+
1017
  prices = prices[prices["sell_price_min"] > 0]
1018
  if prices.empty:
1019
  return pd.DataFrame({"Bilgi": ["Geçerli fiyat yok."]})
1020
 
1021
+ # Kalite bazlı arbitraj — T4.0 ile T4.2 karıştırılmıyor
1022
+ buy = prices.loc[prices.groupby(["item_id", "quality"])["sell_price_min"].idxmin()][
1023
+ ["item_id", "quality", "city", "sell_price_min"]
1024
+ ].rename(columns={"city": "buy_city", "sell_price_min": "buy_price"})
1025
+
1026
+ sell = prices.loc[prices.groupby(["item_id", "quality"])["buy_price_max"].idxmax()][
1027
+ ["item_id", "quality", "city", "buy_price_max"]
1028
+ ].rename(columns={"city": "sell_city", "buy_price_max": "sell_price"})
1029
 
1030
+ arb = buy.merge(sell, on=["item_id", "quality"])
1031
  arb = arb[arb["sell_price"] > arb["buy_price"]].copy()
1032
  if arb.empty:
1033
+ return pd.DataFrame({"Bilgi": ["Arbitraj yok."]})
1034
 
1035
+ arb["brüt"] = arb["sell_price"] - arb["buy_price"]
1036
+ arb["maliyet"] = arb["buy_price"] * SETUP_FEE_RATE + arb["sell_price"] * (SETUP_FEE_RATE + TRANSACTION_TAX_RATE)
1037
+ arb["net_kâr"] = arb["brüt"] - arb["maliyet"]
1038
+ arb["marj_%"] = (arb["net_kâr"] / arb["buy_price"]) * 100
1039
+ arb = arb[arb["net_kâr"] > 0].sort_values("marj_%", ascending=False)
1040
 
1041
  if arb.empty:
1042
+ return pd.DataFrame({"Bilgi": ["Kârlı arbitraj yok."]})
1043
 
1044
  arb["Ürün"] = arb["item_id"].apply(item_display_name)
1045
+ arb["Kalite"] = arb["quality"].apply(quality_name)
1046
  arb["Alış Şehri"] = arb["buy_city"]
1047
  arb["Satış Şehri"] = arb["sell_city"]
1048
+ arb["Alış"] = arb["buy_price"].round(0)
1049
+ arb["Satış"] = arb["sell_price"].round(0)
1050
  arb["Net Kâr"] = arb["net_kâr"].round(0)
1051
+ arb["Marj %"] = arb["marj_%"].round(1)
1052
 
1053
+ cols = ["Ürün", "Kalite", "Alış Şehri", "Satış Şehri", "Alış", "Satış", "Net Kâr", "Marj %"]
1054
+ return arb[cols].head(50)
1055
 
1056
 
1057
  def get_model_performance_plot():
 
1063
  "Önem": pipeline.model.feature_importances_,
1064
  }).sort_values("Önem", ascending=True).tail(20)
1065
  fig = px.bar(df_imp, x="Önem", y="Özellik", orientation="h",
1066
+ title="Feature Importance", template="plotly_dark")
1067
  fig.update_layout(height=600)
1068
  return fig
1069
  except Exception as e:
 
1074
  bt = pipeline.get_backtest()
1075
  if "error" in bt:
1076
  return (pd.DataFrame({"Bilgi": [bt["error"]]}),
1077
+ go.Figure().update_layout(title="Hata", template="plotly_dark"),
1078
  pd.DataFrame())
1079
 
1080
+ rdf = pd.DataFrame(bt["results"])
 
1081
  fig = go.Figure()
1082
+ fig.add_trace(go.Bar(name="Model Acc", x=rdf["split"], y=rdf["accuracy"], marker_color="cyan"))
1083
+ fig.add_trace(go.Bar(name="Baseline", x=rdf["split"], y=rdf["baseline_accuracy"], marker_color="gray"))
1084
+ fig.update_layout(title="Accuracy vs Baseline", xaxis_title="Split",
1085
  yaxis_title="Accuracy", barmode="group", template="plotly_dark")
1086
 
1087
+ sim = pd.DataFrame([bt["simulation"]]) if bt.get("simulation") else pd.DataFrame()
1088
+ return rdf, fig, sim
1089
 
1090
 
1091
  def list_csv_files() -> pd.DataFrame:
 
1119
  bt_table, bt_fig, sim_table = get_backtest_outputs()
1120
  except Exception:
1121
  bt_table, sim_table = pd.DataFrame(), pd.DataFrame()
1122
+ bt_fig = go.Figure().update_layout(title="Hata", template="plotly_dark")
1123
 
1124
  def _safe(fn, default):
1125
  try:
 
1150
  return (
1151
  _safe(get_dashboard_metrics, pd.DataFrame({"Metrik": ["Durum"], "Değer": ["Hazır"]})),
1152
  pd.DataFrame(), pd.DataFrame(), pd.DataFrame(), pd.DataFrame(),
1153
+ go.Figure().update_layout(title="Yenile", template="plotly_dark"),
1154
  pd.DataFrame(),
1155
  go.Figure().update_layout(title="Veri gerekli", template="plotly_dark"),
1156
  pd.DataFrame(),
 
1159
 
1160
 
1161
  # ---------------------------------------------------------------------------
1162
+ # GRADIO
1163
  # ---------------------------------------------------------------------------
1164
 
1165
 
 
1167
 
1168
  gr.Markdown(
1169
  """
1170
+ # 🦅 GOSHAWK ALBION MARKET AI — v2.0
1171
+ **Kalite ayrımı + uzun vadeli trend + spread feature'ları** | Avrupa Sunucusu
1172
  *LightGBM 3-sınıf sınıflandırma: AŞAĞI / SABİT / YUKARI*
1173
 
1174
+ **⚠️ Risk Uyarısı:** Yatırım tavsiyesi değildir.
1175
  """
1176
  )
1177
 
 
1181
 
1182
  with gr.Tabs():
1183
  with gr.TabItem("📊 Dashboard"):
1184
+ dashboard_table = gr.Dataframe(label="Özet", interactive=False, wrap=True)
1185
  gr.Markdown(
1186
+ "### v2.0 Yenilikleri\n"
1187
+ "- **Kalite ayrımı:** T4.0, T4.1, T4.2 ayrı ürün olarak işlenir\n"
1188
+ "- **7/14 günlük trend:** `lag_168h`, `lag_336h`, `chg_168h`, `chg_336h`\n"
1189
+ "- **Spread feature:** Alış-satış farkı modele girdi\n"
1190
+ "- **Arbitraj:** Kalite bazlı, T4.0 ile T4.2 karışmaz\n"
1191
+ "- **7g yüzdelik konum:** Dip noktası daha net\n\n"
1192
+ "### Sınıflar\n"
1193
+ f"- AŞAĞI: log < {CLASS_DOWN:.2f}\n"
1194
+ f"- SABİT: arada\n"
1195
+ f"- YUKARI: log > {CLASS_UP:.2f}\n\n"
1196
  "### Riskler\n"
1197
+ "- Kalite/enchant hâlâ sınırlı\n"
1198
+ "- Overfit kontrolü yapın"
1199
  )
1200
 
1201
  with gr.TabItem("🎯 DİP ALIM"):
 
1208
  analysis_table = gr.Dataframe(label="Ürün Analizi", interactive=False)
1209
 
1210
  with gr.TabItem("🚚 Arbitraj"):
1211
+ arbitrage_table = gr.Dataframe(label="Arbitraj (Kalite Bazlı)", interactive=False)
1212
+ gr.Markdown(
1213
+ "*T4.0 Normal ile T4.2 Outstanding ayrı hesaplanır. "
1214
+ "Thetford'daki ucuz T4.0 Cape, başka şehirde pahalıysa burada görünür.*"
1215
+ )
1216
 
1217
  with gr.TabItem("📈 Model"):
1218
  performance_plot = gr.Plot(label="Feature Importance")
1219
 
1220
  with gr.TabItem("🧪 Backtest"):
1221
  backtest_table = gr.Dataframe(label="Split Sonuçları", interactive=False)
1222
+ backtest_plot = gr.Plot(label="Accuracy vs Baseline")
1223
  simulation_table = gr.Dataframe(label="Simülasyon", interactive=False)
1224
 
1225
  with gr.TabItem("🗄️ Veri"):
1226
  gr.Markdown(
1227
+ "**API:** europe.albion-online-data.com\n"
1228
+ "**Kaliteler:** 1-5 (Normal→Masterpiece)\n"
1229
+ f"**Maliyet:** %{TOTAL_TRADE_COST*100:.1f}\n"
1230
+ f"**Sınıf sınırı:** ±%{CLASS_UP*100:.0f}"
1231
  )
1232
  csv_files = gr.Dataframe(label="CSV Dosyaları", interactive=False,
1233
  headers=["Dosya", "Boyut (KB)", "Son Değişiklik"])