roboquant 0.3.0__tar.gz → 0.3.2__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 (68) hide show
  1. {roboquant-0.3.0 → roboquant-0.3.2}/PKG-INFO +1 -1
  2. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/__init__.py +2 -2
  3. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/account.py +11 -3
  4. roboquant-0.3.2/roboquant/alpaca/__init__.py +2 -0
  5. roboquant-0.3.0/roboquant/brokers/alpacabroker.py → roboquant-0.3.2/roboquant/alpaca/broker.py +0 -20
  6. roboquant-0.3.2/roboquant/alpaca/feed.py +182 -0
  7. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/brokers/simbroker.py +27 -10
  8. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/config.py +4 -1
  9. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/event.py +13 -5
  10. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/feeds/__init__.py +2 -6
  11. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/feeds/aggregate.py +8 -8
  12. roboquant-0.3.2/roboquant/feeds/avrofeed.py +120 -0
  13. roboquant-0.3.2/roboquant/feeds/collect.py +33 -0
  14. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/feeds/csvfeed.py +3 -11
  15. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/feeds/feed.py +10 -7
  16. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/feeds/feedutil.py +11 -0
  17. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/feeds/historic.py +16 -8
  18. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/feeds/live.py +7 -2
  19. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/feeds/randomwalk.py +2 -1
  20. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/feeds/sqllitefeed.py +55 -16
  21. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/journals/alphabeta.py +1 -1
  22. roboquant-0.3.2/roboquant/journals/basicjournal.py +50 -0
  23. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/journals/feedmetric.py +1 -1
  24. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/journals/metricsjournal.py +3 -5
  25. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/journals/tensorboardjournal.py +1 -1
  26. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/ml/envs.py +9 -4
  27. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/ml/features.py +132 -26
  28. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/ml/strategies.py +32 -33
  29. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/order.py +5 -5
  30. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/run.py +2 -1
  31. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/signal.py +19 -10
  32. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/strategies/__init__.py +1 -1
  33. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/strategies/emacrossover.py +1 -1
  34. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/strategies/multistrategy.py +2 -0
  35. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/strategies/smacrossover.py +1 -1
  36. roboquant-0.3.0/roboquant/strategies/barstrategy.py → roboquant-0.3.2/roboquant/strategies/tastrategy.py +3 -3
  37. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/timeframe.py +15 -6
  38. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/traders/flextrader.py +54 -29
  39. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/traders/sizing.py +5 -2
  40. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant.egg-info/PKG-INFO +1 -1
  41. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant.egg-info/SOURCES.txt +6 -3
  42. roboquant-0.3.0/roboquant/feeds/alpacafeed.py +0 -65
  43. roboquant-0.3.0/roboquant/journals/basicjournal.py +0 -40
  44. {roboquant-0.3.0 → roboquant-0.3.2}/LICENSE +0 -0
  45. {roboquant-0.3.0 → roboquant-0.3.2}/README.md +0 -0
  46. {roboquant-0.3.0 → roboquant-0.3.2}/pyproject.toml +0 -0
  47. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/brokers/__init__.py +0 -0
  48. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/brokers/broker.py +0 -0
  49. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/brokers/ibkr.py +0 -0
  50. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/feeds/eventchannel.py +0 -0
  51. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/feeds/sp500.json +0 -0
  52. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/feeds/tiingo.py +0 -0
  53. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/feeds/yahoo.py +0 -0
  54. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/journals/__init__.py +0 -0
  55. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/journals/journal.py +0 -0
  56. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/journals/metric.py +0 -0
  57. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/journals/pnlmetric.py +0 -0
  58. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/journals/pricemetric.py +0 -0
  59. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/journals/runmetric.py +0 -0
  60. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/ml/__init__.py +0 -0
  61. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/strategies/buffer.py +0 -0
  62. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/strategies/strategy.py +0 -0
  63. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/traders/__init__.py +0 -0
  64. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/traders/trader.py +0 -0
  65. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant.egg-info/dependency_links.txt +0 -0
  66. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant.egg-info/requires.txt +0 -0
  67. {roboquant-0.3.0 → roboquant-0.3.2}/roboquant.egg-info/top_level.txt +0 -0
  68. {roboquant-0.3.0 → roboquant-0.3.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: roboquant
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: A fast algo-trading platform
5
5
  Author-email: roboquant team <info@roboquant.org>
6
6
  Project-URL: Homepage, https://roboquant.org
@@ -1,4 +1,4 @@
1
- __version__ = "0.3.0"
1
+ __version__ = "0.3.2"
2
2
 
3
3
  from roboquant import brokers
4
4
  from roboquant import feeds
@@ -12,4 +12,4 @@ from .event import Event, PriceItem, Bar, Trade, Quote
12
12
  from .order import Order, OrderStatus
13
13
  from .run import run
14
14
  from .signal import SignalType, Signal
15
- from .timeframe import Timeframe
15
+ from .timeframe import Timeframe, EMPTY_TIMEFRAME
@@ -19,6 +19,14 @@ class Position:
19
19
  mkt_price: float
20
20
  """latest market price denoted in the currency of the symbol"""
21
21
 
22
+ @property
23
+ def is_short(self):
24
+ return self.size < 0
25
+
26
+ @property
27
+ def is_long(self):
28
+ return self.size > 0
29
+
22
30
 
23
31
  class Converter(ABC):
24
32
  """Abstraction that enables trading symbols that are denoted in different currencies and/or contact sizes"""
@@ -126,7 +134,7 @@ class Account:
126
134
  return float(size) * price * rate
127
135
 
128
136
  def mkt_value(self) -> float:
129
- """Return the sum of the market values of the open positions in the account.
137
+ """Return the sum market values of the open positions in the account.
130
138
 
131
139
  The returned value is denoted in the base currency of the account.
132
140
  """
@@ -142,8 +150,8 @@ class Account:
142
150
  return self.contract_value(symbol, pos.size, pos.mkt_price) if pos else 0.0
143
151
 
144
152
  def equity(self) -> float:
145
- """Return the equity of the account. It calcaluates the sum of the mkt value of
146
- each open position and adds the available cash.
153
+ """Return the equity of the account.
154
+ It calculates the sum mkt values of each open position and adds the available cash.
147
155
 
148
156
  The returned value is denoted in the base currency of the account.
149
157
  """
@@ -0,0 +1,2 @@
1
+ from roboquant.alpaca.feed import AlpacaLiveFeed, AlpacaHistoricCryptoFeed, AlpacaHistoricStockFeed
2
+ from roboquant.alpaca.broker import AlpacaBroker
@@ -110,23 +110,3 @@ class AlpacaBroker(LiveBroker):
110
110
  def _get_replace_request(self, order: Order):
111
111
  result = ReplaceOrderRequest(qty=int(abs(float(order.size))), limit_price=order.limit)
112
112
  return result
113
-
114
-
115
- if __name__ == "__main__":
116
- broker = AlpacaBroker()
117
- account = broker.sync()
118
- print(account)
119
-
120
- tsla_order = Order("TSLA", 10)
121
- broker.place_orders([tsla_order])
122
- time.sleep(5)
123
- account = broker.sync()
124
- print(account)
125
-
126
- tesla_size = account.get_position_size("TSLA")
127
- if tesla_size:
128
- tsla_order = Order("TSLA", -tesla_size)
129
- broker.place_orders([tsla_order])
130
- time.sleep(5)
131
- account = broker.sync()
132
- print(account)
@@ -0,0 +1,182 @@
1
+ from array import array
2
+ from datetime import timedelta
3
+ import threading
4
+ from typing import Literal
5
+ import numpy as np
6
+
7
+ from alpaca.data import DataFeed
8
+ from alpaca.data.enums import Adjustment
9
+ from alpaca.data.historical.crypto import CryptoHistoricalDataClient
10
+ from alpaca.data.historical.stock import StockHistoricalDataClient
11
+ from alpaca.data.live.crypto import CryptoDataStream
12
+ from alpaca.data.live.stock import StockDataStream
13
+ from alpaca.data.live.option import OptionDataStream
14
+ from alpaca.data.models.bars import BarSet
15
+ from alpaca.data.models.quotes import QuoteSet
16
+ from alpaca.data.models.trades import TradeSet
17
+ from alpaca.data.requests import (
18
+ CryptoBarsRequest,
19
+ CryptoTradesRequest,
20
+ StockBarsRequest,
21
+ StockQuotesRequest,
22
+ StockTradesRequest,
23
+ )
24
+ from alpaca.data.timeframe import TimeFrame, TimeFrameUnit
25
+
26
+ from roboquant.config import Config
27
+ from roboquant.event import Event, Quote, Trade, Bar
28
+ from roboquant.feeds.historic import HistoricFeed
29
+ from roboquant.feeds.live import LiveFeed
30
+
31
+
32
+ class AlpacaLiveFeed(LiveFeed):
33
+ """Subscribe to live market data for stocks, crypto currencies or options"""
34
+
35
+ __one_minute = str(timedelta(minutes=1))
36
+
37
+ def __init__(self, market: Literal["iex", "sip", "crypto", "option"] = "iex", api_key=None, secret_key=None) -> None:
38
+ super().__init__()
39
+ config = Config()
40
+ api_key = api_key or config.get("alpaca.public.key")
41
+ secret_key = secret_key or config.get("alpaca.secret.key")
42
+ match market:
43
+ case "sip":
44
+ self.stream = StockDataStream(api_key, secret_key, feed=DataFeed.SIP)
45
+ case "iex":
46
+ self.stream = StockDataStream(api_key, secret_key, feed=DataFeed.IEX)
47
+ case "crypto":
48
+ self.stream = CryptoDataStream(api_key, secret_key)
49
+ case "option":
50
+ self.stream = OptionDataStream(api_key, secret_key)
51
+ case _:
52
+ raise ValueError(f"unsupported value market={market}")
53
+
54
+ thread = threading.Thread(None, self.stream.run, daemon=True)
55
+ thread.start()
56
+
57
+ async def close(self):
58
+ await self.stream.close()
59
+
60
+ def __put_item(self, time, item):
61
+ event = Event(time, [item])
62
+ self.put(event)
63
+
64
+ async def __handle_trades(self, data):
65
+ item = Trade(data.symbol, data.price, data.size)
66
+ self.__put_item(data.timestamp, item)
67
+
68
+ async def __handle_bars(self, data):
69
+ item = Bar(data.symbol, array("f", [data.open, data.high, data.low, data.close, data.volume]), self.__one_minute)
70
+ self.__put_item(data.timestamp, item)
71
+
72
+ async def __handle_quotes(self, data):
73
+ item = Quote(data.symbol, array("f", [data.ask_price, data.ask_size, data.bid_price, data.bid_size]))
74
+ self.__put_item(data.timestamp, item)
75
+
76
+ def subscribe_trades(self, *symbols: str):
77
+ self.stream.subscribe_trades(self.__handle_trades, *symbols)
78
+
79
+ def subscribe_quotes(self, *symbols: str):
80
+ self.stream.subscribe_quotes(self.__handle_quotes, *symbols)
81
+
82
+ def subscribe_bars(self, *symbols: str):
83
+ self.stream.subscribe_bars(self.__handle_bars, *symbols)
84
+
85
+
86
+ class _AlpacaHistoricFeed(HistoricFeed):
87
+
88
+ def _process_bars(self, bar_set, freq: str):
89
+ for symbol, data in bar_set.items():
90
+ for d in data:
91
+ time = d.timestamp
92
+ ohlcv = array("f", [d.open, d.high, d.low, d.close, d.volume])
93
+ item = Bar(symbol, ohlcv, freq)
94
+ super()._add_item(time, item)
95
+
96
+ def _process_trades(self, quote_set):
97
+ for symbol, data in quote_set.items():
98
+ for d in data:
99
+ time = d.timestamp
100
+ item = Trade(symbol, d.price, d.size)
101
+ super()._add_item(time, item)
102
+
103
+ def _process_quotes(self, quote_set):
104
+ for symbol, data in quote_set.items():
105
+ for d in data:
106
+ time = d.timestamp
107
+ arr = array("f", [d.ask_price, d.ask_size, d.bid_price, d.bid_size])
108
+
109
+ if np.all(arr):
110
+ # on rare occasions values are missing and have 0.0 as a value
111
+ item = Quote(symbol, arr)
112
+ super()._add_item(time, item)
113
+
114
+
115
+ class AlpacaHistoricStockFeed(_AlpacaHistoricFeed):
116
+ """Get historic stock prices from Alpaca.
117
+ Support for bars, trades and quotes.
118
+ """
119
+
120
+ def __init__(self, api_key=None, secret_key=None, data_api_url=None, feed: DataFeed | None = None):
121
+ super().__init__()
122
+ config = Config()
123
+ api_key = api_key or config.get("alpaca.public.key")
124
+ secret_key = secret_key or config.get("alpaca.secret.key")
125
+ self.client = StockHistoricalDataClient(api_key, secret_key, url_override=data_api_url)
126
+ self.feed = feed
127
+
128
+ def retrieve_bars(
129
+ self,
130
+ *symbols,
131
+ start=None,
132
+ end=None,
133
+ resolution: TimeFrame | None = None,
134
+ adjustment=Adjustment.ALL
135
+ ):
136
+ resolution = resolution or TimeFrame(amount=1, unit=TimeFrameUnit.Day)
137
+ req = StockBarsRequest(
138
+ symbol_or_symbols=list(symbols), timeframe=resolution, start=start, end=end, adjustment=adjustment, feed=self.feed
139
+ )
140
+ res = self.client.get_stock_bars(req)
141
+ assert isinstance(res, BarSet)
142
+ freq = str(resolution)
143
+ self._process_bars(res.data, freq)
144
+
145
+ def retrieve_trades(self, *symbols, start=None, end=None):
146
+ req = StockTradesRequest(symbol_or_symbols=list(symbols), start=start, end=end, feed=self.feed)
147
+ res = self.client.get_stock_trades(req)
148
+ assert isinstance(res, TradeSet)
149
+ self._process_trades(res.data)
150
+
151
+ def retrieve_quotes(self, *symbols, start=None, end=None):
152
+ req = StockQuotesRequest(symbol_or_symbols=list(symbols), start=start, end=end, feed=self.feed)
153
+ res = self.client.get_stock_quotes(req)
154
+ assert isinstance(res, QuoteSet)
155
+ self._process_quotes(res.data)
156
+
157
+
158
+ class AlpacaHistoricCryptoFeed(_AlpacaHistoricFeed):
159
+ """Get historic crypto-currency prices from Alpaca.
160
+ Support for bars and trades.
161
+ """
162
+
163
+ def __init__(self, api_key=None, secret_key=None, data_api_url=None):
164
+ super().__init__()
165
+ config = Config()
166
+ api_key = api_key or config.get("alpaca.public.key")
167
+ secret_key = secret_key or config.get("alpaca.secret.key")
168
+ self.client = CryptoHistoricalDataClient(api_key, secret_key, url_override=data_api_url)
169
+
170
+ def retrieve_bars(self, *symbols, start=None, end=None, resolution: TimeFrame | None = None):
171
+ resolution = resolution or TimeFrame(amount=1, unit=TimeFrameUnit.Day)
172
+ req = CryptoBarsRequest(symbol_or_symbols=list(symbols), timeframe=resolution, start=start, end=end)
173
+ res = self.client.get_crypto_bars(req)
174
+ assert isinstance(res, BarSet)
175
+ freq = str(resolution)
176
+ self._process_bars(res.data, freq)
177
+
178
+ def retrieve_trades(self, *symbols, start=None, end=None):
179
+ req = CryptoTradesRequest(symbol_or_symbols=list(symbols), start=start, end=end)
180
+ res = self.client.get_crypto_trades(req)
181
+ assert isinstance(res, TradeSet)
182
+ self._process_trades(res.data)
@@ -5,7 +5,7 @@ import logging
5
5
 
6
6
  from roboquant.account import Account, Position
7
7
  from roboquant.brokers.broker import Broker, _update_positions
8
- from roboquant.event import Event, PriceItem
8
+ from roboquant.event import Event, PriceItem, Quote
9
9
  from roboquant.order import Order, OrderStatus
10
10
 
11
11
  logger = logging.getLogger(__name__)
@@ -86,6 +86,8 @@ class SimBroker(Broker):
86
86
 
87
87
  The default implementation is a fixed slippage percentage based on the configured price_type.
88
88
  """
89
+ if isinstance(item, Quote):
90
+ return item.ask_price if order.is_buy else item.bid_price
89
91
 
90
92
  price = item.price(self.price_type)
91
93
  correction = self.slippage if order.is_buy else -self.slippage
@@ -189,17 +191,32 @@ class SimBroker(Broker):
189
191
  item = self._last_known_prices.get(symbol)
190
192
  return item.price(self.price_type) if item else None
191
193
 
192
- def calculate_buyingpower(self):
193
- """Calculate buying power, based on the available cash minus the open orders"""
194
- bp = self._account.cash
194
+ def _calculate_open_orders(self):
195
+ reserved = 0.0
195
196
  for order in self._account.open_orders():
196
197
  old_pos = self._account.get_position_size(order.symbol)
197
198
  remaining = order.size - order.fill
199
+
200
+ # only update reserved amount if remaining order size would increase position size
198
201
  if abs(old_pos + remaining) > abs(old_pos):
199
- price = order.limit or self._get_last_known_price(order.symbol)
200
- if price:
201
- reserve = self._account.contract_value(order.symbol, remaining, price)
202
- bp -= abs(reserve)
202
+ if price := (order.limit or self._get_last_known_price(order.symbol)):
203
+ reserved += abs(self._account.contract_value(order.symbol, remaining, price))
204
+
205
+ return reserved
206
+
207
+ def _calculate_short_positions(self):
208
+ reserved = 0.0
209
+ for symbol, position in self._account.positions.items():
210
+ if position.is_short:
211
+ short_value = self._account.contract_value(symbol, position.size, position.mkt_price)
212
+ reserved += abs(short_value)
213
+ return reserved
214
+
215
+ def _calculate_buyingpower(self):
216
+ """Calculate buying power, based on the available cash minus the open orders and short positions"""
217
+ bp = self._account.cash
218
+ bp -= self._calculate_open_orders()
219
+ bp -= self._calculate_short_positions()
203
220
  return bp
204
221
 
205
222
  def sync(self, event: Event | None = None) -> Account:
@@ -223,9 +240,9 @@ class SimBroker(Broker):
223
240
  _update_positions(acc, event, self.price_type)
224
241
 
225
242
  acc.orders = list(self._create_orders.values())
226
- acc.buying_power = self.calculate_buyingpower()
243
+ acc.buying_power = self._calculate_buyingpower()
227
244
  return acc
228
245
 
229
- def __str__(self) -> str:
246
+ def __repr__(self) -> str:
230
247
  attrs = " ".join([f"{k}={v}" for k, v in self.__dict__.items() if not k.startswith("_")])
231
248
  return f"SimBroker({attrs})"
@@ -4,7 +4,10 @@ from configparser import ConfigParser
4
4
 
5
5
 
6
6
  class Config:
7
- """Access to the roboquant configuration file"""
7
+ """Access to the roboquant configuration file.
8
+ This allows to share the same property file for both the Python and Kotlin
9
+ version of roboquant.
10
+ """
8
11
 
9
12
  def __init__(self, path=None):
10
13
  path = path or os.path.expanduser("~/.roboquant/.env")
@@ -28,7 +28,7 @@ class PriceItem:
28
28
 
29
29
  @dataclass(slots=True)
30
30
  class Quote(PriceItem):
31
- data: array
31
+ data: array # [ask-price, ask-volume, bid-price, bid-volume]
32
32
 
33
33
  def price(self, price_type: str = "DEFAULT") -> float:
34
34
  """Return the price, the default being the mid-point price"""
@@ -42,6 +42,14 @@ class Quote(PriceItem):
42
42
  # Default is the mid-point price
43
43
  return (self.data[0] + self.data[2]) / 2.0
44
44
 
45
+ @property
46
+ def ask_price(self) -> float:
47
+ return self.data[0]
48
+
49
+ @property
50
+ def bid_price(self) -> float:
51
+ return self.data[2]
52
+
45
53
  @property
46
54
  def ask_volume(self) -> float:
47
55
  return self.data[1]
@@ -82,7 +90,7 @@ class Trade(PriceItem):
82
90
 
83
91
  @dataclass(slots=True)
84
92
  class Bar(PriceItem):
85
- """Represents a bar (a.k.a candlestick) with open, high, low, close and volume data.
93
+ """Represents a bar (a.k.a. candlestick) with open-, high-, low-, close-price and volume data.
86
94
  """
87
95
 
88
96
  ohlcv: array
@@ -130,10 +138,10 @@ class Event:
130
138
 
131
139
  def is_empty(self) -> bool:
132
140
  """return True if this is an empty event without any items, False otherwise"""
133
- return len(self) == 0
141
+ return len(self.items) == 0
134
142
 
135
- def __len__(self) -> int:
136
- return len(self.items)
143
+ # def __len__(self) -> int:
144
+ # return len(self.items)
137
145
 
138
146
  @cached_property
139
147
  def price_items(self) -> dict[str, PriceItem]:
@@ -1,5 +1,6 @@
1
1
  from roboquant.feeds import feedutil
2
2
  from .aggregate import AggregatorFeed
3
+ from .collect import CollectorFeed
3
4
  from .csvfeed import CSVFeed
4
5
  from .eventchannel import EventChannel
5
6
  from .feed import Feed
@@ -7,12 +8,7 @@ from .historic import HistoricFeed
7
8
  from .randomwalk import RandomWalk
8
9
  from .sqllitefeed import SQLFeed
9
10
  from .tiingo import TiingoLiveFeed, TiingoHistoricFeed
10
- from .feedutil import get_sp500_symbols
11
-
12
- try:
13
- from .alpacafeed import AlpacaLiveFeed
14
- except ImportError:
15
- pass
11
+ from .feedutil import get_sp500_symbols, print_feed_items, count_events
16
12
 
17
13
  try:
18
14
  from .yahoo import YahooFeed
@@ -8,17 +8,17 @@ from .feed import Feed
8
8
 
9
9
 
10
10
  class AggregatorFeed(Feed):
11
- """Aggregates Trades or Quotes of another feed into a `Bar`.
11
+ """Aggregates Trades or Quotes of another feed into a `Bar` prices.
12
12
 
13
- When trades are used, the actual trade price and volume are used to create the aggregated bars.
14
- When quotes are used, the midpoint price and no volume are used to create the aggregated bars.
13
+ When trades are used, the actual trade prices and volumes are used to create the aggregated bars.
14
+ When quotes are used, the midpoint prices and no volumes are used to create the aggregated bars.
15
15
  """
16
16
 
17
17
  def __init__(
18
18
  self,
19
19
  feed: Feed,
20
20
  frequency: timedelta,
21
- item_type: Literal["trade", "quote"] = "trade",
21
+ price_type: Literal["trade", "quote"] = "trade",
22
22
  send_remaining=False,
23
23
  continuation=True,
24
24
  ):
@@ -27,16 +27,16 @@ class AggregatorFeed(Feed):
27
27
  self.freq = frequency
28
28
  self.send_remaining = send_remaining
29
29
  self.continuation = continuation
30
- self.item_type = item_type
30
+ self.price_type = price_type
31
31
 
32
32
  def __aggr_trade2bar(self, evt: Event, bars: dict[str, Bar], freq: str):
33
33
 
34
34
  for item in evt.items:
35
35
 
36
- if self.item_type == "trade" and isinstance(item, Trade):
36
+ if self.price_type == "trade" and isinstance(item, Trade):
37
37
  price = item.trade_price
38
38
  volume = item.trade_volume
39
- elif self.item_type == "quote" and isinstance(item, Quote):
39
+ elif self.price_type == "quote" and isinstance(item, Quote):
40
40
  price = item.midpoint_price
41
41
  volume = float("nan")
42
42
  else:
@@ -59,7 +59,7 @@ class AggregatorFeed(Feed):
59
59
  result = {}
60
60
  for symbol, item in bars.items():
61
61
  p = item.price("CLOSE")
62
- v = 0.0 if self.item_type == "trade" else float("nan")
62
+ v = 0.0 if self.price_type == "trade" else float("nan")
63
63
  b = Bar(symbol, array("f", [p, p, p, p, v]))
64
64
  result[symbol] = b
65
65
  return result
@@ -0,0 +1,120 @@
1
+ import logging
2
+ import os.path
3
+ from datetime import datetime
4
+ from array import array
5
+ import time
6
+
7
+ from fastavro import writer, reader, parse_schema
8
+
9
+ from roboquant.alpaca.feed import AlpacaHistoricStockFeed
10
+ from roboquant.event import Quote, Bar, Trade
11
+ from roboquant.event import Event
12
+ from roboquant.feeds.eventchannel import EventChannel
13
+ from roboquant.feeds.feed import Feed
14
+ from roboquant.feeds.feedutil import count_events
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class AvroFeed(Feed):
20
+
21
+ _schema = {
22
+ "namespace": "org.roboquant.avro.schema",
23
+ "type": "record",
24
+ "name": "PriceItemV2",
25
+ "fields": [
26
+ {"name": "timestamp", "type": "string"},
27
+ {"name": "symbol", "type": "string"},
28
+ {"name": "type", "type": {"type": "enum", "name": "item_type", "symbols": ["BAR", "TRADE", "QUOTE", "BOOK"]}},
29
+ {"name": "values", "type": {"type": "array", "items": "double"}},
30
+ {"name": "meta", "type": ["null", "string"], "default": None},
31
+ ],
32
+ }
33
+
34
+ def __init__(self, avro_file) -> None:
35
+ super().__init__()
36
+ self.avro_file = avro_file
37
+ logger.info("avro feed file=%s", avro_file)
38
+
39
+ def exists(self):
40
+ return os.path.exists(self.avro_file)
41
+
42
+ def play(self, channel: EventChannel):
43
+ t_old = ""
44
+ items = []
45
+
46
+ with open(self.avro_file, "rb") as fo:
47
+ for row in reader(fo):
48
+ t = row["timestamp"] # type: ignore
49
+
50
+ if t != t_old:
51
+ if items:
52
+ dt = datetime.fromisoformat(t)
53
+ event = Event(dt, items)
54
+ channel.put(event)
55
+ items = []
56
+ t_old = t
57
+
58
+ price_type = str(row["type"]) # type: ignore
59
+ match (price_type):
60
+ case "QUOTE":
61
+ item = Quote(row["symbol"], array("f", row["values"])) # type: ignore
62
+ items.append(item)
63
+ case "BAR":
64
+ item = Bar(row["symbol"], array("f", row["values"]), row["other"]) # type: ignore
65
+ items.append(item)
66
+ case "TRADE":
67
+ prices = row["values"] # type: ignore
68
+ item = Trade(row["symbol"], prices[0], prices[1]) # type: ignore
69
+ items.append(item)
70
+ case _:
71
+ raise ValueError(f"Unsupported priceItem type={price_type}")
72
+
73
+ def record(self, feed: Feed, timeframe=None):
74
+ schema = parse_schema(AvroFeed._schema)
75
+ channel = feed.play_background(timeframe)
76
+ records = []
77
+ while event := channel.get():
78
+ t = event.time.isoformat()
79
+ for item in event.items:
80
+
81
+ match item:
82
+ case Quote():
83
+ data = {"timestamp": t, "type": "QUOTE", "symbol": item.symbol, "values": list(item.data)}
84
+ records.append(data)
85
+ case Trade():
86
+ data = {
87
+ "timestamp": t,
88
+ "type": "TRADE",
89
+ "symbol": item.symbol,
90
+ "values": [item.trade_price, item.trade_volume],
91
+ }
92
+ records.append(data)
93
+ case Bar():
94
+ data = {
95
+ "timestamp": t,
96
+ "type": "BAR",
97
+ "symbol": item.symbol,
98
+ "values": list(item.ohlcv),
99
+ "meta": item.frequency,
100
+ }
101
+ records.append(data)
102
+
103
+ with open(self.avro_file, "wb") as out:
104
+ writer(out, schema, records)
105
+
106
+ def __repr__(self) -> str:
107
+ return f"AvroFeed(path={self.avro_file})"
108
+
109
+
110
+ if __name__ == "__main__":
111
+
112
+ avroFeed = AvroFeed("/tmp/test.avro")
113
+ if not avroFeed.exists():
114
+ alpaca_feed = AlpacaHistoricStockFeed()
115
+ alpaca_feed.retrieve_quotes("AAPL", start="2024-05-24T00:00:00Z", end="2024-05-25T00:00:00Z")
116
+ avroFeed.record(alpaca_feed)
117
+
118
+ start = time.time()
119
+ print("events=", count_events(avroFeed), "time=", time.time() - start)
120
+ # print_feed_items(feed)
@@ -0,0 +1,33 @@
1
+
2
+ from roboquant.event import Event
3
+ from .eventchannel import EventChannel
4
+ from .feed import Feed
5
+
6
+
7
+ class CollectorFeed(Feed):
8
+ """Collect events that occur close after ech other into a single new event.
9
+
10
+ Close to eachother is defined by the timeout is seconds. If there is no new
11
+ event in the specified timeout, all the previously collected event items will
12
+ be bundled together and put on the channel as a single event.
13
+ """
14
+
15
+ def __init__(
16
+ self,
17
+ feed: Feed,
18
+ timeout=5.0,
19
+ ):
20
+ super().__init__()
21
+ self.feed = feed
22
+ self.timeout = timeout
23
+
24
+ def play(self, channel: EventChannel):
25
+ src_channel = self.feed.play_background(channel.timeframe, channel.maxsize)
26
+ items = []
27
+ while event := src_channel.get(self.timeout):
28
+ if event.is_empty() and items:
29
+ new_event = Event(event.time, items)
30
+ channel.put(new_event)
31
+ items = []
32
+
33
+ items.extend(event.items)
@@ -24,7 +24,7 @@ class CSVColumn(str, Enum):
24
24
  ADJ_CLOSE = "Adj CLose"
25
25
  TIME = "Time"
26
26
 
27
- def __str__(self) -> str:
27
+ def __repr__(self) -> str:
28
28
  return self.value
29
29
 
30
30
  @staticmethod
@@ -121,7 +121,7 @@ class CSVFeed(HistoricFeed):
121
121
 
122
122
  class StooqDailyFeed(CSVFeed):
123
123
  def __init__(self):
124
- # from Python 3.11 onwards we can use the fast standard ISO parsing
124
+ # from Python 3.11 onwards, we can use the fast standard ISO parsing
125
125
  if sys.version_info >= (3, 11):
126
126
  super().__init__(path, columns=columns, time_offset="21:00:00+00:00", endswith=".txt", frequency="1d")
127
127
  else:
@@ -147,7 +147,7 @@ class CSVFeed(HistoricFeed):
147
147
 
148
148
  class StooqIntradayFeed(CSVFeed):
149
149
  def __init__(self):
150
- # from Python 3.11 onwards we can use the faster standard ISO parsing
150
+ # from Python 3.11 onwards, we can use the faster standard ISO parsing
151
151
  if sys.version_info >= (3, 11):
152
152
  super().__init__(path, columns=columns, has_time_column=True, endswith=".txt")
153
153
  else:
@@ -171,11 +171,3 @@ class CSVFeed(HistoricFeed):
171
171
  """Parse one or more CSV files that meet the Yahoo Finance format"""
172
172
  columns = ["Date", "Open", "High", "Low", "Close", "Volume", "Adj Close"]
173
173
  return cls(path, columns=columns, adj_close=True, time_offset="21:00:00+00:00", frequency=frequency)
174
-
175
-
176
- if __name__ == "__main__":
177
- t = datetime.strptime("210000", "%H%M%S").time()
178
- print(t)
179
-
180
- t = time.fromisoformat("210000")
181
- print(t)