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.
Files changed (61) hide show
  1. {roboquant-0.2.3/roboquant.egg-info → roboquant-0.2.6}/PKG-INFO +14 -6
  2. {roboquant-0.2.3 → roboquant-0.2.6}/README.md +11 -5
  3. {roboquant-0.2.3 → roboquant-0.2.6}/pyproject.toml +4 -3
  4. {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/__init__.py +2 -2
  5. roboquant-0.2.6/roboquant/account.py +193 -0
  6. {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/brokers/broker.py +12 -0
  7. roboquant-0.2.3/roboquant/brokers/ibkrbroker.py → roboquant-0.2.6/roboquant/brokers/ibkr.py +92 -46
  8. {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/brokers/simbroker.py +51 -49
  9. {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/event.py +14 -5
  10. {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/feeds/__init__.py +3 -3
  11. roboquant-0.2.6/roboquant/feeds/alpacafeed.py +52 -0
  12. roboquant-0.2.6/roboquant/feeds/candlefeed.py +88 -0
  13. {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/feeds/csvfeed.py +34 -37
  14. roboquant-0.2.6/roboquant/feeds/feed.py +90 -0
  15. roboquant-0.2.3/roboquant/feeds/historicfeed.py → roboquant-0.2.6/roboquant/feeds/historic.py +10 -4
  16. {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/feeds/randomwalk.py +1 -1
  17. roboquant-0.2.3/roboquant/feeds/tiingofeed.py → roboquant-0.2.6/roboquant/feeds/tiingo.py +1 -1
  18. roboquant-0.2.3/roboquant/feeds/yahoofeed.py → roboquant-0.2.6/roboquant/feeds/yahoo.py +6 -7
  19. {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/journals/alphabeta.py +1 -1
  20. {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/journals/basicjournal.py +6 -11
  21. {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/journals/journal.py +4 -1
  22. {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/journals/pnlmetric.py +4 -6
  23. {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/journals/tensorboardjournal.py +3 -4
  24. {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/order.py +18 -8
  25. {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/run.py +1 -1
  26. {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/signal.py +7 -0
  27. {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/strategies/emacrossover.py +7 -7
  28. {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/strategies/features.py +116 -3
  29. {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/strategies/nopstrategy.py +1 -1
  30. {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/strategies/smacrossover.py +2 -1
  31. {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/strategies/torch.py +7 -5
  32. {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/timeframe.py +6 -4
  33. {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/traders/flextrader.py +1 -1
  34. {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/traders/trader.py +1 -1
  35. {roboquant-0.2.3 → roboquant-0.2.6/roboquant.egg-info}/PKG-INFO +14 -6
  36. {roboquant-0.2.3 → roboquant-0.2.6}/roboquant.egg-info/SOURCES.txt +5 -4
  37. {roboquant-0.2.3 → roboquant-0.2.6}/roboquant.egg-info/requires.txt +3 -0
  38. roboquant-0.2.3/roboquant/account.py +0 -150
  39. roboquant-0.2.3/roboquant/feeds/candlefeed.py +0 -65
  40. roboquant-0.2.3/roboquant/feeds/feed.py +0 -51
  41. {roboquant-0.2.3 → roboquant-0.2.6}/LICENSE +0 -0
  42. {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/brokers/__init__.py +0 -0
  43. {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/config.py +0 -0
  44. {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/feeds/eventchannel.py +0 -0
  45. {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/feeds/feedutil.py +0 -0
  46. {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/feeds/sqllitefeed.py +0 -0
  47. {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/journals/__init__.py +0 -0
  48. {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/journals/feedmetric.py +0 -0
  49. {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/journals/metric.py +0 -0
  50. {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/journals/metricsjournal.py +0 -0
  51. {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/journals/pricemetric.py +0 -0
  52. {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/journals/runmetric.py +0 -0
  53. {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/strategies/__init__.py +0 -0
  54. {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/strategies/buffer.py +0 -0
  55. {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/strategies/candlestrategy.py +0 -0
  56. {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/strategies/multistrategy.py +0 -0
  57. {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/strategies/strategy.py +0 -0
  58. {roboquant-0.2.3 → roboquant-0.2.6}/roboquant/traders/__init__.py +0 -0
  59. {roboquant-0.2.3 → roboquant-0.2.6}/roboquant.egg-info/dependency_links.txt +0 -0
  60. {roboquant-0.2.3 → roboquant-0.2.6}/roboquant.egg-info/top_level.txt +0 -0
  61. {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
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 [![Binder](https://mybinder.org/badge_logo.svg)](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
- ## Kotlin version
117
- Next to this Python version of `roboquant`, there is also a Kotlin version available.
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
- Which one to use depends very much on personal preferences, skills and usage.
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 [![Binder](https://mybinder.org/badge_logo.svg)](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
- ## Kotlin version
80
- Next to this Python version of `roboquant`, there is also a Kotlin version available.
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
- Which one to use depends very much on personal preferences, skills and usage.
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.3"
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, OptionAccount, Position
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.account = {"EquityWithLoanValue": 0.0, "AvailableFunds": 0.0}
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[symbol] = Position(position, avgCost)
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.account[tag] = float(value)
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
- equity_tag = AccountSummaryTags.NetLiquidation
76
+ cash_tag = AccountSummaryTags.TotalCashValue
75
77
  with self.__account_end:
76
- super().reqAccountSummary(1, "All", f"{buyingpower_tag},{equity_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.account[buyingpower_tag] or 0.0
83
+ return self.__account[buyingpower_tag] or 0.0
82
84
 
83
- def get_equity(self):
84
- equity_tag = AccountSummaryTags.NetLiquidation
85
- return self.account[equity_tag] or 0.0
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
- self,
89
- orderId,
90
- status,
91
- filled,
92
- remaining,
93
- avgFillPrice,
94
- permId,
95
- parentId,
96
- lastFillPrice,
97
- clientId,
98
- whyHeld,
99
- mktCapPrice,
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, account=None, client_id=123) -> None:
128
- self.__account = account or 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 to far in the past now={now} event-time={event.time}")
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.equity = api.get_equity()
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 not order.closed, "cannot place a closed order"
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.__get_order(order)
196
- contract = self.contract_mapping.get(order.symbol) or self._get_default_contract(order.symbol)
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
- def _get_default_contract(self, symbol: str) -> Contract:
200
- """If no contract can be found in the `contract_mapping` dict, this method is called to get a default IBKR contract
201
- for the provided symbol.
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
- The default implementation assumes it is a US stock symbol with SMART exchange routing.
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
- @staticmethod
214
- def __get_order(order: Order):
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
- o.tif = "GTC"
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