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.
- {roboquant-2.3.3 → roboquant-2.4.0}/PKG-INFO +1 -4
- {roboquant-2.3.3 → roboquant-2.4.0}/README.md +0 -3
- {roboquant-2.3.3 → roboquant-2.4.0}/pyproject.toml +1 -2
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/__init__.py +2 -0
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/account.py +29 -28
- roboquant-2.4.0/roboquant/ai/__init__.py +8 -0
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/ai/features.py +99 -52
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/ai/strategies.py +10 -3
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/alpaca/feed.py +2 -2
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/asset.py +8 -10
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/brokers/broker.py +1 -1
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/brokers/simbroker.py +11 -10
- roboquant-2.4.0/roboquant/crypto/__init__.py +4 -0
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/crypto/cryptobroker.py +48 -19
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/crypto/cryptofeed.py +5 -9
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/feeds/__init__.py +0 -2
- {roboquant-2.3.3 → 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.3 → roboquant-2.4.0}/roboquant/feeds/parquet.py +5 -3
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/feeds/randomwalk.py +2 -2
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/feeds/sql.py +2 -1
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/feeds/yahoo.py +12 -2
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/journals/__init__.py +2 -1
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/journals/metricsjournal.py +1 -50
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/monetary.py +26 -2
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/signal.py +3 -0
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/strategies/buffer.py +6 -3
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/strategies/multistrategy.py +2 -2
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/strategies/strategy.py +1 -1
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/ta/__init__.py +7 -12
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/timeframe.py +23 -6
- roboquant-2.4.0/roboquant/timeseries.py +59 -0
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/traders/flextrader.py +7 -1
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/traders/trader.py +1 -1
- roboquant-2.3.3/roboquant/ai/__init__.py +0 -6
- roboquant-2.3.3/roboquant/crypto/__init__.py +0 -3
- roboquant-2.3.3/roboquant/feeds/feed.py +0 -160
- roboquant-2.3.3/roboquant/feeds/historic.py +0 -111
- {roboquant-2.3.3 → roboquant-2.4.0}/LICENSE +0 -0
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/ai/rl.py +0 -0
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/alpaca/__init__.py +0 -0
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/alpaca/broker.py +0 -0
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/brokers/__init__.py +0 -0
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/event.py +0 -0
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/feeds/live.py +0 -0
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/feeds/resources/us10.parquet +0 -0
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/feeds/util.py +0 -0
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/ibkr/__init__.py +0 -0
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/ibkr/broker.py +0 -0
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/ibkr/types.py +0 -0
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/journals/alphabeta.py +0 -0
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/journals/basicjournal.py +0 -0
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/journals/feedmetric.py +0 -0
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/journals/journal.py +0 -0
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/journals/marketmetric.py +0 -0
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/journals/metric.py +0 -0
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/journals/pnlmetric.py +0 -0
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/journals/pricemetric.py +0 -0
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/journals/runmetric.py +0 -0
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/journals/scorecard.py +0 -0
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/journals/tensorboard.py +0 -0
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/order.py +0 -0
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/run.py +0 -0
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/strategies/__init__.py +0 -0
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/strategies/cachedstrategy.py +0 -0
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/strategies/emacrossover.py +0 -0
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/strategies/ibsstrategy.py +0 -0
- {roboquant-2.3.3 → roboquant-2.4.0}/roboquant/strategies/tastrategy.py +0 -0
- {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
|
+
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,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
|
-
|
|
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
|
-
|
|
21
|
-
|
|
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) ->
|
|
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
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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="
|
|
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) ->
|
|
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) ->
|
|
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
|
|
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) ->
|
|
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
|
|
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) ->
|
|
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) ->
|
|
270
|
+
def calc(self, value: T) -> NPFloatArray:
|
|
231
271
|
values = self.feature.calc(value)
|
|
232
|
-
r:
|
|
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[
|
|
287
|
+
self.history: deque[NPFloatArray] = deque(maxlen=period)
|
|
248
288
|
self.feature: Feature = feature
|
|
249
289
|
|
|
250
|
-
def calc(self, value: T) ->
|
|
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[
|
|
318
|
+
self.history: deque[NPFloatArray] = deque(maxlen=period)
|
|
279
319
|
self.feature: Feature = feature
|
|
280
320
|
|
|
281
|
-
def calc(self, value: T) ->
|
|
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[
|
|
347
|
+
self.history: deque[NPFloatArray] = deque(maxlen=period)
|
|
308
348
|
self.feature: Feature = feature
|
|
309
349
|
|
|
310
|
-
def calc(self, value: T) ->
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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
|
|
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,
|
|
694
|
+
self._cache: dict[datetime, NPFloatArray] = {}
|
|
651
695
|
self.validate = validate
|
|
652
696
|
|
|
653
|
-
def calc(self, value: Event) ->
|
|
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.
|
|
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
|
|
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
|
|