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.
- {roboquant-0.3.0 → roboquant-0.3.2}/PKG-INFO +1 -1
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/__init__.py +2 -2
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/account.py +11 -3
- roboquant-0.3.2/roboquant/alpaca/__init__.py +2 -0
- roboquant-0.3.0/roboquant/brokers/alpacabroker.py → roboquant-0.3.2/roboquant/alpaca/broker.py +0 -20
- roboquant-0.3.2/roboquant/alpaca/feed.py +182 -0
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/brokers/simbroker.py +27 -10
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/config.py +4 -1
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/event.py +13 -5
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/feeds/__init__.py +2 -6
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/feeds/aggregate.py +8 -8
- roboquant-0.3.2/roboquant/feeds/avrofeed.py +120 -0
- roboquant-0.3.2/roboquant/feeds/collect.py +33 -0
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/feeds/csvfeed.py +3 -11
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/feeds/feed.py +10 -7
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/feeds/feedutil.py +11 -0
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/feeds/historic.py +16 -8
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/feeds/live.py +7 -2
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/feeds/randomwalk.py +2 -1
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/feeds/sqllitefeed.py +55 -16
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/journals/alphabeta.py +1 -1
- roboquant-0.3.2/roboquant/journals/basicjournal.py +50 -0
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/journals/feedmetric.py +1 -1
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/journals/metricsjournal.py +3 -5
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/journals/tensorboardjournal.py +1 -1
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/ml/envs.py +9 -4
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/ml/features.py +132 -26
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/ml/strategies.py +32 -33
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/order.py +5 -5
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/run.py +2 -1
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/signal.py +19 -10
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/strategies/__init__.py +1 -1
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/strategies/emacrossover.py +1 -1
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/strategies/multistrategy.py +2 -0
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/strategies/smacrossover.py +1 -1
- roboquant-0.3.0/roboquant/strategies/barstrategy.py → roboquant-0.3.2/roboquant/strategies/tastrategy.py +3 -3
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/timeframe.py +15 -6
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/traders/flextrader.py +54 -29
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/traders/sizing.py +5 -2
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant.egg-info/PKG-INFO +1 -1
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant.egg-info/SOURCES.txt +6 -3
- roboquant-0.3.0/roboquant/feeds/alpacafeed.py +0 -65
- roboquant-0.3.0/roboquant/journals/basicjournal.py +0 -40
- {roboquant-0.3.0 → roboquant-0.3.2}/LICENSE +0 -0
- {roboquant-0.3.0 → roboquant-0.3.2}/README.md +0 -0
- {roboquant-0.3.0 → roboquant-0.3.2}/pyproject.toml +0 -0
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/brokers/__init__.py +0 -0
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/brokers/broker.py +0 -0
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/brokers/ibkr.py +0 -0
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/feeds/eventchannel.py +0 -0
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/feeds/sp500.json +0 -0
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/feeds/tiingo.py +0 -0
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/feeds/yahoo.py +0 -0
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/journals/__init__.py +0 -0
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/journals/journal.py +0 -0
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/journals/metric.py +0 -0
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/journals/pnlmetric.py +0 -0
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/journals/pricemetric.py +0 -0
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/journals/runmetric.py +0 -0
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/ml/__init__.py +0 -0
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/strategies/buffer.py +0 -0
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/strategies/strategy.py +0 -0
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/traders/__init__.py +0 -0
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant/traders/trader.py +0 -0
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant.egg-info/dependency_links.txt +0 -0
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant.egg-info/requires.txt +0 -0
- {roboquant-0.3.0 → roboquant-0.3.2}/roboquant.egg-info/top_level.txt +0 -0
- {roboquant-0.3.0 → roboquant-0.3.2}/setup.cfg +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
__version__ = "0.3.
|
|
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
|
|
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.
|
|
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
|
"""
|
roboquant-0.3.0/roboquant/brokers/alpacabroker.py → roboquant-0.3.2/roboquant/alpaca/broker.py
RENAMED
|
@@ -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
|
|
193
|
-
|
|
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
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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.
|
|
243
|
+
acc.buying_power = self._calculate_buyingpower()
|
|
227
244
|
return acc
|
|
228
245
|
|
|
229
|
-
def
|
|
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
|
|
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
|
-
|
|
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
|
|
14
|
-
When quotes are used, the midpoint
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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)
|