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.
- {roboquant-2.3.4 → roboquant-2.4.0}/PKG-INFO +1 -4
- {roboquant-2.3.4 → roboquant-2.4.0}/README.md +0 -3
- {roboquant-2.3.4 → roboquant-2.4.0}/pyproject.toml +1 -2
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/__init__.py +2 -0
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/account.py +29 -28
- roboquant-2.4.0/roboquant/ai/__init__.py +8 -0
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/ai/features.py +79 -47
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/alpaca/feed.py +2 -2
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/asset.py +8 -10
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/brokers/broker.py +1 -1
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/brokers/simbroker.py +11 -10
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/crypto/cryptobroker.py +48 -19
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/crypto/cryptofeed.py +5 -9
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/feeds/__init__.py +0 -2
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/feeds/csvfeed.py +2 -2
- roboquant-2.4.0/roboquant/feeds/feed.py +31 -0
- roboquant-2.4.0/roboquant/feeds/historic.py +256 -0
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/feeds/parquet.py +5 -3
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/feeds/randomwalk.py +2 -2
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/feeds/sql.py +2 -1
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/feeds/yahoo.py +12 -2
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/journals/__init__.py +2 -1
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/journals/metricsjournal.py +1 -50
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/monetary.py +26 -2
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/signal.py +3 -0
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/strategies/buffer.py +6 -3
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/strategies/multistrategy.py +2 -2
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/strategies/strategy.py +1 -1
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/ta/__init__.py +7 -12
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/timeframe.py +23 -6
- roboquant-2.4.0/roboquant/timeseries.py +59 -0
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/traders/flextrader.py +7 -1
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/traders/trader.py +1 -1
- roboquant-2.3.4/roboquant/ai/__init__.py +0 -6
- roboquant-2.3.4/roboquant/feeds/feed.py +0 -160
- roboquant-2.3.4/roboquant/feeds/historic.py +0 -111
- {roboquant-2.3.4 → roboquant-2.4.0}/LICENSE +0 -0
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/ai/rl.py +0 -0
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/ai/strategies.py +0 -0
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/alpaca/__init__.py +0 -0
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/alpaca/broker.py +0 -0
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/brokers/__init__.py +0 -0
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/crypto/__init__.py +0 -0
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/event.py +0 -0
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/feeds/live.py +0 -0
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/feeds/resources/us10.parquet +0 -0
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/feeds/util.py +0 -0
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/ibkr/__init__.py +0 -0
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/ibkr/broker.py +0 -0
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/ibkr/types.py +0 -0
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/journals/alphabeta.py +0 -0
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/journals/basicjournal.py +0 -0
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/journals/feedmetric.py +0 -0
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/journals/journal.py +0 -0
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/journals/marketmetric.py +0 -0
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/journals/metric.py +0 -0
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/journals/pnlmetric.py +0 -0
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/journals/pricemetric.py +0 -0
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/journals/runmetric.py +0 -0
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/journals/scorecard.py +0 -0
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/journals/tensorboard.py +0 -0
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/order.py +0 -0
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/run.py +0 -0
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/strategies/__init__.py +0 -0
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/strategies/cachedstrategy.py +0 -0
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/strategies/emacrossover.py +0 -0
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/strategies/ibsstrategy.py +0 -0
- {roboquant-2.3.4 → roboquant-2.4.0}/roboquant/strategies/tastrategy.py +0 -0
- {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
|
+
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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) ->
|
|
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
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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="
|
|
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) ->
|
|
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) ->
|
|
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
|
|
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) ->
|
|
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
|
|
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) ->
|
|
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) ->
|
|
270
|
+
def calc(self, value: T) -> NPFloatArray:
|
|
239
271
|
values = self.feature.calc(value)
|
|
240
|
-
r:
|
|
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[
|
|
287
|
+
self.history: deque[NPFloatArray] = deque(maxlen=period)
|
|
256
288
|
self.feature: Feature = feature
|
|
257
289
|
|
|
258
|
-
def calc(self, value: T) ->
|
|
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[
|
|
318
|
+
self.history: deque[NPFloatArray] = deque(maxlen=period)
|
|
287
319
|
self.feature: Feature = feature
|
|
288
320
|
|
|
289
|
-
def calc(self, value: T) ->
|
|
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[
|
|
347
|
+
self.history: deque[NPFloatArray] = deque(maxlen=period)
|
|
316
348
|
self.feature: Feature = feature
|
|
317
349
|
|
|
318
|
-
def calc(self, value: T) ->
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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,
|
|
694
|
+
self._cache: dict[datetime, NPFloatArray] = {}
|
|
663
695
|
self.validate = validate
|
|
664
696
|
|
|
665
|
-
def calc(self, value: Event) ->
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
159
|
-
"""Create a Crypto asset from a symbol string.
|
|
160
|
-
|
|
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 =
|
|
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
|
|
208
|
-
"""Create a Forex asset from a symbol string. 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 =
|
|
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):
|