roboquant 2.3.3__tar.gz → 2.4.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.
Files changed (70) hide show
  1. {roboquant-2.3.3 → roboquant-2.4.0}/PKG-INFO +1 -4
  2. {roboquant-2.3.3 → roboquant-2.4.0}/README.md +0 -3
  3. {roboquant-2.3.3 → roboquant-2.4.0}/pyproject.toml +1 -2
  4. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/__init__.py +2 -0
  5. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/account.py +29 -28
  6. roboquant-2.4.0/roboquant/ai/__init__.py +8 -0
  7. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/ai/features.py +99 -52
  8. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/ai/strategies.py +10 -3
  9. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/alpaca/feed.py +2 -2
  10. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/asset.py +8 -10
  11. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/brokers/broker.py +1 -1
  12. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/brokers/simbroker.py +11 -10
  13. roboquant-2.4.0/roboquant/crypto/__init__.py +4 -0
  14. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/crypto/cryptobroker.py +48 -19
  15. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/crypto/cryptofeed.py +5 -9
  16. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/feeds/__init__.py +0 -2
  17. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/feeds/csvfeed.py +2 -2
  18. roboquant-2.4.0/roboquant/feeds/feed.py +31 -0
  19. roboquant-2.4.0/roboquant/feeds/historic.py +256 -0
  20. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/feeds/parquet.py +5 -3
  21. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/feeds/randomwalk.py +2 -2
  22. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/feeds/sql.py +2 -1
  23. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/feeds/yahoo.py +12 -2
  24. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/journals/__init__.py +2 -1
  25. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/journals/metricsjournal.py +1 -50
  26. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/monetary.py +26 -2
  27. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/signal.py +3 -0
  28. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/strategies/buffer.py +6 -3
  29. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/strategies/multistrategy.py +2 -2
  30. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/strategies/strategy.py +1 -1
  31. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/ta/__init__.py +7 -12
  32. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/timeframe.py +23 -6
  33. roboquant-2.4.0/roboquant/timeseries.py +59 -0
  34. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/traders/flextrader.py +7 -1
  35. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/traders/trader.py +1 -1
  36. roboquant-2.3.3/roboquant/ai/__init__.py +0 -6
  37. roboquant-2.3.3/roboquant/crypto/__init__.py +0 -3
  38. roboquant-2.3.3/roboquant/feeds/feed.py +0 -160
  39. roboquant-2.3.3/roboquant/feeds/historic.py +0 -111
  40. {roboquant-2.3.3 → roboquant-2.4.0}/LICENSE +0 -0
  41. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/ai/rl.py +0 -0
  42. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/alpaca/__init__.py +0 -0
  43. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/alpaca/broker.py +0 -0
  44. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/brokers/__init__.py +0 -0
  45. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/event.py +0 -0
  46. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/feeds/live.py +0 -0
  47. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/feeds/resources/us10.parquet +0 -0
  48. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/feeds/util.py +0 -0
  49. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/ibkr/__init__.py +0 -0
  50. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/ibkr/broker.py +0 -0
  51. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/ibkr/types.py +0 -0
  52. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/journals/alphabeta.py +0 -0
  53. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/journals/basicjournal.py +0 -0
  54. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/journals/feedmetric.py +0 -0
  55. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/journals/journal.py +0 -0
  56. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/journals/marketmetric.py +0 -0
  57. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/journals/metric.py +0 -0
  58. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/journals/pnlmetric.py +0 -0
  59. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/journals/pricemetric.py +0 -0
  60. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/journals/runmetric.py +0 -0
  61. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/journals/scorecard.py +0 -0
  62. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/journals/tensorboard.py +0 -0
  63. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/order.py +0 -0
  64. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/run.py +0 -0
  65. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/strategies/__init__.py +0 -0
  66. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/strategies/cachedstrategy.py +0 -0
  67. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/strategies/emacrossover.py +0 -0
  68. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/strategies/ibsstrategy.py +0 -0
  69. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/strategies/tastrategy.py +0 -0
  70. {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/traders/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: roboquant
3
- Version: 2.3.3
3
+ Version: 2.4.0
4
4
  Summary: A fast algorithmic trading platform with support for AI based strategies
5
5
  Keywords: algorithmic,trading,machine learning,AI,investment,finance,crypto,stocks,exchange,forex
6
6
  Author: roboquant team
@@ -121,6 +121,3 @@ Roboquant is made available under the Apache 2.0 license. You can read more abou
121
121
  Absolutely no warranty is implied with this product. Use at your own risk. I provide no guarantee that it will be profitable, or that it won't lose all your money very quickly or does not contain bugs.
122
122
 
123
123
  All financial trading offers the possibility of loss. Leveraged trading, may result in you losing all your money, and still owing more. Back tested results are no guarantee of future performance. I can take no responsibility for any losses caused by live trading using roboquant. Use at your own risk. I am not registered or authorised by any financial regulator.
124
-
125
- ## Kotlin version
126
- Next to this Python version of `roboquant`, there is also a Kotlin version available. Both share a similar API, just the used computer language is different. Which one to use depends very much on personal preferences, skills and usage.
@@ -79,6 +79,3 @@ Roboquant is made available under the Apache 2.0 license. You can read more abou
79
79
  Absolutely no warranty is implied with this product. Use at your own risk. I provide no guarantee that it will be profitable, or that it won't lose all your money very quickly or does not contain bugs.
80
80
 
81
81
  All financial trading offers the possibility of loss. Leveraged trading, may result in you losing all your money, and still owing more. Back tested results are no guarantee of future performance. I can take no responsibility for any losses caused by live trading using roboquant. Use at your own risk. I am not registered or authorised by any financial regulator.
82
-
83
- ## Kotlin version
84
- Next to this Python version of `roboquant`, there is also a Kotlin version available. Both share a similar API, just the used computer language is different. Which one to use depends very much on personal preferences, skills and usage.
@@ -19,7 +19,7 @@ module-name = "roboquant"
19
19
 
20
20
  [project]
21
21
  name = "roboquant"
22
- version = "2.3.3"
22
+ version = "2.4.0"
23
23
  authors = [{ name = "roboquant team", email = "info@roboquant.org" }]
24
24
  description = "A fast algorithmic trading platform with support for AI based strategies"
25
25
  readme = "README.md"
@@ -84,4 +84,3 @@ dev = [
84
84
  "ipywidgets>=8.1.6",
85
85
  "chronos-forecasting>=2.3.1",
86
86
  ]
87
- talib = ["ta-lib==0.6.5"]
@@ -17,6 +17,7 @@ from .monetary import Amount, Wallet
17
17
  from .asset import Asset, Stock, Crypto, Forex, Option
18
18
  from .run import run
19
19
  from .timeframe import Timeframe, utcnow
20
+ from .timeseries import TimeSeries
20
21
 
21
22
  __all__ = [
22
23
  "brokers",
@@ -43,5 +44,6 @@ __all__ = [
43
44
  "Option",
44
45
  "run",
45
46
  "Timeframe",
47
+ "TimeSeries",
46
48
  "utcnow",
47
49
  ]
@@ -32,7 +32,7 @@ class Trade:
32
32
  @dataclass(slots=True)
33
33
  class Position:
34
34
  """The position of an asset in the account. The position prices are denoted in the currency of the asset that
35
- is linked to this posistion. See also `Account.positions`.
35
+ is linked to this posistion. See also :meth:`roboquant.Account.positions`.
36
36
  """
37
37
 
38
38
  size: Decimal
@@ -212,32 +212,14 @@ class Account:
212
212
 
213
213
  def pnl(self) -> Wallet:
214
214
  """
215
- Return the total profit and loss of the account, which is the sum of realized and unrealized PnL.
215
+ Return the total profit and loss of the account, which is
216
+ the sum of realized and unrealized PnL.
216
217
 
217
218
  Returns:
218
219
  Wallet: The total profit and loss.
219
220
  """
220
221
  return self.realized_pnl() + self.unrealized_pnl()
221
222
 
222
- def required_buying_power(self, order: Order) -> Amount:
223
- """
224
- Return the amount of buying power required for a certain order. The underlying logic takes into
225
- account that a reduction in position size doesn't require buying power.
226
-
227
- Args:
228
- order (Order): The order for which to calculate the required buying power.
229
-
230
- Returns:
231
- Amount: The required buying power as an Amount.
232
- """
233
- pos_size = self.get_position_size(order.asset)
234
-
235
- # Only buying power required if the remaining order size increases the position size
236
- if abs(pos_size + order.remaining) > abs(pos_size):
237
- return order.asset.contract_amount(abs(order.remaining), order.limit)
238
-
239
- return Amount(order.asset.currency, 0.0)
240
-
241
223
  def unrealized_pnl_value(self) -> float:
242
224
  """
243
225
  Return the unrealized profit and loss value denoted in the base currency of the account.
@@ -261,7 +243,10 @@ class Account:
261
243
  return pos.size if pos else Decimal()
262
244
 
263
245
  def get_position_list(self) -> list[dict[str, Any]]:
264
- """Return all open positions including their pnl as a list of dicts"""
246
+ """Return all open positions including their pnl as a list of dicts.
247
+ This comes in handy for further processing, like converting
248
+ them to a dataframe.
249
+ """
265
250
  result: list[dict[str, Any]] = []
266
251
  for asset, pos in self.positions.items():
267
252
  result.append(
@@ -280,7 +265,9 @@ class Account:
280
265
  return result
281
266
 
282
267
  def get_order_list(self) -> list[dict[str, Any]]:
283
- """Return all open orders as a list of dicts"""
268
+ """Return all open orders as a list of dicts.
269
+ This comes in handy for further processing, like converting
270
+ them to a dataframe."""
284
271
  result: list[dict[str, Any]] = []
285
272
  for order in self.orders:
286
273
  result.append(
@@ -306,7 +293,21 @@ class Account:
306
293
  return order
307
294
 
308
295
  def __repr__(self) -> str:
309
- """Condensed representation of this account."""
296
+ """Condensed representation of this account. It by default won't
297
+ display decimals for the various amounts. But you can use the float
298
+ formatting spec to influence this behavior: f"{account:{.4f}}"
299
+ """
300
+ return f"{self:,.0f}"
301
+
302
+ def __format__(self, format_spec: str) -> str:
303
+ """Return a float formatted string representation of the wallet.
304
+
305
+ Args:
306
+ format_spec (str): The format specification.
307
+
308
+ Returns:
309
+ str: The formatted string representation.
310
+ """
310
311
  p = [f"{v.size}@{k.symbol}" for k, v in self.positions.items()]
311
312
  p_str = ", ".join(p) or "none"
312
313
 
@@ -316,12 +317,12 @@ class Account:
316
317
  mkt = self.mkt_value() or Amount(self.base_currency, 0.0)
317
318
 
318
319
  result = (
319
- f"buying power : {self.buying_power}\n"
320
- f"cash : {self.cash}\n"
321
- f"equity : {self.equity()}\n"
320
+ f"buying power : {self.buying_power:{format_spec}}\n"
321
+ f"cash : {self.cash:{format_spec}}\n"
322
+ f"equity : {self.equity():{format_spec}}\n"
322
323
  f"positions : {p_str}\n"
323
324
  f"trades : {len(self.trades)}\n"
324
- f"mkt value : {mkt}\n"
325
+ f"mkt value : {mkt:{format_spec}}\n"
325
326
  f"orders : {o_str}\n"
326
327
  f"last update : {self.last_update}"
327
328
  )
@@ -0,0 +1,8 @@
1
+ """roboquant.ai package.
2
+ Provides a number of AI related classes and methods to support the development of AI based trading strategies.
3
+ It relies on Stable Baselines3 for reinforcement learning and PyTorch for deep learning.
4
+
5
+ This package also introduces the concept of Features, which are the building blocks for roboquant machine learning models
6
+ and can be used to extract relevant information. The ones included by default are either based on an `Event` or an `Account`.
7
+ Typically Event features are used for input data and Account features are used for reward/label/output data.
8
+ """
@@ -12,16 +12,24 @@ from roboquant.event import Event, Bar, Quote
12
12
  from roboquant.strategies.buffer import OHLCVBuffer
13
13
 
14
14
  T = TypeVar("T")
15
- FloatArray = NDArray[np.float32]
15
+ NPFloatArray = NDArray[np.float32]
16
+ NPIntArray = NDArray[np.int64]
16
17
 
17
18
 
18
19
  class Feature(Generic[T]):
19
- """Base class for different types of features.
20
- The ones included by default are either based either an `Event` or an `Account`.
21
- Typically Event features are used for input and Account features are used for reward/label/output."""
20
+ """Base class for different types of features. Features are the data building blocks for
21
+ roboquant machine learning models and can be used to extract relevant information.
22
+
23
+ The ones included by default are either based on an `Event` or an `Account`.
24
+ Typically Event features are used for input data and Account features are used for
25
+ reward/label/output data.
26
+
27
+ Features can be combined, sliced, nested, normalized and cached to create more
28
+ complex features.
29
+ """
22
30
 
23
31
  @abstractmethod
24
- def calc(self, value: T) -> FloatArray:
32
+ def calc(self, value: T) -> NPFloatArray:
25
33
  """
26
34
  Perform the calculation and return the result as a 1-dimensional NDArray of type float32.
27
35
  The result should always be the same size. If a value cannot be calculated at a certain
@@ -33,21 +41,22 @@ class Feature(Generic[T]):
33
41
  """Return the size of this feature"""
34
42
 
35
43
  def reset(self):
36
- """Reset the state of the feature"""
44
+ """Reset the state of the feature. This is called at the start of a new epoch.
45
+ If the feature has no state, this can be left empty."""
37
46
 
38
- def _shape(self):
39
- """return the shape of this feature as a tuple"""
47
+ def _shape(self) -> tuple[int]:
48
+ """return the size of this feature as a shape tuple"""
40
49
  return (self.size(),)
41
50
 
42
- def _zeros(self):
51
+ def _zeros(self) -> NPFloatArray:
43
52
  """Return a zero array of the correct shape"""
44
53
  return np.zeros(self._shape(), dtype=np.float32)
45
54
 
46
- def _ones(self):
55
+ def _ones(self) -> NPFloatArray:
47
56
  """Return an array of ones of the correct shape"""
48
57
  return np.ones(self._shape(), dtype=np.float32)
49
58
 
50
- def _full_nan(self) -> FloatArray:
59
+ def _full_nan(self) -> NPFloatArray:
51
60
  """Return a full NaN array of the correct shape"""
52
61
  return np.full(self._shape(), float("nan"), dtype=np.float32)
53
62
 
@@ -56,7 +65,7 @@ class Feature(Generic[T]):
56
65
  return ReturnFeature(self)
57
66
  return LongReturnsFeature(self, period)
58
67
 
59
- def normalize(self, min_period=3) -> "Feature[T]":
68
+ def normalize(self, min_period: int=3) -> "Feature[T]":
60
69
  """Normalize the feature values by calculating the mean and standard deviation."""
61
70
  return NormalizeFeature(self, min_period)
62
71
 
@@ -82,7 +91,7 @@ class SlicedFeature(Feature[T]):
82
91
  self.feature = feature
83
92
  self._size = len(feature._zeros()[args])
84
93
 
85
- def calc(self, value: T) -> FloatArray:
94
+ def calc(self, value: T) -> NPFloatArray:
86
95
  values = self.feature.calc(value)
87
96
  return values[self.args]
88
97
 
@@ -93,8 +102,10 @@ class SlicedFeature(Feature[T]):
93
102
  return self.feature.reset()
94
103
 
95
104
 
96
- class FixedValueFeature(Feature):
97
- """Feature that always returns a fixed value"""
105
+ class FixedValueFeature(Feature[Any]):
106
+ """Feature that always returns a fixed value array.
107
+ This can be used for example to add a bias term to the input features.
108
+ """
98
109
 
99
110
  def __init__(self, value: ArrayLike) -> None:
100
111
  super().__init__()
@@ -103,9 +114,33 @@ class FixedValueFeature(Feature):
103
114
  def size(self) -> int:
104
115
  return len(self.value)
105
116
 
106
- def calc(self, value: Any) -> FloatArray:
117
+ def calc(self, value: Any) -> NPFloatArray:
107
118
  return self.value
108
119
 
120
+ class RandomFeature(Feature[Any]):
121
+ """Feature that always returns a random value array. If a seed is provided,
122
+ the seed will be used to initialize the random number generator,
123
+ so that the same random values are generated each epoch.
124
+
125
+ If used with a seed, you can check your strategy for overfitting by running
126
+ several epochs and checking if the results are improving.
127
+ """
128
+
129
+ def __init__(self, size: int, seed: int|None = None) -> None:
130
+ super().__init__()
131
+ self._size = size
132
+ self._seed = seed
133
+ self.reset()
134
+
135
+ def size(self) -> int:
136
+ return self._size
137
+
138
+ def calc(self, value: Any) -> NPFloatArray:
139
+ return np.random.rand(self._size).astype(np.float32)
140
+
141
+ def reset(self):
142
+ np.random.seed(self._seed)
143
+
109
144
 
110
145
  class CombinedFeature(Feature[T]):
111
146
  """Combine multiple features into one single feature by horizontal stacking them.
@@ -117,7 +152,7 @@ class CombinedFeature(Feature[T]):
117
152
  self.features = features
118
153
  self._size = sum(feature.size() for feature in self.features)
119
154
 
120
- def calc(self, value: T) -> FloatArray:
155
+ def calc(self, value: T) -> NPFloatArray:
121
156
  data = [feature.calc(value) for feature in self.features]
122
157
  return np.hstack(data, dtype=np.float32)
123
158
 
@@ -130,7 +165,9 @@ class CombinedFeature(Feature[T]):
130
165
 
131
166
 
132
167
  class NormalizeFeature(Feature[T]):
133
- """online normalization calculator"""
168
+ """online normalization calculator
169
+ - min_count: minimum number of values to calculate the mean and standard deviation
170
+ """
134
171
 
135
172
  def __init__(self, feature: Feature[T], min_count: int = 3) -> None:
136
173
  super().__init__()
@@ -138,10 +175,11 @@ class NormalizeFeature(Feature[T]):
138
175
  self.min_count = min_count
139
176
  self.existing_aggregate = (self._zero_int(), self._zeros(), self._zeros())
140
177
 
141
- def _zero_int(self):
142
- return np.zeros((self.size(),), dtype="int")
178
+ def _zero_int(self) -> NPIntArray:
179
+ return np.zeros((self.size(),), dtype="int64")
143
180
 
144
- def denormalize(self, value):
181
+ def denormalize(self, value) -> NPFloatArray:
182
+ """Denormalize the value"""
145
183
  (count, mean, m2) = self.existing_aggregate
146
184
  stdev = np.sqrt(m2 / count) - 1e-12
147
185
  return value * stdev + mean
@@ -155,14 +193,14 @@ class NormalizeFeature(Feature[T]):
155
193
  delta2 = new_value - mean
156
194
  m2[mask] += delta[mask] * delta2[mask]
157
195
 
158
- def __normalize_values(self, values) -> FloatArray:
196
+ def __normalize_values(self, values) -> NPFloatArray:
159
197
  (count, mean, m2) = self.existing_aggregate
160
198
  stdev = self._full_nan()
161
199
  mask = count >= self.min_count
162
200
  stdev[mask] = np.sqrt(m2[mask] / count[mask]) + 1e-12 # type: ignore
163
201
  return (values - mean) / stdev
164
202
 
165
- def calc(self, value: T) -> FloatArray:
203
+ def calc(self, value: T) -> NPFloatArray:
166
204
  values = self.feature.calc(value)
167
205
  self.__update(values)
168
206
  return self.__normalize_values(values)
@@ -176,14 +214,15 @@ class NormalizeFeature(Feature[T]):
176
214
 
177
215
 
178
216
  class FillFeature(Feature[T]):
179
- """If a feature contains a NaN value, use the last known value instead"""
217
+ """If the underlying feature contains a NaN value,
218
+ replace it with the last known value instead"""
180
219
 
181
220
  def __init__(self, feature: Feature[T]) -> None:
182
221
  super().__init__()
183
222
  self.feature: Feature = feature
184
223
  self.fill = self._full_nan()
185
224
 
186
- def calc(self, value: T) -> FloatArray:
225
+ def calc(self, value: T) -> NPFloatArray:
187
226
  values = self.feature.calc(value)
188
227
  mask = np.isnan(values)
189
228
  values[mask] = self.fill[mask]
@@ -199,14 +238,15 @@ class FillFeature(Feature[T]):
199
238
 
200
239
 
201
240
  class FillWithConstantFeature(Feature[T]):
202
- """If a feature contains a NaN value, fill it with a constant value"""
241
+ """If the underlying feature contains a NaN value,
242
+ fill it with a constant value"""
203
243
 
204
244
  def __init__(self, feature: Feature[T], constant: float = 0.0) -> None:
205
245
  super().__init__()
206
246
  self.feature: Feature = feature
207
247
  self.fill = np.full(self._shape(), constant, dtype=np.float32)
208
248
 
209
- def calc(self, value: T) -> FloatArray:
249
+ def calc(self, value: T) -> NPFloatArray:
210
250
  values = self.feature.calc(value)
211
251
  mask = np.isnan(values)
212
252
  values[mask] = self.fill[mask]
@@ -227,9 +267,9 @@ class ReturnFeature(Feature[T]):
227
267
  self.feature: Feature = feature
228
268
  self.history = self._full_nan()
229
269
 
230
- def calc(self, value: T) -> FloatArray:
270
+ def calc(self, value: T) -> NPFloatArray:
231
271
  values = self.feature.calc(value)
232
- r: FloatArray = values / self.history - np.float32(1.0) # type: ignore
272
+ r: NPFloatArray = values / self.history - np.float32(1.0) # type: ignore
233
273
  self.history = values
234
274
  return r
235
275
 
@@ -244,10 +284,10 @@ class ReturnFeature(Feature[T]):
244
284
  class LongReturnsFeature(Feature[T]):
245
285
  def __init__(self, feature: Feature[T], period: int) -> None:
246
286
  super().__init__()
247
- self.history: deque[FloatArray] = deque(maxlen=period)
287
+ self.history: deque[NPFloatArray] = deque(maxlen=period)
248
288
  self.feature: Feature = feature
249
289
 
250
- def calc(self, value: T) -> FloatArray:
290
+ def calc(self, value: T) -> NPFloatArray:
251
291
  values = self.feature.calc(value)
252
292
  h = self.history
253
293
 
@@ -275,10 +315,10 @@ class MaxReturnFeature(Feature[T]):
275
315
  def __init__(self, feature: Feature[T], period: int) -> None:
276
316
  super().__init__()
277
317
  assert feature.size() == 1
278
- self.history: deque[FloatArray] = deque(maxlen=period)
318
+ self.history: deque[NPFloatArray] = deque(maxlen=period)
279
319
  self.feature: Feature = feature
280
320
 
281
- def calc(self, value: T) -> FloatArray:
321
+ def calc(self, value: T) -> NPFloatArray:
282
322
  values = self.feature.calc(value)
283
323
  h = self.history
284
324
  h.append(values)
@@ -304,10 +344,10 @@ class MinReturnFeature(Feature[T]):
304
344
 
305
345
  def __init__(self, feature: Feature[T], period: int) -> None:
306
346
  super().__init__()
307
- self.history: deque[FloatArray] = deque(maxlen=period)
347
+ self.history: deque[NPFloatArray] = deque(maxlen=period)
308
348
  self.feature: Feature = feature
309
349
 
310
- def calc(self, value: T) -> FloatArray:
350
+ def calc(self, value: T) -> NPFloatArray:
311
351
  values = self.feature.calc(value)
312
352
  h = self.history
313
353
  h.append(values)
@@ -336,7 +376,7 @@ class SMAFeature(Feature[T]):
336
376
  self.history = np.zeros((self.period, feature.size()), dtype=np.float32)
337
377
  self._cnt = 0
338
378
 
339
- def calc(self, value: T) -> FloatArray:
379
+ def calc(self, value: T) -> NPFloatArray:
340
380
  values = self.feature.calc(value)
341
381
  idx = self._cnt % self.period
342
382
  self.history[idx] = values
@@ -364,7 +404,7 @@ class SMAFeature(Feature[T]):
364
404
  class EquityFeature(Feature[Account]):
365
405
  """Calculates the total equity value of the account"""
366
406
 
367
- def calc(self, value: Account) -> FloatArray:
407
+ def calc(self, value: Account) -> NPFloatArray:
368
408
  return np.array(value.equity_value(), dtype=np.float32)
369
409
 
370
410
  def size(self):
@@ -380,7 +420,7 @@ class UnrealizedPNLFeature(Feature[Account]):
380
420
  ```
381
421
  """
382
422
 
383
- def calc(self, value: Account) -> FloatArray:
423
+ def calc(self, value: Account) -> NPFloatArray:
384
424
  mkt_value = value.convert(value.mkt_value())
385
425
  pnl = value.unrealized_pnl_value()
386
426
  if mkt_value and pnl:
@@ -411,7 +451,7 @@ class DayOfWeekFeature(Feature[Event]):
411
451
  self.tz = tz
412
452
  self.one_hot_encoded = one_hot_encoded
413
453
 
414
- def calc(self, value: Event) -> FloatArray:
454
+ def calc(self, value: Event) -> NPFloatArray:
415
455
  dt = datetime.astimezone(value.time, self.tz)
416
456
  weekday = dt.weekday()
417
457
  if not self.one_hot_encoded:
@@ -429,6 +469,7 @@ class DayOfMonthFeature(Feature[Event]):
429
469
  """Calculate a day of month where the first day of the month is 0 and the last day is 30.
430
470
  Result can be one-hot encoded or not, depending on the `one_hot_encoded` parameter.
431
471
  If `one_hot_encoded` is True, the result will be a 30-element array else the result will be a single value.
472
+
432
473
  For example, if the event time is the 15th of the month, the result will be:
433
474
  - one-hot encoded: [0.0, 0.0, ..., 1.0, 0.0, ..., 0.0] (1.0 at index 14)
434
475
  - not one-hot encoded: [14.0]
@@ -439,7 +480,7 @@ class DayOfMonthFeature(Feature[Event]):
439
480
  self.tz = tz
440
481
  self.one_hot_encoded = one_hot_encoded
441
482
 
442
- def calc(self, value: Event) -> FloatArray:
483
+ def calc(self, value: Event) -> NPFloatArray:
443
484
  dt = datetime.astimezone(value.time, self.tz)
444
485
  day = dt.day - 1 # day of month is 1-31, we want 0-30
445
486
  if not self.one_hot_encoded:
@@ -467,7 +508,7 @@ class MonthOfYearFeature(Feature[Event]):
467
508
  self.tz = tz
468
509
  self.one_hot_encoded = one_hot_encoded
469
510
 
470
- def calc(self, value: Event) -> FloatArray:
511
+ def calc(self, value: Event) -> NPFloatArray:
471
512
  dt = datetime.astimezone(value.time, self.tz)
472
513
  month = dt.month - 1 # month is 1-12, we want 0-11
473
514
  if not self.one_hot_encoded:
@@ -488,7 +529,7 @@ class TimeDifference(Feature[Event]):
488
529
  super().__init__()
489
530
  self._last_time: datetime | None = None
490
531
 
491
- def calc(self, value: Event) -> FloatArray:
532
+ def calc(self, value: Event) -> NPFloatArray:
492
533
  if self._last_time:
493
534
  diff = value.time - self._last_time
494
535
  self._last_time = value.time
@@ -515,7 +556,7 @@ class TaFeature(Feature[Event]):
515
556
  self.period = period
516
557
  self.assets = list(assets)
517
558
 
518
- def calc(self, value: Event) -> FloatArray:
559
+ def calc(self, value: Event) -> NPFloatArray:
519
560
  result = []
520
561
  nan = float("nan")
521
562
  for asset in self.assets:
@@ -598,7 +639,7 @@ class BarFeature(Feature[Event]):
598
639
  super().__init__()
599
640
  self.assets = assets
600
641
 
601
- def calc(self, value: Event) -> FloatArray:
642
+ def calc(self, value: Event) -> NPFloatArray:
602
643
  result = self._full_nan()
603
644
  for idx, asset in enumerate(self.assets):
604
645
  item = value.price_items.get(asset)
@@ -622,7 +663,7 @@ class QuoteFeature(Feature[Event]):
622
663
  super().__init__()
623
664
  self.assets = assets
624
665
 
625
- def calc(self, value: Event) -> FloatArray:
666
+ def calc(self, value: Event) -> NPFloatArray:
626
667
  result = self._full_nan()
627
668
  for idx, asset in enumerate(self.assets):
628
669
  item = value.price_items.get(asset)
@@ -637,20 +678,23 @@ class QuoteFeature(Feature[Event]):
637
678
 
638
679
 
639
680
  class CacheFeature(Feature[Event]):
640
- """Cache the results of an event feature from a previous run. This can speed up the learning process considerable, but
641
- this requires that:
681
+ """Cache the results of an event feature from a previous run. This can speed up the learning process
682
+ considerable, but this requires that:
642
683
 
643
684
  - the feed to have always an increasing time value (monotonic)
644
- - the feature has to produce the same output at a given time.
685
+ - the underlying feature has to produce the same output at a given time (deterministic)
686
+
687
+ A reset doesn't clear the cache, but it will reset the underlying feature. If you want to clear
688
+ the cache, use the `clear()` method.
645
689
  """
646
690
 
647
691
  def __init__(self, feature: Feature[Event], validate=False) -> None:
648
692
  super().__init__()
649
693
  self.feature: Feature = feature
650
- self._cache: dict[datetime, FloatArray] = {}
694
+ self._cache: dict[datetime, NPFloatArray] = {}
651
695
  self.validate = validate
652
696
 
653
- def calc(self, value: Event) -> FloatArray:
697
+ def calc(self, value: Event) -> NPFloatArray:
654
698
  time = value.time
655
699
  if time in self._cache:
656
700
  values = self._cache[time]
@@ -678,14 +722,17 @@ class CacheFeature(Feature[Event]):
678
722
 
679
723
 
680
724
  class VolumeFeature(Feature[Event]):
681
- """Extract the volume for one or more assets in the event"""
725
+ """Extract the trading volume for one or more assets in the event.
726
+ Make sure that the data avaialble in the event contains adjusted volume data
727
+ (for example for stock splits), otherwise the results will be incorrect.
728
+ """
682
729
 
683
730
  def __init__(self, *assets: Asset, volume_type: str = "DEFAULT") -> None:
684
731
  super().__init__()
685
732
  self.assets = assets
686
733
  self.volume_type = volume_type
687
734
 
688
- def calc(self, value: Event):
735
+ def calc(self, value: Event) -> NPFloatArray:
689
736
  volumes = [value.get_volume(asset, self.volume_type) for asset in self.assets]
690
737
  return np.array(volumes, dtype=np.float32)
691
738
 
@@ -19,7 +19,10 @@ logger = logging.getLogger(__name__)
19
19
 
20
20
 
21
21
  class FeatureStrategy(Strategy):
22
- """Abstract base class for strategies that use event features as input."""
22
+ """Abstract base class for strategies that use event features as input.
23
+ Typically these features are used to create input data for machine learning models,
24
+ which can then be used to generate signals.
25
+ """
23
26
 
24
27
  def __init__(self, input_feature: Feature[Event], history: int, dtype="float32"):
25
28
  super().__init__()
@@ -104,9 +107,13 @@ class TimeSeriesStrategy(FeatureStrategy):
104
107
  """Strategy using a sequenced neural network to predict future time series values.
105
108
  The input and label features are calculated from events.
106
109
  The model is expected to be a PyTorch module that takes the input features and outputs predictions.
110
+ An example of a model could be a recurrent neural network (RNN) or a long short-term memory (LSTM) network.
111
+
107
112
  The strategy can be used to generate buy/sell signals based on the model's predictions.
108
113
  This strategy is suitable for time series forecasting tasks where the model learns from historical data
109
- to predict future values. Right now it only supports a single asset, but this can be extended in the future.
114
+ to predict future values.
115
+
116
+ Right now this class only supports a single asset, but this can be extended.
110
117
  """
111
118
 
112
119
  def __init__(
@@ -140,7 +147,7 @@ class TimeSeriesStrategy(FeatureStrategy):
140
147
  else:
141
148
  p = output.item()
142
149
 
143
- logger.info("Prediction p=%s time=%s", p, dt)
150
+ logger.info("Prediction asset=%s p=%s time=%s", self.asset.symbol, p, dt)
144
151
  if p >= self.buy_pct:
145
152
  return [Signal.buy(self.asset)]
146
153
  if p <= self.sell_pct:
@@ -27,7 +27,7 @@ from alpaca.trading.enums import AssetClass
27
27
 
28
28
  from roboquant.asset import Asset, Crypto, Option, Stock
29
29
  from roboquant.event import Bar, Event, PriceItem, Quote, TradePrice
30
- from roboquant.feeds.historic import HistoricFeed
30
+ from roboquant.feeds.historic import InMemoryFeed
31
31
  from roboquant.feeds.live import LiveFeed
32
32
 
33
33
 
@@ -134,7 +134,7 @@ class AlpacaLiveFeed(LiveFeed):
134
134
  logger.warning("cannot subscribe to bars for options")
135
135
 
136
136
 
137
- class _AlpacaHistoricFeed(HistoricFeed):
137
+ class _AlpacaHistoricFeed(InMemoryFeed):
138
138
  """Base class for Alpaca historic feeds.
139
139
  This class is not intended to be used directly."""
140
140