roboquant 0.2.3__tar.gz → 0.2.6__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.2.3/roboquant.egg-info → roboquant-0.2.6}/PKG-INFO +14 -6
- {roboquant-0.2.3 → roboquant-0.2.6}/README.md +11 -5
- {roboquant-0.2.3 → roboquant-0.2.6}/pyproject.toml +4 -3
- {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/__init__.py +2 -2
- roboquant-0.2.6/roboquant/account.py +193 -0
- {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/brokers/broker.py +12 -0
- roboquant-0.2.3/roboquant/brokers/ibkrbroker.py → roboquant-0.2.6/roboquant/brokers/ibkr.py +92 -46
- {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/brokers/simbroker.py +51 -49
- {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/event.py +14 -5
- {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/feeds/__init__.py +3 -3
- roboquant-0.2.6/roboquant/feeds/alpacafeed.py +52 -0
- roboquant-0.2.6/roboquant/feeds/candlefeed.py +88 -0
- {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/feeds/csvfeed.py +34 -37
- roboquant-0.2.6/roboquant/feeds/feed.py +90 -0
- roboquant-0.2.3/roboquant/feeds/historicfeed.py → roboquant-0.2.6/roboquant/feeds/historic.py +10 -4
- {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/feeds/randomwalk.py +1 -1
- roboquant-0.2.3/roboquant/feeds/tiingofeed.py → roboquant-0.2.6/roboquant/feeds/tiingo.py +1 -1
- roboquant-0.2.3/roboquant/feeds/yahoofeed.py → roboquant-0.2.6/roboquant/feeds/yahoo.py +6 -7
- {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/journals/alphabeta.py +1 -1
- {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/journals/basicjournal.py +6 -11
- {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/journals/journal.py +4 -1
- {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/journals/pnlmetric.py +4 -6
- {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/journals/tensorboardjournal.py +3 -4
- {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/order.py +18 -8
- {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/run.py +1 -1
- {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/signal.py +7 -0
- {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/strategies/emacrossover.py +7 -7
- {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/strategies/features.py +116 -3
- {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/strategies/nopstrategy.py +1 -1
- {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/strategies/smacrossover.py +2 -1
- {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/strategies/torch.py +7 -5
- {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/timeframe.py +6 -4
- {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/traders/flextrader.py +1 -1
- {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/traders/trader.py +1 -1
- {roboquant-0.2.3 → roboquant-0.2.6/roboquant.egg-info}/PKG-INFO +14 -6
- {roboquant-0.2.3 → roboquant-0.2.6}/roboquant.egg-info/SOURCES.txt +5 -4
- {roboquant-0.2.3 → roboquant-0.2.6}/roboquant.egg-info/requires.txt +3 -0
- roboquant-0.2.3/roboquant/account.py +0 -150
- roboquant-0.2.3/roboquant/feeds/candlefeed.py +0 -65
- roboquant-0.2.3/roboquant/feeds/feed.py +0 -51
- {roboquant-0.2.3 → roboquant-0.2.6}/LICENSE +0 -0
- {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/brokers/__init__.py +0 -0
- {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/config.py +0 -0
- {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/feeds/eventchannel.py +0 -0
- {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/feeds/feedutil.py +0 -0
- {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/feeds/sqllitefeed.py +0 -0
- {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/journals/__init__.py +0 -0
- {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/journals/feedmetric.py +0 -0
- {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/journals/metric.py +0 -0
- {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/journals/metricsjournal.py +0 -0
- {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/journals/pricemetric.py +0 -0
- {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/journals/runmetric.py +0 -0
- {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/strategies/__init__.py +0 -0
- {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/strategies/buffer.py +0 -0
- {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/strategies/candlestrategy.py +0 -0
- {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/strategies/multistrategy.py +0 -0
- {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/strategies/strategy.py +0 -0
- {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/traders/__init__.py +0 -0
- {roboquant-0.2.3 → roboquant-0.2.6}/roboquant.egg-info/dependency_links.txt +0 -0
- {roboquant-0.2.3 → roboquant-0.2.6}/roboquant.egg-info/top_level.txt +0 -0
- {roboquant-0.2.3 → roboquant-0.2.6}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: roboquant
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6
|
|
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
|
|
@@ -32,6 +32,8 @@ Provides-Extra: yahoo
|
|
|
32
32
|
Requires-Dist: yfinance~=0.2.36; extra == "yahoo"
|
|
33
33
|
Provides-Extra: ibkr
|
|
34
34
|
Requires-Dist: nautilus-ibapi~=10.19.2; extra == "ibkr"
|
|
35
|
+
Provides-Extra: alpaca
|
|
36
|
+
Requires-Dist: alpaca-py; extra == "alpaca"
|
|
35
37
|
Provides-Extra: all
|
|
36
38
|
Requires-Dist: roboquant[ibkr,torch,yahoo]; extra == "all"
|
|
37
39
|
|
|
@@ -50,7 +52,7 @@ Roboquant is an open-source algorithmic trading platform. It is flexible, user-f
|
|
|
50
52
|
So whether you are a beginning retail trader or an established trading firm, roboquant can help you to develop robust and fully automated trading strategies. You can find out more at [roboquant.org](https://roboquant.org).
|
|
51
53
|
|
|
52
54
|
## Usage
|
|
53
|
-
The following code snippet shows the steps to run a full back-test on a number of stocks.
|
|
55
|
+
The following code snippet shows all the steps to run a full back-test on a number of stocks.
|
|
54
56
|
|
|
55
57
|
```python
|
|
56
58
|
import roboquant as rq
|
|
@@ -69,6 +71,9 @@ Make sure you have Python version 3.10 or higher installed.
|
|
|
69
71
|
python3 -m pip install --upgrade roboquant
|
|
70
72
|
```
|
|
71
73
|
|
|
74
|
+
You can also try roboquant in an online Jupyter Notebook [](https://mybinder.org/v2/gh/neurallayer/roboquant-notebooks/HEAD?labpath=%2Fintro_roboquant.ipynb)
|
|
75
|
+
|
|
76
|
+
|
|
72
77
|
The core of roboquant limits the number of dependencies.
|
|
73
78
|
But you can install roboquant including one or more of the optional dependencies if you require certain functionality:
|
|
74
79
|
|
|
@@ -113,8 +118,11 @@ python -m unittest discover -s tests/unit
|
|
|
113
118
|
flake8 roboquant tests
|
|
114
119
|
```
|
|
115
120
|
|
|
116
|
-
##
|
|
117
|
-
|
|
118
|
-
Both (will) share a similar API, just the used computer language is different.
|
|
121
|
+
## License
|
|
122
|
+
Roboquant is made available under the Apache 2.0 license. You can read more about the Apache 2.0 license on this page: https://www.apache.org/licenses/LICENSE-2.0.html
|
|
119
123
|
|
|
120
|
-
|
|
124
|
+
## Disclaimer
|
|
125
|
+
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 doesn't contain bugs. All financial trading offers the possibility of loss. Leveraged trading, may result in you losing all your money, and still owing more. Backtested 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.
|
|
126
|
+
|
|
127
|
+
## Kotlin version
|
|
128
|
+
Next to this Python version of `roboquant`, there is also a Kotlin version available. Both (will) share a similar API, just the used computer language is different. Which one to use depends very much on personal preferences, skills and usage.
|
|
@@ -13,7 +13,7 @@ Roboquant is an open-source algorithmic trading platform. It is flexible, user-f
|
|
|
13
13
|
So whether you are a beginning retail trader or an established trading firm, roboquant can help you to develop robust and fully automated trading strategies. You can find out more at [roboquant.org](https://roboquant.org).
|
|
14
14
|
|
|
15
15
|
## Usage
|
|
16
|
-
The following code snippet shows the steps to run a full back-test on a number of stocks.
|
|
16
|
+
The following code snippet shows all the steps to run a full back-test on a number of stocks.
|
|
17
17
|
|
|
18
18
|
```python
|
|
19
19
|
import roboquant as rq
|
|
@@ -32,6 +32,9 @@ Make sure you have Python version 3.10 or higher installed.
|
|
|
32
32
|
python3 -m pip install --upgrade roboquant
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
+
You can also try roboquant in an online Jupyter Notebook [](https://mybinder.org/v2/gh/neurallayer/roboquant-notebooks/HEAD?labpath=%2Fintro_roboquant.ipynb)
|
|
36
|
+
|
|
37
|
+
|
|
35
38
|
The core of roboquant limits the number of dependencies.
|
|
36
39
|
But you can install roboquant including one or more of the optional dependencies if you require certain functionality:
|
|
37
40
|
|
|
@@ -76,8 +79,11 @@ python -m unittest discover -s tests/unit
|
|
|
76
79
|
flake8 roboquant tests
|
|
77
80
|
```
|
|
78
81
|
|
|
79
|
-
##
|
|
80
|
-
|
|
81
|
-
Both (will) share a similar API, just the used computer language is different.
|
|
82
|
+
## License
|
|
83
|
+
Roboquant is made available under the Apache 2.0 license. You can read more about the Apache 2.0 license on this page: https://www.apache.org/licenses/LICENSE-2.0.html
|
|
82
84
|
|
|
83
|
-
|
|
85
|
+
## Disclaimer
|
|
86
|
+
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 doesn't contain bugs. All financial trading offers the possibility of loss. Leveraged trading, may result in you losing all your money, and still owing more. Backtested 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.
|
|
87
|
+
|
|
88
|
+
## Kotlin version
|
|
89
|
+
Next to this Python version of `roboquant`, there is also a Kotlin version available. Both (will) share a similar API, just the used computer language is different. Which one to use depends very much on personal preferences, skills and usage.
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
[tool.black]
|
|
2
2
|
line-length = 127
|
|
3
3
|
|
|
4
|
-
[tool.flake8]
|
|
5
|
-
max-line-length = 127
|
|
6
|
-
|
|
7
4
|
[tool.pytest.ini_options]
|
|
8
5
|
minversion = "8.0"
|
|
9
6
|
addopts = "-ra -q"
|
|
@@ -11,6 +8,9 @@ testpaths = [
|
|
|
11
8
|
"tests/unit"
|
|
12
9
|
]
|
|
13
10
|
|
|
11
|
+
[tool.pyright]
|
|
12
|
+
reportOptionalOperand = "none"
|
|
13
|
+
|
|
14
14
|
[tool.pylint.'MESSAGES CONTROL']
|
|
15
15
|
max-line-length = 127
|
|
16
16
|
disable = "too-few-public-methods,missing-module-docstring,missing-class-docstring,missing-function-docstring,unnecessary-ellipsis"
|
|
@@ -57,6 +57,7 @@ version = {attr = "roboquant.__version__"}
|
|
|
57
57
|
torch = ["torch>=2.1.0", "tensorboard>=2.15.2"]
|
|
58
58
|
yahoo = ["yfinance~=0.2.36"]
|
|
59
59
|
ibkr = ["nautilus-ibapi~=10.19.2"]
|
|
60
|
+
alpaca = ["alpaca-py"]
|
|
60
61
|
all = [ "roboquant[torch,yahoo,ibkr]" ]
|
|
61
62
|
|
|
62
63
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
__version__ = "0.2.
|
|
1
|
+
__version__ = "0.2.6"
|
|
2
2
|
|
|
3
3
|
from roboquant import brokers
|
|
4
4
|
from roboquant import feeds
|
|
5
5
|
from roboquant import journals
|
|
6
6
|
from roboquant import strategies
|
|
7
7
|
from roboquant import traders
|
|
8
|
-
from .account import Account,
|
|
8
|
+
from .account import Account, Position, Converter, CurrencyConverter, OptionConverter
|
|
9
9
|
from .config import Config
|
|
10
10
|
from .event import Event, PriceItem, Candle, Trade, Quote
|
|
11
11
|
from .order import Order, OrderStatus
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from decimal import Decimal
|
|
5
|
+
|
|
6
|
+
from roboquant.order import Order
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(slots=True)
|
|
10
|
+
class Position:
|
|
11
|
+
"""Position of a symbol"""
|
|
12
|
+
|
|
13
|
+
size: Decimal
|
|
14
|
+
"""Position size"""
|
|
15
|
+
|
|
16
|
+
avg_price: float
|
|
17
|
+
"""Average price paid denoted in the currency of the symbol"""
|
|
18
|
+
|
|
19
|
+
mkt_price: float
|
|
20
|
+
"""latest market price denoted in the currency of the symbol"""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Converter(ABC):
|
|
24
|
+
"""Abstraction that enables trading symbols that are denoted in different currencies and/or contact sizes"""
|
|
25
|
+
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def __call__(self, symbol: str, time: datetime) -> float:
|
|
28
|
+
"""Return the conversion rate for the symbol at the given time"""
|
|
29
|
+
...
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class OptionConverter(Converter):
|
|
33
|
+
"""
|
|
34
|
+
This converter handles common option contracts of size 100 and 10 and serves as an example.
|
|
35
|
+
|
|
36
|
+
If no contract size is registered for a symbol, it calculates one based on the symbol name.
|
|
37
|
+
If the symbol is not recognized as an OCC compliant option symbol, it is assumed to have a
|
|
38
|
+
contract size of 1.0
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self):
|
|
42
|
+
super().__init__()
|
|
43
|
+
self._contract_sizes: dict[str, float] = {}
|
|
44
|
+
|
|
45
|
+
def register(self, symbol: str, contract_size: float = 100.0):
|
|
46
|
+
"""Register a contract-size for a symbol"""
|
|
47
|
+
self._contract_sizes[symbol] = contract_size
|
|
48
|
+
|
|
49
|
+
def __call__(self, symbol: str, time: datetime) -> float:
|
|
50
|
+
contract_size = self._contract_sizes.get(symbol)
|
|
51
|
+
|
|
52
|
+
# If no contract has been registered, we try to defer the contract size from the symbol
|
|
53
|
+
if contract_size is None:
|
|
54
|
+
if len(symbol) == 21:
|
|
55
|
+
# OCC compliant option symbol
|
|
56
|
+
symbol = symbol[0:6].rstrip()
|
|
57
|
+
contract_size = 10.0 if symbol[-1] == "7" else 100.0
|
|
58
|
+
else:
|
|
59
|
+
# not an option symbol
|
|
60
|
+
contract_size = 1.0
|
|
61
|
+
|
|
62
|
+
self._contract_sizes[symbol] = contract_size
|
|
63
|
+
|
|
64
|
+
return contract_size
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class CurrencyConverter(Converter):
|
|
68
|
+
"""Supports trading in symbols that are denoted in a different currency from the base currency of the account"""
|
|
69
|
+
|
|
70
|
+
def __init__(self, base_currency="USD", default_symbol_currency: str | None = "USD"):
|
|
71
|
+
super().__init__()
|
|
72
|
+
self.rates = {}
|
|
73
|
+
self.base_currency = base_currency
|
|
74
|
+
self.default_symbol_currency = default_symbol_currency
|
|
75
|
+
self.registered_symbols: dict[str, str] = {}
|
|
76
|
+
|
|
77
|
+
def register_symbol(self, symbol: str, currency: str):
|
|
78
|
+
"""Register a symbol and its denoted currency"""
|
|
79
|
+
self.registered_symbols[symbol] = currency
|
|
80
|
+
|
|
81
|
+
def register_rate(self, currency: str, rate: float):
|
|
82
|
+
"""Register a conversion rate from a currency to the base_currency"""
|
|
83
|
+
self.rates[currency] = rate
|
|
84
|
+
|
|
85
|
+
def __call__(self, symbol: str, _: datetime) -> float:
|
|
86
|
+
currency = self.registered_symbols.get(symbol, self.default_symbol_currency)
|
|
87
|
+
if not currency:
|
|
88
|
+
raise ValueError(f"no currency or default_symbol_currency registered for symbol={symbol}")
|
|
89
|
+
if currency == self.base_currency:
|
|
90
|
+
return 1.0
|
|
91
|
+
return self.rates[currency]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class Account:
|
|
95
|
+
"""Represents a trading account with all monetary amounts denoted in a single currency.
|
|
96
|
+
The account maintains the following state during a run:
|
|
97
|
+
|
|
98
|
+
- Available buying power for orders in the base currency of the account
|
|
99
|
+
- Cash available
|
|
100
|
+
- The open positions
|
|
101
|
+
- Orders
|
|
102
|
+
- Calculated derived equity value of the account in the base currency of the account
|
|
103
|
+
- The last time the account was updated
|
|
104
|
+
|
|
105
|
+
Only the broker updates the account and does this only during its `sync` method.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
__converter: Converter | None = None
|
|
109
|
+
|
|
110
|
+
def __init__(self):
|
|
111
|
+
self.buying_power: float = 0.0
|
|
112
|
+
self.positions: dict[str, Position] = {}
|
|
113
|
+
self.orders: list[Order] = []
|
|
114
|
+
self.last_update: datetime = datetime.fromisoformat("1900-01-01T00:00:00+00:00")
|
|
115
|
+
self.cash: float = 0.0
|
|
116
|
+
|
|
117
|
+
@staticmethod
|
|
118
|
+
def register_converter(converter: Converter):
|
|
119
|
+
"""Register a converter"""
|
|
120
|
+
Account.__converter = converter
|
|
121
|
+
|
|
122
|
+
def contract_value(self, symbol: str, size: Decimal, price: float) -> float:
|
|
123
|
+
# pylint: disable=not-callable
|
|
124
|
+
"""Return the total value of the provided contract size denoted in the base currency of the account."""
|
|
125
|
+
rate = 1.0 if not Account.__converter else Account.__converter(symbol, self.last_update)
|
|
126
|
+
return float(size) * price * rate
|
|
127
|
+
|
|
128
|
+
def mkt_value(self) -> float:
|
|
129
|
+
"""Return the sum of the market values of the open positions in the account.
|
|
130
|
+
|
|
131
|
+
The returned value is denoted in the base currency of the account.
|
|
132
|
+
"""
|
|
133
|
+
return sum(
|
|
134
|
+
[self.contract_value(symbol, pos.size, pos.mkt_price) for symbol, pos in self.positions.items()],
|
|
135
|
+
0.0,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
def equity(self) -> float:
|
|
139
|
+
"""Return the equity of the account. It calcaluates the sum of the mkt value of
|
|
140
|
+
each open position and adds the available cash.
|
|
141
|
+
|
|
142
|
+
The returned value is denoted in the base currency of the account.
|
|
143
|
+
"""
|
|
144
|
+
return self.cash + self.mkt_value()
|
|
145
|
+
|
|
146
|
+
def unrealized_pnl(self) -> float:
|
|
147
|
+
"""Return the sum of the unrealized profit and loss for the open position.
|
|
148
|
+
|
|
149
|
+
The returned value is denoted in the base currency of the account.
|
|
150
|
+
"""
|
|
151
|
+
return sum(
|
|
152
|
+
[self.contract_value(symbol, pos.size, pos.mkt_price - pos.avg_price) for symbol, pos in self.positions.items()],
|
|
153
|
+
0.0,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def has_open_order(self, symbol: str) -> bool:
|
|
157
|
+
"""Return True if there is at least one open order for the symbol, False otherwise"""
|
|
158
|
+
|
|
159
|
+
for order in self.orders:
|
|
160
|
+
if order.symbol == symbol and order.is_open:
|
|
161
|
+
return True
|
|
162
|
+
return False
|
|
163
|
+
|
|
164
|
+
def get_open_orders(self, symbol: str) -> list[Order]:
|
|
165
|
+
"""Return a list of open orders for the provided symbol"""
|
|
166
|
+
return [order for order in self.orders if order.is_open and order.symbol == symbol]
|
|
167
|
+
|
|
168
|
+
def get_position_size(self, symbol: str) -> Decimal:
|
|
169
|
+
"""Return the position size for a symbol"""
|
|
170
|
+
pos = self.positions.get(symbol)
|
|
171
|
+
return pos.size if pos else Decimal(0)
|
|
172
|
+
|
|
173
|
+
def open_orders(self):
|
|
174
|
+
"""Return a list with the open orders"""
|
|
175
|
+
return [order for order in self.orders if order.is_open]
|
|
176
|
+
|
|
177
|
+
def __repr__(self) -> str:
|
|
178
|
+
p = [f"{v.size}@{k}" for k, v in self.positions.items()]
|
|
179
|
+
p_str = ", ".join(p) or "none"
|
|
180
|
+
|
|
181
|
+
o = [f"{o.size}@{o.symbol}" for o in self.open_orders()]
|
|
182
|
+
o_str = ", ".join(o) or "none"
|
|
183
|
+
|
|
184
|
+
result = (
|
|
185
|
+
f"""buying power : {self.buying_power:_.2f}\n"""
|
|
186
|
+
f"""cash : {self.cash:_.2f}\n"""
|
|
187
|
+
f"""equity : {self.equity():_.2f}\n"""
|
|
188
|
+
f"""positions : {p_str}\n"""
|
|
189
|
+
f"""mkt value : {self.mkt_value():_.2f}\n"""
|
|
190
|
+
f"""open orders : {o_str}\n"""
|
|
191
|
+
f"""last update : {self.last_update}"""
|
|
192
|
+
)
|
|
193
|
+
return result
|
|
@@ -34,3 +34,15 @@ class Broker(Protocol):
|
|
|
34
34
|
|
|
35
35
|
"""
|
|
36
36
|
...
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _update_positions(account: Account, event: Event | None, price_type: str = "DEFAULT"):
|
|
40
|
+
"""update the open positions in the account with the latest market prices"""
|
|
41
|
+
if not event:
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
account.last_update = event.time
|
|
45
|
+
|
|
46
|
+
for symbol, position in account.positions.items():
|
|
47
|
+
if price := event.get_price(symbol, price_type):
|
|
48
|
+
position.mkt_price = price
|
|
@@ -14,7 +14,7 @@ from ibapi.wrapper import EWrapper
|
|
|
14
14
|
from roboquant.account import Account, Position
|
|
15
15
|
from roboquant.event import Event
|
|
16
16
|
from roboquant.order import Order, OrderStatus
|
|
17
|
-
from .broker import Broker
|
|
17
|
+
from .broker import Broker, _update_positions
|
|
18
18
|
|
|
19
19
|
assert VERSION["major"] == 10 and VERSION["minor"] == 19, "Wrong version of the IBAPI found"
|
|
20
20
|
|
|
@@ -28,7 +28,7 @@ class _IBApi(EWrapper, EClient):
|
|
|
28
28
|
EClient.__init__(self, self)
|
|
29
29
|
self.orders: dict[str, Order] = {}
|
|
30
30
|
self.positions: dict[str, Position] = {}
|
|
31
|
-
self.
|
|
31
|
+
self.__account = {AccountSummaryTags.TotalCashValue: 0.0, AccountSummaryTags.BuyingPower: 0.0}
|
|
32
32
|
self.__account_end = threading.Condition()
|
|
33
33
|
self.__order_id = 0
|
|
34
34
|
|
|
@@ -44,11 +44,13 @@ class _IBApi(EWrapper, EClient):
|
|
|
44
44
|
def position(self, account: str, contract: Contract, position: Decimal, avgCost: float):
|
|
45
45
|
logger.debug("position=%s symbol=%s avgCost=%s", position, contract.localSymbol, avgCost)
|
|
46
46
|
symbol = contract.localSymbol or contract.symbol
|
|
47
|
-
self.positions
|
|
47
|
+
old_position = self.positions.get(symbol)
|
|
48
|
+
mkt_price = old_position.mkt_price if old_position else avgCost
|
|
49
|
+
self.positions[symbol] = Position(position, avgCost, mkt_price)
|
|
48
50
|
|
|
49
51
|
def accountSummary(self, reqId: int, account: str, tag: str, value: str, currency: str):
|
|
50
52
|
logger.debug("account %s=%s", tag, value)
|
|
51
|
-
self.
|
|
53
|
+
self.__account[tag] = float(value)
|
|
52
54
|
|
|
53
55
|
def accountSummaryEnd(self, reqId: int):
|
|
54
56
|
with self.__account_end:
|
|
@@ -71,32 +73,32 @@ class _IBApi(EWrapper, EClient):
|
|
|
71
73
|
def request_account(self):
|
|
72
74
|
"""blocking call till account summary has been received"""
|
|
73
75
|
buyingpower_tag = AccountSummaryTags.BuyingPower
|
|
74
|
-
|
|
76
|
+
cash_tag = AccountSummaryTags.TotalCashValue
|
|
75
77
|
with self.__account_end:
|
|
76
|
-
super().reqAccountSummary(1, "All", f"{buyingpower_tag},{
|
|
78
|
+
super().reqAccountSummary(1, "All", f"{buyingpower_tag},{cash_tag}")
|
|
77
79
|
self.__account_end.wait()
|
|
78
80
|
|
|
79
81
|
def get_buying_power(self):
|
|
80
82
|
buyingpower_tag = AccountSummaryTags.BuyingPower
|
|
81
|
-
return self.
|
|
83
|
+
return self.__account[buyingpower_tag] or 0.0
|
|
82
84
|
|
|
83
|
-
def
|
|
84
|
-
|
|
85
|
-
return self.
|
|
85
|
+
def get_cash(self):
|
|
86
|
+
cash_tag = AccountSummaryTags.TotalCashValue
|
|
87
|
+
return self.__account[cash_tag] or 0.0
|
|
86
88
|
|
|
87
89
|
def orderStatus(
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
90
|
+
self,
|
|
91
|
+
orderId,
|
|
92
|
+
status,
|
|
93
|
+
filled,
|
|
94
|
+
remaining,
|
|
95
|
+
avgFillPrice,
|
|
96
|
+
permId,
|
|
97
|
+
parentId,
|
|
98
|
+
lastFillPrice,
|
|
99
|
+
clientId,
|
|
100
|
+
whyHeld,
|
|
101
|
+
mktCapPrice,
|
|
100
102
|
):
|
|
101
103
|
logger.debug("order status orderId=%s status=%s fill=%s", orderId, status, filled)
|
|
102
104
|
orderId = str(orderId)
|
|
@@ -122,31 +124,51 @@ class IBKRBroker(Broker):
|
|
|
122
124
|
Map symbols to IBKR contracts.
|
|
123
125
|
If a symbol is not found, the symbol is assumed to represent a US stock
|
|
124
126
|
|
|
127
|
+
host
|
|
128
|
+
the ip number of the host where TWS or IB Gateway is running.
|
|
129
|
+
|
|
130
|
+
port
|
|
131
|
+
By default, TWS uses socket port 7496 for live sessions and 7497 for paper sessions.
|
|
132
|
+
IB Gateway by contrast uses 4001 for live sessions and 4002 for paper sessions.
|
|
133
|
+
However these are just defaults, and can be modified as desired.
|
|
134
|
+
|
|
135
|
+
client_id
|
|
136
|
+
The client id to use to connect to TWS or IB Gateway.
|
|
125
137
|
"""
|
|
126
138
|
|
|
127
|
-
def __init__(self, host="127.0.0.1", port=4002,
|
|
128
|
-
self.__account =
|
|
139
|
+
def __init__(self, host="127.0.0.1", port=4002, client_id=123) -> None:
|
|
140
|
+
self.__account = Account()
|
|
129
141
|
self.contract_mapping: dict[str, Contract] = {}
|
|
130
142
|
api = _IBApi()
|
|
131
143
|
api.connect(host, port, client_id)
|
|
132
144
|
self.__api = api
|
|
133
145
|
self.__has_new_orders_since_sync = False
|
|
146
|
+
self.price_type = "DEFAULT"
|
|
134
147
|
|
|
135
148
|
# Start the handling in a thread
|
|
136
149
|
self.__api_thread = threading.Thread(target=api.run, daemon=True)
|
|
137
150
|
self.__api_thread.start()
|
|
138
151
|
time.sleep(3.0)
|
|
139
152
|
|
|
153
|
+
@classmethod
|
|
154
|
+
def use_tws(cls, client_id=123):
|
|
155
|
+
"""Return a broker connected to the TWS papertrade instance with its default port (7497) settings"""
|
|
156
|
+
return cls("127.0.0.1", 7497, client_id)
|
|
157
|
+
|
|
158
|
+
@classmethod
|
|
159
|
+
def use_ibgateway(cls, client_id=123):
|
|
160
|
+
"""Return a broker connected to a IB Gateway papertrade instance with its default port (4002) settings"""
|
|
161
|
+
return cls("127.0.0.1", 4002, client_id)
|
|
162
|
+
|
|
140
163
|
def disconnect(self):
|
|
141
|
-
self.__api.reader.conn.disconnect()
|
|
164
|
+
self.__api.reader.conn.disconnect() # type: ignore
|
|
142
165
|
|
|
143
166
|
def _should_sync(self, now: datetime):
|
|
144
167
|
"""Avoid too many API calls"""
|
|
145
168
|
return self.__has_new_orders_since_sync or now - self.__account.last_update > timedelta(seconds=1)
|
|
146
169
|
|
|
147
170
|
def sync(self, event: Event | None = None) -> Account:
|
|
148
|
-
"""Sync with the IBKR account
|
|
149
|
-
"""
|
|
171
|
+
"""Sync with the IBKR account"""
|
|
150
172
|
|
|
151
173
|
logger.debug("start sync")
|
|
152
174
|
now = datetime.now(timezone.utc)
|
|
@@ -155,7 +177,7 @@ class IBKRBroker(Broker):
|
|
|
155
177
|
# Let make sure we don't use IBKRBroker by mistake during a back-test.
|
|
156
178
|
if now - event.time > timedelta(minutes=30):
|
|
157
179
|
logger.critical("received event from the past, now=%s event-time=%s", now, event.time)
|
|
158
|
-
raise ValueError(f"received event
|
|
180
|
+
raise ValueError(f"received event too far in the past now={now} event-time={event.time}")
|
|
159
181
|
|
|
160
182
|
api = self.__api
|
|
161
183
|
acc = self.__account
|
|
@@ -170,8 +192,9 @@ class IBKRBroker(Broker):
|
|
|
170
192
|
acc.positions = {k: v for k, v in api.positions.items() if not v.size.is_zero()}
|
|
171
193
|
acc.orders = list(api.orders.values())
|
|
172
194
|
acc.buying_power = api.get_buying_power()
|
|
173
|
-
acc.
|
|
195
|
+
acc.cash = api.get_cash()
|
|
174
196
|
|
|
197
|
+
_update_positions(acc, event)
|
|
175
198
|
logger.debug("end sync")
|
|
176
199
|
return acc
|
|
177
200
|
|
|
@@ -184,41 +207,64 @@ class IBKRBroker(Broker):
|
|
|
184
207
|
# avoid to many API calls
|
|
185
208
|
time.sleep(1)
|
|
186
209
|
|
|
187
|
-
assert
|
|
210
|
+
assert order.is_open, "can only place open orders"
|
|
188
211
|
if order.size.is_zero():
|
|
189
|
-
assert order.id is not None
|
|
212
|
+
assert order.id is not None, "can only cancel orders with an id"
|
|
190
213
|
self.__api.cancelOrder(int(order.id), "")
|
|
191
214
|
else:
|
|
192
215
|
if order.id is None:
|
|
193
216
|
order.id = self.__api.get_next_order_id()
|
|
194
217
|
self.__api.orders[order.id] = order
|
|
195
|
-
ibkr_order = self.
|
|
196
|
-
contract = self.
|
|
218
|
+
ibkr_order = self._get_order(order)
|
|
219
|
+
contract = self._get_contract(order)
|
|
197
220
|
self.__api.placeOrder(int(order.id), contract, ibkr_order)
|
|
198
221
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
222
|
+
@staticmethod
|
|
223
|
+
def __update_ibkr_object(obj, update):
|
|
224
|
+
if not update:
|
|
225
|
+
return
|
|
226
|
+
assert isinstance(update, dict)
|
|
227
|
+
for name, value in update.items():
|
|
228
|
+
if hasattr(obj, name):
|
|
229
|
+
setattr(obj, name, value)
|
|
230
|
+
else:
|
|
231
|
+
logger.warning("unknown field name=%s value=%s", name, value)
|
|
232
|
+
|
|
233
|
+
def _get_contract(self, order: Order) -> Contract:
|
|
234
|
+
"""Map an order to a IBKR contract."""
|
|
202
235
|
|
|
203
|
-
|
|
204
|
-
|
|
236
|
+
c = self.contract_mapping.get(order.symbol)
|
|
237
|
+
|
|
238
|
+
if not c:
|
|
239
|
+
c = Contract()
|
|
240
|
+
c.symbol = order.symbol
|
|
241
|
+
c.secType = "STK"
|
|
242
|
+
c.currency = "USD"
|
|
243
|
+
c.exchange = "SMART" # use smart routing by default
|
|
244
|
+
|
|
245
|
+
# Override attributes
|
|
246
|
+
IBKRBroker.__update_ibkr_object(c, order.info.get("contract"))
|
|
205
247
|
|
|
206
|
-
c = Contract()
|
|
207
|
-
c.symbol = symbol
|
|
208
|
-
c.secType = "STK"
|
|
209
|
-
c.currency = "USD"
|
|
210
|
-
c.exchange = "SMART" # use smart routing by default
|
|
211
248
|
return c
|
|
212
249
|
|
|
213
|
-
|
|
214
|
-
|
|
250
|
+
def _get_order(self, order: Order) -> IBKROrder:
|
|
251
|
+
"""Map an order to a IBKR order."""
|
|
215
252
|
o = IBKROrder()
|
|
216
253
|
o.action = "BUY" if order.is_buy else "SELL"
|
|
217
254
|
o.totalQuantity = abs(order.size)
|
|
218
|
-
|
|
255
|
+
if order.gtd:
|
|
256
|
+
o.tif = "GTD"
|
|
257
|
+
o.goodTillDate = order.gtd.strftime("%Y%m%d %H:%M:%S %Z")
|
|
258
|
+
else:
|
|
259
|
+
o.tif = "GTC"
|
|
260
|
+
|
|
219
261
|
if order.limit:
|
|
220
262
|
o.orderType = "LMT"
|
|
221
263
|
o.lmtPrice = order.limit
|
|
222
264
|
else:
|
|
223
265
|
o.orderType = "MKT"
|
|
266
|
+
|
|
267
|
+
# Override attributes
|
|
268
|
+
IBKRBroker.__update_ibkr_object(o, order.info.get("order"))
|
|
269
|
+
|
|
224
270
|
return o
|