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,326 @@
|
|
|
1
|
+
"""Real-time strategy testing on paper account."""
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import numpy as np
|
|
5
|
+
from typing import Dict, Any, Optional, Callable, List
|
|
6
|
+
from datetime import datetime, timedelta
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
from ..core.interfaces import IStrategy, IDataProvider
|
|
10
|
+
from ..adapters.brokers.paper_broker import PaperBroker
|
|
11
|
+
from ..core.portfolio import Portfolio
|
|
12
|
+
from ..core.risk_engine import RiskEngine, RiskConfig
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class PaperTestResult:
|
|
17
|
+
"""Result of paper account test."""
|
|
18
|
+
strategy_name: str
|
|
19
|
+
test_duration: timedelta
|
|
20
|
+
total_trades: int
|
|
21
|
+
winning_trades: int
|
|
22
|
+
losing_trades: int
|
|
23
|
+
total_return: float
|
|
24
|
+
sharpe_ratio: float
|
|
25
|
+
max_drawdown: float
|
|
26
|
+
win_rate: float
|
|
27
|
+
final_balance: float
|
|
28
|
+
trades: List[Dict[str, Any]]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class PaperAccountTester:
|
|
32
|
+
"""Test strategies in real-time on paper account."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, data_provider: IDataProvider,
|
|
35
|
+
initial_balance: float = 10000.0,
|
|
36
|
+
risk_config: Optional[RiskConfig] = None):
|
|
37
|
+
"""
|
|
38
|
+
Initialize paper account tester.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
data_provider: Data provider for market data
|
|
42
|
+
initial_balance: Initial paper account balance
|
|
43
|
+
risk_config: Risk management configuration
|
|
44
|
+
"""
|
|
45
|
+
self.data_provider = data_provider
|
|
46
|
+
self.initial_balance = initial_balance
|
|
47
|
+
self.risk_config = risk_config or RiskConfig()
|
|
48
|
+
self.broker = PaperBroker(initial_balance)
|
|
49
|
+
self.portfolio = Portfolio(initial_balance)
|
|
50
|
+
self.risk_engine = RiskEngine(self.risk_config)
|
|
51
|
+
self.active_tests: Dict[str, Dict[str, Any]] = {}
|
|
52
|
+
|
|
53
|
+
def start_test(self, strategy_name: str, strategy: IStrategy,
|
|
54
|
+
symbol: str, update_interval: int = 60) -> str:
|
|
55
|
+
"""
|
|
56
|
+
Start a paper account test for a strategy.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
strategy_name: Name of the strategy
|
|
60
|
+
strategy: Strategy instance
|
|
61
|
+
symbol: Trading symbol
|
|
62
|
+
update_interval: Update interval in seconds
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Test ID
|
|
66
|
+
"""
|
|
67
|
+
import uuid
|
|
68
|
+
test_id = str(uuid.uuid4())
|
|
69
|
+
|
|
70
|
+
self.active_tests[test_id] = {
|
|
71
|
+
'strategy_name': strategy_name,
|
|
72
|
+
'strategy': strategy,
|
|
73
|
+
'symbol': symbol,
|
|
74
|
+
'start_time': datetime.now(),
|
|
75
|
+
'update_interval': update_interval,
|
|
76
|
+
'trades': [],
|
|
77
|
+
'equity_history': [],
|
|
78
|
+
'last_update': datetime.now()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return test_id
|
|
82
|
+
|
|
83
|
+
def update_test(self, test_id: str) -> Dict[str, Any]:
|
|
84
|
+
"""
|
|
85
|
+
Update a paper account test with latest data.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
test_id: Test ID
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Update result
|
|
92
|
+
"""
|
|
93
|
+
if test_id not in self.active_tests:
|
|
94
|
+
return {'error': 'Test not found'}
|
|
95
|
+
|
|
96
|
+
test = self.active_tests[test_id]
|
|
97
|
+
strategy = test['strategy']
|
|
98
|
+
symbol = test['symbol']
|
|
99
|
+
|
|
100
|
+
# Get latest data
|
|
101
|
+
end_date = datetime.now().strftime('%Y-%m-%d')
|
|
102
|
+
start_date = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d')
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
df = self.data_provider.get_data(symbol, start_date, end_date, '1h')
|
|
106
|
+
if df.empty:
|
|
107
|
+
return {'error': 'No data available'}
|
|
108
|
+
|
|
109
|
+
# Calculate indicators
|
|
110
|
+
df = strategy.indicators(df)
|
|
111
|
+
|
|
112
|
+
# Generate signal
|
|
113
|
+
signal = strategy.signal(df)
|
|
114
|
+
volatility = strategy.volatility(df)
|
|
115
|
+
current_price = float(df['Close'].iloc[-1])
|
|
116
|
+
|
|
117
|
+
# Process trading logic
|
|
118
|
+
if signal != 0:
|
|
119
|
+
self._process_signal(test_id, strategy, symbol, signal, current_price, volatility, df)
|
|
120
|
+
else:
|
|
121
|
+
self._check_exit_conditions(test_id, symbol, current_price, strategy, df)
|
|
122
|
+
|
|
123
|
+
# Update portfolio
|
|
124
|
+
self.portfolio.update_equity(self.broker.get_balance())
|
|
125
|
+
|
|
126
|
+
# Record equity
|
|
127
|
+
test['equity_history'].append({
|
|
128
|
+
'timestamp': datetime.now(),
|
|
129
|
+
'equity': self.portfolio.get_total_equity()
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
test['last_update'] = datetime.now()
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
'test_id': test_id,
|
|
136
|
+
'current_equity': self.portfolio.get_total_equity(),
|
|
137
|
+
'total_return': (self.portfolio.get_total_equity() / self.initial_balance) - 1,
|
|
138
|
+
'total_trades': len(test['trades']),
|
|
139
|
+
'signal': signal
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
except Exception as e:
|
|
143
|
+
return {'error': str(e)}
|
|
144
|
+
|
|
145
|
+
def _process_signal(self, test_id: str, strategy: IStrategy, symbol: str,
|
|
146
|
+
signal: int, current_price: float, volatility: Optional[float],
|
|
147
|
+
df: pd.DataFrame) -> None:
|
|
148
|
+
"""Process trading signal."""
|
|
149
|
+
test = self.active_tests[test_id]
|
|
150
|
+
positions = test.get('positions', {})
|
|
151
|
+
|
|
152
|
+
# Close opposite position
|
|
153
|
+
if symbol in positions:
|
|
154
|
+
pos = positions[symbol]
|
|
155
|
+
if (signal > 0 and pos['side'] < 0) or (signal < 0 and pos['side'] > 0):
|
|
156
|
+
self.broker.close_position(symbol, current_price)
|
|
157
|
+
profit = (current_price - pos['entry_price']) * pos['side'] * pos['size']
|
|
158
|
+
test['trades'].append({
|
|
159
|
+
'symbol': symbol,
|
|
160
|
+
'side': pos['side'],
|
|
161
|
+
'entry_price': pos['entry_price'],
|
|
162
|
+
'exit_price': current_price,
|
|
163
|
+
'size': pos['size'],
|
|
164
|
+
'profit': profit,
|
|
165
|
+
'entry_time': pos['entry_time'],
|
|
166
|
+
'exit_time': datetime.now()
|
|
167
|
+
})
|
|
168
|
+
del positions[symbol]
|
|
169
|
+
|
|
170
|
+
# Open new position
|
|
171
|
+
if symbol not in positions:
|
|
172
|
+
size = self.risk_engine.calculate_position_size(
|
|
173
|
+
self.portfolio.get_total_balance(), volatility or 0.01, signal
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
if size > 0:
|
|
177
|
+
sl = strategy.stop_loss(df, current_price, signal)
|
|
178
|
+
tp = strategy.take_profit(df, current_price, signal)
|
|
179
|
+
|
|
180
|
+
self.broker.open_position(symbol, signal, size, current_price, sl, tp)
|
|
181
|
+
positions[symbol] = {
|
|
182
|
+
'side': signal,
|
|
183
|
+
'entry_price': current_price,
|
|
184
|
+
'size': size,
|
|
185
|
+
'entry_time': datetime.now(),
|
|
186
|
+
'stop_loss': sl,
|
|
187
|
+
'take_profit': tp
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
test['positions'] = positions
|
|
191
|
+
|
|
192
|
+
def _check_exit_conditions(self, test_id: str, symbol: str, current_price: float,
|
|
193
|
+
strategy: IStrategy, df: pd.DataFrame) -> None:
|
|
194
|
+
"""Check exit conditions for open positions."""
|
|
195
|
+
test = self.active_tests[test_id]
|
|
196
|
+
positions = test.get('positions', {})
|
|
197
|
+
|
|
198
|
+
if symbol not in positions:
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
pos = positions[symbol]
|
|
202
|
+
|
|
203
|
+
# Check stop loss
|
|
204
|
+
if pos.get('stop_loss') and (
|
|
205
|
+
(pos['side'] > 0 and current_price <= pos['stop_loss']) or
|
|
206
|
+
(pos['side'] < 0 and current_price >= pos['stop_loss'])
|
|
207
|
+
):
|
|
208
|
+
self.broker.close_position(symbol, current_price)
|
|
209
|
+
profit = (current_price - pos['entry_price']) * pos['side'] * pos['size']
|
|
210
|
+
test['trades'].append({
|
|
211
|
+
'symbol': symbol,
|
|
212
|
+
'side': pos['side'],
|
|
213
|
+
'entry_price': pos['entry_price'],
|
|
214
|
+
'exit_price': current_price,
|
|
215
|
+
'size': pos['size'],
|
|
216
|
+
'profit': profit,
|
|
217
|
+
'entry_time': pos['entry_time'],
|
|
218
|
+
'exit_time': datetime.now(),
|
|
219
|
+
'exit_reason': 'stop_loss'
|
|
220
|
+
})
|
|
221
|
+
del positions[symbol]
|
|
222
|
+
|
|
223
|
+
# Check take profit
|
|
224
|
+
if pos.get('take_profit') and (
|
|
225
|
+
(pos['side'] > 0 and current_price >= pos['take_profit']) or
|
|
226
|
+
(pos['side'] < 0 and current_price <= pos['take_profit'])
|
|
227
|
+
):
|
|
228
|
+
self.broker.close_position(symbol, current_price)
|
|
229
|
+
profit = (current_price - pos['entry_price']) * pos['side'] * pos['size']
|
|
230
|
+
test['trades'].append({
|
|
231
|
+
'symbol': symbol,
|
|
232
|
+
'side': pos['side'],
|
|
233
|
+
'entry_price': pos['entry_price'],
|
|
234
|
+
'exit_price': current_price,
|
|
235
|
+
'size': pos['size'],
|
|
236
|
+
'profit': profit,
|
|
237
|
+
'entry_time': pos['entry_time'],
|
|
238
|
+
'exit_time': datetime.now(),
|
|
239
|
+
'exit_reason': 'take_profit'
|
|
240
|
+
})
|
|
241
|
+
del positions[symbol]
|
|
242
|
+
|
|
243
|
+
test['positions'] = positions
|
|
244
|
+
|
|
245
|
+
def stop_test(self, test_id: str) -> PaperTestResult:
|
|
246
|
+
"""
|
|
247
|
+
Stop a paper account test and get results.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
test_id: Test ID
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
PaperTestResult
|
|
254
|
+
"""
|
|
255
|
+
if test_id not in self.active_tests:
|
|
256
|
+
raise ValueError(f"Test {test_id} not found")
|
|
257
|
+
|
|
258
|
+
test = self.active_tests[test_id]
|
|
259
|
+
test_duration = datetime.now() - test['start_time']
|
|
260
|
+
|
|
261
|
+
trades = test['trades']
|
|
262
|
+
winning_trades = [t for t in trades if t.get('profit', 0) > 0]
|
|
263
|
+
losing_trades = [t for t in trades if t.get('profit', 0) < 0]
|
|
264
|
+
|
|
265
|
+
# Calculate metrics
|
|
266
|
+
returns = [t.get('profit', 0) / self.initial_balance for t in trades]
|
|
267
|
+
total_return = sum(returns)
|
|
268
|
+
|
|
269
|
+
if len(returns) > 1:
|
|
270
|
+
sharpe_ratio = np.sqrt(252) * np.mean(returns) / np.std(returns) if np.std(returns) > 0 else 0
|
|
271
|
+
else:
|
|
272
|
+
sharpe_ratio = 0
|
|
273
|
+
|
|
274
|
+
# Calculate max drawdown from equity history
|
|
275
|
+
equity_values = [e['equity'] for e in test['equity_history']]
|
|
276
|
+
if equity_values:
|
|
277
|
+
equity_series = pd.Series(equity_values)
|
|
278
|
+
running_max = equity_series.expanding().max()
|
|
279
|
+
drawdown = (equity_series - running_max) / running_max
|
|
280
|
+
max_drawdown = abs(drawdown.min())
|
|
281
|
+
else:
|
|
282
|
+
max_drawdown = 0.0
|
|
283
|
+
|
|
284
|
+
win_rate = len(winning_trades) / len(trades) if trades else 0
|
|
285
|
+
|
|
286
|
+
result = PaperTestResult(
|
|
287
|
+
strategy_name=test['strategy_name'],
|
|
288
|
+
test_duration=test_duration,
|
|
289
|
+
total_trades=len(trades),
|
|
290
|
+
winning_trades=len(winning_trades),
|
|
291
|
+
losing_trades=len(losing_trades),
|
|
292
|
+
total_return=total_return,
|
|
293
|
+
sharpe_ratio=sharpe_ratio,
|
|
294
|
+
max_drawdown=max_drawdown,
|
|
295
|
+
win_rate=win_rate,
|
|
296
|
+
final_balance=self.portfolio.get_total_equity(),
|
|
297
|
+
trades=trades
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
# Remove test
|
|
301
|
+
del self.active_tests[test_id]
|
|
302
|
+
|
|
303
|
+
return result
|
|
304
|
+
|
|
305
|
+
def get_active_tests(self) -> List[str]:
|
|
306
|
+
"""Get list of active test IDs."""
|
|
307
|
+
return list(self.active_tests.keys())
|
|
308
|
+
|
|
309
|
+
def get_test_status(self, test_id: str) -> Dict[str, Any]:
|
|
310
|
+
"""Get current status of a test."""
|
|
311
|
+
if test_id not in self.active_tests:
|
|
312
|
+
return {'error': 'Test not found'}
|
|
313
|
+
|
|
314
|
+
test = self.active_tests[test_id]
|
|
315
|
+
return {
|
|
316
|
+
'test_id': test_id,
|
|
317
|
+
'strategy_name': test['strategy_name'],
|
|
318
|
+
'symbol': test['symbol'],
|
|
319
|
+
'start_time': test['start_time'],
|
|
320
|
+
'duration': datetime.now() - test['start_time'],
|
|
321
|
+
'total_trades': len(test['trades']),
|
|
322
|
+
'current_equity': self.portfolio.get_total_equity(),
|
|
323
|
+
'total_return': (self.portfolio.get_total_equity() / self.initial_balance) - 1,
|
|
324
|
+
'last_update': test['last_update']
|
|
325
|
+
}
|
|
326
|
+
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""Strategy sandbox environment for safe testing."""
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import numpy as np
|
|
5
|
+
from typing import Dict, Any, Optional, List, Callable
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
import warnings
|
|
8
|
+
|
|
9
|
+
from ..core.interfaces import IStrategy
|
|
10
|
+
from ..adapters.brokers.paper_broker import PaperBroker
|
|
11
|
+
from ..core.portfolio import Portfolio
|
|
12
|
+
from ..core.risk_engine import RiskEngine, RiskConfig
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class StrategySandbox:
|
|
16
|
+
"""Sandbox environment for safe strategy testing."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, initial_balance: float = 10000.0,
|
|
19
|
+
risk_config: Optional[RiskConfig] = None,
|
|
20
|
+
max_drawdown_limit: float = 0.5,
|
|
21
|
+
max_loss_per_trade: float = 0.1):
|
|
22
|
+
"""
|
|
23
|
+
Initialize strategy sandbox.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
initial_balance: Initial balance for testing
|
|
27
|
+
risk_config: Risk management configuration
|
|
28
|
+
max_drawdown_limit: Maximum allowed drawdown (0.5 = 50%)
|
|
29
|
+
max_loss_per_trade: Maximum loss per trade (0.1 = 10%)
|
|
30
|
+
"""
|
|
31
|
+
self.initial_balance = initial_balance
|
|
32
|
+
self.risk_config = risk_config or RiskConfig()
|
|
33
|
+
self.max_drawdown_limit = max_drawdown_limit
|
|
34
|
+
self.max_loss_per_trade = max_loss_per_trade
|
|
35
|
+
self.broker = PaperBroker(initial_balance)
|
|
36
|
+
self.portfolio = Portfolio(initial_balance)
|
|
37
|
+
self.risk_engine = RiskEngine(self.risk_config)
|
|
38
|
+
self.safety_checks_enabled = True
|
|
39
|
+
|
|
40
|
+
def test_strategy(self, strategy: IStrategy, df: pd.DataFrame,
|
|
41
|
+
symbol: str = "TEST") -> Dict[str, Any]:
|
|
42
|
+
"""
|
|
43
|
+
Test strategy in sandbox environment.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
strategy: Strategy to test
|
|
47
|
+
df: Historical data
|
|
48
|
+
symbol: Trading symbol
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Test results with safety checks
|
|
52
|
+
"""
|
|
53
|
+
# Reset sandbox
|
|
54
|
+
self.broker = PaperBroker(self.initial_balance)
|
|
55
|
+
self.portfolio = Portfolio(self.initial_balance)
|
|
56
|
+
|
|
57
|
+
positions = {}
|
|
58
|
+
trades = []
|
|
59
|
+
equity_history = []
|
|
60
|
+
safety_violations = []
|
|
61
|
+
|
|
62
|
+
for i in range(len(df)):
|
|
63
|
+
try:
|
|
64
|
+
current_data = df.iloc[:i+1]
|
|
65
|
+
|
|
66
|
+
if len(current_data) < 20:
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
# Calculate indicators
|
|
70
|
+
current_data = strategy.indicators(current_data)
|
|
71
|
+
|
|
72
|
+
# Get current price
|
|
73
|
+
current_price = float(current_data['Close'].iloc[-1])
|
|
74
|
+
|
|
75
|
+
# Generate signal
|
|
76
|
+
signal = strategy.signal(current_data)
|
|
77
|
+
volatility = strategy.volatility(current_data)
|
|
78
|
+
|
|
79
|
+
# Safety check: Validate signal
|
|
80
|
+
if signal not in [-1, 0, 1]:
|
|
81
|
+
safety_violations.append({
|
|
82
|
+
'step': i,
|
|
83
|
+
'type': 'invalid_signal',
|
|
84
|
+
'message': f'Invalid signal value: {signal}'
|
|
85
|
+
})
|
|
86
|
+
signal = 0
|
|
87
|
+
|
|
88
|
+
# Process trading logic
|
|
89
|
+
if signal != 0:
|
|
90
|
+
# Safety check: Position size
|
|
91
|
+
size = self.risk_engine.calculate_position_size(
|
|
92
|
+
self.portfolio.get_total_balance(), volatility or 0.01, signal
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Check max loss per trade
|
|
96
|
+
potential_loss = abs(size * current_price * self.max_loss_per_trade)
|
|
97
|
+
if potential_loss > self.portfolio.get_total_balance() * self.max_loss_per_trade:
|
|
98
|
+
safety_violations.append({
|
|
99
|
+
'step': i,
|
|
100
|
+
'type': 'max_loss_exceeded',
|
|
101
|
+
'message': f'Potential loss {potential_loss:.2f} exceeds limit'
|
|
102
|
+
})
|
|
103
|
+
size = self.portfolio.get_total_balance() * self.max_loss_per_trade / current_price
|
|
104
|
+
|
|
105
|
+
if size > 0:
|
|
106
|
+
sl = strategy.stop_loss(current_data, current_price, signal)
|
|
107
|
+
tp = strategy.take_profit(current_data, current_price, signal)
|
|
108
|
+
|
|
109
|
+
# Safety check: SL/TP validation
|
|
110
|
+
if sl and tp:
|
|
111
|
+
if signal > 0: # Long
|
|
112
|
+
if sl >= current_price:
|
|
113
|
+
safety_violations.append({
|
|
114
|
+
'step': i,
|
|
115
|
+
'type': 'invalid_sl',
|
|
116
|
+
'message': 'Stop loss above entry for long'
|
|
117
|
+
})
|
|
118
|
+
sl = current_price * 0.98 # Default 2% SL
|
|
119
|
+
if tp <= current_price:
|
|
120
|
+
safety_violations.append({
|
|
121
|
+
'step': i,
|
|
122
|
+
'type': 'invalid_tp',
|
|
123
|
+
'message': 'Take profit below entry for long'
|
|
124
|
+
})
|
|
125
|
+
tp = current_price * 1.04 # Default 4% TP
|
|
126
|
+
|
|
127
|
+
# Open position
|
|
128
|
+
self.broker.open_position(symbol, signal, size, current_price, sl, tp)
|
|
129
|
+
positions[symbol] = {
|
|
130
|
+
'side': signal,
|
|
131
|
+
'entry_price': current_price,
|
|
132
|
+
'size': size,
|
|
133
|
+
'entry_time': datetime.now()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
# Check exit conditions
|
|
137
|
+
if symbol in positions:
|
|
138
|
+
pos = positions[symbol]
|
|
139
|
+
|
|
140
|
+
# Check stop loss
|
|
141
|
+
if pos.get('stop_loss') and (
|
|
142
|
+
(pos['side'] > 0 and current_price <= pos['stop_loss']) or
|
|
143
|
+
(pos['side'] < 0 and current_price >= pos['stop_loss'])
|
|
144
|
+
):
|
|
145
|
+
self.broker.close_position(symbol, current_price)
|
|
146
|
+
profit = (current_price - pos['entry_price']) * pos['side'] * pos['size']
|
|
147
|
+
trades.append({
|
|
148
|
+
'profit': profit,
|
|
149
|
+
'entry_price': pos['entry_price'],
|
|
150
|
+
'exit_price': current_price,
|
|
151
|
+
'exit_reason': 'stop_loss'
|
|
152
|
+
})
|
|
153
|
+
del positions[symbol]
|
|
154
|
+
|
|
155
|
+
# Check take profit
|
|
156
|
+
if pos.get('take_profit') and (
|
|
157
|
+
(pos['side'] > 0 and current_price >= pos['take_profit']) or
|
|
158
|
+
(pos['side'] < 0 and current_price <= pos['take_profit'])
|
|
159
|
+
):
|
|
160
|
+
self.broker.close_position(symbol, current_price)
|
|
161
|
+
profit = (current_price - pos['entry_price']) * pos['side'] * pos['size']
|
|
162
|
+
trades.append({
|
|
163
|
+
'profit': profit,
|
|
164
|
+
'entry_price': pos['entry_price'],
|
|
165
|
+
'exit_price': current_price,
|
|
166
|
+
'exit_reason': 'take_profit'
|
|
167
|
+
})
|
|
168
|
+
del positions[symbol]
|
|
169
|
+
|
|
170
|
+
# Update portfolio
|
|
171
|
+
self.portfolio.update_equity(self.broker.get_balance())
|
|
172
|
+
equity_history.append(self.portfolio.get_total_equity())
|
|
173
|
+
|
|
174
|
+
# Safety check: Max drawdown
|
|
175
|
+
if equity_history:
|
|
176
|
+
equity_series = pd.Series(equity_history)
|
|
177
|
+
running_max = equity_series.expanding().max()
|
|
178
|
+
drawdown = (equity_series - running_max) / running_max
|
|
179
|
+
current_drawdown = abs(drawdown.iloc[-1])
|
|
180
|
+
|
|
181
|
+
if current_drawdown > self.max_drawdown_limit:
|
|
182
|
+
safety_violations.append({
|
|
183
|
+
'step': i,
|
|
184
|
+
'type': 'max_drawdown_exceeded',
|
|
185
|
+
'message': f'Drawdown {current_drawdown:.2%} exceeds limit {self.max_drawdown_limit:.2%}'
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
# Auto-stop if drawdown too high
|
|
189
|
+
if self.safety_checks_enabled:
|
|
190
|
+
break
|
|
191
|
+
|
|
192
|
+
except Exception as e:
|
|
193
|
+
safety_violations.append({
|
|
194
|
+
'step': i,
|
|
195
|
+
'type': 'exception',
|
|
196
|
+
'message': str(e)
|
|
197
|
+
})
|
|
198
|
+
continue
|
|
199
|
+
|
|
200
|
+
# Calculate final metrics
|
|
201
|
+
total_return = (self.portfolio.get_total_equity() / self.initial_balance) - 1
|
|
202
|
+
|
|
203
|
+
# Calculate max drawdown
|
|
204
|
+
if equity_history:
|
|
205
|
+
equity_series = pd.Series(equity_history)
|
|
206
|
+
running_max = equity_series.expanding().max()
|
|
207
|
+
drawdown = (equity_series - running_max) / running_max
|
|
208
|
+
max_drawdown = abs(drawdown.min())
|
|
209
|
+
else:
|
|
210
|
+
max_drawdown = 0.0
|
|
211
|
+
|
|
212
|
+
winning_trades = [t for t in trades if t.get('profit', 0) > 0]
|
|
213
|
+
win_rate = len(winning_trades) / len(trades) if trades else 0
|
|
214
|
+
|
|
215
|
+
# Safety assessment
|
|
216
|
+
is_safe = len(safety_violations) == 0 and max_drawdown <= self.max_drawdown_limit
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
'strategy_name': strategy.name,
|
|
220
|
+
'total_return': total_return,
|
|
221
|
+
'max_drawdown': max_drawdown,
|
|
222
|
+
'total_trades': len(trades),
|
|
223
|
+
'winning_trades': len(winning_trades),
|
|
224
|
+
'win_rate': win_rate,
|
|
225
|
+
'final_balance': self.portfolio.get_total_equity(),
|
|
226
|
+
'safety_violations': safety_violations,
|
|
227
|
+
'is_safe': is_safe,
|
|
228
|
+
'trades': trades,
|
|
229
|
+
'equity_history': equity_history
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
def validate_strategy(self, strategy: IStrategy, df: pd.DataFrame) -> Dict[str, Any]:
|
|
233
|
+
"""
|
|
234
|
+
Validate strategy safety without full backtest.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
strategy: Strategy to validate
|
|
238
|
+
df: Sample data
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
Validation results
|
|
242
|
+
"""
|
|
243
|
+
validation_results = {
|
|
244
|
+
'strategy_name': strategy.name,
|
|
245
|
+
'is_valid': True,
|
|
246
|
+
'warnings': [],
|
|
247
|
+
'errors': []
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
# Check strategy interface
|
|
251
|
+
required_methods = ['indicators', 'signal', 'volatility', 'stop_loss', 'take_profit']
|
|
252
|
+
for method in required_methods:
|
|
253
|
+
if not hasattr(strategy, method):
|
|
254
|
+
validation_results['errors'].append(f'Missing method: {method}')
|
|
255
|
+
validation_results['is_valid'] = False
|
|
256
|
+
|
|
257
|
+
# Test indicator calculation
|
|
258
|
+
try:
|
|
259
|
+
df_with_indicators = strategy.indicators(df)
|
|
260
|
+
if df_with_indicators.empty:
|
|
261
|
+
validation_results['warnings'].append('Indicators return empty DataFrame')
|
|
262
|
+
except Exception as e:
|
|
263
|
+
validation_results['errors'].append(f'Indicator calculation error: {e}')
|
|
264
|
+
validation_results['is_valid'] = False
|
|
265
|
+
|
|
266
|
+
# Test signal generation
|
|
267
|
+
try:
|
|
268
|
+
signal = strategy.signal(df_with_indicators if 'df_with_indicators' in locals() else df)
|
|
269
|
+
if signal not in [-1, 0, 1]:
|
|
270
|
+
validation_results['warnings'].append(f'Signal value {signal} not in [-1, 0, 1]')
|
|
271
|
+
except Exception as e:
|
|
272
|
+
validation_results['errors'].append(f'Signal generation error: {e}')
|
|
273
|
+
validation_results['is_valid'] = False
|
|
274
|
+
|
|
275
|
+
# Test volatility calculation
|
|
276
|
+
try:
|
|
277
|
+
volatility = strategy.volatility(df_with_indicators if 'df_with_indicators' in locals() else df)
|
|
278
|
+
if volatility is not None and (volatility < 0 or volatility > 1):
|
|
279
|
+
validation_results['warnings'].append(f'Volatility value {volatility} outside [0, 1]')
|
|
280
|
+
except Exception as e:
|
|
281
|
+
validation_results['warnings'].append(f'Volatility calculation error: {e}')
|
|
282
|
+
|
|
283
|
+
return validation_results
|
|
284
|
+
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""UI components for ForexSmartBot."""
|
|
2
|
+
|
|
3
|
+
from .main_window import MainWindow
|
|
4
|
+
from .settings_dialog import SettingsDialog
|
|
5
|
+
from .charts import ChartWidget
|
|
6
|
+
from .theme import ThemeManager
|
|
7
|
+
|
|
8
|
+
__all__ = ['MainWindow', 'SettingsDialog', 'ChartWidget', 'ThemeManager']
|