quantex 0.3.2__tar.gz → 0.3.4__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: quantex
3
- Version: 0.3.2
3
+ Version: 0.3.4
4
4
  Summary: A simple quant strategy creation and backtesting package.
5
5
  License: MIT
6
6
  Author: Daniel Green
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "quantex"
3
- version = "0.3.2"
3
+ version = "0.3.4"
4
4
  description = "A simple quant strategy creation and backtesting package."
5
5
  authors = [
6
6
  {name = "Daniel Green",email = "dangreen07@outlook.com"}
@@ -0,0 +1,12 @@
1
+ from .datasource import CSVDataSource as CSVDataSource, ParquetDataSource as ParquetDataSource, DataSource as DataSource
2
+ from .strategy import Strategy as Strategy
3
+ from .backtester import (
4
+ SimpleBacktester as SimpleBacktester,
5
+ BacktestReport as BacktestReport,
6
+ OptimizationResult as OptimizationResult,
7
+ TrainValidateTestSplit as TrainValidateTestSplit,
8
+ DataSplitMode as DataSplitMode,
9
+ create_train_validate_test_split as create_train_validate_test_split,
10
+ )
11
+ from .enums import CommissionType as CommissionType
12
+ from .indicators import indicators as indicators
@@ -15,6 +15,140 @@ import concurrent.futures
15
15
  import pickle
16
16
  import os
17
17
  import gc
18
+ from enum import Enum
19
+
20
+
21
+ class DataSplitMode(Enum):
22
+ """Enumeration for data split modes in optimization."""
23
+ TRAIN = "train"
24
+ VALIDATE = "validate"
25
+ TEST = "test"
26
+
27
+
28
+ @dataclass
29
+ class TrainValidateTestSplit:
30
+ """
31
+ Container for train/validate/test data splits.
32
+
33
+ This class holds the split configuration and indices for dividing
34
+ historical data into training, validation, and test sets for
35
+ machine learning-style optimization workflows.
36
+
37
+ Attributes:
38
+ train_start (int): Starting index for training data.
39
+ train_end (int): Ending index for training data.
40
+ validate_start (int): Starting index for validation data.
41
+ validate_end (int): Ending index for validation data.
42
+ test_start (int): Starting index for test data.
43
+ test_end (int): Ending index for test data.
44
+ train_ratio (float): Ratio of data used for training.
45
+ validate_ratio (float): Ratio of data used for validation.
46
+ test_ratio (float): Ratio of data used for testing.
47
+ """
48
+ train_start: int
49
+ train_end: int
50
+ validate_start: int
51
+ validate_end: int
52
+ test_start: int
53
+ test_end: int
54
+ train_ratio: float = 0.6
55
+ validate_ratio: float = 0.2
56
+ test_ratio: float = 0.2
57
+
58
+ def __post_init__(self):
59
+ """Validate split ratios sum to 1.0."""
60
+ total = self.train_ratio + self.validate_ratio + self.test_ratio
61
+ if not np.isclose(total, 1.0):
62
+ raise ValueError(
63
+ f"Split ratios must sum to 1.0, got {total:.3f}"
64
+ )
65
+
66
+
67
+ def create_train_validate_test_split(
68
+ data_length: int,
69
+ train_ratio: float = 0.6,
70
+ validate_ratio: float = 0.2,
71
+ test_ratio: float = 0.2
72
+ ) -> TrainValidateTestSplit:
73
+ """
74
+ Create indices for train/validate/test split.
75
+
76
+ This function divides the data indices into three sets for ML-style
77
+ optimization: training (parameter fitting), validation (hyperparameter
78
+ selection), and testing (final evaluation).
79
+
80
+ Args:
81
+ data_length (int): Total number of data points.
82
+ train_ratio (float, optional): Fraction of data for training.
83
+ Defaults to 0.6 (60%).
84
+ validate_ratio (float, optional): Fraction of data for validation.
85
+ Defaults to 0.2 (20%).
86
+ test_ratio (float, optional): Fraction of data for testing.
87
+ Defaults to 0.2 (20%).
88
+
89
+ Returns:
90
+ TrainValidateTestSplit: Object containing start/end indices for
91
+ each split.
92
+
93
+ Raises:
94
+ ValueError: If ratios don't sum to 1.0 or are invalid.
95
+
96
+ Example:
97
+ >>> split = create_train_validate_test_split(1000, 0.6, 0.2, 0.2)
98
+ >>> print(f"Train: {split.train_start}-{split.train_end}")
99
+ >>> print(f"Validate: {split.validate_start}-{split.validate_end}")
100
+ >>> print(f"Test: {split.test_start}-{split.test_end}")
101
+ """
102
+ if not np.isclose(train_ratio + validate_ratio + test_ratio, 1.0):
103
+ raise ValueError("Split ratios must sum to 1.0")
104
+
105
+ if train_ratio <= 0 or validate_ratio <= 0 or test_ratio <= 0:
106
+ raise ValueError("All split ratios must be positive")
107
+
108
+ train_end = int(data_length * train_ratio)
109
+ validate_end = int(data_length * (train_ratio + validate_ratio))
110
+
111
+ return TrainValidateTestSplit(
112
+ train_start=0,
113
+ train_end=train_end,
114
+ validate_start=train_end,
115
+ validate_end=validate_end,
116
+ test_start=validate_end,
117
+ test_end=data_length,
118
+ train_ratio=train_ratio,
119
+ validate_ratio=validate_ratio,
120
+ test_ratio=test_ratio
121
+ )
122
+
123
+
124
+ @dataclass
125
+ class OptimizationResult:
126
+ """
127
+ Container for optimization results with train/validate/test splits.
128
+
129
+ This class holds the complete results of an optimization run that
130
+ includes evaluation on all three data splits, enabling proper
131
+ model selection and generalization assessment.
132
+
133
+ Attributes:
134
+ best_params (dict): Best parameter values found.
135
+ train_report: Backtest report for training data.
136
+ validate_report: Backtest report for validation data.
137
+ test_report: Backtest report for test data.
138
+ train_metrics (dict): Computed metrics for training performance.
139
+ validate_metrics (dict): Computed metrics for validation performance.
140
+ test_metrics (dict): Computed metrics for test performance.
141
+ all_results (pd.DataFrame): DataFrame with all parameter combinations
142
+ and their metrics for each split.
143
+ """
144
+ best_params: dict
145
+ train_report: Any
146
+ validate_report: Any
147
+ test_report: Any
148
+ train_metrics: dict
149
+ validate_metrics: dict
150
+ test_metrics: dict
151
+ all_results: pd.DataFrame
18
152
 
19
153
  def max_drawdown(equity: pd.Series) -> float:
20
154
  """
@@ -907,3 +1041,592 @@ class SimpleBacktester():
907
1041
  best_report = bt.run(progress_bar=False)
908
1042
 
909
1043
  return best_params, best_report, results_df
1044
+
1045
+ def optimize_with_split(
1046
+ self,
1047
+ params: dict[str, Any],
1048
+ constraint: Callable[[dict[str, Any]], bool] | None = None,
1049
+ objective: str = "sharpe",
1050
+ risk_tolerance: dict[str, float] | None = None,
1051
+ train_ratio: float = 0.6,
1052
+ validate_ratio: float = 0.2,
1053
+ test_ratio: float = 0.2,
1054
+ selection_criterion: str = "validate",
1055
+ ) -> OptimizationResult:
1056
+ """
1057
+ Optimize strategy parameters using train/validate/test splits.
1058
+
1059
+ This method implements ML-style optimization with three data splits:
1060
+ - Training set: Used to fit strategy parameters
1061
+ - Validation set: Used to select the best parameters
1062
+ - Test set: Used for final out-of-sample evaluation
1063
+
1064
+ This approach helps prevent overfitting by evaluating generalization
1065
+ performance on held-out data before final selection.
1066
+
1067
+ Args:
1068
+ params (dict[str, range]): Dictionary mapping strategy attribute names
1069
+ to iterables of candidate values (same format as optimize()).
1070
+ constraint (Callable[[dict[str, Any]], bool] | None, optional):
1071
+ Optional callable for parameter constraints. Defaults to None.
1072
+ objective (str, optional): Metric to optimize. Defaults to "sharpe".
1073
+ Supports same metrics as optimize().
1074
+ risk_tolerance (dict[str, float] | None, optional): Optional maximum
1075
+ allowed metric values. Defaults to None.
1076
+ train_ratio (float, optional): Fraction of data for training.
1077
+ Defaults to 0.6 (60%).
1078
+ validate_ratio (float, optional): Fraction of data for validation.
1079
+ Defaults to 0.2 (20%).
1080
+ test_ratio (float, optional): Fraction of data for testing.
1081
+ Defaults to 0.2 (20%).
1082
+ selection_criterion (str, optional): Which split to use for final
1083
+ parameter selection. Options: "train", "validate", "test".
1084
+ Defaults to "validate".
1085
+
1086
+ Returns:
1087
+ OptimizationResult: Object containing:
1088
+ - best_params: Best parameters found
1089
+ - train_report: BacktestReport for training data
1090
+ - validate_report: BacktestReport for validation data
1091
+ - test_report: BacktestReport for test data
1092
+ - train_metrics: Metrics computed on training data
1093
+ - validate_metrics: Metrics computed on validation data
1094
+ - test_metrics: Metrics computed on test data
1095
+ - all_results: DataFrame with all results
1096
+
1097
+ Raises:
1098
+ ValueError: If split ratios don't sum to 1.0 or selection_criterion
1099
+ is invalid.
1100
+
1101
+ Example:
1102
+ >>> bt = SimpleBacktester(strategy)
1103
+ >>> result = bt.optimize_with_split(
1104
+ ... {'fast_period': [5, 10, 15], 'slow_period': [20, 30, 50]},
1105
+ ... selection_criterion='validate'
1106
+ ... )
1107
+ >>> print(f"Best params: {result.best_params}")
1108
+ >>> print(f"Train Sharpe: {result.train_metrics['sharpe']}")
1109
+ >>> print(f"Validate Sharpe: {result.validate_metrics['sharpe']}")
1110
+ >>> print(f"Test Sharpe: {result.test_metrics['sharpe']}")
1111
+ """
1112
+ # Validate selection criterion
1113
+ valid_criteria = {"train", "validate", "test"}
1114
+ if selection_criterion not in valid_criteria:
1115
+ raise ValueError(
1116
+ f"selection_criterion must be one of {valid_criteria}, "
1117
+ f"got '{selection_criterion}'"
1118
+ )
1119
+
1120
+ # Get data length from the strategy's data source
1121
+ source = self.strategy.positions[list(self.strategy.positions.keys())[0]].source
1122
+ data_length = len(source.data)
1123
+
1124
+ # Create the split
1125
+ split = create_train_validate_test_split(
1126
+ data_length,
1127
+ train_ratio,
1128
+ validate_ratio,
1129
+ test_ratio
1130
+ )
1131
+
1132
+ # Prepare parameter combinations
1133
+ if not params:
1134
+ raise ValueError("params must not be empty")
1135
+
1136
+ keys = list(params.keys())
1137
+ value_lists = []
1138
+ for k in keys:
1139
+ vals = params[k]
1140
+ try:
1141
+ candidates = list(vals)
1142
+ except TypeError:
1143
+ raise TypeError(f"Parameter '{k}' must be iterable")
1144
+ if len(candidates) == 0:
1145
+ raise ValueError(f"Parameter '{k}' has no candidate values")
1146
+ value_lists.append(candidates)
1147
+
1148
+ # Store results for each split
1149
+ train_results = []
1150
+ validate_results = []
1151
+ test_results = []
1152
+
1153
+ valid_metrics = {"final_cash", "total_return", "sharpe", "max_drawdown", "trades"}
1154
+
1155
+ total_combos = len(list(itertools.product(*value_lists)))
1156
+
1157
+ # Create a modified strategy that uses data slices
1158
+ def create_split_strategy(params_dict: dict, split_mode: DataSplitMode):
1159
+ """Create a strategy copy with data sliced to the specified split."""
1160
+ strat_copy = copy.deepcopy(self.strategy)
1161
+ for k, v in params_dict.items():
1162
+ # Convert float to int if the strategy expects integer parameters
1163
+ if isinstance(v, float) and v == int(v):
1164
+ v = int(v)
1165
+ setattr(strat_copy, k, v)
1166
+
1167
+ # Slice each data source to the appropriate split
1168
+ for key, broker in strat_copy.positions.items():
1169
+ source = broker.source
1170
+ if split_mode == DataSplitMode.TRAIN:
1171
+ start, end = split.train_start, split.train_end
1172
+ elif split_mode == DataSplitMode.VALIDATE:
1173
+ start, end = split.validate_start, split.validate_end
1174
+ else: # TEST
1175
+ start, end = split.test_start, split.test_end
1176
+
1177
+ # Create a new data source with sliced data
1178
+ sliced_df = source.data.iloc[start:end].copy()
1179
+ from .datasource import DataSource
1180
+ new_source = DataSource(sliced_df)
1181
+ broker.source = new_source
1182
+ # Also update the strategy's data dictionary
1183
+ strat_copy.data[key] = new_source
1184
+
1185
+ return strat_copy, split_mode
1186
+
1187
+ # Run optimization for each split
1188
+ for combo in tqdm(itertools.product(*value_lists), total=total_combos, desc="Optimizing"):
1189
+ row_params = {k: v for k, v in zip(keys, combo)}
1190
+
1191
+ # Apply constraint
1192
+ if constraint is not None:
1193
+ try:
1194
+ if not bool(constraint(row_params)):
1195
+ continue
1196
+ except Exception:
1197
+ continue
1198
+
1199
+ # Evaluate on all three splits
1200
+ for mode in [DataSplitMode.TRAIN, DataSplitMode.VALIDATE, DataSplitMode.TEST]:
1201
+ strat_copy, _ = create_split_strategy(row_params, mode)
1202
+
1203
+ bt = SimpleBacktester(
1204
+ strat_copy,
1205
+ cash=self.cash,
1206
+ commission=self.commission,
1207
+ commission_type=self.commission_type,
1208
+ lot_size=self.lot_size,
1209
+ )
1210
+ report = bt.run(progress_bar=False)
1211
+ metrics = _compute_backtest_metrics(report)
1212
+
1213
+ # Apply risk tolerance filter
1214
+ if risk_tolerance is not None:
1215
+ if not _risk_tolerance_passes(report, risk_tolerance):
1216
+ continue
1217
+
1218
+ # Compute objective score
1219
+ if objective in valid_metrics:
1220
+ score = metrics.get(objective)
1221
+ else:
1222
+ score = getattr(report, objective, None)
1223
+ if callable(score):
1224
+ score = score()
1225
+
1226
+ if score is None or not np.isfinite(float(score)): # type: ignore[arg-type]
1227
+ continue
1228
+
1229
+ row = dict(row_params)
1230
+ row["objective_score"] = float(score) # type: ignore[arg-type]
1231
+ row.update(metrics)
1232
+
1233
+ if mode == DataSplitMode.TRAIN:
1234
+ train_results.append(row)
1235
+ elif mode == DataSplitMode.VALIDATE:
1236
+ validate_results.append(row)
1237
+ else:
1238
+ test_results.append(row)
1239
+
1240
+ # Create DataFrames
1241
+ train_df = pd.DataFrame(train_results) if train_results else pd.DataFrame()
1242
+ validate_df = pd.DataFrame(validate_results) if validate_results else pd.DataFrame()
1243
+ test_df = pd.DataFrame(test_results) if test_results else pd.DataFrame()
1244
+
1245
+ # Select best parameters based on selection criterion
1246
+ if selection_criterion == "validate" and not validate_df.empty:
1247
+ validate_df_sorted = validate_df.sort_values(
1248
+ by=["objective_score"], ascending=False, kind="mergesort"
1249
+ )
1250
+ best_idx = validate_df_sorted.index[0]
1251
+ best_params = {k: validate_df.loc[best_idx, k] for k in keys}
1252
+ best_validate_score = validate_df.loc[best_idx, "objective_score"]
1253
+ elif selection_criterion == "train" and not train_df.empty:
1254
+ train_df_sorted = train_df.sort_values(
1255
+ by=["objective_score"], ascending=False, kind="mergesort"
1256
+ )
1257
+ best_idx = train_df_sorted.index[0]
1258
+ best_params = {k: train_df.loc[best_idx, k] for k in keys}
1259
+ best_validate_score = train_df.loc[best_idx, "objective_score"]
1260
+ elif selection_criterion == "test" and not test_df.empty:
1261
+ test_df_sorted = test_df.sort_values(
1262
+ by=["objective_score"], ascending=False, kind="mergesort"
1263
+ )
1264
+ best_idx = test_df_sorted.index[0]
1265
+ best_params = {k: test_df.loc[best_idx, k] for k in keys}
1266
+ best_validate_score = test_df.loc[best_idx, "objective_score"]
1267
+ else:
1268
+ best_params = {}
1269
+ best_validate_score = -np.inf
1270
+
1271
+ # Get full reports for best parameters
1272
+ train_report = None
1273
+ validate_report = None
1274
+ test_report = None
1275
+ train_metrics = {}
1276
+ validate_metrics = {}
1277
+ test_metrics = {}
1278
+
1279
+ if best_params:
1280
+ # Run full backtests for best parameters on each split
1281
+ for mode, report_attr, metrics_attr in [
1282
+ (DataSplitMode.TRAIN, 'train_report', 'train_metrics'),
1283
+ (DataSplitMode.VALIDATE, 'validate_report', 'validate_metrics'),
1284
+ (DataSplitMode.TEST, 'test_report', 'test_metrics'),
1285
+ ]:
1286
+ strat_copy, _ = create_split_strategy(best_params, mode)
1287
+ bt = SimpleBacktester(
1288
+ strat_copy,
1289
+ cash=self.cash,
1290
+ commission=self.commission,
1291
+ commission_type=self.commission_type,
1292
+ lot_size=self.lot_size,
1293
+ )
1294
+ report = bt.run(progress_bar=False)
1295
+ metrics = _compute_backtest_metrics(report)
1296
+
1297
+ if mode == DataSplitMode.TRAIN:
1298
+ train_report = report
1299
+ train_metrics = metrics
1300
+ elif mode == DataSplitMode.VALIDATE:
1301
+ validate_report = report
1302
+ validate_metrics = metrics
1303
+ else:
1304
+ test_report = report
1305
+ test_metrics = metrics
1306
+
1307
+ # Combine all results
1308
+ all_results = pd.DataFrame()
1309
+ if not train_df.empty:
1310
+ train_df_copy = train_df.copy()
1311
+ train_df_copy["split"] = "train"
1312
+ all_results = pd.concat([all_results, train_df_copy], ignore_index=True)
1313
+ if not validate_df.empty:
1314
+ validate_df_copy = validate_df.copy()
1315
+ validate_df_copy["split"] = "validate"
1316
+ all_results = pd.concat([all_results, validate_df_copy], ignore_index=True)
1317
+ if not test_df.empty:
1318
+ test_df_copy = test_df.copy()
1319
+ test_df_copy["split"] = "test"
1320
+ all_results = pd.concat([all_results, test_df_copy], ignore_index=True)
1321
+
1322
+ return OptimizationResult(
1323
+ best_params=best_params,
1324
+ train_report=train_report,
1325
+ validate_report=validate_report,
1326
+ test_report=test_report,
1327
+ train_metrics=train_metrics,
1328
+ validate_metrics=validate_metrics,
1329
+ test_metrics=test_metrics,
1330
+ all_results=all_results
1331
+ )
1332
+
1333
+ def optimize_gradient_descent(
1334
+ self,
1335
+ param_init: dict[str, float],
1336
+ param_bounds: dict[str, tuple[float, float]],
1337
+ objective: str = "sharpe",
1338
+ learning_rate: float = 0.01,
1339
+ max_iterations: int = 100,
1340
+ tolerance: float = 1e-6,
1341
+ momentum: float = 0.9,
1342
+ train_ratio: float = 0.7,
1343
+ validate_ratio: float = 0.15,
1344
+ test_ratio: float = 0.15,
1345
+ selection_criterion: str = "validate",
1346
+ progress_bar: bool = True,
1347
+ integer_params: set[str] | None = None,
1348
+ ) -> OptimizationResult:
1349
+ """
1350
+ Optimize strategy parameters using gradient descent.
1351
+
1352
+ This method performs gradient-based optimization on continuous
1353
+ strategy parameters, similar to machine learning workflows. It uses
1354
+ train/validate/test splits to prevent overfitting and select the best
1355
+ model based on validation performance.
1356
+
1357
+ The optimization computes numerical gradients by evaluating small
1358
+ perturbations around the current parameter values.
1359
+
1360
+ Args:
1361
+ param_init (dict[str, float]): Initial parameter values.
1362
+ param_bounds (dict[str, tuple[float, float]]): Bounds for each
1363
+ parameter as (min, max) tuples.
1364
+ objective (str, optional): Metric to optimize. Defaults to "sharpe".
1365
+ Supports same metrics as optimize().
1366
+ learning_rate (float, optional): Step size for gradient descent.
1367
+ Defaults to 0.01.
1368
+ max_iterations (int, optional): Maximum number of iterations.
1369
+ Defaults to 100.
1370
+ tolerance (float, optional): Convergence tolerance. Optimization
1371
+ stops when gradient magnitude falls below this threshold.
1372
+ Defaults to 1e-6.
1373
+ momentum (float, optional): Momentum factor for accelerated
1374
+ descent. Defaults to 0.9.
1375
+ train_ratio (float, optional): Fraction of data for training.
1376
+ Defaults to 0.7 (70%).
1377
+ validate_ratio (float, optional): Fraction of data for validation.
1378
+ Defaults to 0.15 (15%).
1379
+ test_ratio (float, optional): Fraction of data for testing.
1380
+ Defaults to 0.15 (15%).
1381
+ selection_criterion (str, optional): Which split to use for final
1382
+ parameter selection. Options: "train", "validate", "test".
1383
+ Defaults to "validate".
1384
+ progress_bar (bool, optional): Whether to show progress bar.
1385
+ Defaults to True.
1386
+ integer_params (set[str] | None, optional): Set of parameter names
1387
+ that should be treated as integers. These parameters will be
1388
+ rounded to the nearest integer after each gradient update.
1389
+ Defaults to None (all parameters are continuous).
1390
+
1391
+ Returns:
1392
+ OptimizationResult: Object containing:
1393
+ - best_params: Optimized parameter values
1394
+ - train_report: BacktestReport for training data
1395
+ - validate_report: BacktestReport for validation data
1396
+ - test_report: BacktestReport for test data
1397
+ - train_metrics: Metrics computed on training data
1398
+ - validate_metrics: Metrics computed on validation data
1399
+ - test_metrics: Metrics computed on test data
1400
+ - all_results: DataFrame with iteration history
1401
+
1402
+ Example:
1403
+ >>> # Optimize with integer parameters
1404
+ >>> result = bt.optimize_gradient_descent(
1405
+ ... param_init={'fast_period': 10.0, 'slow_period': 30.0},
1406
+ ... param_bounds={
1407
+ ... 'fast_period': (2.0, 50.0),
1408
+ ... 'slow_period': (10.0, 100.0)
1409
+ ... },
1410
+ ... integer_params={'fast_period', 'slow_period'},
1411
+ ... learning_rate=0.05,
1412
+ ... max_iterations=50
1413
+ ... )
1414
+ >>> print(f"Optimized params: {result.best_params}")
1415
+ >>> print(f"Final validation Sharpe: {result.validate_metrics['sharpe']}")
1416
+ """
1417
+ if integer_params is None:
1418
+ integer_params = set()
1419
+ # Validate selection criterion
1420
+ valid_criteria = {"train", "validate", "test"}
1421
+ if selection_criterion not in valid_criteria:
1422
+ raise ValueError(
1423
+ f"selection_criterion must be one of {valid_criteria}, "
1424
+ f"got '{selection_criterion}'"
1425
+ )
1426
+
1427
+ # Validate parameters
1428
+ if not param_init:
1429
+ raise ValueError("param_init must not be empty")
1430
+ if set(param_init.keys()) != set(param_bounds.keys()):
1431
+ raise ValueError("param_init and param_bounds must have the same keys")
1432
+
1433
+ # Get data length from the strategy's data source
1434
+ source = self.strategy.positions[list(self.strategy.positions.keys())[0]].source
1435
+ data_length = len(source.data)
1436
+
1437
+ # Create the split
1438
+ split = create_train_validate_test_split(
1439
+ data_length,
1440
+ train_ratio,
1441
+ validate_ratio,
1442
+ test_ratio
1443
+ )
1444
+
1445
+ valid_metrics = {"final_cash", "total_return", "sharpe", "max_drawdown", "trades"}
1446
+
1447
+ # Helper to create sliced strategy
1448
+ def create_split_strategy(params_dict: dict, split_mode: DataSplitMode):
1449
+ """Create a strategy copy with data sliced to the specified split."""
1450
+ strat_copy = copy.deepcopy(self.strategy)
1451
+ for k, v in params_dict.items():
1452
+ # Convert float to int if the strategy expects integer parameters
1453
+ if isinstance(v, float) and v == int(v):
1454
+ v = int(v)
1455
+ setattr(strat_copy, k, v)
1456
+
1457
+ # Slice each data source to the appropriate split
1458
+ for key, broker in strat_copy.positions.items():
1459
+ source = broker.source
1460
+ if split_mode == DataSplitMode.TRAIN:
1461
+ start, end = split.train_start, split.train_end
1462
+ elif split_mode == DataSplitMode.VALIDATE:
1463
+ start, end = split.validate_start, split.validate_end
1464
+ else: # TEST
1465
+ start, end = split.test_start, split.test_end
1466
+
1467
+ sliced_df = source.data.iloc[start:end].copy()
1468
+ from .datasource import DataSource
1469
+ new_source = DataSource(sliced_df)
1470
+ broker.source = new_source
1471
+ # Also update the strategy's data dictionary
1472
+ strat_copy.data[key] = new_source
1473
+
1474
+ return strat_copy
1475
+
1476
+ # Function to evaluate parameters on a specific split
1477
+ def evaluate_params(params_dict: dict, split_mode: DataSplitMode) -> float:
1478
+ """Evaluate objective function on specified split."""
1479
+ strat_copy = create_split_strategy(params_dict, split_mode)
1480
+ bt = SimpleBacktester(
1481
+ strat_copy,
1482
+ cash=self.cash,
1483
+ commission=self.commission,
1484
+ commission_type=self.commission_type,
1485
+ lot_size=self.lot_size,
1486
+ )
1487
+ report = bt.run(progress_bar=False)
1488
+ metrics = _compute_backtest_metrics(report)
1489
+
1490
+ if objective in valid_metrics:
1491
+ score = metrics.get(objective)
1492
+ else:
1493
+ score = getattr(report, objective, None)
1494
+ if callable(score):
1495
+ score = score()
1496
+
1497
+ if score is None or not np.isfinite(float(score)): # type: ignore[arg-type]
1498
+ return -np.inf
1499
+
1500
+ return float(score) # type: ignore[arg-type]
1501
+
1502
+ # Compute numerical gradient
1503
+ def compute_gradient(params: dict, eps: float = 1e-5) -> dict:
1504
+ """Compute numerical gradient using central differences."""
1505
+ grad = {}
1506
+ for key in params:
1507
+ params_plus = params.copy()
1508
+ params_minus = params.copy()
1509
+ params_plus[key] = params[key] + eps
1510
+ params_minus[key] = params[key] - eps
1511
+
1512
+ # Use validation set for gradient computation
1513
+ f_plus = evaluate_params(params_plus, DataSplitMode.VALIDATE)
1514
+ f_minus = evaluate_params(params_minus, DataSplitMode.VALIDATE)
1515
+
1516
+ grad[key] = (f_plus - f_minus) / (2 * eps)
1517
+
1518
+ return grad
1519
+
1520
+ # Gradient descent optimization
1521
+ current_params = param_init.copy()
1522
+ velocities = {k: 0.0 for k in current_params}
1523
+
1524
+ iteration_history = []
1525
+ best_params = current_params.copy()
1526
+ best_score = -np.inf
1527
+
1528
+ param_names = list(current_params.keys())
1529
+
1530
+ iterator = range(max_iterations)
1531
+ if progress_bar:
1532
+ iterator = tqdm(iterator, desc="Gradient Descent")
1533
+
1534
+ for iteration in iterator:
1535
+ # Compute gradient
1536
+ gradient = compute_gradient(current_params)
1537
+
1538
+ # Check for convergence (gradient magnitude)
1539
+ grad_magnitude = np.sqrt(sum(g**2 for g in gradient.values()))
1540
+ if grad_magnitude < tolerance:
1541
+ if progress_bar:
1542
+ print(f"\nConverged at iteration {iteration}")
1543
+ break
1544
+
1545
+ # Update velocities with momentum
1546
+ for key in param_names:
1547
+ velocities[key] = momentum * velocities[key] - learning_rate * gradient[key]
1548
+
1549
+ # Update parameters
1550
+ for key in param_names:
1551
+ current_params[key] += velocities[key]
1552
+
1553
+ # Apply bounds
1554
+ min_val, max_val = param_bounds[key]
1555
+ current_params[key] = np.clip(current_params[key], min_val, max_val)
1556
+
1557
+ # Round integer parameters to nearest integer
1558
+ if key in integer_params:
1559
+ current_params[key] = round(current_params[key])
1560
+
1561
+ # Evaluate on all splits
1562
+ train_score = evaluate_params(current_params, DataSplitMode.TRAIN)
1563
+ validate_score = evaluate_params(current_params, DataSplitMode.VALIDATE)
1564
+ test_score = evaluate_params(current_params, DataSplitMode.TEST)
1565
+
1566
+ # Track best parameters based on selection criterion
1567
+ if selection_criterion == "validate" and validate_score > best_score:
1568
+ best_score = validate_score
1569
+ best_params = current_params.copy()
1570
+ elif selection_criterion == "train" and train_score > best_score:
1571
+ best_score = train_score
1572
+ best_params = current_params.copy()
1573
+ elif selection_criterion == "test" and test_score > best_score:
1574
+ best_score = test_score
1575
+ best_params = current_params.copy()
1576
+
1577
+ # Record iteration history
1578
+ row = current_params.copy()
1579
+ row["iteration"] = iteration
1580
+ row["train_score"] = train_score
1581
+ row["validate_score"] = validate_score
1582
+ row["test_score"] = test_score
1583
+ row["gradient_magnitude"] = grad_magnitude
1584
+ iteration_history.append(row)
1585
+
1586
+ # Create history DataFrame
1587
+ history_df = pd.DataFrame(iteration_history)
1588
+
1589
+ # Get final reports for best parameters
1590
+ train_report = None
1591
+ validate_report = None
1592
+ test_report = None
1593
+ train_metrics = {}
1594
+ validate_metrics = {}
1595
+ test_metrics = {}
1596
+
1597
+ for mode, report_attr, metrics_attr in [
1598
+ (DataSplitMode.TRAIN, 'train_report', 'train_metrics'),
1599
+ (DataSplitMode.VALIDATE, 'validate_report', 'validate_metrics'),
1600
+ (DataSplitMode.TEST, 'test_report', 'test_metrics'),
1601
+ ]:
1602
+ strat_copy = create_split_strategy(best_params, mode)
1603
+ bt = SimpleBacktester(
1604
+ strat_copy,
1605
+ cash=self.cash,
1606
+ commission=self.commission,
1607
+ commission_type=self.commission_type,
1608
+ lot_size=self.lot_size,
1609
+ )
1610
+ report = bt.run(progress_bar=False)
1611
+ metrics = _compute_backtest_metrics(report)
1612
+
1613
+ if mode == DataSplitMode.TRAIN:
1614
+ train_report = report
1615
+ train_metrics = metrics
1616
+ elif mode == DataSplitMode.VALIDATE:
1617
+ validate_report = report
1618
+ validate_metrics = metrics
1619
+ else:
1620
+ test_report = report
1621
+ test_metrics = metrics
1622
+
1623
+ return OptimizationResult(
1624
+ best_params=best_params,
1625
+ train_report=train_report,
1626
+ validate_report=validate_report,
1627
+ test_report=test_report,
1628
+ train_metrics=train_metrics,
1629
+ validate_metrics=validate_metrics,
1630
+ test_metrics=test_metrics,
1631
+ all_results=history_df
1632
+ )
@@ -1,5 +0,0 @@
1
- from .datasource import CSVDataSource as CSVDataSource, ParquetDataSource as ParquetDataSource, DataSource as DataSource
2
- from .strategy import Strategy as Strategy
3
- from .backtester import SimpleBacktester as SimpleBacktester
4
- from .enums import CommissionType as CommissionType
5
- from .indicators import indicators as indicators
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes