quantex 0.5.2__tar.gz → 0.6.1__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.
quantex-0.6.1/PKG-INFO ADDED
@@ -0,0 +1,83 @@
1
+ Metadata-Version: 2.4
2
+ Name: quantex
3
+ Version: 0.6.1
4
+ Summary: A simple quant strategy creation and backtesting package.
5
+ License-Expression: MIT
6
+ License-File: LICENSE.md
7
+ Author: Daniel Green
8
+ Author-email: dangreen07@outlook.com
9
+ Requires-Python: >=3.11,<4
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Requires-Dist: fastparquet (>=2024.11.0,<2025.0.0)
16
+ Requires-Dist: matplotlib (>=3.10.8,<4.0.0)
17
+ Requires-Dist: mkdocs (>=1.6.1,<2.0.0)
18
+ Requires-Dist: mplfinance (>=0.12.10b0,<0.13.0)
19
+ Requires-Dist: numpy (>=2.4.3,<3.0.0)
20
+ Requires-Dist: optuna (>=4.8.0,<5.0.0)
21
+ Requires-Dist: pandas (>=2.3.0,<3.0.0)
22
+ Requires-Dist: ta-lib (>=0.7.1,<0.8.0)
23
+ Requires-Dist: tqdm (>=4.67.1,<5.0.0)
24
+ Requires-Dist: yfinance (>=1.7.0,<2.0.0)
25
+ Description-Content-Type: text/markdown
26
+
27
+ # QuantEx Documentation
28
+
29
+ QuantEx is a Python backtesting library for OHLCV data. It is designed to be easy to use and extend, and to provide a simple interface for backtesting trading strategies.
30
+
31
+ ## Getting Started
32
+
33
+ To get started with QuantEx, you can install it using pip:
34
+ ```bash
35
+ pip install quantex
36
+ ```
37
+
38
+ QuantEx is designed to be used by writing your strategy as event-driven code. An example buy and hold strategy is shown below:
39
+
40
+ ```python
41
+ from quantex.strategy import Strategy
42
+
43
+ class BuyAndHold(Strategy):
44
+ def init(self):
45
+ pass
46
+
47
+ def next(self):
48
+ if self.broker.is_closed():
49
+ self.broker.buy(amount=50)
50
+ ```
51
+
52
+ This strategy will buy 50 shares of the stock when there are no open positions. To run this strategy, we must first get some data. Currently, QuantEx supports Yahoo Finance and pandas DataFrames as data sources.
53
+ To run the strategy with the data, we can use the Backtester class:
54
+ ```python
55
+ from quantex.backtester import Backtester
56
+ from quantex.datasource import YahooDataSource
57
+
58
+ bt = Backtester(BuyAndHold)
59
+ source = YahooDataSource("NVDA", start="2020-01-01", end="2025-12-31")
60
+ bt.add_data(source, "NVDA")
61
+ result = bt.run()
62
+ ```
63
+
64
+ The result object contains information about the strategy, such as the total return, annualized return, and max drawdown.
65
+
66
+ If you would like to include commissions in your strategy, you can use the Commission class:
67
+
68
+ ```python
69
+ from quantex.commission import Commission, ApplicationType
70
+ from quantex.backtester import Backtester
71
+ from quantex.datasource import YahooDataSource
72
+
73
+ bt = Backtester(BuyAndHold, commission=Commission(ApplicationType.PER_ORDER, fixed=1))
74
+ source = YahooDataSource("NVDA", start="2020-01-01", end="2025-12-31")
75
+ bt.add_data(source, "NVDA")
76
+ result = bt.run()
77
+ ```
78
+
79
+ This will charge a fixed commission of 1 dollar (or whatever the currency price is quoted in) per order.
80
+
81
+ ## Documentation
82
+
83
+ The documentation for QuantEx can be found [here](https://dangreen07.github.io/quantex/).
@@ -0,0 +1,57 @@
1
+ # QuantEx Documentation
2
+
3
+ QuantEx is a Python backtesting library for OHLCV data. It is designed to be easy to use and extend, and to provide a simple interface for backtesting trading strategies.
4
+
5
+ ## Getting Started
6
+
7
+ To get started with QuantEx, you can install it using pip:
8
+ ```bash
9
+ pip install quantex
10
+ ```
11
+
12
+ QuantEx is designed to be used by writing your strategy as event-driven code. An example buy and hold strategy is shown below:
13
+
14
+ ```python
15
+ from quantex.strategy import Strategy
16
+
17
+ class BuyAndHold(Strategy):
18
+ def init(self):
19
+ pass
20
+
21
+ def next(self):
22
+ if self.broker.is_closed():
23
+ self.broker.buy(amount=50)
24
+ ```
25
+
26
+ This strategy will buy 50 shares of the stock when there are no open positions. To run this strategy, we must first get some data. Currently, QuantEx supports Yahoo Finance and pandas DataFrames as data sources.
27
+ To run the strategy with the data, we can use the Backtester class:
28
+ ```python
29
+ from quantex.backtester import Backtester
30
+ from quantex.datasource import YahooDataSource
31
+
32
+ bt = Backtester(BuyAndHold)
33
+ source = YahooDataSource("NVDA", start="2020-01-01", end="2025-12-31")
34
+ bt.add_data(source, "NVDA")
35
+ result = bt.run()
36
+ ```
37
+
38
+ The result object contains information about the strategy, such as the total return, annualized return, and max drawdown.
39
+
40
+ If you would like to include commissions in your strategy, you can use the Commission class:
41
+
42
+ ```python
43
+ from quantex.commission import Commission, ApplicationType
44
+ from quantex.backtester import Backtester
45
+ from quantex.datasource import YahooDataSource
46
+
47
+ bt = Backtester(BuyAndHold, commission=Commission(ApplicationType.PER_ORDER, fixed=1))
48
+ source = YahooDataSource("NVDA", start="2020-01-01", end="2025-12-31")
49
+ bt.add_data(source, "NVDA")
50
+ result = bt.run()
51
+ ```
52
+
53
+ This will charge a fixed commission of 1 dollar (or whatever the currency price is quoted in) per order.
54
+
55
+ ## Documentation
56
+
57
+ The documentation for QuantEx can be found [here](https://dangreen07.github.io/quantex/).
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "quantex"
3
- version = "0.5.2"
3
+ version = "0.6.1"
4
4
  description = "A simple quant strategy creation and backtesting package."
5
5
  authors = [
6
6
  {name = "Daniel Green",email = "dangreen07@outlook.com"}
@@ -11,12 +11,14 @@ license = "MIT"
11
11
  dependencies = [
12
12
  "pandas (>=2.3.0,<3.0.0)",
13
13
  "fastparquet (>=2024.11.0,<2025.0.0)",
14
- "pyarrow (>=20.0.0,<21.0.0)",
15
14
  "tqdm (>=4.67.1,<5.0.0)",
16
15
  "numpy (>=2.4.3,<3.0.0)",
17
16
  "optuna (>=4.8.0,<5.0.0)",
18
17
  "matplotlib (>=3.10.8,<4.0.0)",
19
18
  "mplfinance (>=0.12.10b0,<0.13.0)",
19
+ "yfinance (>=1.7.0,<2.0.0)",
20
+ "ta-lib (>=0.7.1,<0.8.0)",
21
+ "mkdocs (>=1.6.1,<2.0.0)",
20
22
  ]
21
23
 
22
24
  [tool.poetry]
@@ -44,6 +46,9 @@ pytest-xdist = "^3.8.0"
44
46
  matplotlib = "^3.10.3"
45
47
  ipykernel = "^7.2.0"
46
48
 
49
+ [tool.pytest.ini_options]
50
+ pythonpath = ["src"]
51
+
47
52
  [build-system]
48
53
  requires = ["poetry-core>=2.0.0,<3.0.0"]
49
54
  build-backend = "poetry.core.masonry.api"
File without changes
@@ -0,0 +1,288 @@
1
+ from quantex.commission import Commission
2
+ from quantex.datasource import DataSource, PricingData
3
+ from quantex.strategy import Indicator, Strategy
4
+ from collections.abc import Callable
5
+ from dataclasses import dataclass
6
+ from itertools import product
7
+ from enum import Enum
8
+ import pandas as pd
9
+ import numpy as np
10
+ import optuna
11
+ import tqdm
12
+ import math
13
+ import copy
14
+
15
+
16
+ class SearchType(Enum):
17
+ GRID = 1
18
+ OPTUNA = 2
19
+
20
+
21
+ @dataclass
22
+ class Result:
23
+ """
24
+ This class contains the result of a backtest.
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ equity: np.ndarray,
30
+ starting_cash: float,
31
+ run_strategy: Strategy,
32
+ total_trades: int = 0,
33
+ ):
34
+ self.equity = equity
35
+ self.starting_cash = starting_cash
36
+ self.run_strategy = run_strategy
37
+ self.total_trades = total_trades
38
+
39
+ @property
40
+ def timedelta(self) -> pd.Timedelta:
41
+ """
42
+ The amount of time elapsed between the start and end of the backtest.
43
+ """
44
+ return (
45
+ self.run_strategy.data.Timestamp[-1] - self.run_strategy.data.Timestamp[0]
46
+ )
47
+
48
+ @property
49
+ def start(self) -> pd.Timestamp:
50
+ """
51
+ The start time of the backtest.
52
+ """
53
+ return self.run_strategy.data.Timestamp[0]
54
+
55
+ @property
56
+ def end(self) -> pd.Timestamp:
57
+ """
58
+ The end time of the backtest.
59
+ """
60
+ return self.run_strategy.data.Timestamp[-1]
61
+
62
+ @property
63
+ def total_return(self) -> float:
64
+ """
65
+ The total return of the backtest as a percentage.
66
+ """
67
+ return (self.equity[-1] - self.starting_cash) / self.starting_cash
68
+
69
+ def sharpe_ratio(self, risk_free_rate: float = 0.04) -> float:
70
+ """
71
+ The yearly sharpe ratio of the backtest.
72
+
73
+ Parameters:
74
+ risk_free_rate:
75
+ The risk free rate to be used in the calculation.
76
+
77
+ Returns:
78
+ The yearly sharpe ratio of the backtest.
79
+ """
80
+ total_time = (
81
+ self.run_strategy.data.Timestamp[-1] - self.run_strategy.data.Timestamp[0]
82
+ )
83
+ years = total_time.days / 365.25
84
+ periods_per_year = len(self.equity) / years
85
+ returns = pd.Series(
86
+ self.equity, index=self.run_strategy.data.Timestamp
87
+ ).pct_change()
88
+ risk_free = (1 + risk_free_rate) ** (1 / periods_per_year) - 1
89
+ std = returns.std()
90
+ if std == 0:
91
+ return 0
92
+ sharpe = (returns.mean() - risk_free) / std * np.sqrt(periods_per_year)
93
+ return sharpe
94
+
95
+ @property
96
+ def annualized_return(self):
97
+ """
98
+ The annualized return of the backtest.
99
+ """
100
+ total_time = (
101
+ self.run_strategy.data.Timestamp[-1] - self.run_strategy.data.Timestamp[0]
102
+ )
103
+ years = total_time.days / (365.25)
104
+ return (1 + self.total_return) ** (1 / years) - 1
105
+
106
+ @property
107
+ def max_drawdown(self) -> tuple[float, float]:
108
+ """
109
+ The maximum drawdown of the backtest
110
+
111
+ Returns:
112
+ The maximum drawdown in dollars and percentage, respectively.
113
+ """
114
+ peak = self.equity[0]
115
+ max_drawdown_dollars = 0
116
+ max_drawdown_percent = 0
117
+ for value in self.equity:
118
+ drawdown_dollars = value - peak
119
+ drawdown_percent = (value - peak) / peak
120
+ max_drawdown_dollars = min(
121
+ max_drawdown_dollars,
122
+ drawdown_dollars,
123
+ )
124
+ max_drawdown_percent = min(
125
+ max_drawdown_percent,
126
+ drawdown_percent,
127
+ )
128
+ return abs(max_drawdown_dollars), abs(max_drawdown_percent)
129
+
130
+
131
+ class Backtester:
132
+ def __init__(
133
+ self,
134
+ strategy: type[Strategy],
135
+ commission: Commission | None = None,
136
+ cash: float = 10_000,
137
+ ):
138
+ """
139
+ This class is used to run a backtest on a strategy.
140
+
141
+ Parameters:
142
+ strategy:
143
+ The strategy to be run.
144
+ commission:
145
+ The commission to be applied to the strategy. If None, no commission will be applied.
146
+ cash:
147
+ The starting cash to be used in the backtest.
148
+ """
149
+ self.strategy = strategy
150
+ self.data = PricingData()
151
+ self.cash = cash
152
+ self.commission = commission or Commission()
153
+
154
+ def add_data(self, data: DataSource, name: str | None = None):
155
+ """
156
+ Adds a data source to the backtest.
157
+
158
+ Parameters:
159
+ data:
160
+ The data source to be added.
161
+ name:
162
+ The name of the data source. If None, the name will be the same as the data source.
163
+ """
164
+ if name is None:
165
+ name = data.name
166
+ self.data.add_data(data, name)
167
+
168
+ def run(self, params: dict | None = None) -> Result:
169
+ """
170
+ Runs the backtest.
171
+
172
+ Parameters:
173
+ params: The parameters to use for the backtest. If None, the default parameters will be used.
174
+
175
+ Returns:
176
+ Result: The result of the backtest.
177
+ """
178
+ data = copy.deepcopy(self.data)
179
+ strat = self.strategy(data, cash=self.cash)
180
+ if params is not None:
181
+ for name, value in params.items():
182
+ setattr(strat, name, value)
183
+ strat.init()
184
+ indicators = [
185
+ i for i in strat.__dict__.keys() if isinstance(strat.__dict__[i], Indicator)
186
+ ]
187
+ for name in data.datas.keys():
188
+ data.datas[name]._current = 0 ## Reset the current index
189
+ for indicator in indicators:
190
+ strat.__dict__[indicator]._current = 0
191
+ initial_cash = strat.broker.cash
192
+ equity = np.full(len(data.index), initial_cash, dtype=np.float64)
193
+ for i in range(len(data.index)):
194
+ for name in data.datas.keys():
195
+ data.datas[
196
+ name
197
+ ]._current += (
198
+ 1 ## TODO: Handle multiple data sources with different indexes
199
+ )
200
+ skip_nan = False
201
+ for indicator in indicators:
202
+ strat.__dict__[
203
+ indicator
204
+ ]._current += 1 ## Shouldn't have to handle multiple indexers as they should all be the same
205
+ ind: Indicator = strat.__dict__[indicator]
206
+ if np.isnan(ind[-1]):
207
+ skip_nan = True
208
+ if skip_nan:
209
+ continue
210
+ strat.broker.__process_orders__()
211
+ strat.next()
212
+ equity[i] = strat.broker.equity()
213
+ result = Result(equity, initial_cash, strat, strat.broker.total_trades)
214
+ return result
215
+
216
+ def optimize(
217
+ self,
218
+ params: dict[str, list],
219
+ constraint: Callable[[dict], bool] | None = None,
220
+ max_trials: int = 100,
221
+ search_type: SearchType = SearchType.GRID,
222
+ risk_free_rate: float = 0.04,
223
+ seed: int = 0,
224
+ ) -> tuple[float, dict]:
225
+ """
226
+ Optimizes the backtest with the given parameters.
227
+
228
+ Parameters:
229
+ params: The parameters to optimize.
230
+ max_trials: The maximum number of trials to run.
231
+ search_type: The search type to use.
232
+ """
233
+ self.results = []
234
+ self.max_sharpe = -np.inf
235
+ self.best_trial = {}
236
+ if search_type == SearchType.GRID:
237
+ search_space = (
238
+ dict(zip(params.keys(), values)) for values in product(*params.values())
239
+ )
240
+ search_space = list(search_space)
241
+ if constraint is not None:
242
+ search_space = [trial for trial in search_space if constraint(trial)]
243
+ search_space = search_space[:max_trials]
244
+
245
+ def run_trial(trial):
246
+ result = self.run(params=trial)
247
+ sharpe = result.sharpe_ratio(risk_free_rate=risk_free_rate)
248
+ if sharpe > self.max_sharpe:
249
+ self.max_sharpe = sharpe
250
+ self.best_trial = trial
251
+ self.results.append((trial, sharpe))
252
+
253
+ for trial in tqdm.tqdm(search_space, desc="Processing"):
254
+ run_trial(trial)
255
+ elif search_type == SearchType.OPTUNA:
256
+ optuna.logging.set_verbosity(optuna.logging.WARNING)
257
+ sampler = optuna.samplers.TPESampler(seed=seed)
258
+ study = optuna.create_study(
259
+ direction="maximize",
260
+ sampler=sampler,
261
+ )
262
+
263
+ def objective(optuna_trial: optuna.Trial):
264
+ trial_params = {
265
+ name: optuna_trial.suggest_categorical(name, values)
266
+ for name, values in params.items()
267
+ }
268
+
269
+ if constraint is not None and not constraint(trial_params):
270
+ raise optuna.TrialPruned("Parameter constraint failed")
271
+
272
+ result = self.run(params=trial_params)
273
+ sharpe = result.sharpe_ratio(risk_free_rate=risk_free_rate)
274
+
275
+ if sharpe is None or not math.isfinite(sharpe):
276
+ raise optuna.TrialPruned("Invalid Sharpe ratio")
277
+
278
+ self.results.append((trial_params, sharpe))
279
+ return sharpe
280
+
281
+ study.optimize(objective, n_trials=max_trials, show_progress_bar=True)
282
+
283
+ if study.best_trial is None:
284
+ raise ValueError("No valid parameter combination was found")
285
+
286
+ self.best_trial = study.best_params
287
+ self.max_sharpe = study.best_value
288
+ return (self.max_sharpe, self.best_trial)