quantex 0.5.2__tar.gz → 0.6.0__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.0/PKG-INFO ADDED
@@ -0,0 +1,83 @@
1
+ Metadata-Version: 2.4
2
+ Name: quantex
3
+ Version: 0.6.0
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.0"
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,265 @@
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 total_return(self) -> float:
41
+ """
42
+ The total return of the backtest as a percentage.
43
+ """
44
+ return (self.equity[-1] - self.starting_cash) / self.starting_cash
45
+
46
+ def sharpe_ratio(self, risk_free_rate: float = 0.04) -> float:
47
+ """
48
+ The yearly sharpe ratio of the backtest.
49
+
50
+ Parameters:
51
+ risk_free_rate:
52
+ The risk free rate to be used in the calculation.
53
+
54
+ Returns:
55
+ The yearly sharpe ratio of the backtest.
56
+ """
57
+ total_time = (
58
+ self.run_strategy.data.Timestamp[-1] - self.run_strategy.data.Timestamp[0]
59
+ )
60
+ years = total_time.days / 365.25
61
+ periods_per_year = len(self.equity) / years
62
+ returns = pd.Series(
63
+ self.equity, index=self.run_strategy.data.Timestamp
64
+ ).pct_change()
65
+ risk_free = (1 + risk_free_rate) ** (1 / periods_per_year) - 1
66
+ std = returns.std()
67
+ if std == 0:
68
+ return 0
69
+ sharpe = (returns.mean() - risk_free) / std * np.sqrt(periods_per_year)
70
+ return sharpe
71
+
72
+ @property
73
+ def annualized_return(self):
74
+ """
75
+ The annualized return of the backtest.
76
+ """
77
+ total_time = (
78
+ self.run_strategy.data.Timestamp[-1] - self.run_strategy.data.Timestamp[0]
79
+ )
80
+ years = total_time.days / (365.25)
81
+ return (1 + self.total_return) ** (1 / years) - 1
82
+
83
+ @property
84
+ def max_drawdown(self) -> tuple[float, float]:
85
+ """
86
+ The maximum drawdown of the backtest
87
+
88
+ Returns:
89
+ The maximum drawdown in dollars and percentage, respectively.
90
+ """
91
+ peak = self.equity[0]
92
+ max_drawdown_dollars = 0
93
+ max_drawdown_percent = 0
94
+ for value in self.equity:
95
+ drawdown_dollars = value - peak
96
+ drawdown_percent = (value - peak) / peak
97
+ max_drawdown_dollars = min(
98
+ max_drawdown_dollars,
99
+ drawdown_dollars,
100
+ )
101
+ max_drawdown_percent = min(
102
+ max_drawdown_percent,
103
+ drawdown_percent,
104
+ )
105
+ return abs(max_drawdown_dollars), abs(max_drawdown_percent)
106
+
107
+
108
+ class Backtester:
109
+ def __init__(
110
+ self,
111
+ strategy: type[Strategy],
112
+ commission: Commission | None = None,
113
+ cash: float = 10_000,
114
+ ):
115
+ """
116
+ This class is used to run a backtest on a strategy.
117
+
118
+ Parameters:
119
+ strategy:
120
+ The strategy to be run.
121
+ commission:
122
+ The commission to be applied to the strategy. If None, no commission will be applied.
123
+ cash:
124
+ The starting cash to be used in the backtest.
125
+ """
126
+ self.strategy = strategy
127
+ self.data = PricingData()
128
+ self.cash = cash
129
+ self.commission = commission or Commission()
130
+
131
+ def add_data(self, data: DataSource, name: str | None = None):
132
+ """
133
+ Adds a data source to the backtest.
134
+
135
+ Parameters:
136
+ data:
137
+ The data source to be added.
138
+ name:
139
+ The name of the data source. If None, the name will be the same as the data source.
140
+ """
141
+ if name is None:
142
+ name = data.name
143
+ self.data.add_data(data, name)
144
+
145
+ def run(self, params: dict | None = None) -> Result:
146
+ """
147
+ Runs the backtest.
148
+
149
+ Parameters:
150
+ params: The parameters to use for the backtest. If None, the default parameters will be used.
151
+
152
+ Returns:
153
+ Result: The result of the backtest.
154
+ """
155
+ data = copy.deepcopy(self.data)
156
+ strat = self.strategy(data, cash=self.cash)
157
+ if params is not None:
158
+ for name, value in params.items():
159
+ setattr(strat, name, value)
160
+ strat.init()
161
+ indicators = [
162
+ i for i in strat.__dict__.keys() if isinstance(strat.__dict__[i], Indicator)
163
+ ]
164
+ for name in data.datas.keys():
165
+ data.datas[name]._current = 0 ## Reset the current index
166
+ for indicator in indicators:
167
+ strat.__dict__[indicator]._current = 0
168
+ initial_cash = strat.broker.cash
169
+ equity = np.full(len(data.index), initial_cash, dtype=np.float64)
170
+ for i in range(len(data.index)):
171
+ for name in data.datas.keys():
172
+ data.datas[
173
+ name
174
+ ]._current += (
175
+ 1 ## TODO: Handle multiple data sources with different indexes
176
+ )
177
+ skip_nan = False
178
+ for indicator in indicators:
179
+ strat.__dict__[
180
+ indicator
181
+ ]._current += 1 ## Shouldn't have to handle multiple indexers as they should all be the same
182
+ ind: Indicator = strat.__dict__[indicator]
183
+ if np.isnan(ind[-1]):
184
+ skip_nan = True
185
+ if skip_nan:
186
+ continue
187
+ strat.broker.__process_orders__()
188
+ strat.next()
189
+ equity[i] = strat.broker.equity()
190
+ result = Result(equity, initial_cash, strat, strat.broker.total_trades)
191
+ return result
192
+
193
+ def optimize(
194
+ self,
195
+ params: dict[str, list],
196
+ constraint: Callable[[dict], bool] | None = None,
197
+ max_trials: int = 100,
198
+ search_type: SearchType = SearchType.GRID,
199
+ risk_free_rate: float = 0.04,
200
+ seed: int = 0,
201
+ ) -> tuple[float, dict]:
202
+ """
203
+ Optimizes the backtest with the given parameters.
204
+
205
+ Parameters:
206
+ params: The parameters to optimize.
207
+ max_trials: The maximum number of trials to run.
208
+ search_type: The search type to use.
209
+ """
210
+ self.results = []
211
+ self.max_sharpe = -np.inf
212
+ self.best_trial = {}
213
+ if search_type == SearchType.GRID:
214
+ search_space = (
215
+ dict(zip(params.keys(), values)) for values in product(*params.values())
216
+ )
217
+ search_space = list(search_space)
218
+ if constraint is not None:
219
+ search_space = [trial for trial in search_space if constraint(trial)]
220
+ search_space = search_space[:max_trials]
221
+
222
+ def run_trial(trial):
223
+ result = self.run(params=trial)
224
+ sharpe = result.sharpe_ratio(risk_free_rate=risk_free_rate)
225
+ if sharpe > self.max_sharpe:
226
+ self.max_sharpe = sharpe
227
+ self.best_trial = trial
228
+ self.results.append((trial, sharpe))
229
+
230
+ for trial in tqdm.tqdm(search_space, desc="Processing"):
231
+ run_trial(trial)
232
+ elif search_type == SearchType.OPTUNA:
233
+ optuna.logging.set_verbosity(optuna.logging.WARNING)
234
+ sampler = optuna.samplers.TPESampler(seed=seed)
235
+ study = optuna.create_study(
236
+ direction="maximize",
237
+ sampler=sampler,
238
+ )
239
+
240
+ def objective(optuna_trial: optuna.Trial):
241
+ trial_params = {
242
+ name: optuna_trial.suggest_categorical(name, values)
243
+ for name, values in params.items()
244
+ }
245
+
246
+ if constraint is not None and not constraint(trial_params):
247
+ raise optuna.TrialPruned("Parameter constraint failed")
248
+
249
+ result = self.run(params=trial_params)
250
+ sharpe = result.sharpe_ratio(risk_free_rate=risk_free_rate)
251
+
252
+ if sharpe is None or not math.isfinite(sharpe):
253
+ raise optuna.TrialPruned("Invalid Sharpe ratio")
254
+
255
+ self.results.append((trial_params, sharpe))
256
+ return sharpe
257
+
258
+ study.optimize(objective, n_trials=max_trials, show_progress_bar=True)
259
+
260
+ if study.best_trial is None:
261
+ raise ValueError("No valid parameter combination was found")
262
+
263
+ self.best_trial = study.best_params
264
+ self.max_sharpe = study.best_value
265
+ return (self.max_sharpe, self.best_trial)