GoshawkVortexAI commited on
Commit
410df8f
·
verified ·
1 Parent(s): 9fffddb

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -1236
app.py DELETED
@@ -1,1236 +0,0 @@
1
- # app.py
2
- # GOSHAWK ALBION MARKET AI v3.1
3
- # 24h model + 90d hibrit (model varsa model, yoksa trend projeksiyonu)
4
-
5
- import os
6
- import time
7
- import warnings
8
- import datetime as dt
9
- from typing import List, Dict, Tuple, Optional
10
-
11
- import numpy as np
12
- import pandas as pd
13
- import requests
14
- import plotly.graph_objects as go
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
-
22
- try:
23
- import joblib
24
- _HAS_JOBLIB = True
25
- except ImportError:
26
- _HAS_JOBLIB = False
27
-
28
- warnings.filterwarnings("ignore")
29
-
30
- # ---------------------------------------------------------------------------
31
- # SABİTLER
32
- # ---------------------------------------------------------------------------
33
-
34
- API_BASE = "https://europe.albion-online-data.com/api/v2"
35
-
36
- CITIES = [
37
- "Bridgewatch", "Fort Sterling", "Lymhurst",
38
- "Martlock", "Thetford", "Caerleon",
39
- ]
40
-
41
- QUALITY_NAMES = {1: "Normal", 2: "Good", 3: "Outstanding", 4: "Excellent", 5: "Masterpiece"}
42
-
43
- _ITEM_POOL = [
44
- "T4_BAG", "T5_BAG", "T6_BAG", "T7_BAG", "T8_BAG",
45
- "T4_CAPE", "T5_CAPE", "T6_CAPE", "T7_CAPE", "T8_CAPE",
46
- "T4_ARMOR_PLATE_SET1", "T5_ARMOR_PLATE_SET1", "T6_ARMOR_PLATE_SET1",
47
- "T4_ARMOR_LEATHER_SET1", "T5_ARMOR_LEATHER_SET1", "T6_ARMOR_LEATHER_SET1",
48
- "T4_ARMOR_CLOTH_SET1", "T5_ARMOR_CLOTH_SET1", "T6_ARMOR_CLOTH_SET1",
49
- "T4_HEAD_PLATE_SET1", "T5_HEAD_PLATE_SET1", "T6_HEAD_PLATE_SET1",
50
- "T4_HEAD_LEATHER_SET1", "T5_HEAD_LEATHER_SET1", "T6_HEAD_LEATHER_SET1",
51
- "T4_HEAD_CLOTH_SET1", "T5_HEAD_CLOTH_SET1", "T6_HEAD_CLOTH_SET1",
52
- "T4_SHOES_PLATE_SET1", "T5_SHOES_PLATE_SET1", "T6_SHOES_PLATE_SET1",
53
- "T4_SHOES_LEATHER_SET1", "T5_SHOES_LEATHER_SET1", "T6_SHOES_LEATHER_SET1",
54
- "T4_SHOES_CLOTH_SET1", "T5_SHOES_CLOTH_SET1", "T6_SHOES_CLOTH_SET1",
55
- "T4_MAIN_SWORD", "T5_MAIN_SWORD", "T6_MAIN_SWORD",
56
- "T4_MAIN_AXE", "T5_MAIN_AXE", "T6_MAIN_AXE",
57
- "T4_MAIN_BOW", "T5_MAIN_BOW", "T6_MAIN_BOW",
58
- "T4_MAIN_CROSSBOW", "T5_MAIN_CROSSBOW", "T6_MAIN_CROSSBOW",
59
- "T4_MAIN_FIRE", "T5_MAIN_FIRE", "T6_MAIN_FIRE",
60
- "T4_MAIN_FROST", "T5_MAIN_FROST", "T6_MAIN_FROST",
61
- "T4_MAIN_HOLY", "T5_MAIN_HOLY", "T6_MAIN_HOLY",
62
- "T4_MAIN_NATURE", "T5_MAIN_NATURE", "T6_MAIN_NATURE",
63
- "T4_MAIN_CURSED", "T5_MAIN_CURSED", "T6_MAIN_CURSED",
64
- "T4_MAIN_HAMMER", "T5_MAIN_HAMMER", "T6_MAIN_HAMMER",
65
- "T4_MAIN_SPEAR", "T5_MAIN_SPEAR", "T6_MAIN_SPEAR",
66
- "T4_MAIN_MACE", "T5_MAIN_MACE", "T6_MAIN_MACE",
67
- "T4_OFF_SHIELD", "T5_OFF_SHIELD", "T6_OFF_SHIELD",
68
- "T4_OFF_TORCH", "T5_OFF_TORCH", "T6_OFF_TORCH",
69
- "T4_OFF_BOOK", "T5_OFF_BOOK", "T6_OFF_BOOK",
70
- "T4_2H_STAFF", "T5_2H_STAFF", "T6_2H_STAFF",
71
- "T4_2H_BOW", "T5_2H_BOW", "T6_2H_BOW",
72
- "T4_2H_CROSSBOW", "T5_2H_CROSSBOW", "T6_2H_CROSSBOW",
73
- "T4_2H_FIRE", "T5_2H_FIRE", "T6_2H_FIRE",
74
- "T4_2H_FROST", "T5_2H_FROST", "T6_2H_FROST",
75
- "T4_2H_HOLY", "T5_2H_HOLY", "T6_2H_HOLY",
76
- "T4_2H_NATURE", "T5_2H_NATURE", "T6_2H_NATURE",
77
- "T4_2H_CURSED", "T5_2H_CURSED", "T6_2H_CURSED",
78
- "T4_2H_HAMMER", "T5_2H_HAMMER", "T6_2H_HAMMER",
79
- "T4_2H_SPEAR", "T5_2H_SPEAR", "T6_2H_SPEAR",
80
- "T4_2H_MACE", "T5_2H_MACE", "T6_2H_MACE",
81
- ]
82
- ITEM_IDS = list(dict.fromkeys(_ITEM_POOL))[:100]
83
-
84
-
85
- def item_display_name(item_id: str) -> str:
86
- return item_id.replace("_", " ").title()
87
-
88
-
89
- def quality_name(q) -> str:
90
- try:
91
- return QUALITY_NAMES.get(int(q), f"Q{q}")
92
- except Exception:
93
- return f"Q{q}"
94
-
95
-
96
- SETUP_FEE_RATE = 0.025
97
- TRANSACTION_TAX_RATE = 0.08
98
- TOTAL_TRADE_COST = SETUP_FEE_RATE + TRANSACTION_TAX_RATE
99
-
100
- MIN_PROFIT_MARGIN = 0.02
101
- LOW_CONFIDENCE_THRESHOLD = TOTAL_TRADE_COST + MIN_PROFIT_MARGIN
102
-
103
- DATA_DIR = "gos_hawk_data"
104
- os.makedirs(DATA_DIR, exist_ok=True)
105
- MODEL_DIR = os.path.join(DATA_DIR, "models")
106
- os.makedirs(MODEL_DIR, exist_ok=True)
107
- MODEL_PATH_24H = os.path.join(MODEL_DIR, "gos_hawk_24h.pkl")
108
- MODEL_PATH_30D = os.path.join(MODEL_DIR, "gos_hawk_30d.pkl")
109
-
110
- MIN_TRAIN_SAMPLES = 150
111
- REQUEST_TIMEOUT = 60
112
- MAX_RETRIES = 3
113
- RETRY_BACKOFF = 2
114
-
115
- HISTORY_TIME_SCALE = 6
116
-
117
- # 24h model
118
- CLASS_DOWN_24H = -0.03
119
- CLASS_UP_24H = 0.03
120
- TARGET_TOLERANCE_HOURS_24H = 12
121
-
122
- # 30 GÜN model (3 ay yerine 30 gün — veri bulunabilirliği için)
123
- HORIZON_DAYS_30D = 30
124
- CLASS_DOWN_30D = -0.10
125
- CLASS_UP_30D = 0.10
126
- TARGET_TOLERANCE_HOURS_30D = 48
127
-
128
- # Trend projeksiyon (30d veri yoksa fallback)
129
- TREND_PROJECTION_DAYS = 90
130
-
131
- # Dip sinyali
132
- DIP_RSI_MAX = 40.0
133
- DIP_PERCENTILE_MAX = 0.25
134
- DIP_MIN_VOLUME_RATIO = 1.0
135
- DIP_MIN_PREDICTED_PCT = -1.0
136
- DIP_MAX_SPREAD_PCT = 0.15
137
-
138
- FEATURE_COLS_24H = [
139
- "hour", "dayofweek",
140
- "lag_24h_log", "lag_168h_log", "lag_336h_log",
141
- "chg_1h", "chg_6h", "chg_24h", "chg_168h", "chg_336h",
142
- "rsi_14", "volatility_24h",
143
- "volume_ratio",
144
- "ma_24h_ratio", "ma_168h_ratio",
145
- "city_price_deviation",
146
- "spread_pct", "buy_sell_ratio", "qual_code",
147
- "item_code", "city_code",
148
- ]
149
-
150
- FEATURE_COLS_30D = [
151
- "month", "dayofweek",
152
- "lag_168h_log", "lag_336h_log", "lag_720h_log",
153
- "chg_168h", "chg_336h", "chg_720h",
154
- "rsi_14", "volatility_24h", "volatility_168h",
155
- "volume_ratio",
156
- "ma_168h_ratio", "ma_720h_ratio",
157
- "trend_slope_30d",
158
- "city_price_deviation",
159
- "spread_pct", "buy_sell_ratio", "qual_code",
160
- "item_code", "city_code",
161
- ]
162
-
163
- # ---------------------------------------------------------------------------
164
- # PUBLIC API
165
- # ---------------------------------------------------------------------------
166
-
167
-
168
- def safe_request(url: str, params: Optional[dict] = None) -> Optional[list]:
169
- headers = {"User-Agent": "GOSHAWK-ALBION-MARKET-AI/3.1", "Accept": "application/json"}
170
- for attempt in range(MAX_RETRIES):
171
- try:
172
- resp = requests.get(url, params=params, headers=headers, timeout=REQUEST_TIMEOUT)
173
- if resp.status_code == 200:
174
- return resp.json()
175
- elif resp.status_code == 429:
176
- time.sleep(RETRY_BACKOFF * (attempt + 1) * 2)
177
- continue
178
- elif resp.status_code >= 500:
179
- time.sleep(RETRY_BACKOFF * (attempt + 1))
180
- continue
181
- else:
182
- print(f"API: {resp.status_code} - {url}")
183
- return None
184
- except requests.exceptions.Timeout:
185
- print(f"Timeout: {url}")
186
- except requests.exceptions.RequestException as e:
187
- print(f"Hata: {e}")
188
- time.sleep(RETRY_BACKOFF * (attempt + 1))
189
- return None
190
-
191
-
192
- def fetch_current_prices(item_ids: List[str], cities: List[str], qualities: str = "1,2,3") -> pd.DataFrame:
193
- all_data = []
194
- for i in range(0, len(item_ids), 40):
195
- chunk = item_ids[i:i+40]
196
- url = f"{API_BASE}/stats/prices/{','.join(chunk)}.json"
197
- params = {"locations": ",".join(cities), "qualities": qualities}
198
- data = safe_request(url, params)
199
- if data:
200
- all_data.extend(data)
201
- time.sleep(0.4)
202
- if not all_data:
203
- return pd.DataFrame()
204
-
205
- df = pd.DataFrame(all_data)
206
- for col in ["sell_price_min", "sell_price_max", "buy_price_min", "buy_price_max"]:
207
- if col in df.columns:
208
- df[col] = pd.to_numeric(df[col], errors="coerce").astype("float64")
209
- if "quality" in df.columns:
210
- df["quality"] = pd.to_numeric(df["quality"], errors="coerce").fillna(1).astype(int)
211
- return df
212
-
213
-
214
- def fetch_history(item_ids: List[str], cities: List[str], time_scale: int = 6,
215
- qualities: str = "1,2,3") -> pd.DataFrame:
216
- all_records = []
217
- for i in range(0, len(item_ids), 15):
218
- chunk = item_ids[i:i+15]
219
- url = f"{API_BASE}/stats/history/{','.join(chunk)}.json"
220
- params = {"locations": ",".join(cities), "qualities": qualities,
221
- "time-scale": time_scale}
222
- data = safe_request(url, params)
223
- if data:
224
- for entry in data:
225
- item_id = entry.get("item_id")
226
- loc = entry.get("location")
227
- quality = entry.get("quality", 1)
228
- for point in entry.get("data", []):
229
- all_records.append({
230
- "item_id": item_id,
231
- "city": loc,
232
- "quality": quality,
233
- "timestamp": point.get("timestamp"),
234
- "avg_price": point.get("avg_price"),
235
- "item_count": point.get("item_count", 0),
236
- })
237
- time.sleep(0.6)
238
- if not all_records:
239
- return pd.DataFrame()
240
-
241
- df = pd.DataFrame(all_records)
242
- df["timestamp"] = pd.to_datetime(df["timestamp"], errors="coerce")
243
- df["avg_price"] = pd.to_numeric(df["avg_price"], errors="coerce").astype("float64")
244
- df["item_count"] = pd.to_numeric(df["item_count"], errors="coerce").fillna(0).astype("float64")
245
- if "quality" in df.columns:
246
- df["quality"] = pd.to_numeric(df["quality"], errors="coerce").fillna(1).astype(int)
247
- return df
248
-
249
-
250
- def check_api_health() -> Tuple[bool, str]:
251
- try:
252
- url = f"{API_BASE}/stats/prices/T4_BAG.json"
253
- data = safe_request(url, {"locations": "Bridgewatch", "qualities": "1"})
254
- return (True, "OK") if data is not None else (False, "Yanıt yok")
255
- except Exception as e:
256
- return False, str(e)
257
-
258
-
259
- def clean_market_data(df: pd.DataFrame) -> pd.DataFrame:
260
- if df.empty:
261
- return df
262
- df = df.copy()
263
- df["avg_price"] = pd.to_numeric(df["avg_price"], errors="coerce").astype("float64")
264
- df["item_count"] = pd.to_numeric(df["item_count"], errors="coerce").fillna(0).astype("float64")
265
- if "quality" in df.columns:
266
- df["quality"] = pd.to_numeric(df["quality"], errors="coerce").fillna(1).astype(int)
267
- else:
268
- df["quality"] = 1
269
- df = df[df["avg_price"] > 0].copy()
270
- df = df.dropna(subset=["timestamp"]).copy()
271
-
272
- for (item, qual) in df[["item_id", "quality"]].drop_duplicates().itertuples(index=False):
273
- mask = (df["item_id"] == item) & (df["quality"] == qual)
274
- if mask.sum() < 10:
275
- continue
276
- lower = df.loc[mask, "avg_price"].quantile(0.005)
277
- upper = df.loc[mask, "avg_price"].quantile(0.995)
278
- if pd.notna(lower) and pd.notna(upper) and upper > lower:
279
- df.loc[mask, "avg_price"] = df.loc[mask, "avg_price"].clip(lower, upper)
280
-
281
- return df.sort_values(["item_id", "city", "quality", "timestamp"]).reset_index(drop=True)
282
-
283
-
284
- def save_to_csv(df: pd.DataFrame, name: str) -> str:
285
- if df.empty:
286
- return ""
287
- path = os.path.join(DATA_DIR, f"{name}_{dt.datetime.now().strftime('%Y%m%d_%H%M%S')}.csv")
288
- try:
289
- df.to_csv(path, index=False)
290
- except Exception as e:
291
- print(f"CSV hatası: {e}")
292
- return ""
293
- return path
294
-
295
-
296
- # ---------------------------------------------------------------------------
297
- # FEATURES
298
- # ---------------------------------------------------------------------------
299
-
300
-
301
- def compute_rsi(series: pd.Series, period: int = 14) -> pd.Series:
302
- delta = series.diff()
303
- gain = delta.clip(lower=0).rolling(period).mean()
304
- loss = (-delta.clip(upper=0)).rolling(period).mean()
305
- rs = gain / loss.replace(0, np.nan)
306
- return (100 - (100 / (1 + rs))).fillna(50)
307
-
308
-
309
- def _linear_trend_slope(y: np.ndarray) -> float:
310
- if len(y) < 5:
311
- return 0.0
312
- x = np.arange(len(y), dtype=float)
313
- try:
314
- return float(np.polyfit(x, y, 1)[0])
315
- except Exception:
316
- return 0.0
317
-
318
-
319
- def build_features(df: pd.DataFrame) -> pd.DataFrame:
320
- if df.empty:
321
- return df
322
- if "quality" not in df.columns:
323
- df["quality"] = 1
324
-
325
- df = df.sort_values(["item_id", "city", "quality", "timestamp"]).copy()
326
- df["avg_price"] = pd.to_numeric(df["avg_price"], errors="coerce").astype("float64")
327
- df["item_count"] = pd.to_numeric(df["item_count"], errors="coerce").fillna(0).astype("float64")
328
-
329
- df["hour"] = df["timestamp"].dt.hour
330
- df["dayofweek"] = df["timestamp"].dt.dayofweek
331
- df["month"] = df["timestamp"].dt.month
332
- df["is_weekend"] = (df["dayofweek"] >= 5).astype(int)
333
- df["qual_code"] = df["quality"].astype(int)
334
-
335
- group_cols = ["item_id", "city", "quality"]
336
- df["log_price"] = np.log(df["avg_price"].replace(0, np.nan))
337
-
338
- # 24h lag'ler
339
- df["lag_24h_log"] = df.groupby(group_cols)["log_price"].shift(24)
340
- df["lag_168h_log"] = df.groupby(group_cols)["log_price"].shift(168)
341
- df["lag_336h_log"] = df.groupby(group_cols)["log_price"].shift(336)
342
-
343
- # 30d lag'ler
344
- df["lag_720h_log"] = df.groupby(group_cols)["log_price"].shift(720)
345
- df["lag_1440h_log"] = df.groupby(group_cols)["log_price"].shift(1440)
346
-
347
- for chg in [1, 6, 24, 168, 336, 720, 1440]:
348
- df[f"chg_{chg}h"] = df.groupby(group_cols)["log_price"].shift(chg)
349
-
350
- df["ma_24h_raw"] = df.groupby(group_cols)["avg_price"].transform(
351
- lambda x: x.rolling(24, min_periods=6).mean()
352
- )
353
- df["ma_24h_ratio"] = df["ma_24h_raw"] / df["avg_price"].replace(0, np.nan) - 1.0
354
-
355
- df["ma_168h_raw"] = df.groupby(group_cols)["avg_price"].transform(
356
- lambda x: x.rolling(168, min_periods=24).mean()
357
- )
358
- df["ma_168h_ratio"] = df["ma_168h_raw"] / df["avg_price"].replace(0, np.nan) - 1.0
359
-
360
- df["ma_720h_raw"] = df.groupby(group_cols)["avg_price"].transform(
361
- lambda x: x.rolling(720, min_periods=72).mean()
362
- )
363
- df["ma_720h_ratio"] = df["ma_720h_raw"] / df["avg_price"].replace(0, np.nan) - 1.0
364
-
365
- df["rsi_14"] = df.groupby(group_cols)["avg_price"].transform(lambda x: compute_rsi(x, 14))
366
- df["volatility_24h"] = df.groupby(group_cols)["log_price"].transform(
367
- lambda x: x.rolling(24, min_periods=6).std()
368
- )
369
- df["volatility_168h"] = df.groupby(group_cols)["log_price"].transform(
370
- lambda x: x.rolling(168, min_periods=24).std()
371
- )
372
- df["volume_ma_24h"] = df.groupby(group_cols)["item_count"].transform(
373
- lambda x: x.rolling(24, min_periods=6).mean()
374
- )
375
- df["volume_ratio"] = df["item_count"] / df["volume_ma_24h"].replace(0, np.nan)
376
-
377
- df["trend_slope_30d"] = df.groupby(group_cols)["log_price"].transform(
378
- lambda x: x.rolling(720, min_periods=72).apply(_linear_trend_slope, raw=True)
379
- )
380
-
381
- city_avg = df.groupby(["item_id", "quality", "timestamp"])["avg_price"].transform("mean")
382
- df["city_price_deviation"] = (df["avg_price"] - city_avg) / city_avg.replace(0, np.nan)
383
-
384
- df["price_pct_7d"] = df.groupby(group_cols)["avg_price"].transform(
385
- lambda x: x.rolling(168, min_periods=24).apply(
386
- lambda arr: (arr[-1] - np.min(arr)) / (np.max(arr) - np.min(arr) + 1e-9),
387
- raw=True
388
- )
389
- )
390
- df["is_downtrend"] = ((df["chg_6h"] < 0) & (df["chg_24h"] < 0)).astype(int)
391
-
392
- df = _compute_targets(df)
393
- return df
394
-
395
-
396
- def _compute_targets(df: pd.DataFrame) -> pd.DataFrame:
397
- if df.empty or "log_price" not in df.columns:
398
- for c in ["target_log_return_24h", "target_class_24h",
399
- "target_log_return_30d", "target_class_30d"]:
400
- df[c] = np.nan
401
- return df
402
-
403
- df["target_log_return_24h"] = np.nan
404
- df["target_log_return_30d"] = np.nan
405
-
406
- group_cols = ["item_id", "city", "quality"]
407
-
408
- for _, g in df.groupby(group_cols):
409
- if len(g) < 5:
410
- continue
411
- g = g.sort_values("timestamp")
412
- left = pd.DataFrame({
413
- "timestamp": g["timestamp"].values,
414
- "log_price_now": g["log_price"].values,
415
- "idx": g.index.values,
416
- })
417
- right = pd.DataFrame({
418
- "timestamp": g["timestamp"].values,
419
- "log_price_future": g["log_price"].values,
420
- }).sort_values("timestamp")
421
-
422
- # 24h
423
- left["target_t_24h"] = left["timestamp"] + pd.Timedelta(hours=24)
424
- m24 = pd.merge_asof(
425
- left.sort_values("target_t_24h"),
426
- right.sort_values("timestamp"),
427
- left_on="target_t_24h", right_on="timestamp",
428
- direction="nearest", tolerance=pd.Timedelta(hours=TARGET_TOLERANCE_HOURS_24H),
429
- suffixes=("", "_f"),
430
- )
431
- df.loc[m24["idx"].values, "target_log_return_24h"] = (
432
- m24["log_price_future"].values - m24["log_price_now"].values
433
- )
434
-
435
- # 30g
436
- left["target_t_30d"] = left["timestamp"] + pd.Timedelta(days=HORIZON_DAYS_30D)
437
- m30 = pd.merge_asof(
438
- left.sort_values("target_t_30d"),
439
- right.sort_values("timestamp"),
440
- left_on="target_t_30d", right_on="timestamp",
441
- direction="nearest", tolerance=pd.Timedelta(hours=TARGET_TOLERANCE_HOURS_30D),
442
- suffixes=("", "_f"),
443
- )
444
- df.loc[m30["idx"].values, "target_log_return_30d"] = (
445
- m30["log_price_future"].values - m30["log_price_now"].values
446
- )
447
-
448
- t24 = df["target_log_return_24h"]
449
- df["target_class_24h"] = np.where(t24 < CLASS_DOWN_24H, 0, np.where(t24 > CLASS_UP_24H, 2, 1))
450
- df.loc[t24.isna(), "target_class_24h"] = np.nan
451
-
452
- t30 = df["target_log_return_30d"]
453
- df["target_class_30d"] = np.where(t30 < CLASS_DOWN_30D, 0, np.where(t30 > CLASS_UP_30D, 2, 1))
454
- df.loc[t30.isna(), "target_class_30d"] = np.nan
455
-
456
- return df
457
-
458
-
459
- def enrich_spread_features(featured_df: pd.DataFrame, prices_df: pd.DataFrame) -> pd.DataFrame:
460
- if featured_df.empty or prices_df.empty:
461
- featured_df["spread_pct"] = 0.0
462
- featured_df["buy_sell_ratio"] = 1.0
463
- return featured_df
464
-
465
- prices = prices_df.copy()
466
- if "quality" not in prices.columns:
467
- prices["quality"] = 1
468
- prices["quality"] = pd.to_numeric(prices["quality"], errors="coerce").fillna(1).astype(int)
469
- for c in ["sell_price_min", "buy_price_max"]:
470
- prices[c] = pd.to_numeric(prices[c], errors="coerce").astype("float64")
471
-
472
- prices["spread_pct"] = np.where(
473
- prices["buy_price_max"] > 0,
474
- (prices["sell_price_min"] - prices["buy_price_max"]) / prices["buy_price_max"], np.nan
475
- )
476
- prices["buy_sell_ratio"] = np.where(
477
- prices["sell_price_min"] > 0,
478
- prices["buy_price_max"] / prices["sell_price_min"], np.nan
479
- )
480
-
481
- pf = prices[["item_id", "city", "quality", "spread_pct", "buy_sell_ratio"]].drop_duplicates(
482
- subset=["item_id", "city", "quality"]
483
- )
484
- out = featured_df.merge(pf, on=["item_id", "city", "quality"], how="left")
485
- out["spread_pct"] = out["spread_pct"].fillna(0.0)
486
- out["buy_sell_ratio"] = out["buy_sell_ratio"].fillna(1.0)
487
- return out
488
-
489
-
490
- # ---------------------------------------------------------------------------
491
- # MODEL EĞİTİMİ
492
- # ---------------------------------------------------------------------------
493
-
494
-
495
- def _prepare_model_df(featured_df: pd.DataFrame, horizon: str) -> Tuple[pd.DataFrame, pd.Series, pd.DataFrame]:
496
- if featured_df.empty:
497
- return pd.DataFrame(), pd.Series(dtype=int), pd.DataFrame()
498
-
499
- target_col = f"target_class_{horizon}"
500
- if target_col not in featured_df.columns:
501
- return pd.DataFrame(), pd.Series(dtype=int), pd.DataFrame()
502
-
503
- df = featured_df.copy()
504
- df["item_code"] = df["item_id"].astype("category").cat.codes
505
- df["city_code"] = df["city"].astype("category").cat.codes
506
- if "qual_code" not in df.columns:
507
- df["qual_code"] = df.get("quality", 1)
508
-
509
- df = df.dropna(subset=[target_col]).copy()
510
- df[target_col] = df[target_col].astype(int)
511
-
512
- fill_chg = ["chg_1h", "chg_6h", "chg_24h", "chg_168h", "chg_336h", "chg_720h", "chg_1440h"]
513
- for c in fill_chg:
514
- if c in df.columns:
515
- df[c] = df[c].fillna(0.0)
516
-
517
- for c in ["lag_24h_log", "lag_168h_log", "lag_336h_log", "lag_720h_log", "lag_1440h_log"]:
518
- if c in df.columns:
519
- df[c] = df[c].fillna(df["log_price"])
520
-
521
- df["rsi_14"] = df["rsi_14"].fillna(50.0)
522
- for c in ["volatility_24h", "volatility_168h", "volume_ratio", "ma_24h_ratio",
523
- "ma_168h_ratio", "ma_720h_ratio", "trend_slope_30d", "city_price_deviation",
524
- "spread_pct", "buy_sell_ratio"]:
525
- if c in df.columns:
526
- df[c] = df[c].fillna(0.0)
527
-
528
- fcols = FEATURE_COLS_24H if horizon == "24h" else FEATURE_COLS_30D
529
- fcols = [c for c in fcols if c in df.columns]
530
- model_df = df.dropna(subset=fcols + [target_col]).copy()
531
- if model_df.empty:
532
- return pd.DataFrame(), pd.Series(dtype=int), pd.DataFrame()
533
-
534
- X = model_df[fcols].astype("float64")
535
- y = model_df[target_col]
536
- meta = model_df[["item_id", "city", "quality", "timestamp", "avg_price"]].copy()
537
- return X, y, meta
538
-
539
-
540
- def train_model(X: pd.DataFrame, y: pd.Series, horizon: str) -> Optional[lgb.LGBMClassifier]:
541
- if len(X) < MIN_TRAIN_SAMPLES:
542
- return None
543
- cc = y.value_counts().to_dict()
544
- print(f"[{horizon}] sınıf: {cc}")
545
- if len(cc) < 2:
546
- return None
547
- if len(cc) < 3:
548
- print(f"[{horizon}] UYARI: sadece {len(cc)} sınıf var")
549
-
550
- split = int(len(X) * 0.8)
551
- X_tr, X_val = X.iloc[:split], X.iloc[split:]
552
- y_tr, y_val = y.iloc[:split], y.iloc[split:]
553
-
554
- n_est = 300 if horizon == "24h" else 250
555
- model = lgb.LGBMClassifier(
556
- objective="multiclass", num_class=3, metric="multi_logloss",
557
- boosting_type="gbdt",
558
- num_leaves=15, max_depth=5, min_child_samples=50,
559
- learning_rate=0.03, n_estimators=n_est,
560
- feature_fraction=0.8, bagging_fraction=0.8, bagging_freq=5,
561
- reg_alpha=0.5, reg_lambda=1.0,
562
- verbose=-1, random_state=42, n_jobs=-1,
563
- )
564
- try:
565
- model.fit(
566
- X_tr, y_tr, eval_set=[(X_val, y_val)],
567
- callbacks=[lgb.early_stopping(30, verbose=False), lgb.log_evaluation(0)],
568
- )
569
- acc = accuracy_score(y_val, model.predict(X_val))
570
- print(f"[{horizon}] val_accuracy={acc:.4f}")
571
- except Exception as e:
572
- print(f"[{horizon}] eğitim hatası: {e}")
573
- return None
574
- return model
575
-
576
-
577
- def predict(model, latest_features: pd.DataFrame, horizon: str) -> pd.DataFrame:
578
- if latest_features.empty or model is None:
579
- return pd.DataFrame()
580
-
581
- df = latest_features.copy()
582
- df["item_code"] = df["item_id"].astype("category").cat.codes
583
- df["city_code"] = df["city"].astype("category").cat.codes
584
- if "qual_code" not in df.columns:
585
- df["qual_code"] = df.get("quality", 1)
586
-
587
- for c in ["chg_1h", "chg_6h", "chg_24h", "chg_168h", "chg_336h", "chg_720h", "chg_1440h"]:
588
- if c in df.columns:
589
- df[c] = df[c].fillna(0.0)
590
- for c in ["lag_24h_log", "lag_168h_log", "lag_336h_log", "lag_720h_log", "lag_1440h_log"]:
591
- if c in df.columns:
592
- df[c] = df[c].fillna(df["log_price"])
593
- df["rsi_14"] = df["rsi_14"].fillna(50.0)
594
- for c in ["volatility_24h", "volatility_168h", "volume_ratio", "ma_24h_ratio",
595
- "ma_168h_ratio", "ma_720h_ratio", "trend_slope_30d", "city_price_deviation",
596
- "spread_pct", "buy_sell_ratio"]:
597
- if c in df.columns:
598
- df[c] = df[c].fillna(0.0)
599
-
600
- fcols = FEATURE_COLS_24H if horizon == "24h" else FEATURE_COLS_30D
601
- fcols = [c for c in fcols if c in df.columns]
602
- X_ready = df[fcols].astype("float64").dropna()
603
- if X_ready.empty:
604
- return pd.DataFrame()
605
-
606
- probs = model.predict_proba(X_ready)
607
- pred_class = np.argmax(probs, axis=1)
608
-
609
- if horizon == "24h":
610
- exp_log = probs[:, 0] * CLASS_DOWN_24H + probs[:, 2] * CLASS_UP_24H
611
- clipped = np.clip(exp_log, CLASS_DOWN_24H, CLASS_UP_24H)
612
- else:
613
- exp_log = probs[:, 0] * CLASS_DOWN_30D + probs[:, 2] * CLASS_UP_30D
614
- clipped = np.clip(exp_log, CLASS_DOWN_30D, CLASS_UP_30D)
615
-
616
- keep = ["item_id", "city", "quality", "avg_price", "item_count",
617
- "volatility_24h", "rsi_14", "volume_ratio", "price_pct_7d",
618
- "is_downtrend", "chg_6h", "chg_24h", "chg_168h", "trend_slope_30d",
619
- "spread_pct", "buy_sell_ratio"]
620
- keep = [c for c in keep if c in df.columns]
621
- out = df.loc[X_ready.index, keep].copy()
622
- out["pred_class"] = pred_class
623
- out["prob_down"] = probs[:, 0]
624
- out["prob_flat"] = probs[:, 1] if probs.shape[1] > 1 else 0.0
625
- out["prob_up"] = probs[:, 2] if probs.shape[1] > 2 else 0.0
626
- out["predicted_log_return"] = clipped
627
- out["predicted_return_pct"] = (np.exp(clipped) - 1) * 100
628
- out["predicted_price"] = out["avg_price"] * np.exp(clipped)
629
- out["confidence"] = probs.max(axis=1)
630
- return out
631
-
632
-
633
- # ---------------------------------------------------------------------------
634
- # TREND PROJEKSİYONU (30d model yoksa fallback)
635
- # ---------------------------------------------------------------------------
636
-
637
-
638
- def trend_projection_90d(latest: pd.DataFrame) -> pd.DataFrame:
639
- """
640
- 30d model yoksa, trend_slope_30d kullanarak 90 günlük basit projeksiyon.
641
- Formül: fiyat × exp(slope × gün_sayısı)
642
- slope günlük log değişim → 90 günde slope × 90
643
- """
644
- if latest.empty:
645
- return pd.DataFrame()
646
-
647
- df = latest.copy()
648
- df["trend_slope_30d"] = df.get("trend_slope_30d", pd.Series(0.0, index=df.index)).fillna(0.0)
649
-
650
- # slope * 90 gün = 90 günlük log getiri
651
- log_return_90d = df["trend_slope_30d"] * TREND_PROJECTION_DAYS
652
-
653
- # Aşırı projeksiyonu clip'le (90 günde ±%100)
654
- log_return_90d = np.clip(log_return_90d, -1.0, 1.0)
655
-
656
- df["predicted_log_return"] = log_return_90d
657
- df["predicted_return_pct"] = (np.exp(log_return_90d) - 1) * 100
658
- df["predicted_price"] = df["avg_price"] * np.exp(log_return_90d)
659
-
660
- # Güven: trend gücüne göre
661
- df["confidence"] = np.abs(df["trend_slope_30d"]).clip(0, 0.005) / 0.005 * 0.6 + 0.3
662
-
663
- df["pred_class"] = np.where(log_return_90d < -0.05, 0,
664
- np.where(log_return_90d > 0.05, 2, 1))
665
- df["prob_up"] = np.where(log_return_90d > 0.05, 0.6, 0.2)
666
- df["prob_down"] = np.where(log_return_90d < -0.05, 0.6, 0.2)
667
- df["prob_flat"] = 1.0 - df["prob_up"] - df["prob_down"]
668
-
669
- return df
670
-
671
-
672
- # ---------------------------------------------------------------------------
673
- # DİP SİNYALİ
674
- # ---------------------------------------------------------------------------
675
-
676
-
677
- def compute_dip_signal(row: pd.Series) -> Tuple[str, str]:
678
- reasons, passed = [], 0
679
- rsi = row.get("rsi_14", 50)
680
- if pd.notna(rsi) and rsi < DIP_RSI_MAX:
681
- passed += 1; reasons.append(f"RSI={rsi:.0f}")
682
- pct = row.get("price_pct_7d", 0.5)
683
- if pd.notna(pct) and pct < DIP_PERCENTILE_MAX:
684
- passed += 1; reasons.append(f"7g%={pct*100:.0f}")
685
- vr = row.get("volume_ratio", 1.0)
686
- if pd.notna(vr) and vr >= DIP_MIN_VOLUME_RATIO:
687
- passed += 1; reasons.append(f"H×{vr:.1f}")
688
- pp = row.get("predicted_return_pct", 0)
689
- if pd.notna(pp) and pp >= DIP_MIN_PREDICTED_PCT:
690
- passed += 1; reasons.append(f"M={pp:+.1f}%")
691
- sp = row.get("spread_pct", np.nan)
692
- if pd.notna(sp) and abs(sp) < DIP_MAX_SPREAD_PCT:
693
- passed += 1; reasons.append(f"Sp={sp*100:.1f}%")
694
- down = row.get("is_downtrend", 0)
695
- if not down or (pd.notna(pp) and pp > 2):
696
- passed += 1; reasons.append("Trend✓")
697
-
698
- if passed >= 5: sig = "🟢 GÜÇLÜ AL"
699
- elif passed == 4: sig = "🟡 ZAYIF AL"
700
- elif passed == 3 and pd.notna(rsi) and rsi < 45: sig = "🔵 İZLE"
701
- elif down and pd.notna(pp) and pp < -2: sig = "🔴 ALMA"
702
- else: sig = "⚪ BEKLE"
703
- return sig, " · ".join(reasons) if reasons else "—"
704
-
705
-
706
- # ---------------------------------------------------------------------------
707
- # PIPELINE
708
- # ---------------------------------------------------------------------------
709
-
710
-
711
- class DataPipeline:
712
- def __init__(self):
713
- self.history_df = pd.DataFrame()
714
- self.prices_df = pd.DataFrame()
715
- self.featured_df = pd.DataFrame()
716
- self.model_24h = None
717
- self.model_30d = None
718
- self.last_update = None
719
- self.status_message = "Veri yüklenmedi."
720
- self._diagnostics = {}
721
- self._projection_mode = "MODEL" # veya "TREND"
722
-
723
- if _HAS_JOBLIB:
724
- try:
725
- if os.path.exists(MODEL_PATH_24H):
726
- self.model_24h = joblib.load(MODEL_PATH_24H)
727
- if os.path.exists(MODEL_PATH_30D):
728
- self.model_30d = joblib.load(MODEL_PATH_30D)
729
- if self.model_24h or self.model_30d:
730
- self.status_message = "Kayıtlı model(ler) yüklendi."
731
- except Exception:
732
- pass
733
-
734
- def fetch_data(self, item_ids=None, cities=None):
735
- item_ids = item_ids or ITEM_IDS
736
- cities = cities or CITIES
737
-
738
- self.status_message = "Public API'den veri çekiliyor..."
739
- ok, msg = check_api_health()
740
- if not ok:
741
- self.status_message = f"API erişilemiyor: {msg}"
742
- return
743
-
744
- try:
745
- hist = fetch_history(item_ids, cities, time_scale=HISTORY_TIME_SCALE)
746
- prices = fetch_current_prices(item_ids, cities)
747
- except Exception as e:
748
- self.status_message = f"API hatası: {e}"
749
- return
750
-
751
- if hist.empty:
752
- self.status_message = "Tarihsel veri alınamadı."
753
- return
754
-
755
- self.history_df = clean_market_data(hist)
756
- self.prices_df = prices
757
- n_q = self.history_df["quality"].nunique()
758
- self.status_message = f"✓ {len(self.history_df)} satır ({n_q} kalite), {len(self.prices_df)} anlık."
759
-
760
- save_to_csv(self.history_df, "history")
761
- save_to_csv(self.prices_df, "prices")
762
-
763
- self.featured_df = build_features(self.history_df)
764
- self.featured_df = enrich_spread_features(self.featured_df, self.prices_df)
765
- self._update_diagnostics()
766
-
767
- def _update_diagnostics(self):
768
- df = self.featured_df
769
- if df.empty:
770
- self._diagnostics = {}
771
- return
772
- n24 = int(df["target_class_24h"].notna().sum())
773
- n30 = int(df["target_class_30d"].notna().sum())
774
- cd24 = df["target_class_24h"].value_counts().to_dict() if n24 > 0 else {}
775
- cd30 = df["target_class_30d"].value_counts().to_dict() if n30 > 0 else {}
776
- date_range_days = (df["timestamp"].max() - df["timestamp"].min()).days if len(df) > 0 else 0
777
-
778
- self._diagnostics = {
779
- "n_total": len(df),
780
- "n_24h": n24,
781
- "n_30d": n30,
782
- "class_24h": cd24,
783
- "class_30d": cd30,
784
- "date_range_days": date_range_days,
785
- }
786
- print(f"[TANI] total={len(df)}, 24h={n24}, 30d={n30}, gün={date_range_days}")
787
-
788
- def train_all(self):
789
- if self.featured_df.empty:
790
- self.status_message = "Önce veri çekin."
791
- return False
792
-
793
- # 24h
794
- X24, y24, _ = _prepare_model_df(self.featured_df, "24h")
795
- if len(X24) >= MIN_TRAIN_SAMPLES:
796
- self.model_24h = train_model(X24, y24, "24h")
797
- if _HAS_JOBLIB and self.model_24h:
798
- try:
799
- joblib.dump(self.model_24h, MODEL_PATH_24H)
800
- except Exception:
801
- pass
802
-
803
- # 30d
804
- X30, y30, _ = _prepare_model_df(self.featured_df, "30d")
805
- n30 = len(X30)
806
- if n30 >= MIN_TRAIN_SAMPLES:
807
- self.model_30d = train_model(X30, y30, "30d")
808
- if _HAS_JOBLIB and self.model_30d:
809
- try:
810
- joblib.dump(self.model_30d, MODEL_PATH_30D)
811
- except Exception:
812
- pass
813
-
814
- # Projeksiyon modu belirle
815
- if self.model_30d is not None:
816
- self._projection_mode = "MODEL"
817
- else:
818
- self._projection_mode = "TREND"
819
-
820
- msgs = []
821
- msgs.append("24h ✓" if self.model_24h else "24h ✗")
822
- if self.model_30d:
823
- msgs.append(f"30d ✓ ({n30})")
824
- else:
825
- msgs.append(f"30d ✗ ({n30}) → trend projeksiyonu")
826
- self.status_message = " · ".join(msgs)
827
- self.last_update = dt.datetime.now()
828
- return True
829
-
830
- def get_latest(self) -> pd.DataFrame:
831
- if self.featured_df.empty:
832
- return pd.DataFrame()
833
- return self.featured_df.sort_values("timestamp").groupby(
834
- ["item_id", "city", "quality"]
835
- ).tail(1).copy()
836
-
837
- def predict_24h(self) -> pd.DataFrame:
838
- latest = self.get_latest()
839
- if latest.empty or self.model_24h is None:
840
- return pd.DataFrame()
841
- preds = predict(self.model_24h, latest, "24h")
842
- if preds.empty:
843
- return preds
844
-
845
- if not self.prices_df.empty:
846
- pi = self.prices_df[["item_id", "city", "quality",
847
- "sell_price_min", "buy_price_max"]].copy()
848
- for c in ["sell_price_min", "buy_price_max"]:
849
- pi[c] = pd.to_numeric(pi[c], errors="coerce").astype("float64")
850
- preds = preds.merge(pi, on=["item_id", "city", "quality"], how="left")
851
- preds["spread"] = preds["sell_price_min"] - preds["buy_price_max"]
852
- else:
853
- preds["spread"] = np.nan
854
-
855
- preds["net_return_pct"] = preds["predicted_return_pct"] - (TOTAL_TRADE_COST * 100)
856
- dip = preds.apply(compute_dip_signal, axis=1, result_type="expand")
857
- preds["Dip Sinyali"] = dip[0]
858
- preds["Sinyal Detayı"] = dip[1]
859
- return preds
860
-
861
- def predict_long_horizon(self) -> Tuple[pd.DataFrame, str]:
862
- """30d model varsa model, yoksa trend projeksiyonu."""
863
- latest = self.get_latest()
864
- if latest.empty:
865
- return pd.DataFrame(), "veri_yok"
866
-
867
- if self.model_30d is not None:
868
- preds = predict(self.model_30d, latest, "30d")
869
- return preds, "model_30d"
870
- else:
871
- preds = trend_projection_90d(latest)
872
- return preds, "trend_projection"
873
-
874
-
875
- pipeline = DataPipeline()
876
-
877
-
878
- # ---------------------------------------------------------------------------
879
- # GRADIO YARDIMCILARI
880
- # ---------------------------------------------------------------------------
881
-
882
-
883
- def get_dashboard_metrics():
884
- diag = pipeline._diagnostics
885
- if pipeline.featured_df.empty:
886
- return pd.DataFrame({"Metrik": ["Durum"], "Değer": ["Veri yok."]})
887
-
888
- df = pipeline.featured_df
889
- m = {
890
- "Veri Kaynağı": "Public AODP API",
891
- "Veri Süresi": f"{diag.get('date_range_days', 0)} gün",
892
- "Toplam Gözlem": len(df),
893
- "Hedefli 24h": diag.get("n_24h", 0),
894
- "Hedefli 30d": diag.get("n_30d", 0),
895
- "Sınıf 24h": str(diag.get("class_24h", {})),
896
- "Sınıf 30d": str(diag.get("class_30d", {})),
897
- "24h Model": "Eğitildi" if pipeline.model_24h else "Yok",
898
- "30d Model": "Eğitildi" if pipeline.model_30d else "Yok",
899
- "Uzun Vadeli Mod": pipeline._projection_mode,
900
- "Son Güncelleme": pipeline.last_update.strftime("%Y-%m-%d %H:%M") if pipeline.last_update else "—",
901
- }
902
- return pd.DataFrame(list(m.items()), columns=["Metrik", "Değer"])
903
-
904
-
905
- def get_prediction_24h_table():
906
- preds = pipeline.predict_24h()
907
- if preds.empty:
908
- return pd.DataFrame({"Bilgi": ["24h tahmini yok."]})
909
- preds = preds.copy()
910
- preds["Ürün"] = preds["item_id"].apply(item_display_name)
911
- preds["Kalite"] = preds["quality"].apply(quality_name)
912
- preds["Şehir"] = preds["city"]
913
- preds["Mevcut"] = preds["avg_price"].round(0)
914
- preds["24s"] = preds["predicted_price"].round(0)
915
- preds["Değişim %"] = preds["predicted_return_pct"].round(2)
916
- preds["Sınıf"] = preds["pred_class"].map({0: "↓", 1: "→", 2: "↑"})
917
- preds["P(↑)"] = (preds["prob_up"] * 100).round(0).astype(int).astype(str) + "%"
918
- preds["Güven"] = (preds["confidence"] * 100).round(0).astype(int).astype(str) + "%"
919
- preds["Net %"] = preds["net_return_pct"].round(2)
920
- cols = ["Ürün", "Kalite", "Şehir", "Mevcut", "24s", "Değişim %",
921
- "Sınıf", "P(↑)", "Güven", "Net %", "Dip Sinyali"]
922
- return preds[cols].sort_values("Güven", ascending=False)
923
-
924
-
925
- def get_prediction_long_table():
926
- preds, mode = pipeline.predict_long_horizon()
927
- if preds.empty:
928
- return pd.DataFrame({"Bilgi": ["Uzun vadeli tahmin yok."]}), mode
929
-
930
- preds = preds.copy()
931
- preds["Ürün"] = preds["item_id"].apply(item_display_name)
932
- preds["Kalite"] = preds["quality"].apply(quality_name)
933
- preds["Şehir"] = preds["city"]
934
- preds["Mevcut"] = preds["avg_price"].round(0)
935
-
936
- if mode == "model_30d":
937
- preds["30 Gün Tahmini"] = preds["predicted_price"].round(0)
938
- preds["Değişim %"] = preds["predicted_return_pct"].round(2)
939
- preds["Trend"] = preds["pred_class"].map({0: "↓ AŞAĞI", 1: "→ SABİT", 2: "↑ YUKARI"})
940
- preds["P(↑)"] = (preds["prob_up"] * 100).round(0).astype(int).astype(str) + "%"
941
- preds["Güven"] = (preds["confidence"] * 100).round(0).astype(int).astype(str) + "%"
942
- cols = ["Ürün", "Kalite", "Şehir", "Mevcut", "30 Gün Tahmini",
943
- "Değişim %", "Trend", "P(↑)", "Güven"]
944
- else:
945
- # Trend projeksiyon
946
- preds["90 Gün Tahmini"] = preds["predicted_price"].round(0)
947
- preds["Değişim %"] = preds["predicted_return_pct"].round(2)
948
- preds["Trend"] = preds["pred_class"].map({0: "↓ AŞAĞI", 1: "→ SABİT", 2: "↑ YUKARI"})
949
- preds["Güven"] = (preds["confidence"] * 100).round(0).astype(int).astype(str) + "%"
950
- cols = ["Ürün", "Kalite", "Şehir", "Mevcut", "90 Gün Tahmini",
951
- "Değişim %", "Trend", "Güven"]
952
-
953
- return preds[cols].sort_values("Güven", ascending=False), mode
954
-
955
-
956
- def get_analysis_table():
957
- if pipeline.featured_df.empty:
958
- return pd.DataFrame()
959
-
960
- latest = pipeline.get_latest()
961
- if not pipeline.prices_df.empty:
962
- pi = pipeline.prices_df[["item_id", "city", "quality",
963
- "sell_price_min", "buy_price_max"]].copy()
964
- for c in ["sell_price_min", "buy_price_max"]:
965
- pi[c] = pd.to_numeric(pi[c], errors="coerce").astype("float64")
966
- latest = latest.merge(pi, on=["item_id", "city", "quality"], how="left")
967
- else:
968
- latest["sell_price_min"] = np.nan
969
- latest["buy_price_max"] = np.nan
970
-
971
- latest["Ürün"] = latest["item_id"].apply(item_display_name)
972
- latest["Kalite"] = latest["quality"].apply(quality_name)
973
- latest["Şehir"] = latest["city"]
974
- latest["Fiyat"] = latest["avg_price"].round(0)
975
- latest["Hacim"] = latest["item_count"].fillna(0).astype(int)
976
- latest["Spread"] = (latest["sell_price_min"] - latest["buy_price_max"]).round(0)
977
- latest["RSI"] = latest["rsi_14"].round(0)
978
- latest["7g%"] = (latest["price_pct_7d"] * 100).round(0)
979
- latest["30g Trend"] = latest["trend_slope_30d"].round(4)
980
- cols = ["Ürün", "Kalite", "Şehir", "Fiyat", "Hacim", "Spread",
981
- "RSI", "7g%", "30g Trend"]
982
- return latest[cols].sort_values("Hacim", ascending=False).head(100)
983
-
984
-
985
- def get_dip_table():
986
- preds = pipeline.predict_24h()
987
- if preds.empty:
988
- return pd.DataFrame({"Bilgi": ["Tahmin yok."]})
989
- dip = preds[preds["Dip Sinyali"].isin(["🟢 GÜÇLÜ AL", "🟡 ZAYIF AL", "🔵 İZLE"])].copy()
990
- if dip.empty:
991
- return pd.DataFrame({"Bilgi": ["Dip sinyali yok."]})
992
-
993
- order = {"🟢 GÜÇLÜ AL": 0, "🟡 ZAYIF AL": 1, "🔵 İZLE": 2}
994
- dip["_r"] = dip["Dip Sinyali"].map(order)
995
- dip["Ürün"] = dip["item_id"].apply(item_display_name)
996
- dip["Kalite"] = dip["quality"].apply(quality_name)
997
- dip["Şehir"] = dip["city"]
998
- dip["Mevcut"] = dip["avg_price"].round(0)
999
- dip["RSI"] = dip["rsi_14"].round(0)
1000
- dip["7g%"] = (dip["price_pct_7d"] * 100).round(0)
1001
- dip["P(↑)"] = (dip["prob_up"] * 100).round(0).astype(int).astype(str) + "%"
1002
- dip["Net %"] = dip["net_return_pct"].round(2)
1003
- dip = dip.sort_values(["_r", "confidence"], ascending=[True, False])
1004
- cols = ["Dip Sinyali", "Ürün", "Kalite", "Şehir", "Mevcut",
1005
- "RSI", "7g%", "P(↑)", "Net %", "Sinyal Detayı"]
1006
- return dip[cols].head(50)
1007
-
1008
-
1009
- def get_arbitrage_table():
1010
- if pipeline.prices_df.empty:
1011
- return pd.DataFrame({"Bilgi": ["Fiyat yok."]})
1012
-
1013
- prices = pipeline.prices_df.copy()
1014
- if "quality" not in prices.columns:
1015
- prices["quality"] = 1
1016
- prices["quality"] = pd.to_numeric(prices["quality"], errors="coerce").fillna(1).astype(int)
1017
- for c in ["sell_price_min", "buy_price_max"]:
1018
- prices[c] = pd.to_numeric(prices[c], errors="coerce").astype("float64")
1019
- prices = prices[prices["sell_price_min"] > 0]
1020
- if prices.empty:
1021
- return pd.DataFrame({"Bilgi": ["Fiyat yok."]})
1022
-
1023
- buy = prices.loc[prices.groupby(["item_id", "quality"])["sell_price_min"].idxmin()][
1024
- ["item_id", "quality", "city", "sell_price_min"]
1025
- ].rename(columns={"city": "buy_city", "sell_price_min": "buy_price"})
1026
-
1027
- sell = prices.loc[prices.groupby(["item_id", "quality"])["buy_price_max"].idxmax()][
1028
- ["item_id", "quality", "city", "buy_price_max"]
1029
- ].rename(columns={"city": "sell_city", "buy_price_max": "sell_price"})
1030
-
1031
- arb = buy.merge(sell, on=["item_id", "quality"])
1032
- arb = arb[arb["sell_price"] > arb["buy_price"]].copy()
1033
- if arb.empty:
1034
- return pd.DataFrame({"Bilgi": ["Arbitraj yok."]})
1035
-
1036
- arb["brüt"] = arb["sell_price"] - arb["buy_price"]
1037
- arb["maliyet"] = arb["buy_price"] * SETUP_FEE_RATE + arb["sell_price"] * (SETUP_FEE_RATE + TRANSACTION_TAX_RATE)
1038
- arb["net"] = arb["brüt"] - arb["maliyet"]
1039
- arb["marj"] = (arb["net"] / arb["buy_price"]) * 100
1040
- arb = arb[arb["net"] > 0].sort_values("marj", ascending=False)
1041
-
1042
- if arb.empty:
1043
- return pd.DataFrame({"Bilgi": ["Kârlı arbitraj yok."]})
1044
-
1045
- arb["Ürün"] = arb["item_id"].apply(item_display_name)
1046
- arb["Kalite"] = arb["quality"].apply(quality_name)
1047
- arb["Alış"] = arb["buy_city"]
1048
- arb["Satış"] = arb["sell_city"]
1049
- arb["Alış Fiyatı"] = arb["buy_price"].round(0)
1050
- arb["Satış Fiyatı"] = arb["sell_price"].round(0)
1051
- arb["Net Kâr"] = arb["net"].round(0)
1052
- arb["Marj %"] = arb["marj"].round(1)
1053
- cols = ["Ürün", "Kalite", "Alış", "Satış", "Alış Fiyatı", "Satış Fiyatı", "Net Kâr", "Marj %"]
1054
- return arb[cols].head(50)
1055
-
1056
-
1057
- def get_importance_plot(horizon: str = "24h"):
1058
- model = pipeline.model_24h if horizon == "24h" else pipeline.model_30d
1059
- if model is None:
1060
- return go.Figure().update_layout(title=f"{horizon} model yok", template="plotly_dark")
1061
- try:
1062
- df = pd.DataFrame({
1063
- "Özellik": model.feature_name_,
1064
- "Önem": model.feature_importances_,
1065
- }).sort_values("Önem", ascending=True).tail(20)
1066
- fig = px.bar(df, x="Önem", y="Özellik", orientation="h",
1067
- title=f"{horizon} Feature Importance", template="plotly_dark")
1068
- fig.update_layout(height=600)
1069
- return fig
1070
- except Exception as e:
1071
- return go.Figure().update_layout(title=f"Hata: {e}", template="plotly_dark")
1072
-
1073
-
1074
- def list_csv_files() -> pd.DataFrame:
1075
- rows = []
1076
- if os.path.isdir(DATA_DIR):
1077
- for f in sorted(os.listdir(DATA_DIR)):
1078
- if f.endswith(".csv"):
1079
- fp = os.path.join(DATA_DIR, f)
1080
- try:
1081
- rows.append({
1082
- "Dosya": f,
1083
- "Boyut (KB)": round(os.path.getsize(fp) / 1024, 2),
1084
- "Son Değişiklik": dt.datetime.fromtimestamp(os.path.getmtime(fp)).strftime("%Y-%m-%d %H:%M:%S"),
1085
- })
1086
- except OSError:
1087
- continue
1088
- return pd.DataFrame(rows) if rows else pd.DataFrame(columns=["Dosya", "Boyut (KB)", "Son Değişiklik"])
1089
-
1090
-
1091
- def refresh_all():
1092
- try:
1093
- pipeline.fetch_data()
1094
- if not pipeline.featured_df.empty:
1095
- pipeline.train_all()
1096
- except Exception as e:
1097
- pipeline.status_message = f"Hata: {e}"
1098
- import traceback
1099
- traceback.print_exc()
1100
-
1101
- try:
1102
- long_table, long_mode = get_prediction_long_table()
1103
- except Exception:
1104
- long_table, long_mode = pd.DataFrame(), "error"
1105
-
1106
- def _safe(fn, default):
1107
- try:
1108
- return fn()
1109
- except Exception:
1110
- return default
1111
-
1112
- mode_msg = f"Uzun vade: {'MODEL (30d)' if long_mode == 'model_30d' else 'TREND PROJEKSİYONU (90d)'}"
1113
-
1114
- return (
1115
- f"{pipeline.status_message} | {mode_msg}",
1116
- _safe(get_dashboard_metrics, pd.DataFrame()),
1117
- _safe(get_analysis_table, pd.DataFrame()),
1118
- _safe(get_prediction_24h_table, pd.DataFrame()),
1119
- long_table,
1120
- _safe(get_dip_table, pd.DataFrame()),
1121
- _safe(get_arbitrage_table, pd.DataFrame()),
1122
- _safe(get_importance_plot, go.Figure().update_layout(title="—", template="plotly_dark")),
1123
- _safe(lambda: get_importance_plot("30d"), go.Figure().update_layout(title="—", template="plotly_dark")),
1124
- _safe(list_csv_files, pd.DataFrame()),
1125
- )
1126
-
1127
-
1128
- def initial_load():
1129
- def _safe(fn, default):
1130
- try:
1131
- return fn()
1132
- except Exception:
1133
- return default
1134
- return (
1135
- _safe(get_dashboard_metrics, pd.DataFrame({"Metrik": ["Durum"], "Değer": ["Hazır"]})),
1136
- pd.DataFrame(), pd.DataFrame(), pd.DataFrame(),
1137
- pd.DataFrame({"Bilgi": ["Yenile'ye basın"]}),
1138
- pd.DataFrame(), pd.DataFrame(),
1139
- go.Figure().update_layout(title="Yenile", template="plotly_dark"),
1140
- go.Figure().update_layout(title="Yenile", template="plotly_dark"),
1141
- _safe(list_csv_files, pd.DataFrame()),
1142
- )
1143
-
1144
-
1145
- # ---------------------------------------------------------------------------
1146
- # GRADIO
1147
- # ---------------------------------------------------------------------------
1148
-
1149
-
1150
- with gr.Blocks(title="GOSHAWK ALBION MARKET AI", theme=gr.themes.Soft(primary_hue="cyan", secondary_hue="slate")) as demo:
1151
-
1152
- gr.Markdown(
1153
- """
1154
- # 🦅 GOSHAWK ALBION MARKET AI v3.1
1155
- **Hibrit yaklaşım:** 24h model + 30d model (yoksa 90d trend projeksiyonu)
1156
-
1157
- **⚠️ Uyarı:** Uzun vadeli tahmin **düşük güvenilirlikte**. Trend yönü için kullanın.
1158
- """
1159
- )
1160
-
1161
- with gr.Row():
1162
- refresh_btn = gr.Button("🔄 Verileri Yenile", variant="primary", scale=1)
1163
- status_text = gr.Textbox(label="Durum", interactive=False, scale=2, value=pipeline.status_message)
1164
-
1165
- with gr.Tabs():
1166
- with gr.TabItem("📊 Dashboard"):
1167
- dashboard_table = gr.Dataframe(label="Özet & Teşhis", interactive=False, wrap=True)
1168
- gr.Markdown(
1169
- "### Hibrit Yaklaşım\n"
1170
- "**24h Model:** Her zaman denenir\n"
1171
- "**30d Model:** Yeterli veri varsa (min 150 örnek) eğitilir\n"
1172
- "**Trend Projeksiyonu:** 30d model yoksa, 30 günlük lineer trend eğiminden 90 günlük tahmin\n\n"
1173
- "### Neden 30d, 90d değil?\n"
1174
- "- 90d model için **6 ay geçmiş veri** gerekir\n"
1175
- "- API'de genelde 2-4 ay var → 90d model eğitilemez\n"
1176
- "- 30d model için **2 ay geçmiş** yeterli → çoğu üründe çalışır\n\n"
1177
- "### Trend Projeksiyonu Formülü\n"
1178
- "`fiyat × exp(trend_slope_30d × 90 gün)`\n"
1179
- "- Basit ama az veriyle çalışır\n"
1180
- "- Patch/sezon değişimini **görmez**"
1181
- )
1182
-
1183
- with gr.TabItem("🔮 24 Saat"):
1184
- pred_24h_table = gr.Dataframe(label="24s Tahminler", interactive=False)
1185
-
1186
- with gr.TabItem("📅 Uzun Vade"):
1187
- pred_long_table = gr.Dataframe(label="30 Gün Modeli veya 90 Gün Trend Projeksiyonu", interactive=False)
1188
- gr.Markdown(
1189
- "*30d model eğitildiyse 30 günlük tahmin, aksi halde 30 günlük trendden 90 günlük projeksiyon. "
1190
- "**Güven %50 altındaysa işlem yapmayın.** Patch değişimleri modeli tamamen bozar.*"
1191
- )
1192
-
1193
- with gr.TabItem("🎯 DİP ALIM"):
1194
- dip_table = gr.Dataframe(label="Dip Alım", interactive=False)
1195
-
1196
- with gr.TabItem("📦 Ürün Analizi"):
1197
- analysis_table = gr.Dataframe(label="Ürün Analizi", interactive=False)
1198
-
1199
- with gr.TabItem("🚚 Arbitraj"):
1200
- arbitrage_table = gr.Dataframe(label="Arbitraj", interactive=False)
1201
-
1202
- with gr.TabItem("📈 24h Model"):
1203
- importance_24h = gr.Plot(label="24h Önem")
1204
-
1205
- with gr.TabItem("📈 30d Model"):
1206
- importance_30d = gr.Plot(label="30d Önem")
1207
-
1208
- with gr.TabItem("🗄️ Veri"):
1209
- gr.Markdown(
1210
- "**API:** europe.albion-online-data.com\n"
1211
- "**Kalite:** 1-3\n"
1212
- f"**24h sınır:** ±%{CLASS_UP_24H*100:.0f}\n"
1213
- f"**30d sınır:** ±%{CLASS_UP_30D*100:.0f}\n"
1214
- f"**Trend projeksiyon:** {TREND_PROJECTION_DAYS} gün"
1215
- )
1216
- csv_files = gr.Dataframe(label="CSV", interactive=False,
1217
- headers=["Dosya", "Boyut (KB)", "Son Değişiklik"])
1218
-
1219
- refresh_btn.click(
1220
- fn=refresh_all,
1221
- inputs=[],
1222
- outputs=[status_text, dashboard_table, analysis_table, pred_24h_table,
1223
- pred_long_table, dip_table, arbitrage_table, importance_24h,
1224
- importance_30d, csv_files],
1225
- )
1226
-
1227
- demo.load(
1228
- fn=initial_load,
1229
- inputs=[],
1230
- outputs=[dashboard_table, analysis_table, pred_24h_table, pred_long_table,
1231
- dip_table, arbitrage_table, importance_24h, importance_30d, csv_files],
1232
- )
1233
-
1234
-
1235
- if __name__ == "__main__":
1236
- demo.launch(server_name="0.0.0.0", server_port=7860, share=False)