Spaces:
Sleeping
Sleeping
Update ml_engine/sniper_engine.py
Browse files- ml_engine/sniper_engine.py +36 -16
ml_engine/sniper_engine.py
CHANGED
|
@@ -1,6 +1,10 @@
|
|
| 1 |
# ============================================================
|
| 2 |
# ๐ฏ ml_engine/sniper_engine.py
|
| 3 |
-
# (V2.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
# ============================================================
|
| 5 |
|
| 6 |
import os
|
|
@@ -13,19 +17,23 @@ import traceback
|
|
| 13 |
from typing import List, Dict, Any, Optional
|
| 14 |
|
| 15 |
N_SPLITS = 5
|
| 16 |
-
|
|
|
|
|
|
|
| 17 |
|
| 18 |
# ============================================================
|
| 19 |
# ๐ง 1. Feature Engineering (Standard + Liquidity Proxies)
|
| 20 |
# ============================================================
|
| 21 |
|
| 22 |
def _z_score_rolling(x, w=500):
|
| 23 |
-
|
| 24 |
-
|
|
|
|
| 25 |
z = (x - r) / s
|
| 26 |
return z.fillna(0)
|
| 27 |
|
| 28 |
def _add_liquidity_proxies(df):
|
|
|
|
| 29 |
df_proxy = df.copy()
|
| 30 |
if 'datetime' not in df_proxy.index.names and 'timestamp' in df_proxy.columns:
|
| 31 |
df_proxy['datetime'] = pd.to_datetime(df_proxy['timestamp'], unit='ms')
|
|
@@ -37,7 +45,7 @@ def _add_liquidity_proxies(df):
|
|
| 37 |
df_proxy['amihud'] = (df_proxy['ret'].abs() / df_proxy['dollar_vol'].replace(0, np.nan)).fillna(np.inf)
|
| 38 |
|
| 39 |
dp = df_proxy['close'].diff()
|
| 40 |
-
roll_cov = dp.rolling(64).cov(dp.shift(1))
|
| 41 |
df_proxy['roll_spread'] = (2 * np.sqrt(np.maximum(0, -roll_cov))).bfill()
|
| 42 |
|
| 43 |
sign = np.sign(df_proxy['close'].diff()).fillna(0)
|
|
@@ -100,7 +108,7 @@ def _add_standard_features(df):
|
|
| 100 |
return df_feat
|
| 101 |
|
| 102 |
# ============================================================
|
| 103 |
-
# ๐ฏ 2. SniperEngine Class (
|
| 104 |
# ============================================================
|
| 105 |
|
| 106 |
class SniperEngine:
|
|
@@ -110,6 +118,7 @@ class SniperEngine:
|
|
| 110 |
self.models: List[lgb.Booster] = []
|
| 111 |
self.feature_names: List[str] = []
|
| 112 |
|
|
|
|
| 113 |
self.entry_threshold = 0.40
|
| 114 |
self.wall_ratio_limit = 0.40
|
| 115 |
self.weight_ml = 0.60
|
|
@@ -126,7 +135,7 @@ class SniperEngine:
|
|
| 126 |
|
| 127 |
self._wall_cache = {}
|
| 128 |
|
| 129 |
-
print("๐ฏ [SniperEngine V2.
|
| 130 |
|
| 131 |
def configure_settings(self,
|
| 132 |
threshold: float,
|
|
@@ -176,7 +185,7 @@ class SniperEngine:
|
|
| 176 |
return pd.DataFrame()
|
| 177 |
|
| 178 |
# ==============================================================================
|
| 179 |
-
# ๐ 3. Smart Order Book Logic (
|
| 180 |
# ==============================================================================
|
| 181 |
def _score_order_book(self, order_book: Dict[str, Any], symbol: str = None) -> Dict[str, Any]:
|
| 182 |
try:
|
|
@@ -186,7 +195,7 @@ class SniperEngine:
|
|
| 186 |
if not bids or not asks:
|
| 187 |
return {'score': 0.0, 'imbalance': 0.0, 'veto': True, 'reason': 'Empty OB'}
|
| 188 |
|
| 189 |
-
# โ
Safe Indexing
|
| 190 |
best_bid = float(bids[0][0])
|
| 191 |
best_ask = float(asks[0][0])
|
| 192 |
spread_pct = (best_ask - best_bid) / best_bid
|
|
@@ -207,7 +216,7 @@ class SniperEngine:
|
|
| 207 |
for i in range(depth):
|
| 208 |
weight = 1.0 / (1.0 + (self.ob_depth_decay * i))
|
| 209 |
|
| 210 |
-
# โ
Safe Indexing
|
| 211 |
bid_vol = float(bids[i][1])
|
| 212 |
ask_vol = float(asks[i][1])
|
| 213 |
|
|
@@ -221,8 +230,7 @@ class SniperEngine:
|
|
| 221 |
max_valid_wall = 0.0
|
| 222 |
limit_price = best_ask * (1 + self.max_wall_dist)
|
| 223 |
|
| 224 |
-
# โ
Safe
|
| 225 |
-
# Was: for price, vol in asks[:depth]:
|
| 226 |
for item in asks[:depth]:
|
| 227 |
p = float(item[0])
|
| 228 |
v = float(item[1])
|
|
@@ -264,6 +272,9 @@ class SniperEngine:
|
|
| 264 |
except Exception as e:
|
| 265 |
return {'score': 0.0, 'veto': True, 'reason': f"OB Error: {e}"}
|
| 266 |
|
|
|
|
|
|
|
|
|
|
| 267 |
async def check_entry_signal_async(self,
|
| 268 |
ohlcv_1m_data: List[List],
|
| 269 |
order_book_data: Dict[str, Any] = None,
|
|
@@ -275,7 +286,9 @@ class SniperEngine:
|
|
| 275 |
ml_score = 0.5
|
| 276 |
ml_reason = "No Data"
|
| 277 |
|
| 278 |
-
|
|
|
|
|
|
|
| 279 |
try:
|
| 280 |
df = pd.DataFrame(ohlcv_1m_data, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
|
| 281 |
df[['open', 'high', 'low', 'close', 'volume']] = df[['open', 'high', 'low', 'close', 'volume']].astype(float)
|
|
@@ -290,23 +303,30 @@ class SniperEngine:
|
|
| 290 |
print(f"โ [Sniper] ML Error: {e}")
|
| 291 |
ml_reason = "ML Err"
|
| 292 |
|
|
|
|
| 293 |
ob_res = {'score': 0.5, 'imbalance': 0.5, 'veto': False, 'reason': 'No OB'}
|
| 294 |
if order_book_data:
|
| 295 |
ob_res = self._score_order_book(order_book_data, symbol=symbol)
|
| 296 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 297 |
if ob_res.get('veto', False):
|
| 298 |
final_score = 0.0
|
| 299 |
signal = 'WAIT'
|
| 300 |
-
|
|
|
|
| 301 |
else:
|
| 302 |
final_score = (ml_score * self.weight_ml) + (ob_res['score'] * self.weight_ob)
|
| 303 |
|
| 304 |
if final_score >= self.entry_threshold:
|
| 305 |
signal = 'BUY'
|
| 306 |
-
reason_str = f"โ
GO: {final_score:.2f} | {ml_reason} |
|
| 307 |
else:
|
| 308 |
signal = 'WAIT'
|
| 309 |
-
|
|
|
|
| 310 |
|
| 311 |
return {
|
| 312 |
'signal': signal,
|
|
|
|
| 1 |
# ============================================================
|
| 2 |
# ๐ฏ ml_engine/sniper_engine.py
|
| 3 |
+
# (V2.2 - GEM-Architect: Full Visibility & OKX Patch)
|
| 4 |
+
# ============================================================
|
| 5 |
+
# - Fixed: Logs now ALWAYS show ML and OB scores (Pass or Fail).
|
| 6 |
+
# - Fixed: Data requirements relaxed to prevent 'No Data' on minor gaps.
|
| 7 |
+
# - Integrity: OKX/Binance Unpacking Logic Preserved.
|
| 8 |
# ============================================================
|
| 9 |
|
| 10 |
import os
|
|
|
|
| 17 |
from typing import List, Dict, Any, Optional
|
| 18 |
|
| 19 |
N_SPLITS = 5
|
| 20 |
+
# ุชู
ุชุฎููู ุงูุดุฑุท ููุง ููุณู
ุงุญ ููู
ูุฏูู ุจุงูุนู
ู ุญุชู ูู ุงูุจูุงูุงุช ูุงูุตุฉ ููููุงู
|
| 21 |
+
# ุงูู
ูุฏูู ุณูุชุนุงู
ู ู
ุน ุงูู NaN ุจุดูู ุทุจูุนู
|
| 22 |
+
LOOKBACK_WINDOW = 200
|
| 23 |
|
| 24 |
# ============================================================
|
| 25 |
# ๐ง 1. Feature Engineering (Standard + Liquidity Proxies)
|
| 26 |
# ============================================================
|
| 27 |
|
| 28 |
def _z_score_rolling(x, w=500):
|
| 29 |
+
# Use min_periods to allow calculation even with less data
|
| 30 |
+
r = x.rolling(w, min_periods=100).mean()
|
| 31 |
+
s = x.rolling(w, min_periods=100).std().replace(0, np.nan)
|
| 32 |
z = (x - r) / s
|
| 33 |
return z.fillna(0)
|
| 34 |
|
| 35 |
def _add_liquidity_proxies(df):
|
| 36 |
+
"""ุญุณุงุจ ู
ุคุดุฑุงุช ุงูุณูููุฉ ุงูู
ุชูุฏู
ุฉ"""
|
| 37 |
df_proxy = df.copy()
|
| 38 |
if 'datetime' not in df_proxy.index.names and 'timestamp' in df_proxy.columns:
|
| 39 |
df_proxy['datetime'] = pd.to_datetime(df_proxy['timestamp'], unit='ms')
|
|
|
|
| 45 |
df_proxy['amihud'] = (df_proxy['ret'].abs() / df_proxy['dollar_vol'].replace(0, np.nan)).fillna(np.inf)
|
| 46 |
|
| 47 |
dp = df_proxy['close'].diff()
|
| 48 |
+
roll_cov = dp.rolling(64, min_periods=30).cov(dp.shift(1))
|
| 49 |
df_proxy['roll_spread'] = (2 * np.sqrt(np.maximum(0, -roll_cov))).bfill()
|
| 50 |
|
| 51 |
sign = np.sign(df_proxy['close'].diff()).fillna(0)
|
|
|
|
| 108 |
return df_feat
|
| 109 |
|
| 110 |
# ============================================================
|
| 111 |
+
# ๐ฏ 2. SniperEngine Class (Robust)
|
| 112 |
# ============================================================
|
| 113 |
|
| 114 |
class SniperEngine:
|
|
|
|
| 118 |
self.models: List[lgb.Booster] = []
|
| 119 |
self.feature_names: List[str] = []
|
| 120 |
|
| 121 |
+
# --- Configurable Thresholds ---
|
| 122 |
self.entry_threshold = 0.40
|
| 123 |
self.wall_ratio_limit = 0.40
|
| 124 |
self.weight_ml = 0.60
|
|
|
|
| 135 |
|
| 136 |
self._wall_cache = {}
|
| 137 |
|
| 138 |
+
print("๐ฏ [SniperEngine V2.2] High-Visibility Logic Loaded.")
|
| 139 |
|
| 140 |
def configure_settings(self,
|
| 141 |
threshold: float,
|
|
|
|
| 185 |
return pd.DataFrame()
|
| 186 |
|
| 187 |
# ==============================================================================
|
| 188 |
+
# ๐ 3. Smart Order Book Logic (OKX Safe)
|
| 189 |
# ==============================================================================
|
| 190 |
def _score_order_book(self, order_book: Dict[str, Any], symbol: str = None) -> Dict[str, Any]:
|
| 191 |
try:
|
|
|
|
| 195 |
if not bids or not asks:
|
| 196 |
return {'score': 0.0, 'imbalance': 0.0, 'veto': True, 'reason': 'Empty OB'}
|
| 197 |
|
| 198 |
+
# โ
Safe Indexing
|
| 199 |
best_bid = float(bids[0][0])
|
| 200 |
best_ask = float(asks[0][0])
|
| 201 |
spread_pct = (best_ask - best_bid) / best_bid
|
|
|
|
| 216 |
for i in range(depth):
|
| 217 |
weight = 1.0 / (1.0 + (self.ob_depth_decay * i))
|
| 218 |
|
| 219 |
+
# โ
Safe Indexing
|
| 220 |
bid_vol = float(bids[i][1])
|
| 221 |
ask_vol = float(asks[i][1])
|
| 222 |
|
|
|
|
| 230 |
max_valid_wall = 0.0
|
| 231 |
limit_price = best_ask * (1 + self.max_wall_dist)
|
| 232 |
|
| 233 |
+
# โ
Safe Unpacking Loop
|
|
|
|
| 234 |
for item in asks[:depth]:
|
| 235 |
p = float(item[0])
|
| 236 |
v = float(item[1])
|
|
|
|
| 272 |
except Exception as e:
|
| 273 |
return {'score': 0.0, 'veto': True, 'reason': f"OB Error: {e}"}
|
| 274 |
|
| 275 |
+
# ==============================================================================
|
| 276 |
+
# ๐ฏ 4. Main Signal Check (Fixed Logging)
|
| 277 |
+
# ==============================================================================
|
| 278 |
async def check_entry_signal_async(self,
|
| 279 |
ohlcv_1m_data: List[List],
|
| 280 |
order_book_data: Dict[str, Any] = None,
|
|
|
|
| 286 |
ml_score = 0.5
|
| 287 |
ml_reason = "No Data"
|
| 288 |
|
| 289 |
+
# โ
Relaxed Check: Allow partial data (min 100) instead of strict 500
|
| 290 |
+
# LightGBM handles NaNs, better to run with partial data than show 'No Data'
|
| 291 |
+
if len(ohlcv_1m_data) >= 100 and self.models:
|
| 292 |
try:
|
| 293 |
df = pd.DataFrame(ohlcv_1m_data, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
|
| 294 |
df[['open', 'high', 'low', 'close', 'volume']] = df[['open', 'high', 'low', 'close', 'volume']].astype(float)
|
|
|
|
| 303 |
print(f"โ [Sniper] ML Error: {e}")
|
| 304 |
ml_reason = "ML Err"
|
| 305 |
|
| 306 |
+
# --- Smart Order Book Analysis ---
|
| 307 |
ob_res = {'score': 0.5, 'imbalance': 0.5, 'veto': False, 'reason': 'No OB'}
|
| 308 |
if order_book_data:
|
| 309 |
ob_res = self._score_order_book(order_book_data, symbol=symbol)
|
| 310 |
|
| 311 |
+
# โ
FIXED: Prepare Score Strings for Visibility in ALL cases
|
| 312 |
+
ob_str = f"OB:{ob_res['score']:.2f}"
|
| 313 |
+
|
| 314 |
+
# --- Final Hybrid Score ---
|
| 315 |
if ob_res.get('veto', False):
|
| 316 |
final_score = 0.0
|
| 317 |
signal = 'WAIT'
|
| 318 |
+
# โ
Added ml_reason to veto log
|
| 319 |
+
reason_str = f"โ {ob_res['reason']} | {ml_reason} | {ob_str}"
|
| 320 |
else:
|
| 321 |
final_score = (ml_score * self.weight_ml) + (ob_res['score'] * self.weight_ob)
|
| 322 |
|
| 323 |
if final_score >= self.entry_threshold:
|
| 324 |
signal = 'BUY'
|
| 325 |
+
reason_str = f"โ
GO: {final_score:.2f} | {ml_reason} | {ob_str}"
|
| 326 |
else:
|
| 327 |
signal = 'WAIT'
|
| 328 |
+
# โ
ADDED: Missing Info Restored here
|
| 329 |
+
reason_str = f"๐ Low Score: {final_score:.2f} | {ml_reason} | {ob_str}"
|
| 330 |
|
| 331 |
return {
|
| 332 |
'signal': signal,
|