Riy777 commited on
Commit
40ec9eb
·
1 Parent(s): b0a91f6

Update simulation_engine/virtual_exchange.py

Browse files
Files changed (1) hide show
  1. simulation_engine/virtual_exchange.py +95 -23
simulation_engine/virtual_exchange.py CHANGED
@@ -1,24 +1,70 @@
1
  # simulation_engine/virtual_exchange.py
2
- # (V1.2 - include numeric 'score' in trade_record)
3
 
4
  from datetime import datetime
5
  import uuid
6
 
7
  class VirtualExchange:
8
- def __init__(self, initial_balance=10.0, fee_rate=0.001):
9
- self.initial_balance = initial_balance
10
- self.balance = initial_balance
11
- self.fee_rate = fee_rate
12
- self.positions = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  self.trade_history = []
 
 
14
  self.metrics = {
15
  "wins": 0, "losses": 0, "total_pnl_usd": 0.0,
16
- "max_drawdown": 0.0, "peak_balance": initial_balance
17
  }
18
 
 
 
 
 
19
  def get_balance(self):
20
  return self.balance
21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  def update_positions(self, current_prices, timestamp):
23
  closed_trades = []
24
  for symbol in list(self.positions.keys()):
@@ -29,40 +75,61 @@ class VirtualExchange:
29
 
30
  pos["highest_price"] = max(pos.get("highest_price", 0), current_price)
31
 
32
- tp_price = pos["entry_price"] * 1.015
33
- sl_price = pos["entry_price"] * 0.9925
34
 
35
  closed_trade = None
36
  if current_price >= tp_price:
37
- closed_trade = self.execute_sell(symbol, tp_price, timestamp, "TAKE_PROFIT")
38
  elif current_price <= sl_price:
39
- closed_trade = self.execute_sell(symbol, sl_price, timestamp, "STOP_LOSS")
 
 
 
 
 
 
40
 
41
  if closed_trade:
42
  closed_trades.append(closed_trade)
43
  return closed_trades
44
 
45
- def execute_buy(self, symbol, price, amount_usd, timestamp, score_data=None):
46
- if price <= 0 or self.balance < amount_usd:
 
 
 
 
 
 
 
 
47
  return False
48
 
49
  fee = amount_usd * self.fee_rate
50
- self.balance -= amount_usd
51
  net_invested = amount_usd - fee
 
 
 
52
  quantity = net_invested / price
 
53
 
54
  self.positions[symbol] = {
55
  "entry_price": price,
56
  "quantity": quantity,
57
  "invested_usd": amount_usd,
58
  "entry_time": timestamp,
59
- "scores": score_data or {}, # يجب أن تحتوي الآن على final_score
60
  "highest_price": price,
61
- "trade_id": f"sim_{uuid.uuid4().hex[:8]}"
62
  }
63
  return True
64
 
65
  def execute_sell(self, symbol, price, timestamp, reason):
 
 
 
 
66
  pos = self.positions.get(symbol)
67
  if not pos:
68
  return None
@@ -73,12 +140,11 @@ class VirtualExchange:
73
  self.balance += net_revenue
74
 
75
  pnl_usd = net_revenue - pos["invested_usd"]
76
- pnl_pct = (pnl_usd / pos["invested_usd"]) * 100
77
 
78
- # حاول استخراج درجة الدخول النهائية إن وُجدت
79
- entry_score = pos.get("scores", {}).get("final_score")
80
  try:
81
- entry_score = float(entry_score) if entry_score is not None else None
82
  except Exception:
83
  entry_score = None
84
 
@@ -94,9 +160,9 @@ class VirtualExchange:
94
  "pnl_percent": pnl_pct,
95
  "close_reason": reason,
96
  "strategy": "HYBRID_TITAN",
97
- "score": entry_score, # ✅ يقرأه Reflector كعدد
98
  "decision_data": {
99
- "components": pos["scores"],
100
  "hybrid_weights_at_entry": {"titan": 0.5, "patterns": 0.4, "monte_carlo": 0.1}
101
  }
102
  }
@@ -108,8 +174,14 @@ class VirtualExchange:
108
  else:
109
  self.metrics["losses"] += 1
110
  self.metrics["peak_balance"] = max(self.metrics["peak_balance"], self.balance)
111
- current_drawdown = (self.metrics["peak_balance"] - self.balance) / self.metrics["peak_balance"]
112
  self.metrics["max_drawdown"] = max(self.metrics["max_drawdown"], current_drawdown)
113
 
 
 
 
 
 
 
114
  del self.positions[symbol]
115
  return trade_record
 
1
  # simulation_engine/virtual_exchange.py
2
+ # Single concurrent position allowed (configurable), zero cooldown supported, fast time-stop
3
 
4
  from datetime import datetime
5
  import uuid
6
 
7
  class VirtualExchange:
8
+ def __init__(
9
+ self,
10
+ initial_balance=10.0,
11
+ fee_rate=0.001,
12
+ tp_pct=1.0,
13
+ sl_pct=1.0,
14
+ time_stop_bars=6,
15
+ bar_ms=300_000,
16
+ cooldown_bars=0,
17
+ min_trade_usd=0.10,
18
+ position_fraction=0.95,
19
+ max_concurrent=1,
20
+ ):
21
+ self.initial_balance = float(initial_balance)
22
+ self.balance = float(initial_balance)
23
+ self.fee_rate = float(fee_rate)
24
+
25
+ self.tp_pct = float(tp_pct) / 100.0
26
+ self.sl_pct = float(sl_pct) / 100.0
27
+ self.time_stop_bars = int(time_stop_bars) if time_stop_bars is not None else None
28
+ self.bar_ms = int(bar_ms) if bar_ms is not None else None
29
+ self.cooldown_bars = int(cooldown_bars) if cooldown_bars is not None else 0
30
+ self.min_trade_usd = float(min_trade_usd)
31
+ self.position_fraction = float(position_fraction)
32
+ self.max_concurrent = int(max_concurrent)
33
+
34
+ self.positions = {} # symbol -> position dict
35
  self.trade_history = []
36
+ self.cooldowns = {} # symbol -> cooldown_until_ts
37
+
38
  self.metrics = {
39
  "wins": 0, "losses": 0, "total_pnl_usd": 0.0,
40
+ "max_drawdown": 0.0, "peak_balance": self.balance
41
  }
42
 
43
+ # ------- Helpers -------
44
+ def open_positions_count(self):
45
+ return len(self.positions)
46
+
47
  def get_balance(self):
48
  return self.balance
49
 
50
+ def can_enter(self, symbol, current_ts):
51
+ # cooldown per symbol
52
+ cd_until = self.cooldowns.get(symbol)
53
+ if cd_until is not None and current_ts < cd_until:
54
+ return False
55
+ # single-position constraint or configured max
56
+ if self.open_positions_count() >= self.max_concurrent:
57
+ return False
58
+ # avoid duplicate position on same symbol
59
+ if symbol in self.positions:
60
+ return False
61
+ # check min trade size
62
+ planned = self.balance * self.position_fraction
63
+ if planned < self.min_trade_usd:
64
+ return False
65
+ return True
66
+
67
+ # ------- Lifecycle -------
68
  def update_positions(self, current_prices, timestamp):
69
  closed_trades = []
70
  for symbol in list(self.positions.keys()):
 
75
 
76
  pos["highest_price"] = max(pos.get("highest_price", 0), current_price)
77
 
78
+ tp_price = pos["entry_price"] * (1.0 + self.tp_pct)
79
+ sl_price = pos["entry_price"] * (1.0 - self.sl_pct)
80
 
81
  closed_trade = None
82
  if current_price >= tp_price:
83
+ closed_trade = self._close(symbol, tp_price, timestamp, "TAKE_PROFIT")
84
  elif current_price <= sl_price:
85
+ closed_trade = self._close(symbol, sl_price, timestamp, "STOP_LOSS")
86
+ else:
87
+ # time stop
88
+ if self.time_stop_bars and self.bar_ms:
89
+ age_ms = timestamp - pos["entry_time"]
90
+ if age_ms >= self.time_stop_bars * self.bar_ms:
91
+ closed_trade = self._close(symbol, current_price, timestamp, "TIME_STOP")
92
 
93
  if closed_trade:
94
  closed_trades.append(closed_trade)
95
  return closed_trades
96
 
97
+ def execute_buy(self, symbol, price, timestamp, score_data=None):
98
+ if price <= 0:
99
+ return False
100
+ if self.open_positions_count() >= self.max_concurrent:
101
+ return False
102
+ if symbol in self.positions:
103
+ return False
104
+
105
+ amount_usd = self.balance * self.position_fraction
106
+ if amount_usd < self.min_trade_usd or amount_usd > self.balance:
107
  return False
108
 
109
  fee = amount_usd * self.fee_rate
 
110
  net_invested = amount_usd - fee
111
+ if net_invested <= 0:
112
+ return False
113
+
114
  quantity = net_invested / price
115
+ self.balance -= amount_usd
116
 
117
  self.positions[symbol] = {
118
  "entry_price": price,
119
  "quantity": quantity,
120
  "invested_usd": amount_usd,
121
  "entry_time": timestamp,
122
+ "scores": score_data or {},
123
  "highest_price": price,
124
+ "trade_id": f"sim_{uuid.uuid4().hex[:8]}",
125
  }
126
  return True
127
 
128
  def execute_sell(self, symbol, price, timestamp, reason):
129
+ return self._close(symbol, price, timestamp, reason)
130
+
131
+ # ------- Internal close -------
132
+ def _close(self, symbol, price, timestamp, reason):
133
  pos = self.positions.get(symbol)
134
  if not pos:
135
  return None
 
140
  self.balance += net_revenue
141
 
142
  pnl_usd = net_revenue - pos["invested_usd"]
143
+ pnl_pct = (pnl_usd / pos["invested_usd"]) * 100.0
144
 
145
+ entry_score = None
 
146
  try:
147
+ entry_score = float((pos.get("scores") or {}).get("final_score"))
148
  except Exception:
149
  entry_score = None
150
 
 
160
  "pnl_percent": pnl_pct,
161
  "close_reason": reason,
162
  "strategy": "HYBRID_TITAN",
163
+ "score": entry_score,
164
  "decision_data": {
165
+ "components": pos.get("scores") or {},
166
  "hybrid_weights_at_entry": {"titan": 0.5, "patterns": 0.4, "monte_carlo": 0.1}
167
  }
168
  }
 
174
  else:
175
  self.metrics["losses"] += 1
176
  self.metrics["peak_balance"] = max(self.metrics["peak_balance"], self.balance)
177
+ current_drawdown = (self.metrics["peak_balance"] - self.balance) / max(self.metrics["peak_balance"], 1e-9)
178
  self.metrics["max_drawdown"] = max(self.metrics["max_drawdown"], current_drawdown)
179
 
180
+ # apply per-symbol cooldown if configured (>0)
181
+ if self.cooldown_bars and self.bar_ms:
182
+ self.cooldowns[symbol] = timestamp + self.cooldown_bars * self.bar_ms
183
+ else:
184
+ self.cooldowns[symbol] = None # zero cooldown => immediate eligibility
185
+
186
  del self.positions[symbol]
187
  return trade_record