roboquant 2.3.4__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 (69) hide show
  1. {roboquant-2.3.4 → roboquant-2.4.0}/PKG-INFO +1 -4
  2. {roboquant-2.3.4 → roboquant-2.4.0}/README.md +0 -3
  3. {roboquant-2.3.4 → roboquant-2.4.0}/pyproject.toml +1 -2
  4. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/__init__.py +2 -0
  5. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/account.py +29 -28
  6. roboquant-2.4.0/roboquant/ai/__init__.py +8 -0
  7. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/ai/features.py +79 -47
  8. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/alpaca/feed.py +2 -2
  9. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/asset.py +8 -10
  10. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/brokers/broker.py +1 -1
  11. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/brokers/simbroker.py +11 -10
  12. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/crypto/cryptobroker.py +48 -19
  13. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/crypto/cryptofeed.py +5 -9
  14. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/feeds/__init__.py +0 -2
  15. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/feeds/csvfeed.py +2 -2
  16. roboquant-2.4.0/roboquant/feeds/feed.py +31 -0
  17. roboquant-2.4.0/roboquant/feeds/historic.py +256 -0
  18. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/feeds/parquet.py +5 -3
  19. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/feeds/randomwalk.py +2 -2
  20. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/feeds/sql.py +2 -1
  21. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/feeds/yahoo.py +12 -2
  22. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/journals/__init__.py +2 -1
  23. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/journals/metricsjournal.py +1 -50
  24. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/monetary.py +26 -2
  25. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/signal.py +3 -0
  26. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/strategies/buffer.py +6 -3
  27. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/strategies/multistrategy.py +2 -2
  28. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/strategies/strategy.py +1 -1
  29. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/ta/__init__.py +7 -12
  30. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/timeframe.py +23 -6
  31. roboquant-2.4.0/roboquant/timeseries.py +59 -0
  32. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/traders/flextrader.py +7 -1
  33. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/traders/trader.py +1 -1
  34. roboquant-2.3.4/roboquant/ai/__init__.py +0 -6
  35. roboquant-2.3.4/roboquant/feeds/feed.py +0 -160
  36. roboquant-2.3.4/roboquant/feeds/historic.py +0 -111
  37. {roboquant-2.3.4 → roboquant-2.4.0}/LICENSE +0 -0
  38. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/ai/rl.py +0 -0
  39. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/ai/strategies.py +0 -0
  40. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/alpaca/__init__.py +0 -0
  41. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/alpaca/broker.py +0 -0
  42. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/brokers/__init__.py +0 -0
  43. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/crypto/__init__.py +0 -0
  44. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/event.py +0 -0
  45. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/feeds/live.py +0 -0
  46. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/feeds/resources/us10.parquet +0 -0
  47. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/feeds/util.py +0 -0
  48. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/ibkr/__init__.py +0 -0
  49. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/ibkr/broker.py +0 -0
  50. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/ibkr/types.py +0 -0
  51. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/journals/alphabeta.py +0 -0
  52. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/journals/basicjournal.py +0 -0
  53. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/journals/feedmetric.py +0 -0
  54. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/journals/journal.py +0 -0
  55. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/journals/marketmetric.py +0 -0
  56. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/journals/metric.py +0 -0
  57. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/journals/pnlmetric.py +0 -0
  58. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/journals/pricemetric.py +0 -0
  59. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/journals/runmetric.py +0 -0
  60. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/journals/scorecard.py +0 -0
  61. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/journals/tensorboard.py +0 -0
  62. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/order.py +0 -0
  63. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/run.py +0 -0
  64. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/strategies/__init__.py +0 -0
  65. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/strategies/cachedstrategy.py +0 -0
  66. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/strategies/emacrossover.py +0 -0
  67. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/strategies/ibsstrategy.py +0 -0
  68. {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/strategies/tastrategy.py +0 -0
  69. {roboquant-2.3.4 → 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.4
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.4"
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,14 +12,15 @@ 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
20
  """Base class for different types of features. Features are the data building blocks for
20
21
  roboquant machine learning models and can be used to extract relevant information.
21
22
 
22
- The ones included by default are either based either an `Event` or an `Account`.
23
+ The ones included by default are either based on an `Event` or an `Account`.
23
24
  Typically Event features are used for input data and Account features are used for
24
25
  reward/label/output data.
25
26
 
@@ -28,7 +29,7 @@ class Feature(Generic[T]):
28
29
  """
29
30
 
30
31
  @abstractmethod
31
- def calc(self, value: T) -> FloatArray:
32
+ def calc(self, value: T) -> NPFloatArray:
32
33
  """
33
34
  Perform the calculation and return the result as a 1-dimensional NDArray of type float32.
34
35
  The result should always be the same size. If a value cannot be calculated at a certain
@@ -43,19 +44,19 @@ class Feature(Generic[T]):
43
44
  """Reset the state of the feature. This is called at the start of a new epoch.
44
45
  If the feature has no state, this can be left empty."""
45
46
 
46
- def _shape(self):
47
- """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"""
48
49
  return (self.size(),)
49
50
 
50
- def _zeros(self):
51
+ def _zeros(self) -> NPFloatArray:
51
52
  """Return a zero array of the correct shape"""
52
53
  return np.zeros(self._shape(), dtype=np.float32)
53
54
 
54
- def _ones(self):
55
+ def _ones(self) -> NPFloatArray:
55
56
  """Return an array of ones of the correct shape"""
56
57
  return np.ones(self._shape(), dtype=np.float32)
57
58
 
58
- def _full_nan(self) -> FloatArray:
59
+ def _full_nan(self) -> NPFloatArray:
59
60
  """Return a full NaN array of the correct shape"""
60
61
  return np.full(self._shape(), float("nan"), dtype=np.float32)
61
62
 
@@ -64,7 +65,7 @@ class Feature(Generic[T]):
64
65
  return ReturnFeature(self)
65
66
  return LongReturnsFeature(self, period)
66
67
 
67
- def normalize(self, min_period=3) -> "Feature[T]":
68
+ def normalize(self, min_period: int=3) -> "Feature[T]":
68
69
  """Normalize the feature values by calculating the mean and standard deviation."""
69
70
  return NormalizeFeature(self, min_period)
70
71
 
@@ -90,7 +91,7 @@ class SlicedFeature(Feature[T]):
90
91
  self.feature = feature
91
92
  self._size = len(feature._zeros()[args])
92
93
 
93
- def calc(self, value: T) -> FloatArray:
94
+ def calc(self, value: T) -> NPFloatArray:
94
95
  values = self.feature.calc(value)
95
96
  return values[self.args]
96
97
 
@@ -101,8 +102,10 @@ class SlicedFeature(Feature[T]):
101
102
  return self.feature.reset()
102
103
 
103
104
 
104
- class FixedValueFeature(Feature):
105
- """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
+ """
106
109
 
107
110
  def __init__(self, value: ArrayLike) -> None:
108
111
  super().__init__()
@@ -111,9 +114,33 @@ class FixedValueFeature(Feature):
111
114
  def size(self) -> int:
112
115
  return len(self.value)
113
116
 
114
- def calc(self, value: Any) -> FloatArray:
117
+ def calc(self, value: Any) -> NPFloatArray:
115
118
  return self.value
116
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
+
117
144
 
118
145
  class CombinedFeature(Feature[T]):
119
146
  """Combine multiple features into one single feature by horizontal stacking them.
@@ -125,7 +152,7 @@ class CombinedFeature(Feature[T]):
125
152
  self.features = features
126
153
  self._size = sum(feature.size() for feature in self.features)
127
154
 
128
- def calc(self, value: T) -> FloatArray:
155
+ def calc(self, value: T) -> NPFloatArray:
129
156
  data = [feature.calc(value) for feature in self.features]
130
157
  return np.hstack(data, dtype=np.float32)
131
158
 
@@ -138,7 +165,9 @@ class CombinedFeature(Feature[T]):
138
165
 
139
166
 
140
167
  class NormalizeFeature(Feature[T]):
141
- """online normalization calculator"""
168
+ """online normalization calculator
169
+ - min_count: minimum number of values to calculate the mean and standard deviation
170
+ """
142
171
 
143
172
  def __init__(self, feature: Feature[T], min_count: int = 3) -> None:
144
173
  super().__init__()
@@ -146,10 +175,11 @@ class NormalizeFeature(Feature[T]):
146
175
  self.min_count = min_count
147
176
  self.existing_aggregate = (self._zero_int(), self._zeros(), self._zeros())
148
177
 
149
- def _zero_int(self):
150
- return np.zeros((self.size(),), dtype="int")
178
+ def _zero_int(self) -> NPIntArray:
179
+ return np.zeros((self.size(),), dtype="int64")
151
180
 
152
- def denormalize(self, value):
181
+ def denormalize(self, value) -> NPFloatArray:
182
+ """Denormalize the value"""
153
183
  (count, mean, m2) = self.existing_aggregate
154
184
  stdev = np.sqrt(m2 / count) - 1e-12
155
185
  return value * stdev + mean
@@ -163,14 +193,14 @@ class NormalizeFeature(Feature[T]):
163
193
  delta2 = new_value - mean
164
194
  m2[mask] += delta[mask] * delta2[mask]
165
195
 
166
- def __normalize_values(self, values) -> FloatArray:
196
+ def __normalize_values(self, values) -> NPFloatArray:
167
197
  (count, mean, m2) = self.existing_aggregate
168
198
  stdev = self._full_nan()
169
199
  mask = count >= self.min_count
170
200
  stdev[mask] = np.sqrt(m2[mask] / count[mask]) + 1e-12 # type: ignore
171
201
  return (values - mean) / stdev
172
202
 
173
- def calc(self, value: T) -> FloatArray:
203
+ def calc(self, value: T) -> NPFloatArray:
174
204
  values = self.feature.calc(value)
175
205
  self.__update(values)
176
206
  return self.__normalize_values(values)
@@ -184,14 +214,15 @@ class NormalizeFeature(Feature[T]):
184
214
 
185
215
 
186
216
  class FillFeature(Feature[T]):
187
- """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"""
188
219
 
189
220
  def __init__(self, feature: Feature[T]) -> None:
190
221
  super().__init__()
191
222
  self.feature: Feature = feature
192
223
  self.fill = self._full_nan()
193
224
 
194
- def calc(self, value: T) -> FloatArray:
225
+ def calc(self, value: T) -> NPFloatArray:
195
226
  values = self.feature.calc(value)
196
227
  mask = np.isnan(values)
197
228
  values[mask] = self.fill[mask]
@@ -207,14 +238,15 @@ class FillFeature(Feature[T]):
207
238
 
208
239
 
209
240
  class FillWithConstantFeature(Feature[T]):
210
- """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"""
211
243
 
212
244
  def __init__(self, feature: Feature[T], constant: float = 0.0) -> None:
213
245
  super().__init__()
214
246
  self.feature: Feature = feature
215
247
  self.fill = np.full(self._shape(), constant, dtype=np.float32)
216
248
 
217
- def calc(self, value: T) -> FloatArray:
249
+ def calc(self, value: T) -> NPFloatArray:
218
250
  values = self.feature.calc(value)
219
251
  mask = np.isnan(values)
220
252
  values[mask] = self.fill[mask]
@@ -235,9 +267,9 @@ class ReturnFeature(Feature[T]):
235
267
  self.feature: Feature = feature
236
268
  self.history = self._full_nan()
237
269
 
238
- def calc(self, value: T) -> FloatArray:
270
+ def calc(self, value: T) -> NPFloatArray:
239
271
  values = self.feature.calc(value)
240
- r: FloatArray = values / self.history - np.float32(1.0) # type: ignore
272
+ r: NPFloatArray = values / self.history - np.float32(1.0) # type: ignore
241
273
  self.history = values
242
274
  return r
243
275
 
@@ -252,10 +284,10 @@ class ReturnFeature(Feature[T]):
252
284
  class LongReturnsFeature(Feature[T]):
253
285
  def __init__(self, feature: Feature[T], period: int) -> None:
254
286
  super().__init__()
255
- self.history: deque[FloatArray] = deque(maxlen=period)
287
+ self.history: deque[NPFloatArray] = deque(maxlen=period)
256
288
  self.feature: Feature = feature
257
289
 
258
- def calc(self, value: T) -> FloatArray:
290
+ def calc(self, value: T) -> NPFloatArray:
259
291
  values = self.feature.calc(value)
260
292
  h = self.history
261
293
 
@@ -283,10 +315,10 @@ class MaxReturnFeature(Feature[T]):
283
315
  def __init__(self, feature: Feature[T], period: int) -> None:
284
316
  super().__init__()
285
317
  assert feature.size() == 1
286
- self.history: deque[FloatArray] = deque(maxlen=period)
318
+ self.history: deque[NPFloatArray] = deque(maxlen=period)
287
319
  self.feature: Feature = feature
288
320
 
289
- def calc(self, value: T) -> FloatArray:
321
+ def calc(self, value: T) -> NPFloatArray:
290
322
  values = self.feature.calc(value)
291
323
  h = self.history
292
324
  h.append(values)
@@ -312,10 +344,10 @@ class MinReturnFeature(Feature[T]):
312
344
 
313
345
  def __init__(self, feature: Feature[T], period: int) -> None:
314
346
  super().__init__()
315
- self.history: deque[FloatArray] = deque(maxlen=period)
347
+ self.history: deque[NPFloatArray] = deque(maxlen=period)
316
348
  self.feature: Feature = feature
317
349
 
318
- def calc(self, value: T) -> FloatArray:
350
+ def calc(self, value: T) -> NPFloatArray:
319
351
  values = self.feature.calc(value)
320
352
  h = self.history
321
353
  h.append(values)
@@ -344,7 +376,7 @@ class SMAFeature(Feature[T]):
344
376
  self.history = np.zeros((self.period, feature.size()), dtype=np.float32)
345
377
  self._cnt = 0
346
378
 
347
- def calc(self, value: T) -> FloatArray:
379
+ def calc(self, value: T) -> NPFloatArray:
348
380
  values = self.feature.calc(value)
349
381
  idx = self._cnt % self.period
350
382
  self.history[idx] = values
@@ -372,7 +404,7 @@ class SMAFeature(Feature[T]):
372
404
  class EquityFeature(Feature[Account]):
373
405
  """Calculates the total equity value of the account"""
374
406
 
375
- def calc(self, value: Account) -> FloatArray:
407
+ def calc(self, value: Account) -> NPFloatArray:
376
408
  return np.array(value.equity_value(), dtype=np.float32)
377
409
 
378
410
  def size(self):
@@ -388,7 +420,7 @@ class UnrealizedPNLFeature(Feature[Account]):
388
420
  ```
389
421
  """
390
422
 
391
- def calc(self, value: Account) -> FloatArray:
423
+ def calc(self, value: Account) -> NPFloatArray:
392
424
  mkt_value = value.convert(value.mkt_value())
393
425
  pnl = value.unrealized_pnl_value()
394
426
  if mkt_value and pnl:
@@ -419,7 +451,7 @@ class DayOfWeekFeature(Feature[Event]):
419
451
  self.tz = tz
420
452
  self.one_hot_encoded = one_hot_encoded
421
453
 
422
- def calc(self, value: Event) -> FloatArray:
454
+ def calc(self, value: Event) -> NPFloatArray:
423
455
  dt = datetime.astimezone(value.time, self.tz)
424
456
  weekday = dt.weekday()
425
457
  if not self.one_hot_encoded:
@@ -448,7 +480,7 @@ class DayOfMonthFeature(Feature[Event]):
448
480
  self.tz = tz
449
481
  self.one_hot_encoded = one_hot_encoded
450
482
 
451
- def calc(self, value: Event) -> FloatArray:
483
+ def calc(self, value: Event) -> NPFloatArray:
452
484
  dt = datetime.astimezone(value.time, self.tz)
453
485
  day = dt.day - 1 # day of month is 1-31, we want 0-30
454
486
  if not self.one_hot_encoded:
@@ -476,7 +508,7 @@ class MonthOfYearFeature(Feature[Event]):
476
508
  self.tz = tz
477
509
  self.one_hot_encoded = one_hot_encoded
478
510
 
479
- def calc(self, value: Event) -> FloatArray:
511
+ def calc(self, value: Event) -> NPFloatArray:
480
512
  dt = datetime.astimezone(value.time, self.tz)
481
513
  month = dt.month - 1 # month is 1-12, we want 0-11
482
514
  if not self.one_hot_encoded:
@@ -497,7 +529,7 @@ class TimeDifference(Feature[Event]):
497
529
  super().__init__()
498
530
  self._last_time: datetime | None = None
499
531
 
500
- def calc(self, value: Event) -> FloatArray:
532
+ def calc(self, value: Event) -> NPFloatArray:
501
533
  if self._last_time:
502
534
  diff = value.time - self._last_time
503
535
  self._last_time = value.time
@@ -524,7 +556,7 @@ class TaFeature(Feature[Event]):
524
556
  self.period = period
525
557
  self.assets = list(assets)
526
558
 
527
- def calc(self, value: Event) -> FloatArray:
559
+ def calc(self, value: Event) -> NPFloatArray:
528
560
  result = []
529
561
  nan = float("nan")
530
562
  for asset in self.assets:
@@ -607,7 +639,7 @@ class BarFeature(Feature[Event]):
607
639
  super().__init__()
608
640
  self.assets = assets
609
641
 
610
- def calc(self, value: Event) -> FloatArray:
642
+ def calc(self, value: Event) -> NPFloatArray:
611
643
  result = self._full_nan()
612
644
  for idx, asset in enumerate(self.assets):
613
645
  item = value.price_items.get(asset)
@@ -631,7 +663,7 @@ class QuoteFeature(Feature[Event]):
631
663
  super().__init__()
632
664
  self.assets = assets
633
665
 
634
- def calc(self, value: Event) -> FloatArray:
666
+ def calc(self, value: Event) -> NPFloatArray:
635
667
  result = self._full_nan()
636
668
  for idx, asset in enumerate(self.assets):
637
669
  item = value.price_items.get(asset)
@@ -659,10 +691,10 @@ class CacheFeature(Feature[Event]):
659
691
  def __init__(self, feature: Feature[Event], validate=False) -> None:
660
692
  super().__init__()
661
693
  self.feature: Feature = feature
662
- self._cache: dict[datetime, FloatArray] = {}
694
+ self._cache: dict[datetime, NPFloatArray] = {}
663
695
  self.validate = validate
664
696
 
665
- def calc(self, value: Event) -> FloatArray:
697
+ def calc(self, value: Event) -> NPFloatArray:
666
698
  time = value.time
667
699
  if time in self._cache:
668
700
  values = self._cache[time]
@@ -691,8 +723,8 @@ class CacheFeature(Feature[Event]):
691
723
 
692
724
  class VolumeFeature(Feature[Event]):
693
725
  """Extract the trading volume for one or more assets in the event.
694
- Make sure that the data avaialble in the event contains adjusted volume data (for example for stock splits),
695
- otherwise the results will be incorrect.
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.
696
728
  """
697
729
 
698
730
  def __init__(self, *assets: Asset, volume_type: str = "DEFAULT") -> None:
@@ -700,7 +732,7 @@ class VolumeFeature(Feature[Event]):
700
732
  self.assets = assets
701
733
  self.volume_type = volume_type
702
734
 
703
- def calc(self, value: Event):
735
+ def calc(self, value: Event) -> NPFloatArray:
704
736
  volumes = [value.get_volume(asset, self.volume_type) for asset in self.assets]
705
737
  return np.array(volumes, dtype=np.float32)
706
738
 
@@ -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
 
@@ -1,4 +1,5 @@
1
1
  import logging
2
+ import re
2
3
  from abc import ABC, abstractmethod
3
4
  from dataclasses import dataclass
4
5
  from decimal import Decimal
@@ -155,18 +156,17 @@ class Crypto(Asset):
155
156
  """
156
157
 
157
158
  @staticmethod
158
- def from_symbol(symbol: str, sep: str = "/"):
159
- """Create a Crypto asset from a symbol string. Based on the separator it will
160
- try to determine the currency.
159
+ def from_symbol(symbol: str):
160
+ """Create a Crypto asset from a symbol string. It will automatically extract the quote currency from the symbol,
161
+ which is assumed to be the last part of the symbol.
161
162
 
162
163
  Args:
163
164
  symbol (str): The symbol string of the crypto asset.
164
- sep (str, optional): The separator used in the symbol string. Defaults to "/".
165
165
 
166
166
  Returns:
167
167
  Crypto: The created crypto asset.
168
168
  """
169
- currency = symbol.split(sep)[-1]
169
+ currency = re.split(r"[^a-zA-Z0-9\s]", symbol)[-1]
170
170
  return Crypto(symbol, Currency(currency))
171
171
 
172
172
  def serialize(self):
@@ -204,18 +204,16 @@ class Forex(Asset):
204
204
  """
205
205
 
206
206
  @staticmethod
207
- def from_symbol(symbol: str, sep: str = "/"):
208
- """Create a Forex asset from a symbol string. The second part of the symbol is the
207
+ def from_symbol(symbol: str) -> "Forex":
208
+ """Create a Forex asset from a symbol string. The last part of the symbol is assumed to be the
209
209
  currency.
210
210
 
211
211
  Args:
212
212
  symbol (str): The symbol string of the forex asset.
213
- sep (str, optional): The separator used in the symbol string. Defaults to "/".
214
-
215
213
  Returns:
216
214
  Forex: The created forex asset.
217
215
  """
218
- currency = symbol.split(sep)[-1]
216
+ currency = re.split(r"[^a-zA-Z0-9\s]", symbol)[-1]
219
217
  return Forex(symbol, Currency(currency))
220
218
 
221
219
  def serialize(self):
@@ -149,7 +149,7 @@ class LiveBroker(Broker):
149
149
  order = Order(
150
150
  asset=asset,
151
151
  size=-Decimal(size), # Negative size for sell orders
152
- limit=float(limit),
152
+ limit=limit,
153
153
  tif=tif,
154
154
  )
155
155
  order.fill = - Decimal(fill) # Negative fill for sell orders