Spaces:
Running
Running
File size: 6,992 Bytes
fb9437e 40b816f 8f27110 73faa44 fb9437e 40ec9eb 40b816f 2270a6d 40b816f 40ec9eb 40b816f 40ec9eb 40b816f 40ec9eb 2270a6d 40ec9eb 2270a6d 40ec9eb 73faa44 40ec9eb fb9437e 73faa44 40ec9eb fb9437e 40ec9eb fb9437e 40ec9eb 73faa44 8f27110 2270a6d fb9437e 2270a6d 8f27110 73faa44 2270a6d 73faa44 40ec9eb 73faa44 40ec9eb 2270a6d 8f27110 73faa44 40ec9eb 8f27110 73faa44 40ec9eb fb9437e 40ec9eb 8f27110 fb9437e 73faa44 fb9437e 40ec9eb 73faa44 40ec9eb fb9437e 40ec9eb fb9437e 8f27110 73faa44 fb9437e 8f27110 fb9437e 40ec9eb 8f27110 40ec9eb 8f27110 40ec9eb 8f27110 fb9437e 73faa44 fb9437e 73faa44 fb9437e 73faa44 fb9437e 73faa44 fb9437e 73faa44 8f27110 40ec9eb 2270a6d fb9437e 8f27110 73faa44 fb9437e 8f27110 fb9437e 2270a6d 8f27110 40b816f 40ec9eb 2270a6d 40ec9eb fb9437e 8f27110 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 |
# simulation_engine/virtual_exchange.py
# V2.0 — Single-position engine with cooldown and soft/hard time-stops (≈1h / 2h)
from datetime import datetime
import uuid
class VirtualExchange:
def __init__(
self,
initial_balance=10.0,
fee_rate=0.001,
tp_pct=1.6,
sl_pct=0.8,
bar_ms=300_000, # 5m
cooldown_bars=1,
min_trade_usd=0.10,
position_fraction=0.45,
max_concurrent=1,
soft_time_stop_bars=12, # ~1h at 5m
hard_time_stop_bars=24, # ~2h at 5m
):
self.initial_balance = float(initial_balance)
self.balance = float(initial_balance)
self.fee_rate = float(fee_rate)
self.tp_pct = float(tp_pct) / 100.0
self.sl_pct = float(sl_pct) / 100.0
self.bar_ms = int(bar_ms) if bar_ms else None
self.cooldown_bars = int(cooldown_bars) if cooldown_bars is not None else 0
self.min_trade_usd = float(min_trade_usd)
self.position_fraction = float(position_fraction)
self.max_concurrent = int(max_concurrent)
self.soft_time_stop_bars = int(soft_time_stop_bars) if soft_time_stop_bars else None
self.hard_time_stop_bars = int(hard_time_stop_bars) if hard_time_stop_bars else None
self.positions = {} # symbol -> position dict
self.trade_history = []
self.cooldowns = {} # symbol -> cooldown_until_ts
self.metrics = {
"wins": 0, "losses": 0, "total_pnl_usd": 0.0,
"max_drawdown": 0.0, "peak_balance": self.balance
}
# ------- Helpers -------
def open_positions_count(self):
return len(self.positions)
def get_balance(self):
return self.balance
def can_enter(self, symbol, current_ts):
cd_until = self.cooldowns.get(symbol)
if cd_until is not None and current_ts < cd_until:
return False
if self.open_positions_count() >= self.max_concurrent:
return False
if symbol in self.positions:
return False
planned = self.balance * self.position_fraction
if planned < self.min_trade_usd:
return False
return True
# ------- Lifecycle -------
def update_positions(self, current_prices, timestamp):
closed_trades = []
for symbol in list(self.positions.keys()):
pos = self.positions[symbol]
current_price = current_prices.get(symbol)
if not current_price:
continue
entry = pos["entry_price"]
pos["highest_price"] = max(pos.get("highest_price", entry), current_price)
tp_price = entry * (1.0 + self.tp_pct)
sl_price = entry * (1.0 - self.sl_pct)
age_ms = timestamp - pos["entry_time"]
age_bars = int(age_ms // self.bar_ms) if self.bar_ms else 0
closed_trade = None
# 1) Price-based exits
if current_price >= tp_price:
closed_trade = self._close(symbol, tp_price, timestamp, "TAKE_PROFIT")
elif current_price <= sl_price:
closed_trade = self._close(symbol, sl_price, timestamp, "STOP_LOSS")
else:
# 2) Time-based exits: soft then hard
if self.soft_time_stop_bars and age_bars >= self.soft_time_stop_bars:
if current_price >= entry:
closed_trade = self._close(symbol, current_price, timestamp, "SOFT_TIME_STOP")
if not closed_trade and self.hard_time_stop_bars and age_bars >= self.hard_time_stop_bars:
closed_trade = self._close(symbol, current_price, timestamp, "HARD_TIME_STOP")
if closed_trade:
closed_trades.append(closed_trade)
return closed_trades
def execute_buy(self, symbol, price, timestamp, score_data=None):
if price <= 0:
return False
if self.open_positions_count() >= self.max_concurrent:
return False
if symbol in self.positions:
return False
amount_usd = self.balance * self.position_fraction
if amount_usd < self.min_trade_usd or amount_usd > self.balance:
return False
fee = amount_usd * self.fee_rate
net_invested = amount_usd - fee
if net_invested <= 0:
return False
quantity = net_invested / price
self.balance -= amount_usd
self.positions[symbol] = {
"entry_price": price,
"quantity": quantity,
"invested_usd": amount_usd,
"entry_time": timestamp,
"scores": score_data or {},
"highest_price": price,
"trade_id": f"sim_{uuid.uuid4().hex[:8]}",
}
return True
def execute_sell(self, symbol, price, timestamp, reason):
return self._close(symbol, price, timestamp, reason)
# ------- Internal close -------
def _close(self, symbol, price, timestamp, reason):
pos = self.positions.get(symbol)
if not pos:
return None
revenue = pos["quantity"] * price
fee = revenue * self.fee_rate
net_revenue = revenue - fee
self.balance += net_revenue
pnl_usd = net_revenue - pos["invested_usd"]
pnl_pct = (pnl_usd / pos["invested_usd"]) * 100.0
entry_score = None
try:
entry_score = float((pos.get("scores") or {}).get("final_score"))
except Exception:
entry_score = None
trade_record = {
"id": pos["trade_id"],
"symbol": symbol,
"status": "CLOSED",
"entry_time": datetime.fromtimestamp(pos["entry_time"] / 1000).isoformat(),
"close_time": datetime.fromtimestamp(timestamp / 1000).isoformat(),
"entry_price": pos["entry_price"],
"close_price": price,
"pnl_usd": pnl_usd,
"pnl_percent": pnl_pct,
"close_reason": reason,
"strategy": "HYBRID_TITAN",
"score": entry_score,
"decision_data": {"components": pos.get("scores") or {}}
}
self.trade_history.append(trade_record)
self.metrics["total_pnl_usd"] += pnl_usd
if pnl_usd > 0:
self.metrics["wins"] += 1
else:
self.metrics["losses"] += 1
self.metrics["peak_balance"] = max(self.metrics["peak_balance"], self.balance)
current_dd = (self.metrics["peak_balance"] - self.balance) / max(self.metrics["peak_balance"], 1e-9)
self.metrics["max_drawdown"] = max(self.metrics["max_drawdown"], current_dd)
# تبريد لكل رمز
if self.cooldown_bars and self.bar_ms:
self.cooldowns[symbol] = timestamp + self.cooldown_bars * self.bar_ms
else:
self.cooldowns[symbol] = None
del self.positions[symbol]
return trade_record |