forexsmartbot 3.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- forexsmartbot/__init__.py +1 -0
- forexsmartbot/adapters/__init__.py +4 -0
- forexsmartbot/adapters/brokers/__init__.py +8 -0
- forexsmartbot/adapters/brokers/ib_tws_broker.py +244 -0
- forexsmartbot/adapters/brokers/mt4_broker.py +266 -0
- forexsmartbot/adapters/brokers/paper_broker.py +131 -0
- forexsmartbot/adapters/brokers/rest_broker.py +304 -0
- forexsmartbot/adapters/data/__init__.py +27 -0
- forexsmartbot/adapters/data/alpha_vantage_provider.py +261 -0
- forexsmartbot/adapters/data/config.py +101 -0
- forexsmartbot/adapters/data/csv_provider.py +137 -0
- forexsmartbot/adapters/data/dummy_provider.py +35 -0
- forexsmartbot/adapters/data/mt4_provider.py +217 -0
- forexsmartbot/adapters/data/multi_provider.py +191 -0
- forexsmartbot/adapters/data/oanda_provider.py +173 -0
- forexsmartbot/adapters/data/stooq_provider.py +114 -0
- forexsmartbot/adapters/data/twelve_data_provider.py +117 -0
- forexsmartbot/adapters/data/yfinance_provider.py +246 -0
- forexsmartbot/analytics/__init__.py +28 -0
- forexsmartbot/analytics/chart_patterns.py +322 -0
- forexsmartbot/analytics/correlation_matrix.py +186 -0
- forexsmartbot/analytics/economic_calendar.py +616 -0
- forexsmartbot/analytics/enhanced_chart.py +57 -0
- forexsmartbot/analytics/market_depth.py +134 -0
- forexsmartbot/analytics/performance_attribution.py +210 -0
- forexsmartbot/analytics/portfolio_analytics.py +303 -0
- forexsmartbot/analytics/risk_analytics.py +308 -0
- forexsmartbot/analytics/strategy_comparison.py +194 -0
- forexsmartbot/analytics/trade_journal.py +235 -0
- forexsmartbot/brokers/mt4_bridge.py +49 -0
- forexsmartbot/builder/__init__.py +12 -0
- forexsmartbot/builder/code_generator.py +173 -0
- forexsmartbot/builder/strategy_builder.py +119 -0
- forexsmartbot/builder/strategy_template.py +138 -0
- forexsmartbot/cloud/__init__.py +24 -0
- forexsmartbot/cloud/api_client.py +188 -0
- forexsmartbot/cloud/api_server.py +366 -0
- forexsmartbot/cloud/cloud_sync.py +279 -0
- forexsmartbot/cloud/error_handler.py +150 -0
- forexsmartbot/cloud/mobile_api.py +213 -0
- forexsmartbot/cloud/remote_monitor.py +310 -0
- forexsmartbot/cloud/security.py +208 -0
- forexsmartbot/core/backtester.py +57 -0
- forexsmartbot/core/config.py +38 -0
- forexsmartbot/core/interfaces.py +253 -0
- forexsmartbot/core/paper_broker.py +28 -0
- forexsmartbot/core/portfolio.py +227 -0
- forexsmartbot/core/risk.py +24 -0
- forexsmartbot/core/risk_engine.py +203 -0
- forexsmartbot/core/strategy.py +44 -0
- forexsmartbot/core/trade_manager.py +261 -0
- forexsmartbot/gui/main_window.py +168 -0
- forexsmartbot/gui/theme.py +32 -0
- forexsmartbot/marketplace/__init__.py +10 -0
- forexsmartbot/marketplace/marketplace.py +178 -0
- forexsmartbot/marketplace/strategy_rating.py +64 -0
- forexsmartbot/monitoring/__init__.py +12 -0
- forexsmartbot/monitoring/health_check.py +109 -0
- forexsmartbot/monitoring/performance_tracker.py +213 -0
- forexsmartbot/monitoring/strategy_monitor.py +175 -0
- forexsmartbot/optimization/__init__.py +26 -0
- forexsmartbot/optimization/adaptive_parameters.py +224 -0
- forexsmartbot/optimization/genetic_optimizer.py +116 -0
- forexsmartbot/optimization/hyperparameter_optimizer.py +77 -0
- forexsmartbot/optimization/monte_carlo.py +114 -0
- forexsmartbot/optimization/multi_objective_optimizer.py +299 -0
- forexsmartbot/optimization/parameter_sensitivity.py +187 -0
- forexsmartbot/optimization/walk_forward.py +131 -0
- forexsmartbot/services/__init__.py +7 -0
- forexsmartbot/services/backtest.py +354 -0
- forexsmartbot/services/calculation_engine.py +99 -0
- forexsmartbot/services/controller.py +419 -0
- forexsmartbot/services/data_streaming.py +116 -0
- forexsmartbot/services/enhanced_backtest.py +390 -0
- forexsmartbot/services/fast_trading_controller.py +317 -0
- forexsmartbot/services/gpu_backtest.py +303 -0
- forexsmartbot/services/language_manager.py +621 -0
- forexsmartbot/services/notification_service.py +283 -0
- forexsmartbot/services/persistence.py +299 -0
- forexsmartbot/services/startup_manager.py +200 -0
- forexsmartbot/strategies/__init__.py +120 -0
- forexsmartbot/strategies/adaptive_trend_flow.py +311 -0
- forexsmartbot/strategies/breakout_atr.py +136 -0
- forexsmartbot/strategies/ensemble_ml_strategy.py +209 -0
- forexsmartbot/strategies/fear_index.py +174 -0
- forexsmartbot/strategies/lstm_strategy.py +269 -0
- forexsmartbot/strategies/mean_reversion.py +158 -0
- forexsmartbot/strategies/ml_adaptive_supertrend.py +272 -0
- forexsmartbot/strategies/momentum_breakout.py +183 -0
- forexsmartbot/strategies/multi_timeframe_strategy.py +186 -0
- forexsmartbot/strategies/news_trading.py +187 -0
- forexsmartbot/strategies/rl_strategy.py +277 -0
- forexsmartbot/strategies/rsi_reversion.py +162 -0
- forexsmartbot/strategies/scalping_ma.py +195 -0
- forexsmartbot/strategies/sma_crossover.py +118 -0
- forexsmartbot/strategies/svm_strategy.py +199 -0
- forexsmartbot/strategies/transformer_strategy.py +233 -0
- forexsmartbot/testing/__init__.py +14 -0
- forexsmartbot/testing/ci_cd_pipeline.py +286 -0
- forexsmartbot/testing/paper_account_tester.py +326 -0
- forexsmartbot/testing/strategy_sandbox.py +284 -0
- forexsmartbot/ui/__init__.py +8 -0
- forexsmartbot/ui/analytics_dialog.py +262 -0
- forexsmartbot/ui/analytics_widget.py +68 -0
- forexsmartbot/ui/backtest_dialog.py +908 -0
- forexsmartbot/ui/charts.py +172 -0
- forexsmartbot/ui/cloud_dialog.py +371 -0
- forexsmartbot/ui/enhanced_charts.py +491 -0
- forexsmartbot/ui/enhanced_main_window.py +2800 -0
- forexsmartbot/ui/export_dialog.py +333 -0
- forexsmartbot/ui/main_window.py +648 -0
- forexsmartbot/ui/marketplace_dialog.py +438 -0
- forexsmartbot/ui/monitoring_dialog.py +215 -0
- forexsmartbot/ui/monitoring_widget.py +80 -0
- forexsmartbot/ui/optimization_dialog.py +603 -0
- forexsmartbot/ui/settings_dialog.py +671 -0
- forexsmartbot/ui/simple_chart.py +309 -0
- forexsmartbot/ui/strategy_builder_dialog.py +595 -0
- forexsmartbot/ui/theme.py +109 -0
- forexsmartbot/ui/tradingview_chart.py +736 -0
- forexsmartbot/utils/__init__.py +42 -0
- forexsmartbot/utils/gpu_numpy.py +194 -0
- forexsmartbot/utils/gpu_utils.py +260 -0
- forexsmartbot-3.2.0.dist-info/METADATA +177 -0
- forexsmartbot-3.2.0.dist-info/RECORD +129 -0
- forexsmartbot-3.2.0.dist-info/WHEEL +5 -0
- forexsmartbot-3.2.0.dist-info/entry_points.txt +2 -0
- forexsmartbot-3.2.0.dist-info/licenses/LICENSE +150 -0
- forexsmartbot-3.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# ForexSmartBot Package
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""Interactive Brokers TWS broker adapter."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Dict, Optional, Tuple
|
|
6
|
+
|
|
7
|
+
from ...core.interfaces import IBroker, Position
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class IBTWSBroker(IBroker):
|
|
11
|
+
"""Interactive Brokers broker via TWS / IB Gateway using ib_insync."""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
host: str = "127.0.0.1",
|
|
16
|
+
port: int = 7497,
|
|
17
|
+
client_id: int = 1,
|
|
18
|
+
account_id: str = "",
|
|
19
|
+
):
|
|
20
|
+
self._host = host
|
|
21
|
+
self._port = int(port)
|
|
22
|
+
self._client_id = int(client_id)
|
|
23
|
+
self._account_id = account_id
|
|
24
|
+
self._connected = False
|
|
25
|
+
self._contracts: Dict[str, object] = {}
|
|
26
|
+
self._orders: Dict[str, Tuple[str, float]] = {}
|
|
27
|
+
self._ib = None
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
from ib_insync import IB
|
|
31
|
+
|
|
32
|
+
self._ib = IB()
|
|
33
|
+
except Exception:
|
|
34
|
+
self._ib = None
|
|
35
|
+
|
|
36
|
+
def connect(self) -> bool:
|
|
37
|
+
if self._ib is None:
|
|
38
|
+
print("IBTWSBroker: ib_insync is not installed. Install with: pip install ib_insync")
|
|
39
|
+
return False
|
|
40
|
+
try:
|
|
41
|
+
self._ib.connect(self._host, self._port, clientId=self._client_id, timeout=10)
|
|
42
|
+
self._connected = bool(self._ib.isConnected())
|
|
43
|
+
return self._connected
|
|
44
|
+
except Exception as e:
|
|
45
|
+
print(f"IBTWSBroker: Connection failed: {e}")
|
|
46
|
+
self._connected = False
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
def disconnect(self) -> None:
|
|
50
|
+
try:
|
|
51
|
+
if self._ib is not None and self._ib.isConnected():
|
|
52
|
+
self._ib.disconnect()
|
|
53
|
+
finally:
|
|
54
|
+
self._connected = False
|
|
55
|
+
|
|
56
|
+
def is_connected(self) -> bool:
|
|
57
|
+
return self._connected and self._ib is not None and self._ib.isConnected()
|
|
58
|
+
|
|
59
|
+
def _get_contract(self, symbol: str):
|
|
60
|
+
if symbol in self._contracts:
|
|
61
|
+
return self._contracts[symbol]
|
|
62
|
+
|
|
63
|
+
from ib_insync import Forex
|
|
64
|
+
|
|
65
|
+
normalized = symbol.replace("=", "").replace("/", "").upper()
|
|
66
|
+
if normalized.endswith("X"):
|
|
67
|
+
normalized = normalized[:-1]
|
|
68
|
+
if len(normalized) != 6:
|
|
69
|
+
raise ValueError(f"Unsupported symbol format for IB Forex: {symbol}")
|
|
70
|
+
|
|
71
|
+
contract = Forex(normalized)
|
|
72
|
+
self._ib.qualifyContracts(contract)
|
|
73
|
+
self._contracts[symbol] = contract
|
|
74
|
+
return contract
|
|
75
|
+
|
|
76
|
+
def get_price(self, symbol: str) -> Optional[float]:
|
|
77
|
+
if not self.is_connected():
|
|
78
|
+
return None
|
|
79
|
+
try:
|
|
80
|
+
contract = self._get_contract(symbol)
|
|
81
|
+
ticker = self._ib.reqMktData(contract, "", False, False)
|
|
82
|
+
self._ib.sleep(0.5)
|
|
83
|
+
|
|
84
|
+
if ticker and ticker.last and ticker.last > 0:
|
|
85
|
+
return float(ticker.last)
|
|
86
|
+
if ticker and ticker.bid and ticker.ask and ticker.bid > 0 and ticker.ask > 0:
|
|
87
|
+
return float((ticker.bid + ticker.ask) / 2.0)
|
|
88
|
+
if ticker and ticker.close and ticker.close > 0:
|
|
89
|
+
return float(ticker.close)
|
|
90
|
+
return None
|
|
91
|
+
except Exception as e:
|
|
92
|
+
print(f"IBTWSBroker: get_price failed for {symbol}: {e}")
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
def submit_order(
|
|
96
|
+
self,
|
|
97
|
+
symbol: str,
|
|
98
|
+
side: int,
|
|
99
|
+
quantity: float,
|
|
100
|
+
stop_loss: Optional[float] = None,
|
|
101
|
+
take_profit: Optional[float] = None,
|
|
102
|
+
) -> Optional[str]:
|
|
103
|
+
if not self.is_connected():
|
|
104
|
+
return None
|
|
105
|
+
try:
|
|
106
|
+
from ib_insync import LimitOrder, MarketOrder, StopOrder
|
|
107
|
+
|
|
108
|
+
contract = self._get_contract(symbol)
|
|
109
|
+
action = "BUY" if side > 0 else "SELL"
|
|
110
|
+
exit_action = "SELL" if action == "BUY" else "BUY"
|
|
111
|
+
order_qty = float(abs(quantity))
|
|
112
|
+
if order_qty <= 0:
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
order = MarketOrder(action, order_qty, transmit=True)
|
|
116
|
+
if self._account_id:
|
|
117
|
+
order.account = self._account_id
|
|
118
|
+
|
|
119
|
+
# If SL/TP are provided, place as linked child orders for risk control.
|
|
120
|
+
if stop_loss is not None or take_profit is not None:
|
|
121
|
+
order.transmit = False
|
|
122
|
+
|
|
123
|
+
trade = self._ib.placeOrder(contract, order)
|
|
124
|
+
self._ib.sleep(0.5)
|
|
125
|
+
|
|
126
|
+
order_id = str(trade.order.orderId)
|
|
127
|
+
self._orders[order_id] = (symbol, order_qty)
|
|
128
|
+
|
|
129
|
+
if stop_loss is not None:
|
|
130
|
+
sl_order = StopOrder(
|
|
131
|
+
action=exit_action,
|
|
132
|
+
totalQuantity=order_qty,
|
|
133
|
+
stopPrice=float(stop_loss),
|
|
134
|
+
parentId=trade.order.orderId,
|
|
135
|
+
transmit=(take_profit is None),
|
|
136
|
+
)
|
|
137
|
+
if self._account_id:
|
|
138
|
+
sl_order.account = self._account_id
|
|
139
|
+
self._ib.placeOrder(contract, sl_order)
|
|
140
|
+
|
|
141
|
+
if take_profit is not None:
|
|
142
|
+
tp_order = LimitOrder(
|
|
143
|
+
action=exit_action,
|
|
144
|
+
totalQuantity=order_qty,
|
|
145
|
+
lmtPrice=float(take_profit),
|
|
146
|
+
parentId=trade.order.orderId,
|
|
147
|
+
transmit=True,
|
|
148
|
+
)
|
|
149
|
+
if self._account_id:
|
|
150
|
+
tp_order.account = self._account_id
|
|
151
|
+
self._ib.placeOrder(contract, tp_order)
|
|
152
|
+
|
|
153
|
+
self._ib.sleep(0.3)
|
|
154
|
+
return order_id
|
|
155
|
+
except Exception as e:
|
|
156
|
+
print(f"IBTWSBroker: submit_order failed for {symbol}: {e}")
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
def close_all(self, symbol: str) -> bool:
|
|
160
|
+
if not self.is_connected():
|
|
161
|
+
return False
|
|
162
|
+
try:
|
|
163
|
+
from ib_insync import MarketOrder
|
|
164
|
+
|
|
165
|
+
closed_any = False
|
|
166
|
+
target = symbol.replace("=", "").replace("/", "").upper()
|
|
167
|
+
if target.endswith("X"):
|
|
168
|
+
target = target[:-1]
|
|
169
|
+
|
|
170
|
+
for pos in self._ib.positions():
|
|
171
|
+
local_symbol = (pos.contract.localSymbol or "").replace(".", "").upper()
|
|
172
|
+
con_symbol = f"{pos.contract.symbol}{getattr(pos.contract, 'currency', '')}".upper()
|
|
173
|
+
if target not in (local_symbol, con_symbol):
|
|
174
|
+
continue
|
|
175
|
+
|
|
176
|
+
qty = float(pos.position)
|
|
177
|
+
if qty == 0:
|
|
178
|
+
continue
|
|
179
|
+
action = "SELL" if qty > 0 else "BUY"
|
|
180
|
+
order = MarketOrder(action, abs(qty))
|
|
181
|
+
if self._account_id:
|
|
182
|
+
order.account = self._account_id
|
|
183
|
+
self._ib.placeOrder(pos.contract, order)
|
|
184
|
+
closed_any = True
|
|
185
|
+
|
|
186
|
+
if closed_any:
|
|
187
|
+
self._ib.sleep(0.5)
|
|
188
|
+
return closed_any
|
|
189
|
+
except Exception as e:
|
|
190
|
+
print(f"IBTWSBroker: close_all failed for {symbol}: {e}")
|
|
191
|
+
return False
|
|
192
|
+
|
|
193
|
+
def get_positions(self) -> Dict[str, Position]:
|
|
194
|
+
positions: Dict[str, Position] = {}
|
|
195
|
+
if not self.is_connected():
|
|
196
|
+
return positions
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
for pos in self._ib.positions():
|
|
200
|
+
qty = float(pos.position)
|
|
201
|
+
if qty == 0:
|
|
202
|
+
continue
|
|
203
|
+
|
|
204
|
+
symbol = f"{pos.contract.symbol}{getattr(pos.contract, 'currency', '')}"
|
|
205
|
+
market_price = self.get_price(symbol) or float(pos.avgCost)
|
|
206
|
+
side = 1 if qty > 0 else -1
|
|
207
|
+
unrealized = side * abs(qty) * (market_price - float(pos.avgCost))
|
|
208
|
+
|
|
209
|
+
positions[symbol] = Position(
|
|
210
|
+
symbol=symbol,
|
|
211
|
+
side=side,
|
|
212
|
+
quantity=abs(qty),
|
|
213
|
+
entry_price=float(pos.avgCost),
|
|
214
|
+
current_price=float(market_price),
|
|
215
|
+
unrealized_pnl=float(unrealized),
|
|
216
|
+
)
|
|
217
|
+
except Exception as e:
|
|
218
|
+
print(f"IBTWSBroker: get_positions failed: {e}")
|
|
219
|
+
|
|
220
|
+
return positions
|
|
221
|
+
|
|
222
|
+
def get_balance(self) -> float:
|
|
223
|
+
if not self.is_connected():
|
|
224
|
+
return 0.0
|
|
225
|
+
try:
|
|
226
|
+
values = self._ib.accountSummary()
|
|
227
|
+
for row in values:
|
|
228
|
+
if row.tag == "TotalCashValue" and (not self._account_id or row.account == self._account_id):
|
|
229
|
+
return float(row.value)
|
|
230
|
+
except Exception as e:
|
|
231
|
+
print(f"IBTWSBroker: get_balance failed: {e}")
|
|
232
|
+
return 0.0
|
|
233
|
+
|
|
234
|
+
def get_equity(self) -> float:
|
|
235
|
+
if not self.is_connected():
|
|
236
|
+
return 0.0
|
|
237
|
+
try:
|
|
238
|
+
values = self._ib.accountSummary()
|
|
239
|
+
for row in values:
|
|
240
|
+
if row.tag == "NetLiquidation" and (not self._account_id or row.account == self._account_id):
|
|
241
|
+
return float(row.value)
|
|
242
|
+
except Exception as e:
|
|
243
|
+
print(f"IBTWSBroker: get_equity failed: {e}")
|
|
244
|
+
return 0.0
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""MetaTrader 4 broker implementation via file communication."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
from typing import Dict, Optional
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from ...core.interfaces import IBroker, Position, Order
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MT4Broker(IBroker):
|
|
13
|
+
"""MT4 broker implementation using file communication."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, host: str = "127.0.0.1", port: int = 5555):
|
|
16
|
+
self._host = host
|
|
17
|
+
self._port = port
|
|
18
|
+
self._connected = False
|
|
19
|
+
self._files_dir = self._find_mt4_files_dir()
|
|
20
|
+
|
|
21
|
+
def _find_mt4_files_dir(self) -> str:
|
|
22
|
+
"""Find MT4 Files directory."""
|
|
23
|
+
# First try to find the terminal-specific directory
|
|
24
|
+
terminal_dirs = []
|
|
25
|
+
base_path = os.path.join(os.getenv('APPDATA'), 'MetaQuotes', 'Terminal')
|
|
26
|
+
if os.path.exists(base_path):
|
|
27
|
+
for item in os.listdir(base_path):
|
|
28
|
+
if os.path.isdir(os.path.join(base_path, item)) and item != 'MQL4':
|
|
29
|
+
mql4_path = os.path.join(base_path, item, 'MQL4', 'Files')
|
|
30
|
+
if os.path.exists(mql4_path):
|
|
31
|
+
terminal_dirs.append(mql4_path)
|
|
32
|
+
|
|
33
|
+
# Use the exact terminal-specific directory that MT4 EA is looking in
|
|
34
|
+
terminal_specific_path = os.path.join(os.getenv('APPDATA'), 'MetaQuotes', 'Terminal', '0727F3F88B5F0FE006962B330B91FF37', 'MQL4', 'Files')
|
|
35
|
+
|
|
36
|
+
# Add other possible paths
|
|
37
|
+
possible_paths = [
|
|
38
|
+
terminal_specific_path,
|
|
39
|
+
os.path.join(os.getenv('APPDATA'), 'MetaQuotes', 'Terminal', 'MQL4', 'Files'),
|
|
40
|
+
] + terminal_dirs + [
|
|
41
|
+
os.path.join(os.getenv('APPDATA'), 'MetaQuotes', 'Terminal', 'Files'),
|
|
42
|
+
os.path.join(os.getenv('USERPROFILE'), 'AppData', 'Roaming', 'MetaQuotes', 'Terminal', 'MQL4', 'Files')
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
for path in possible_paths:
|
|
46
|
+
if os.path.exists(path):
|
|
47
|
+
print(f"MT4Broker: Found MT4 Files directory: {path}")
|
|
48
|
+
return path
|
|
49
|
+
|
|
50
|
+
# Create default directory if none exists
|
|
51
|
+
default_path = os.path.join(os.getenv('APPDATA'), 'MetaQuotes', 'Terminal', 'MQL4', 'Files')
|
|
52
|
+
os.makedirs(default_path, exist_ok=True)
|
|
53
|
+
print(f"MT4Broker: Created default MT4 Files directory: {default_path}")
|
|
54
|
+
return default_path
|
|
55
|
+
|
|
56
|
+
def connect(self) -> bool:
|
|
57
|
+
"""Connect to MT4."""
|
|
58
|
+
try:
|
|
59
|
+
# Check if MT4 is running (check both terminal.exe and terminal64.exe)
|
|
60
|
+
# Use errors='ignore' to handle any encoding issues with subprocess output
|
|
61
|
+
result = subprocess.run(['tasklist', '/FI', 'IMAGENAME eq terminal.exe'],
|
|
62
|
+
capture_output=True, text=True, encoding='utf-8', errors='ignore')
|
|
63
|
+
result64 = subprocess.run(['tasklist', '/FI', 'IMAGENAME eq terminal64.exe'],
|
|
64
|
+
capture_output=True, text=True, encoding='utf-8', errors='ignore')
|
|
65
|
+
|
|
66
|
+
if 'terminal.exe' in result.stdout or 'terminal64.exe' in result64.stdout:
|
|
67
|
+
self._connected = True
|
|
68
|
+
print(f"MT4Broker: Connected to running MT4 instance")
|
|
69
|
+
print(f"MT4Broker: Using files directory: {self._files_dir}")
|
|
70
|
+
return True
|
|
71
|
+
else:
|
|
72
|
+
print("MT4Broker: MT4 not running - please start MT4 first")
|
|
73
|
+
self._connected = False
|
|
74
|
+
return False
|
|
75
|
+
except Exception as e:
|
|
76
|
+
print(f"MT4Broker: Connection error: {e}")
|
|
77
|
+
self._connected = False
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
def disconnect(self) -> None:
|
|
81
|
+
"""Disconnect from MT4."""
|
|
82
|
+
self._connected = False
|
|
83
|
+
|
|
84
|
+
def is_connected(self) -> bool:
|
|
85
|
+
"""Check if connected to MT4."""
|
|
86
|
+
return self._connected
|
|
87
|
+
|
|
88
|
+
def _execute_command(self, command: str) -> bool:
|
|
89
|
+
"""Execute command via file communication with MT4 EA."""
|
|
90
|
+
try:
|
|
91
|
+
command_file = os.path.join(self._files_dir, 'ForexSmartBot_Command.txt')
|
|
92
|
+
|
|
93
|
+
# Wait for EA to finish processing any previous command
|
|
94
|
+
max_wait = 10 # Wait up to 10 seconds
|
|
95
|
+
wait_count = 0
|
|
96
|
+
while os.path.exists(command_file) and wait_count < max_wait:
|
|
97
|
+
time.sleep(0.5)
|
|
98
|
+
wait_count += 1
|
|
99
|
+
|
|
100
|
+
with open(command_file, 'w') as f:
|
|
101
|
+
f.write(command)
|
|
102
|
+
|
|
103
|
+
# Wait for EA to process the command
|
|
104
|
+
time.sleep(2.0) # Give EA time to process
|
|
105
|
+
|
|
106
|
+
print(f"MT4Broker: Command written to file: {command}")
|
|
107
|
+
return True
|
|
108
|
+
|
|
109
|
+
except Exception as e:
|
|
110
|
+
print(f"MT4Broker: Error writing command file: {e}")
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
def get_price(self, symbol: str) -> Optional[float]:
|
|
114
|
+
"""Get current price from MT4."""
|
|
115
|
+
if not self._connected:
|
|
116
|
+
print("MT4Broker: Not connected to MT4")
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
# Request price from MT4
|
|
121
|
+
self._execute_command(f"GetPrice({symbol})")
|
|
122
|
+
|
|
123
|
+
# Wait for EA to process
|
|
124
|
+
time.sleep(1.0)
|
|
125
|
+
|
|
126
|
+
# Read price from response file
|
|
127
|
+
response_file = os.path.join(self._files_dir, 'ForexSmartBot_Price.txt')
|
|
128
|
+
if os.path.exists(response_file):
|
|
129
|
+
with open(response_file, 'r') as f:
|
|
130
|
+
content = f.read().strip()
|
|
131
|
+
if content:
|
|
132
|
+
price = float(content)
|
|
133
|
+
print(f"MT4Broker: Got real price from MT4 for {symbol}: {price}")
|
|
134
|
+
return price
|
|
135
|
+
except Exception as e:
|
|
136
|
+
print(f"MT4Broker: Error reading price: {e}")
|
|
137
|
+
|
|
138
|
+
print(f"MT4Broker: Using fallback price for {symbol}")
|
|
139
|
+
return 1.0700 if symbol == "EURUSD" else 1.2500
|
|
140
|
+
|
|
141
|
+
def submit_order(self, symbol: str, side: int, quantity: float,
|
|
142
|
+
stop_loss: Optional[float] = None,
|
|
143
|
+
take_profit: Optional[float] = None) -> Optional[str]:
|
|
144
|
+
"""Submit order to MT4."""
|
|
145
|
+
if not self._connected:
|
|
146
|
+
print("MT4Broker: Not connected to MT4")
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
# Create order command for MT4
|
|
151
|
+
order_type = "BUY" if side > 0 else "SELL"
|
|
152
|
+
sl_str = f" SL={stop_loss}" if stop_loss else ""
|
|
153
|
+
tp_str = f" TP={take_profit}" if take_profit else ""
|
|
154
|
+
|
|
155
|
+
# Use a simpler command format that the EA can detect
|
|
156
|
+
command = f"OrderSend({symbol},{order_type},{quantity}{sl_str}{tp_str})"
|
|
157
|
+
print(f"MT4Broker: Command format: {command}")
|
|
158
|
+
print(f"MT4Broker: Command length: {len(command)}")
|
|
159
|
+
|
|
160
|
+
print(f"MT4Broker: Executing real MT4 order: {command}")
|
|
161
|
+
|
|
162
|
+
# Execute the order in MT4
|
|
163
|
+
if self._execute_command(command):
|
|
164
|
+
order_id = f"MT4_{int(time.time() * 1000)}"
|
|
165
|
+
print(f"MT4Broker: Order submitted successfully: {order_id}")
|
|
166
|
+
return order_id
|
|
167
|
+
else:
|
|
168
|
+
print("MT4Broker: Failed to submit order")
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
except Exception as e:
|
|
172
|
+
print(f"MT4Broker: Error submitting order: {e}")
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
def close_all(self, symbol: str) -> bool:
|
|
176
|
+
"""Close all positions for symbol."""
|
|
177
|
+
if not self._connected:
|
|
178
|
+
print("MT4Broker: Not connected to MT4")
|
|
179
|
+
return False
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
command = f"CloseAll({symbol})"
|
|
183
|
+
print(f"MT4Broker: Closing all positions for {symbol}")
|
|
184
|
+
|
|
185
|
+
if self._execute_command(command):
|
|
186
|
+
print(f"MT4Broker: Successfully closed all positions for {symbol}")
|
|
187
|
+
return True
|
|
188
|
+
else:
|
|
189
|
+
print(f"MT4Broker: Failed to close positions for {symbol}")
|
|
190
|
+
return False
|
|
191
|
+
|
|
192
|
+
except Exception as e:
|
|
193
|
+
print(f"MT4Broker: Error closing positions: {e}")
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
def get_positions(self) -> Dict[str, Position]:
|
|
197
|
+
"""Get positions from MT4."""
|
|
198
|
+
positions = {}
|
|
199
|
+
|
|
200
|
+
if not self._connected:
|
|
201
|
+
print("MT4Broker: Not connected to MT4")
|
|
202
|
+
return positions
|
|
203
|
+
|
|
204
|
+
try:
|
|
205
|
+
# Request positions from MT4
|
|
206
|
+
self._execute_command("GetPositions()")
|
|
207
|
+
|
|
208
|
+
# Wait for EA to process
|
|
209
|
+
time.sleep(1.0)
|
|
210
|
+
|
|
211
|
+
# Read positions from MT4 response file
|
|
212
|
+
response_file = os.path.join(self._files_dir, 'ForexSmartBot_Positions.txt')
|
|
213
|
+
if os.path.exists(response_file):
|
|
214
|
+
with open(response_file, 'r') as f:
|
|
215
|
+
content = f.read().strip()
|
|
216
|
+
if content:
|
|
217
|
+
import json
|
|
218
|
+
positions_data = json.loads(content)
|
|
219
|
+
for pos_data in positions_data:
|
|
220
|
+
symbol = pos_data['symbol']
|
|
221
|
+
positions[symbol] = Position(
|
|
222
|
+
symbol=symbol,
|
|
223
|
+
side=pos_data['side'],
|
|
224
|
+
quantity=pos_data['volume'],
|
|
225
|
+
entry_price=pos_data['entry_price'],
|
|
226
|
+
current_price=pos_data['current_price'],
|
|
227
|
+
stop_loss=pos_data.get('sl', 0),
|
|
228
|
+
take_profit=pos_data.get('tp', 0),
|
|
229
|
+
pnl=pos_data.get('profit', 0)
|
|
230
|
+
)
|
|
231
|
+
print(f"MT4Broker: Retrieved {len(positions)} positions from MT4")
|
|
232
|
+
except Exception as e:
|
|
233
|
+
print(f"MT4Broker: Error reading positions: {e}")
|
|
234
|
+
|
|
235
|
+
return positions
|
|
236
|
+
|
|
237
|
+
def get_balance(self) -> float:
|
|
238
|
+
"""Get account balance from MT4."""
|
|
239
|
+
try:
|
|
240
|
+
# Request balance from MT4
|
|
241
|
+
self._execute_command("GetBalance()")
|
|
242
|
+
|
|
243
|
+
# Wait a moment for the EA to process
|
|
244
|
+
import time
|
|
245
|
+
time.sleep(0.5)
|
|
246
|
+
|
|
247
|
+
# Read balance from response file
|
|
248
|
+
response_file = os.path.join(self._files_dir, 'ForexSmartBot_Balance.txt')
|
|
249
|
+
if os.path.exists(response_file):
|
|
250
|
+
with open(response_file, 'r') as f:
|
|
251
|
+
content = f.read().strip()
|
|
252
|
+
if content:
|
|
253
|
+
balance = float(content)
|
|
254
|
+
print(f"MT4Broker: Got real balance from MT4: ${balance}")
|
|
255
|
+
return balance
|
|
256
|
+
except Exception as e:
|
|
257
|
+
print(f"MT4Broker: Error reading balance: {e}")
|
|
258
|
+
|
|
259
|
+
print("MT4Broker: Using fallback balance - EA not responding")
|
|
260
|
+
return 10000.0 # Fallback
|
|
261
|
+
|
|
262
|
+
def get_equity(self) -> float:
|
|
263
|
+
"""Get account equity from MT4."""
|
|
264
|
+
# For now, return a simulated equity
|
|
265
|
+
# In production, this would read from MT4's account data
|
|
266
|
+
return 10000.0
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Paper trading broker implementation."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Optional
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from ...core.interfaces import IBroker, Position, Order
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PaperBroker(IBroker):
|
|
9
|
+
"""Paper trading broker for backtesting and simulation."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, initial_balance: float = 10000.0):
|
|
12
|
+
self._balance = initial_balance
|
|
13
|
+
self._positions: Dict[str, Position] = {}
|
|
14
|
+
self._orders: Dict[str, Order] = {}
|
|
15
|
+
self._connected = False
|
|
16
|
+
self._order_counter = 0
|
|
17
|
+
|
|
18
|
+
def connect(self) -> bool:
|
|
19
|
+
"""Connect to paper broker (always successful)."""
|
|
20
|
+
self._connected = True
|
|
21
|
+
return True
|
|
22
|
+
|
|
23
|
+
def disconnect(self) -> None:
|
|
24
|
+
"""Disconnect from paper broker."""
|
|
25
|
+
self._connected = False
|
|
26
|
+
|
|
27
|
+
def is_connected(self) -> bool:
|
|
28
|
+
"""Check if connected."""
|
|
29
|
+
return self._connected
|
|
30
|
+
|
|
31
|
+
def get_price(self, symbol: str) -> Optional[float]:
|
|
32
|
+
"""Get current price with realistic simulation."""
|
|
33
|
+
# Get base price for symbol
|
|
34
|
+
base_prices = {
|
|
35
|
+
'EURUSD': 1.1800,
|
|
36
|
+
'GBPUSD': 1.2500,
|
|
37
|
+
'USDJPY': 150.0,
|
|
38
|
+
'AUDUSD': 0.7500,
|
|
39
|
+
'USDCAD': 1.3500
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
base_price = base_prices.get(symbol, 1.0000)
|
|
43
|
+
|
|
44
|
+
# Add realistic price movement
|
|
45
|
+
import random
|
|
46
|
+
import time
|
|
47
|
+
|
|
48
|
+
# Use time-based seed for consistent but changing prices
|
|
49
|
+
random.seed(int(time.time() / 60)) # Change every minute
|
|
50
|
+
price_change = random.uniform(-0.001, 0.001) # Small random change
|
|
51
|
+
|
|
52
|
+
return base_price + price_change
|
|
53
|
+
|
|
54
|
+
def submit_order(self, symbol: str, side: int, quantity: float,
|
|
55
|
+
stop_loss: Optional[float] = None,
|
|
56
|
+
take_profit: Optional[float] = None) -> Optional[str]:
|
|
57
|
+
"""Submit a paper order."""
|
|
58
|
+
if not self._connected:
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
order_id = f"PAPER_{self._order_counter}"
|
|
62
|
+
self._order_counter += 1
|
|
63
|
+
|
|
64
|
+
# Create order
|
|
65
|
+
order = Order(
|
|
66
|
+
symbol=symbol,
|
|
67
|
+
side=side,
|
|
68
|
+
quantity=quantity,
|
|
69
|
+
entry_price=0.0, # Will be filled with current price
|
|
70
|
+
stop_loss=stop_loss,
|
|
71
|
+
take_profit=take_profit
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
self._orders[order_id] = order
|
|
75
|
+
return order_id
|
|
76
|
+
|
|
77
|
+
def close_all(self, symbol: str) -> bool:
|
|
78
|
+
"""Close all positions for symbol."""
|
|
79
|
+
if symbol in self._positions:
|
|
80
|
+
del self._positions[symbol]
|
|
81
|
+
return True
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
def get_positions(self) -> Dict[str, Position]:
|
|
85
|
+
"""Get all open positions."""
|
|
86
|
+
return self._positions.copy()
|
|
87
|
+
|
|
88
|
+
def get_balance(self) -> float:
|
|
89
|
+
"""Get account balance."""
|
|
90
|
+
return self._balance
|
|
91
|
+
|
|
92
|
+
def get_equity(self) -> float:
|
|
93
|
+
"""Get account equity."""
|
|
94
|
+
unrealized_pnl = sum(pos.unrealized_pnl for pos in self._positions.values())
|
|
95
|
+
return self._balance + unrealized_pnl
|
|
96
|
+
|
|
97
|
+
def update_position(self, symbol: str, side: int, quantity: float,
|
|
98
|
+
entry_price: float, current_price: float) -> None:
|
|
99
|
+
"""Update or create a position."""
|
|
100
|
+
unrealized_pnl = side * quantity * (current_price - entry_price)
|
|
101
|
+
|
|
102
|
+
position = Position(
|
|
103
|
+
symbol=symbol,
|
|
104
|
+
side=side,
|
|
105
|
+
quantity=quantity,
|
|
106
|
+
entry_price=entry_price,
|
|
107
|
+
current_price=current_price,
|
|
108
|
+
unrealized_pnl=unrealized_pnl
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
self._positions[symbol] = position
|
|
112
|
+
|
|
113
|
+
def close_position(self, symbol: str, exit_price: float) -> Optional[float]:
|
|
114
|
+
"""Close a position and return realized PnL."""
|
|
115
|
+
if symbol not in self._positions:
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
position = self._positions.pop(symbol)
|
|
119
|
+
realized_pnl = position.side * position.quantity * (exit_price - position.entry_price)
|
|
120
|
+
self._balance += realized_pnl
|
|
121
|
+
|
|
122
|
+
return realized_pnl
|
|
123
|
+
|
|
124
|
+
def update_prices(self, prices: Dict[str, float]) -> None:
|
|
125
|
+
"""Update position prices and calculate unrealized PnL."""
|
|
126
|
+
for symbol, position in self._positions.items():
|
|
127
|
+
if symbol in prices:
|
|
128
|
+
position.current_price = prices[symbol]
|
|
129
|
+
position.unrealized_pnl = position.side * position.quantity * (
|
|
130
|
+
position.current_price - position.entry_price
|
|
131
|
+
)
|