mcli-framework 7.1.1__py3-none-any.whl → 7.1.2__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.
Potentially problematic release.
This version of mcli-framework might be problematic. Click here for more details.
- mcli/app/completion_cmd.py +59 -49
- mcli/app/completion_helpers.py +60 -138
- mcli/app/logs_cmd.py +6 -2
- mcli/app/main.py +17 -14
- mcli/app/model_cmd.py +19 -4
- mcli/chat/chat.py +3 -2
- mcli/lib/search/cached_vectorizer.py +1 -0
- mcli/lib/services/data_pipeline.py +12 -5
- mcli/lib/services/lsh_client.py +68 -57
- mcli/ml/api/app.py +28 -36
- mcli/ml/api/middleware.py +8 -16
- mcli/ml/api/routers/admin_router.py +3 -1
- mcli/ml/api/routers/auth_router.py +32 -56
- mcli/ml/api/routers/backtest_router.py +3 -1
- mcli/ml/api/routers/data_router.py +3 -1
- mcli/ml/api/routers/model_router.py +35 -74
- mcli/ml/api/routers/monitoring_router.py +3 -1
- mcli/ml/api/routers/portfolio_router.py +3 -1
- mcli/ml/api/routers/prediction_router.py +60 -65
- mcli/ml/api/routers/trade_router.py +6 -2
- mcli/ml/api/routers/websocket_router.py +12 -9
- mcli/ml/api/schemas.py +10 -2
- mcli/ml/auth/auth_manager.py +49 -114
- mcli/ml/auth/models.py +30 -15
- mcli/ml/auth/permissions.py +12 -19
- mcli/ml/backtesting/backtest_engine.py +134 -108
- mcli/ml/backtesting/performance_metrics.py +142 -108
- mcli/ml/cache.py +12 -18
- mcli/ml/cli/main.py +37 -23
- mcli/ml/config/settings.py +29 -12
- mcli/ml/dashboard/app.py +122 -130
- mcli/ml/dashboard/app_integrated.py +216 -150
- mcli/ml/dashboard/app_supabase.py +176 -108
- mcli/ml/dashboard/app_training.py +212 -206
- mcli/ml/dashboard/cli.py +14 -5
- mcli/ml/data_ingestion/api_connectors.py +51 -81
- mcli/ml/data_ingestion/data_pipeline.py +127 -125
- mcli/ml/data_ingestion/stream_processor.py +72 -80
- mcli/ml/database/migrations/env.py +3 -2
- mcli/ml/database/models.py +112 -79
- mcli/ml/database/session.py +6 -5
- mcli/ml/experimentation/ab_testing.py +149 -99
- mcli/ml/features/ensemble_features.py +9 -8
- mcli/ml/features/political_features.py +6 -5
- mcli/ml/features/recommendation_engine.py +15 -14
- mcli/ml/features/stock_features.py +7 -6
- mcli/ml/features/test_feature_engineering.py +8 -7
- mcli/ml/logging.py +10 -15
- mcli/ml/mlops/data_versioning.py +57 -64
- mcli/ml/mlops/experiment_tracker.py +49 -41
- mcli/ml/mlops/model_serving.py +59 -62
- mcli/ml/mlops/pipeline_orchestrator.py +203 -149
- mcli/ml/models/base_models.py +8 -7
- mcli/ml/models/ensemble_models.py +6 -5
- mcli/ml/models/recommendation_models.py +7 -6
- mcli/ml/models/test_models.py +18 -14
- mcli/ml/monitoring/drift_detection.py +95 -74
- mcli/ml/monitoring/metrics.py +10 -22
- mcli/ml/optimization/portfolio_optimizer.py +172 -132
- mcli/ml/predictions/prediction_engine.py +62 -50
- mcli/ml/preprocessing/data_cleaners.py +6 -5
- mcli/ml/preprocessing/feature_extractors.py +7 -6
- mcli/ml/preprocessing/ml_pipeline.py +3 -2
- mcli/ml/preprocessing/politician_trading_preprocessor.py +11 -10
- mcli/ml/preprocessing/test_preprocessing.py +4 -4
- mcli/ml/scripts/populate_sample_data.py +36 -16
- mcli/ml/tasks.py +82 -83
- mcli/ml/tests/test_integration.py +86 -76
- mcli/ml/tests/test_training_dashboard.py +169 -142
- mcli/mygroup/test_cmd.py +2 -1
- mcli/self/self_cmd.py +31 -16
- mcli/self/test_cmd.py +2 -1
- mcli/workflow/dashboard/dashboard_cmd.py +13 -6
- mcli/workflow/lsh_integration.py +46 -58
- mcli/workflow/politician_trading/commands.py +576 -427
- mcli/workflow/politician_trading/config.py +7 -7
- mcli/workflow/politician_trading/connectivity.py +35 -33
- mcli/workflow/politician_trading/data_sources.py +72 -71
- mcli/workflow/politician_trading/database.py +18 -16
- mcli/workflow/politician_trading/demo.py +4 -3
- mcli/workflow/politician_trading/models.py +5 -5
- mcli/workflow/politician_trading/monitoring.py +13 -13
- mcli/workflow/politician_trading/scrapers.py +332 -224
- mcli/workflow/politician_trading/scrapers_california.py +116 -94
- mcli/workflow/politician_trading/scrapers_eu.py +70 -71
- mcli/workflow/politician_trading/scrapers_uk.py +118 -90
- mcli/workflow/politician_trading/scrapers_us_states.py +125 -92
- mcli/workflow/politician_trading/workflow.py +98 -71
- {mcli_framework-7.1.1.dist-info → mcli_framework-7.1.2.dist-info}/METADATA +1 -1
- {mcli_framework-7.1.1.dist-info → mcli_framework-7.1.2.dist-info}/RECORD +94 -94
- {mcli_framework-7.1.1.dist-info → mcli_framework-7.1.2.dist-info}/WHEEL +0 -0
- {mcli_framework-7.1.1.dist-info → mcli_framework-7.1.2.dist-info}/entry_points.txt +0 -0
- {mcli_framework-7.1.1.dist-info → mcli_framework-7.1.2.dist-info}/licenses/LICENSE +0 -0
- {mcli_framework-7.1.1.dist-info → mcli_framework-7.1.2.dist-info}/top_level.txt +0 -0
|
@@ -1,26 +1,28 @@
|
|
|
1
1
|
"""Backtesting engine for trading strategies"""
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
6
7
|
from dataclasses import dataclass, field
|
|
7
8
|
from datetime import datetime, timedelta
|
|
8
9
|
from enum import Enum
|
|
9
|
-
import logging
|
|
10
10
|
from pathlib import Path
|
|
11
|
-
import
|
|
11
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
import pandas as pd
|
|
12
15
|
|
|
13
|
-
import sys
|
|
14
|
-
import os
|
|
15
16
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../.."))
|
|
16
17
|
|
|
17
|
-
from ml.models.recommendation_models import
|
|
18
|
+
from ml.models.recommendation_models import PortfolioRecommendation, StockRecommendationModel
|
|
18
19
|
|
|
19
20
|
logger = logging.getLogger(__name__)
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
class OrderType(Enum):
|
|
23
24
|
"""Order types"""
|
|
25
|
+
|
|
24
26
|
MARKET = "market"
|
|
25
27
|
LIMIT = "limit"
|
|
26
28
|
STOP = "stop"
|
|
@@ -29,6 +31,7 @@ class OrderType(Enum):
|
|
|
29
31
|
|
|
30
32
|
class OrderSide(Enum):
|
|
31
33
|
"""Order side"""
|
|
34
|
+
|
|
32
35
|
BUY = "buy"
|
|
33
36
|
SELL = "sell"
|
|
34
37
|
|
|
@@ -36,6 +39,7 @@ class OrderSide(Enum):
|
|
|
36
39
|
@dataclass
|
|
37
40
|
class BacktestConfig:
|
|
38
41
|
"""Backtesting configuration"""
|
|
42
|
+
|
|
39
43
|
initial_capital: float = 100000.0
|
|
40
44
|
start_date: Optional[datetime] = None
|
|
41
45
|
end_date: Optional[datetime] = None
|
|
@@ -54,6 +58,7 @@ class BacktestConfig:
|
|
|
54
58
|
@dataclass
|
|
55
59
|
class BacktestResult:
|
|
56
60
|
"""Backtesting results"""
|
|
61
|
+
|
|
57
62
|
portfolio_value: pd.Series
|
|
58
63
|
returns: pd.Series
|
|
59
64
|
positions: pd.DataFrame
|
|
@@ -71,9 +76,9 @@ class TradingStrategy:
|
|
|
71
76
|
self.current_positions = {}
|
|
72
77
|
self.pending_orders = []
|
|
73
78
|
|
|
74
|
-
def generate_signals(
|
|
75
|
-
|
|
76
|
-
|
|
79
|
+
def generate_signals(
|
|
80
|
+
self, data: pd.DataFrame, current_date: datetime, portfolio_value: float
|
|
81
|
+
) -> List[Dict[str, Any]]:
|
|
77
82
|
"""Generate trading signals"""
|
|
78
83
|
signals = []
|
|
79
84
|
|
|
@@ -89,7 +94,7 @@ class TradingStrategy:
|
|
|
89
94
|
"position_size": rec.position_size,
|
|
90
95
|
"entry_price": rec.entry_price,
|
|
91
96
|
"target_price": rec.target_price,
|
|
92
|
-
"stop_loss": rec.stop_loss
|
|
97
|
+
"stop_loss": rec.stop_loss,
|
|
93
98
|
}
|
|
94
99
|
signals.append(signal)
|
|
95
100
|
else:
|
|
@@ -98,17 +103,18 @@ class TradingStrategy:
|
|
|
98
103
|
|
|
99
104
|
return signals
|
|
100
105
|
|
|
101
|
-
def _get_model_recommendations(
|
|
102
|
-
|
|
106
|
+
def _get_model_recommendations(
|
|
107
|
+
self, data: pd.DataFrame, current_date: datetime
|
|
108
|
+
) -> List[PortfolioRecommendation]:
|
|
103
109
|
"""Get recommendations from ML model"""
|
|
104
110
|
# Filter data up to current date
|
|
105
|
-
historical_data = data[data[
|
|
111
|
+
historical_data = data[data["date"] <= current_date]
|
|
106
112
|
|
|
107
113
|
# Extract features (simplified)
|
|
108
114
|
features = historical_data.select_dtypes(include=[np.number]).values[-1:, :]
|
|
109
115
|
|
|
110
116
|
# Get unique tickers
|
|
111
|
-
tickers = historical_data[
|
|
117
|
+
tickers = historical_data["symbol"].unique()[:5] # Limit to 5 for speed
|
|
112
118
|
|
|
113
119
|
# Generate recommendations
|
|
114
120
|
try:
|
|
@@ -118,42 +124,42 @@ class TradingStrategy:
|
|
|
118
124
|
logger.warning(f"Model prediction failed: {e}")
|
|
119
125
|
return []
|
|
120
126
|
|
|
121
|
-
def _momentum_strategy(
|
|
122
|
-
|
|
127
|
+
def _momentum_strategy(
|
|
128
|
+
self, data: pd.DataFrame, current_date: datetime
|
|
129
|
+
) -> List[Dict[str, Any]]:
|
|
123
130
|
"""Simple momentum strategy"""
|
|
124
131
|
signals = []
|
|
125
132
|
|
|
126
133
|
# Get recent data
|
|
127
|
-
recent_data = data[data[
|
|
134
|
+
recent_data = data[data["date"] <= current_date].tail(20)
|
|
128
135
|
|
|
129
136
|
if len(recent_data) < 20:
|
|
130
137
|
return signals
|
|
131
138
|
|
|
132
139
|
# Calculate momentum for each ticker
|
|
133
|
-
for ticker in recent_data[
|
|
134
|
-
ticker_data = recent_data[recent_data[
|
|
140
|
+
for ticker in recent_data["symbol"].unique():
|
|
141
|
+
ticker_data = recent_data[recent_data["symbol"] == ticker]
|
|
135
142
|
|
|
136
143
|
if len(ticker_data) < 2:
|
|
137
144
|
continue
|
|
138
145
|
|
|
139
146
|
# Simple momentum: compare current price to 20-day average
|
|
140
|
-
current_price = ticker_data[
|
|
141
|
-
avg_price = ticker_data[
|
|
147
|
+
current_price = ticker_data["close"].iloc[-1]
|
|
148
|
+
avg_price = ticker_data["close"].mean()
|
|
142
149
|
|
|
143
150
|
if current_price > avg_price * 1.05: # 5% above average
|
|
144
|
-
signals.append(
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
151
|
+
signals.append(
|
|
152
|
+
{
|
|
153
|
+
"ticker": ticker,
|
|
154
|
+
"action": "buy",
|
|
155
|
+
"confidence": 0.6,
|
|
156
|
+
"position_size": 0.05, # 5% position
|
|
157
|
+
}
|
|
158
|
+
)
|
|
150
159
|
elif current_price < avg_price * 0.95: # 5% below average
|
|
151
|
-
signals.append(
|
|
152
|
-
"ticker": ticker,
|
|
153
|
-
|
|
154
|
-
"confidence": 0.6,
|
|
155
|
-
"position_size": 0.05
|
|
156
|
-
})
|
|
160
|
+
signals.append(
|
|
161
|
+
{"ticker": ticker, "action": "sell", "confidence": 0.6, "position_size": 0.05}
|
|
162
|
+
)
|
|
157
163
|
|
|
158
164
|
return signals
|
|
159
165
|
|
|
@@ -167,8 +173,9 @@ class PositionManager:
|
|
|
167
173
|
self.cash = config.initial_capital
|
|
168
174
|
self.portfolio_value = config.initial_capital
|
|
169
175
|
|
|
170
|
-
def open_position(
|
|
171
|
-
|
|
176
|
+
def open_position(
|
|
177
|
+
self, ticker: str, quantity: int, price: float, date: datetime, signal: Dict[str, Any]
|
|
178
|
+
):
|
|
172
179
|
"""Open a new position"""
|
|
173
180
|
cost = quantity * price * (1 + self.config.commission + self.config.slippage)
|
|
174
181
|
|
|
@@ -185,7 +192,7 @@ class PositionManager:
|
|
|
185
192
|
"stop_loss": signal.get("stop_loss"),
|
|
186
193
|
"take_profit": signal.get("target_price"),
|
|
187
194
|
"unrealized_pnl": 0,
|
|
188
|
-
"realized_pnl": 0
|
|
195
|
+
"realized_pnl": 0,
|
|
189
196
|
}
|
|
190
197
|
|
|
191
198
|
logger.debug(f"Opened position: {ticker} - {quantity} shares @ ${price:.2f}")
|
|
@@ -271,21 +278,22 @@ class BacktestEngine:
|
|
|
271
278
|
"""Set trading strategy"""
|
|
272
279
|
self.strategy = strategy
|
|
273
280
|
|
|
274
|
-
def run(
|
|
275
|
-
|
|
281
|
+
def run(
|
|
282
|
+
self, price_data: pd.DataFrame, trading_data: Optional[pd.DataFrame] = None
|
|
283
|
+
) -> BacktestResult:
|
|
276
284
|
"""Run backtest"""
|
|
277
285
|
logger.info("Starting backtest...")
|
|
278
286
|
|
|
279
287
|
# Prepare data
|
|
280
|
-
price_data = price_data.sort_values(
|
|
288
|
+
price_data = price_data.sort_values("date")
|
|
281
289
|
|
|
282
290
|
if self.config.start_date:
|
|
283
|
-
price_data = price_data[price_data[
|
|
291
|
+
price_data = price_data[price_data["date"] >= self.config.start_date]
|
|
284
292
|
if self.config.end_date:
|
|
285
|
-
price_data = price_data[price_data[
|
|
293
|
+
price_data = price_data[price_data["date"] <= self.config.end_date]
|
|
286
294
|
|
|
287
295
|
# Get unique dates
|
|
288
|
-
dates = price_data[
|
|
296
|
+
dates = price_data["date"].unique()
|
|
289
297
|
|
|
290
298
|
# Initialize results
|
|
291
299
|
portfolio_values = []
|
|
@@ -301,15 +309,16 @@ class BacktestEngine:
|
|
|
301
309
|
|
|
302
310
|
# Handle stop loss or take profit triggers
|
|
303
311
|
if trigger_ticker:
|
|
304
|
-
self._execute_exit(
|
|
305
|
-
|
|
312
|
+
self._execute_exit(
|
|
313
|
+
trigger_ticker, current_prices[trigger_ticker], current_date, trigger_type
|
|
314
|
+
)
|
|
306
315
|
|
|
307
316
|
# Generate signals (e.g., weekly rebalancing)
|
|
308
317
|
if self._should_rebalance(i, current_date):
|
|
309
318
|
signals = self.strategy.generate_signals(
|
|
310
|
-
price_data[price_data[
|
|
319
|
+
price_data[price_data["date"] <= current_date],
|
|
311
320
|
current_date,
|
|
312
|
-
self.position_manager.get_portfolio_value(current_prices)
|
|
321
|
+
self.position_manager.get_portfolio_value(current_prices),
|
|
313
322
|
)
|
|
314
323
|
|
|
315
324
|
# Execute signals
|
|
@@ -317,12 +326,14 @@ class BacktestEngine:
|
|
|
317
326
|
|
|
318
327
|
# Record portfolio value
|
|
319
328
|
portfolio_value = self.position_manager.get_portfolio_value(current_prices)
|
|
320
|
-
portfolio_values.append(
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
329
|
+
portfolio_values.append(
|
|
330
|
+
{
|
|
331
|
+
"date": current_date,
|
|
332
|
+
"value": portfolio_value,
|
|
333
|
+
"cash": self.position_manager.cash,
|
|
334
|
+
"positions_value": portfolio_value - self.position_manager.cash,
|
|
335
|
+
}
|
|
336
|
+
)
|
|
326
337
|
|
|
327
338
|
# Record positions
|
|
328
339
|
position_snapshot = self._get_position_snapshot(current_date, current_prices)
|
|
@@ -330,10 +341,10 @@ class BacktestEngine:
|
|
|
330
341
|
|
|
331
342
|
# Create results
|
|
332
343
|
portfolio_df = pd.DataFrame(portfolio_values)
|
|
333
|
-
portfolio_df.set_index(
|
|
344
|
+
portfolio_df.set_index("date", inplace=True)
|
|
334
345
|
|
|
335
346
|
# Calculate returns
|
|
336
|
-
portfolio_df[
|
|
347
|
+
portfolio_df["returns"] = portfolio_df["value"].pct_change().fillna(0)
|
|
337
348
|
|
|
338
349
|
# Calculate metrics
|
|
339
350
|
metrics = self._calculate_metrics(portfolio_df)
|
|
@@ -342,24 +353,25 @@ class BacktestEngine:
|
|
|
342
353
|
benchmark_returns = self._get_benchmark_returns(price_data, dates)
|
|
343
354
|
|
|
344
355
|
result = BacktestResult(
|
|
345
|
-
portfolio_value=portfolio_df[
|
|
346
|
-
returns=portfolio_df[
|
|
356
|
+
portfolio_value=portfolio_df["value"],
|
|
357
|
+
returns=portfolio_df["returns"],
|
|
347
358
|
positions=pd.DataFrame(daily_positions),
|
|
348
359
|
trades=pd.DataFrame(self.trades),
|
|
349
360
|
metrics=metrics,
|
|
350
361
|
benchmark_returns=benchmark_returns,
|
|
351
|
-
strategy_name=self.strategy.__class__.__name__
|
|
362
|
+
strategy_name=self.strategy.__class__.__name__,
|
|
352
363
|
)
|
|
353
364
|
|
|
354
365
|
logger.info(f"Backtest complete. Total return: {metrics['total_return']:.2%}")
|
|
355
366
|
|
|
356
367
|
return result
|
|
357
368
|
|
|
358
|
-
def _get_current_prices(
|
|
359
|
-
|
|
369
|
+
def _get_current_prices(
|
|
370
|
+
self, price_data: pd.DataFrame, current_date: datetime
|
|
371
|
+
) -> Dict[str, float]:
|
|
360
372
|
"""Get current prices for all tickers"""
|
|
361
|
-
current_data = price_data[price_data[
|
|
362
|
-
return dict(zip(current_data[
|
|
373
|
+
current_data = price_data[price_data["date"] == current_date]
|
|
374
|
+
return dict(zip(current_data["symbol"], current_data["close"]))
|
|
363
375
|
|
|
364
376
|
def _should_rebalance(self, day_index: int, current_date: datetime) -> bool:
|
|
365
377
|
"""Check if should rebalance portfolio"""
|
|
@@ -371,9 +383,12 @@ class BacktestEngine:
|
|
|
371
383
|
return day_index % 21 == 0
|
|
372
384
|
return False
|
|
373
385
|
|
|
374
|
-
def _execute_signals(
|
|
375
|
-
|
|
376
|
-
|
|
386
|
+
def _execute_signals(
|
|
387
|
+
self,
|
|
388
|
+
signals: List[Dict[str, Any]],
|
|
389
|
+
current_prices: Dict[str, float],
|
|
390
|
+
current_date: datetime,
|
|
391
|
+
):
|
|
377
392
|
"""Execute trading signals"""
|
|
378
393
|
for signal in signals:
|
|
379
394
|
ticker = signal["ticker"]
|
|
@@ -399,52 +414,60 @@ class BacktestEngine:
|
|
|
399
414
|
)
|
|
400
415
|
|
|
401
416
|
if success:
|
|
402
|
-
self.trades.append(
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
417
|
+
self.trades.append(
|
|
418
|
+
{
|
|
419
|
+
"date": current_date,
|
|
420
|
+
"ticker": ticker,
|
|
421
|
+
"action": "buy",
|
|
422
|
+
"quantity": quantity,
|
|
423
|
+
"price": price,
|
|
424
|
+
"value": quantity * price,
|
|
425
|
+
}
|
|
426
|
+
)
|
|
410
427
|
|
|
411
428
|
elif signal["action"] == "sell":
|
|
412
429
|
if ticker in self.position_manager.positions:
|
|
413
430
|
pnl = self.position_manager.close_position(ticker, price, current_date)
|
|
414
431
|
|
|
415
|
-
self.trades.append(
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
432
|
+
self.trades.append(
|
|
433
|
+
{
|
|
434
|
+
"date": current_date,
|
|
435
|
+
"ticker": ticker,
|
|
436
|
+
"action": "sell",
|
|
437
|
+
"quantity": self.position_manager.positions.get(ticker, {}).get(
|
|
438
|
+
"quantity", 0
|
|
439
|
+
),
|
|
440
|
+
"price": price,
|
|
441
|
+
"pnl": pnl,
|
|
442
|
+
}
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
def _execute_exit(self, ticker: str, price: float, current_date: datetime, exit_type: str):
|
|
426
446
|
"""Execute position exit"""
|
|
427
447
|
if ticker in self.position_manager.positions:
|
|
428
448
|
position = self.position_manager.positions[ticker]
|
|
429
449
|
pnl = self.position_manager.close_position(ticker, price, current_date)
|
|
430
450
|
|
|
431
|
-
self.trades.append(
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
451
|
+
self.trades.append(
|
|
452
|
+
{
|
|
453
|
+
"date": current_date,
|
|
454
|
+
"ticker": ticker,
|
|
455
|
+
"action": f"sell_{exit_type}",
|
|
456
|
+
"quantity": position["quantity"],
|
|
457
|
+
"price": price,
|
|
458
|
+
"pnl": pnl,
|
|
459
|
+
}
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
def _get_position_snapshot(
|
|
463
|
+
self, date: datetime, current_prices: Dict[str, float]
|
|
464
|
+
) -> Dict[str, Any]:
|
|
442
465
|
"""Get current position snapshot"""
|
|
443
466
|
snapshot = {
|
|
444
467
|
"date": date,
|
|
445
468
|
"num_positions": len(self.position_manager.positions),
|
|
446
469
|
"cash": self.position_manager.cash,
|
|
447
|
-
"portfolio_value": self.position_manager.get_portfolio_value(current_prices)
|
|
470
|
+
"portfolio_value": self.position_manager.get_portfolio_value(current_prices),
|
|
448
471
|
}
|
|
449
472
|
|
|
450
473
|
# Add position weights
|
|
@@ -456,15 +479,17 @@ class BacktestEngine:
|
|
|
456
479
|
|
|
457
480
|
def _calculate_metrics(self, portfolio_df: pd.DataFrame) -> Dict[str, float]:
|
|
458
481
|
"""Calculate performance metrics"""
|
|
459
|
-
returns = portfolio_df[
|
|
482
|
+
returns = portfolio_df["returns"]
|
|
460
483
|
|
|
461
484
|
# Basic metrics
|
|
462
|
-
total_return = (portfolio_df[
|
|
485
|
+
total_return = (portfolio_df["value"].iloc[-1] / portfolio_df["value"].iloc[0]) - 1
|
|
463
486
|
annualized_return = (1 + total_return) ** (252 / len(returns)) - 1
|
|
464
487
|
|
|
465
488
|
# Risk metrics
|
|
466
489
|
volatility = returns.std() * np.sqrt(252)
|
|
467
|
-
sharpe_ratio = (
|
|
490
|
+
sharpe_ratio = (
|
|
491
|
+
(annualized_return - self.config.risk_free_rate) / volatility if volatility > 0 else 0
|
|
492
|
+
)
|
|
468
493
|
|
|
469
494
|
# Drawdown
|
|
470
495
|
cumulative = (1 + returns).cumprod()
|
|
@@ -485,18 +510,19 @@ class BacktestEngine:
|
|
|
485
510
|
"max_drawdown": max_drawdown,
|
|
486
511
|
"win_rate": win_rate,
|
|
487
512
|
"total_trades": len(self.trades),
|
|
488
|
-
"final_value": portfolio_df[
|
|
489
|
-
"initial_value": self.config.initial_capital
|
|
513
|
+
"final_value": portfolio_df["value"].iloc[-1],
|
|
514
|
+
"initial_value": self.config.initial_capital,
|
|
490
515
|
}
|
|
491
516
|
|
|
492
|
-
def _get_benchmark_returns(
|
|
493
|
-
|
|
517
|
+
def _get_benchmark_returns(
|
|
518
|
+
self, price_data: pd.DataFrame, dates: np.ndarray
|
|
519
|
+
) -> Optional[pd.Series]:
|
|
494
520
|
"""Get benchmark returns"""
|
|
495
|
-
if self.config.benchmark not in price_data[
|
|
521
|
+
if self.config.benchmark not in price_data["symbol"].unique():
|
|
496
522
|
return None
|
|
497
523
|
|
|
498
|
-
benchmark_data = price_data[price_data[
|
|
499
|
-
benchmark_data = benchmark_data.set_index(
|
|
524
|
+
benchmark_data = price_data[price_data["symbol"] == self.config.benchmark]
|
|
525
|
+
benchmark_data = benchmark_data.set_index("date")["close"]
|
|
500
526
|
benchmark_returns = benchmark_data.pct_change().fillna(0)
|
|
501
527
|
|
|
502
|
-
return benchmark_returns[benchmark_returns.index.isin(dates)]
|
|
528
|
+
return benchmark_returns[benchmark_returns.index.isin(dates)]
|