diff --git a/.coveragerc b/.coveragerc index 741d29654985..417526041ad8 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,7 +1,7 @@ [run] plugins = Cython.Coverage source = nautilus_trader -omit = nautilus_trader/examples/* +omit = nautilus_trader/adapters/binance*,nautilus_trader/adapters/ftx*,nautilus_trader/adapters/ib*,nautilus_trader/examples* [report] fail_under = 0 diff --git a/.gitignore b/.gitignore index b10029fe435e..d1adf780aca0 100644 --- a/.gitignore +++ b/.gitignore @@ -42,4 +42,5 @@ PERF.JSON *dask-worker-space* output.json .ipynb_checkpoints -examples/backtest/notebooks/catalog \ No newline at end of file +examples/backtest/notebooks/catalog +nautilus_trader/**/.gitignore \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fcc13ed52d25..951579e3a579 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,14 +31,14 @@ repos: exclude: "nautilus_trader/adapters/betfair/parsing.py|nautilus_trader/adapters/betfair/execution.py|tests/integration_tests/adapters/betfair/test_kit.py" - repo: https://github.com/hadialqattan/pycln - rev: v1.0.3 + rev: v1.1.0 hooks: - id: pycln name: pycln (Python unused imports) exclude: "nautilus_trader/live/node.py|nautilus_trader/adapters/betfair/execution.py" - repo: https://github.com/psf/black - rev: 21.10b0 + rev: 21.11b0 hooks: - id: black args: [ @@ -46,7 +46,7 @@ repos: ] - repo: https://github.com/pycqa/isort - rev: 5.10.0 + rev: 5.10.1 hooks: - id: isort args: [ diff --git a/README.md b/README.md index dfbcf984dbab..a8f8ed87fbfc 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ including FX, Equities, Futures, Options, CFDs, Crypto and Betting - across mult - **Reliable:** Type safety through Cython. Redis backed performant state persistence. - **Flexible:** OS independent, runs on Linux, macOS, Windows. Deploy using Docker. - **Integrated:** Modular adapters mean any REST/FIX/WebSocket API can be integrated. -- **Advanced:** Time-in-force options `GTD`, `IOC`, `FOK` etc, advanced order types and triggers, `post-only`, `reduce-only`, `hidden`. Contingency order lists including `OCO`, `OTO` etc. +- **Advanced:** Time-in-force options `GTD`, `IOC`, `FOK` etc, advanced order types and triggers, `post-only`, `reduce-only`, and icebergs. Contingency order lists including `OCO`, `OTO` etc. - **Backtesting:** Run with multiple venues, instruments and strategies simultaneously using historical quote tick, trade tick, bar, order book and custom data with nanosecond resolution. - **Multi-venue:** Multiple venue capabilities facilitate market making and statistical arbitrage strategies. - **AI Agent Training:** Backtest engine fast enough to be used to train AI trading agents (RL/ES). diff --git a/RELEASES.md b/RELEASES.md index 682a64bb4015..78cdda221b71 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,3 +1,24 @@ +# NautilusTrader 1.134.0 Beta - Release Notes + +Released on 22nd, November 2021 + +## Breaking Changes +- Changed `hidden` order option to `display_qty` to support iceberg orders. +- Renamed `Trader.component_ids()` to `Trader.actor_ids()`. +- Renamed `Trader.component_states()` to `Trader.actor_states()`. +- Renamed `Trader.add_component()` to `Trader.add_actor()`. +- Renamed `Trader.add_components()` to `Trader.add_actors()`. +- Renamed `Trader.clear_components()` to `Trader.clear_actors()`. + +## Enhancements +- Added initial implementation of Binance SPOT integration (beta stage testing). +- Added support for display quantity/iceberg orders. + +## Fixes +- Fixed `Actor` clock time advancement in backtest engine. + +--- + # NautilusTrader 1.133.0 Beta - Release Notes Released on 8th, November 2021 diff --git a/docs/artwork/nautech-systems-logo.png b/docs/artwork/nautech-systems-logo.png deleted file mode 100644 index bea5a4de1005..000000000000 Binary files a/docs/artwork/nautech-systems-logo.png and /dev/null differ diff --git a/docs/artwork/ns-logo.png b/docs/artwork/ns-logo.png index 182d4bcd4712..7f1d096a372a 100644 Binary files a/docs/artwork/ns-logo.png and b/docs/artwork/ns-logo.png differ diff --git a/examples/live/binance_example.py b/examples/live/binance_example_ema_cross.py similarity index 95% rename from examples/live/binance_example.py rename to examples/live/binance_example_ema_cross.py index 15c01a10a3be..2e058bfd3b4a 100644 --- a/examples/live/binance_example.py +++ b/examples/live/binance_example_ema_cross.py @@ -31,8 +31,8 @@ # Configure the trading node config_node = TradingNodeConfig( trader_id="TESTER-001", - log_level="DEBUG", - cache_database=None, + log_level="INFO", + # cache_database=CacheDatabaseConfig(), data_clients={ "BINANCE": { # "api_key": "YOUR_BINANCE_API_KEY", @@ -61,10 +61,10 @@ # Configure your strategy strat_config = EMACrossConfig( instrument_id="ETHUSDT.BINANCE", - bar_type="ETHUSDT.BINANCE-100-TICK-LAST-INTERNAL", + bar_type="ETHUSDT.BINANCE-1-MINUTE-LAST-EXTERNAL", fast_ema_period=10, slow_ema_period=20, - trade_size=Decimal("0.01"), + trade_size=Decimal("0.005"), order_id_tag="001", ) # Instantiate your strategy diff --git a/examples/live/binance_example_market_maker.py b/examples/live/binance_example_market_maker.py new file mode 100644 index 000000000000..ab9dbe62279f --- /dev/null +++ b/examples/live/binance_example_market_maker.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2021 Nautech Systems Pty Ltd. All rights reserved. +# https://nautechsystems.io +# +# Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ------------------------------------------------------------------------------------------------- + +from decimal import Decimal + +from nautilus_trader.adapters.binance.factories import BinanceLiveDataClientFactory +from nautilus_trader.adapters.binance.factories import BinanceLiveExecutionClientFactory +from nautilus_trader.examples.strategies.volatility_market_maker import VolatilityMarketMaker +from nautilus_trader.examples.strategies.volatility_market_maker import VolatilityMarketMakerConfig +from nautilus_trader.live.node import TradingNode +from nautilus_trader.live.node import TradingNodeConfig + + +# *** THIS IS A TEST STRATEGY WITH NO ALPHA ADVANTAGE WHATSOEVER. *** +# *** IT IS NOT INTENDED TO BE USED TO TRADE LIVE WITH REAL MONEY. *** + +# Configure the trading node +config_node = TradingNodeConfig( + trader_id="TESTER-001", + log_level="INFO", + # cache_database=CacheDatabaseConfig(), + data_clients={ + "BINANCE": { + # "api_key": "YOUR_BINANCE_API_KEY", + # "api_secret": "YOUR_BINANCE_API_SECRET", + # "account_id": "YOUR_BINANCE_ACCOUNT_ID", (optional) + "sandbox_mode": False, # If client uses the testnet + }, + }, + exec_clients={ + "BINANCE": { + # "api_key": "YOUR_BINANCE_API_KEY", + # "api_secret": "YOUR_BINANCE_API_SECRET", + # "account_id": "YOUR_BINANCE_ACCOUNT_ID", (optional) + "sandbox_mode": False, # If client uses the testnet, + }, + }, + timeout_connection=5.0, + timeout_reconciliation=5.0, + timeout_portfolio=5.0, + timeout_disconnection=5.0, + check_residuals_delay=2.0, +) +# Instantiate the node with a configuration +node = TradingNode(config=config_node) + +# Configure your strategy +strat_config = VolatilityMarketMakerConfig( + instrument_id="ETHUSDT.BINANCE", + bar_type="ETHUSDT.BINANCE-1-MINUTE-LAST-EXTERNAL", + atr_period=20, + atr_multiple=6.0, + trade_size=Decimal("0.01"), +) +# Instantiate your strategy +strategy = VolatilityMarketMaker(config=strat_config) + +# Add your strategies and modules +node.trader.add_strategy(strategy) + +# Register your client factories with the node (can take user defined factories) +node.add_data_client_factory("BINANCE", BinanceLiveDataClientFactory) +node.add_exec_client_factory("BINANCE", BinanceLiveExecutionClientFactory) +node.build() + + +# Stop and dispose of the node with SIGINT/CTRL+C +if __name__ == "__main__": + try: + node.start() + finally: + node.dispose() diff --git a/nautilus_trader/accounting/accounts/base.pxd b/nautilus_trader/accounting/accounts/base.pxd index 4972b489999d..25a639ac8769 100644 --- a/nautilus_trader/accounting/accounts/base.pxd +++ b/nautilus_trader/accounting/accounts/base.pxd @@ -67,7 +67,7 @@ cdef class Account: # -- COMMANDS -------------------------------------------------------------------------------------- cpdef void apply(self, AccountState event) except * - cpdef void update_balances(self, list balances) except * + cpdef void update_balances(self, list balances, bint allow_zero=*) except * cpdef void update_commissions(self, Money commission) except * # -- CALCULATIONS ---------------------------------------------------------------------------------- diff --git a/nautilus_trader/accounting/accounts/base.pyx b/nautilus_trader/accounting/accounts/base.pyx index 8ae6d27bafe5..2230b024b77f 100644 --- a/nautilus_trader/accounting/accounts/base.pyx +++ b/nautilus_trader/accounting/accounts/base.pyx @@ -381,7 +381,7 @@ cdef class Account: self._events.append(event) self.update_balances(event.balances) - cpdef void update_balances(self, list balances) except *: + cpdef void update_balances(self, list balances, bint allow_zero=True) except *: """ Update the account balances. @@ -391,6 +391,9 @@ cdef class Account: Parameters ---------- balances : list[AccountBalance] + The balances for the update. + allow_zero : bool, default True + If zero balances are allowed (will then just clear the assets balance). Raises ------ @@ -402,8 +405,19 @@ cdef class Account: cdef AccountBalance balance for balance in balances: - if balance.total.as_decimal() <= 0: - raise RuntimeError("account blow up (balance zero).") + total: Decimal = balance.total.as_decimal() + if total <= 0: + if total < 0: + raise RuntimeError( + f"account blow up (balance was {balance.total}).", + ) + if total == 0 and not allow_zero: + raise RuntimeError( + f"account blow up (balance was {balance.total}).", + ) + else: + # Clear asset balance + self._balances.pop(balance.currency, None) self._balances[balance.currency] = balance cpdef void update_commissions(self, Money commission) except *: diff --git a/nautilus_trader/adapters/binance/data.py b/nautilus_trader/adapters/binance/data.py index 8b37bd935ae9..8d5a8a4d418d 100644 --- a/nautilus_trader/adapters/binance/data.py +++ b/nautilus_trader/adapters/binance/data.py @@ -14,13 +14,14 @@ # ------------------------------------------------------------------------------------------------- import asyncio -from typing import Dict, List, Optional +from typing import Any, Dict, List, Optional import orjson import pandas as pd from nautilus_trader.adapters.binance.common import BINANCE_VENUE from nautilus_trader.adapters.binance.data_types import BinanceBar +from nautilus_trader.adapters.binance.data_types import BinanceTicker from nautilus_trader.adapters.binance.http.api.spot_market import BinanceSpotMarketHttpAPI from nautilus_trader.adapters.binance.http.client import BinanceHttpClient from nautilus_trader.adapters.binance.http.error import BinanceError @@ -43,6 +44,7 @@ from nautilus_trader.live.data_client import LiveMarketDataClient from nautilus_trader.model.c_enums.bar_aggregation import BarAggregationParser from nautilus_trader.model.data.bar import BarType +from nautilus_trader.model.data.tick import QuoteTick from nautilus_trader.model.data.tick import TradeTick from nautilus_trader.model.enums import BarAggregation from nautilus_trader.model.enums import BookType @@ -51,6 +53,7 @@ from nautilus_trader.model.identifiers import InstrumentId from nautilus_trader.model.identifiers import Symbol from nautilus_trader.model.orderbook.data import OrderBookData +from nautilus_trader.model.orderbook.data import OrderBookDeltas from nautilus_trader.model.orderbook.data import OrderBookSnapshot from nautilus_trader.msgbus.bus import MessageBus @@ -106,7 +109,11 @@ def __init__( self._update_instrument_interval: int = 60 * 60 # Once per hour (hardcode) self._update_instruments_task: Optional[asyncio.Task] = None + + # HTTP API self._spot = BinanceSpotMarketHttpAPI(client=self._client) + + # WebSocket API self._ws_spot = BinanceSpotWebSocket( loop=loop, clock=clock, @@ -131,17 +138,19 @@ def disconnect(self): self._loop.create_task(self._disconnect()) async def _connect(self): + # Connect HTTP client if not self._client.connected: await self._client.connect() try: await self._instrument_provider.load_all_or_wait_async() except BinanceError as ex: - self._log.ex(ex) + self._log.exception(ex) return self._send_all_instruments_to_data_engine() - # self._schedule_subscribed_instruments_update(self._update_instrument_interval) + self._update_instruments_task = self._loop.create_task(self._update_instruments()) + # Connect WebSocket clients self._loop.create_task(self._connect_websockets()) self._set_connected(True) @@ -153,15 +162,27 @@ async def _connect_websockets(self): if self._ws_spot.has_subscriptions: await self._ws_spot.connect() + async def _update_instruments(self): + while True: + self._log.debug( + f"Scheduled `update_instruments` to run in " + f"{self._update_instruments_interval}s." + ) + await asyncio.sleep(self._update_instruments_interval) + await self._instrument_provider.load_all_async() + self._send_all_instruments_to_data_engine() + async def _disconnect(self): + # Cancel tasks if self._update_instruments_task: - self._log.debug("Canceling update instruments task...") + self._log.debug("Canceling `update_instruments` task...") self._update_instruments_task.cancel() + # Disconnect WebSocket clients if self._ws_spot.is_connected: - self._log.debug("Disconnecting websockets...") await self._ws_spot.disconnect() + # Disconnect HTTP client if self._client.connected: await self._client.disconnect() @@ -266,8 +287,10 @@ async def _subscribe_order_book( while not self._ws_spot.is_connected: await self.sleep0() - raw: bytes = await self._spot.depth(instrument_id.symbol.value, limit=depth) - data: Dict = orjson.loads(raw) + data: Dict[str, Any] = await self._spot.depth( + symbol=instrument_id.symbol.value, + limit=depth, + ) ts_event: int = self._clock.timestamp_ns() last_update_id: int = data.get("lastUpdateId") @@ -330,7 +353,8 @@ def subscribe_bars(self, bar_type: BarType): ) self._ws_spot.subscribe_bars( - symbol=bar_type.instrument_id.symbol.value, interval=f"{bar_type.spec.step}{resolution}" + symbol=bar_type.instrument_id.symbol.value, + interval=f"{bar_type.spec.step}{resolution}", ) self._add_subscription_bars(bar_type) @@ -429,8 +453,7 @@ async def _request_trade_ticks( limit: int, correlation_id: UUID4, ): - data: bytes = await self._spot.trades(instrument_id.symbol.value, limit) - response: List = orjson.loads(data) + response: List[Dict[str, Any]] = await self._spot.trades(instrument_id.symbol.value, limit) ticks: List[TradeTick] = [ parse_trade_tick( @@ -513,7 +536,7 @@ async def _request_bars( start_time_ms = from_datetime.to_datetime64() * 1000 if from_datetime is not None else None end_time_ms = to_datetime.to_datetime64() * 1000 if to_datetime is not None else None - data: bytes = await self._spot.klines( + data: List[List[Any]] = await self._spot.klines( symbol=bar_type.instrument_id.symbol.value, interval=f"{bar_type.spec.step}{resolution}", start_time_ms=start_time_ms, @@ -521,23 +544,13 @@ async def _request_bars( limit=limit, ) - response: List = orjson.loads(data) - bars: List[BinanceBar] = [ - parse_bar(bar_type, values=b, ts_init=self._clock.timestamp_ns()) for b in response + parse_bar(bar_type, values=b, ts_init=self._clock.timestamp_ns()) for b in data ] partial: BinanceBar = bars.pop() self._handle_bars(bar_type, bars, partial, correlation_id) - async def _subscribed_instruments_update(self, delay): - await self._instrument_provider.load_all_async() - - self._send_all_instruments_to_data_engine() - - update = self.run_after_delay(delay, self._subscribed_instruments_update(delay)) - self._update_instruments_task = self._loop.create_task(update, name="update_instruments") - def _send_all_instruments_to_data_engine(self): for instrument in self._instrument_provider.get_all().values(): self._handle_data(instrument) @@ -545,92 +558,122 @@ def _send_all_instruments_to_data_engine(self): for currency in self._instrument_provider.currencies().values(): self._cache.add_currency(currency) - def _schedule_subscribed_instruments_update(self, delay: int): - update = self.run_after_delay(delay, self._subscribed_instruments_update(delay)) - self._update_instruments_task = self._loop.create_task(update) - def _handle_spot_ws_message(self, raw: bytes): - msg: Dict = orjson.loads(raw) - msg_data = msg.get("data") + msg: Dict[str, Any] = orjson.loads(raw) + data: Dict[str, Any] = msg.get("data") - msg_type: str = msg_data.get("e") + msg_type: str = data.get("e") if msg_type is None: - last_update_id = msg_data.get("lastUpdateId") - if last_update_id is not None: - instrument_id = InstrumentId( - symbol=Symbol(msg["stream"].partition("@")[0].upper()), - venue=BINANCE_VENUE, - ) - data = parse_book_snapshot_ws( - instrument_id=instrument_id, - msg=msg_data, - update_id=last_update_id, - ts_init=self._clock.timestamp_ns(), - ) - book_buffer = self._book_buffer.get(instrument_id) - if book_buffer is not None: - book_buffer.append(data) - return - else: - instrument_id = InstrumentId( - symbol=Symbol(msg_data["s"]), - venue=BINANCE_VENUE, - ) - data = parse_quote_tick_ws( - instrument_id=instrument_id, - msg=msg_data, - ts_init=self._clock.timestamp_ns(), - ) + self._handle_market_update(msg, data) elif msg_type == "depthUpdate": - instrument_id = InstrumentId( - symbol=Symbol(msg_data["s"]), - venue=BINANCE_VENUE, - ) - data = parse_diff_depth_stream_ws( - instrument_id=instrument_id, - msg=msg_data, - ts_init=self._clock.timestamp_ns(), - ) - book_buffer = self._book_buffer.get(instrument_id) - if book_buffer is not None: - book_buffer.append(data) - return + self._handle_depth_update(data) elif msg_type == "24hrTicker": - instrument_id = InstrumentId( - symbol=Symbol(msg_data["s"]), - venue=BINANCE_VENUE, - ) - data = parse_ticker_ws( - instrument_id=instrument_id, - msg=msg_data, - ts_init=self._clock.timestamp_ns(), - ) + self._handle_24hr_ticker(data) elif msg_type == "trade": - instrument_id = InstrumentId( - symbol=Symbol(msg_data["s"]), - venue=BINANCE_VENUE, - ) - data = parse_trade_tick_ws( - instrument_id=instrument_id, - msg=msg_data, - ts_init=self._clock.timestamp_ns(), - ) + self._handle_trade(data) elif msg_type == "kline": - kline = msg_data["k"] - if msg_data["E"] < kline["T"]: - return # Bar has not closed yet - instrument_id = InstrumentId( - symbol=Symbol(kline["s"]), - venue=BINANCE_VENUE, - ) - data = parse_bar_ws( - instrument_id=instrument_id, - kline=kline, - ts_event=millis_to_nanos(msg_data["E"]), - ts_init=self._clock.timestamp_ns(), - ) + self._handle_kline(data) else: self._log.error(f"Unrecognized websocket message type, was {msg_type}") return - self._handle_data(data) + def _handle_market_update(self, msg: Dict[str, Any], data: Dict[str, Any]): + last_update_id: int = data.get("lastUpdateId") + if last_update_id is not None: + self._handle_book_snapshot( + data=data, + last_update_id=last_update_id, + symbol=msg["stream"].partition("@")[0].upper(), + ) + else: + self._handle_quote_tick(data) + + def _handle_book_snapshot( + self, + data: Dict[str, Any], + symbol: str, + last_update_id: int, + ): + instrument_id = InstrumentId( + symbol=Symbol(symbol), + venue=BINANCE_VENUE, + ) + book_snapshot: OrderBookSnapshot = parse_book_snapshot_ws( + instrument_id=instrument_id, + msg=data, + update_id=last_update_id, + ts_init=self._clock.timestamp_ns(), + ) + book_buffer: List[OrderBookData] = self._book_buffer.get(instrument_id) + if book_buffer is not None: + book_buffer.append(book_snapshot) + return + self._handle_data(book_snapshot) + + def _handle_quote_tick(self, data: Dict[str, Any]): + instrument_id = InstrumentId( + symbol=Symbol(data["s"]), + venue=BINANCE_VENUE, + ) + quote_tick: QuoteTick = parse_quote_tick_ws( + instrument_id=instrument_id, + msg=data, + ts_init=self._clock.timestamp_ns(), + ) + self._handle_data(quote_tick) + + def _handle_depth_update(self, data: Dict[str, Any]): + instrument_id = InstrumentId( + symbol=Symbol(data["s"]), + venue=BINANCE_VENUE, + ) + book_deltas: OrderBookDeltas = parse_diff_depth_stream_ws( + instrument_id=instrument_id, + msg=data, + ts_init=self._clock.timestamp_ns(), + ) + book_buffer: List[OrderBookData] = self._book_buffer.get(instrument_id) + if book_buffer is not None: + book_buffer.append(book_deltas) + return + self._handle_data(book_deltas) + + def _handle_24hr_ticker(self, data: Dict[str, Any]): + instrument_id = InstrumentId( + symbol=Symbol(data["s"]), + venue=BINANCE_VENUE, + ) + ticker: BinanceTicker = parse_ticker_ws( + instrument_id=instrument_id, + msg=data, + ts_init=self._clock.timestamp_ns(), + ) + self._handle_data(ticker) + + def _handle_trade(self, data: Dict[str, Any]): + instrument_id = InstrumentId( + symbol=Symbol(data["s"]), + venue=BINANCE_VENUE, + ) + trade_tick: TradeTick = parse_trade_tick_ws( + instrument_id=instrument_id, + msg=data, + ts_init=self._clock.timestamp_ns(), + ) + self._handle_data(trade_tick) + + def _handle_kline(self, data: Dict[str, Any]): + kline = data["k"] + if data["E"] < kline["T"]: + return # Bar has not closed yet + instrument_id = InstrumentId( + symbol=Symbol(kline["s"]), + venue=BINANCE_VENUE, + ) + bar: BinanceBar = parse_bar_ws( + instrument_id=instrument_id, + kline=kline, + ts_event=millis_to_nanos(data["E"]), + ts_init=self._clock.timestamp_ns(), + ) + self._handle_data(bar) diff --git a/nautilus_trader/adapters/binance/execution.py b/nautilus_trader/adapters/binance/execution.py index 3efc214c913c..76f6c0419c9f 100644 --- a/nautilus_trader/adapters/binance/execution.py +++ b/nautilus_trader/adapters/binance/execution.py @@ -15,32 +15,64 @@ import asyncio from datetime import datetime -from typing import List +from decimal import Decimal +from typing import Any, Dict, List, Optional + +import orjson from nautilus_trader.adapters.binance.common import BINANCE_VENUE +from nautilus_trader.adapters.binance.http.api.spot_account import BinanceSpotAccountHttpAPI +from nautilus_trader.adapters.binance.http.api.spot_market import BinanceSpotMarketHttpAPI +from nautilus_trader.adapters.binance.http.api.user import BinanceUserDataHttpAPI from nautilus_trader.adapters.binance.http.client import BinanceHttpClient from nautilus_trader.adapters.binance.http.error import BinanceError +from nautilus_trader.adapters.binance.parsing import binance_order_type +from nautilus_trader.adapters.binance.parsing import parse_account_balances +from nautilus_trader.adapters.binance.parsing import parse_account_balances_ws +from nautilus_trader.adapters.binance.parsing import parse_order_type from nautilus_trader.adapters.binance.providers import BinanceInstrumentProvider +from nautilus_trader.adapters.binance.websocket.user import BinanceUserDataWebSocket from nautilus_trader.cache.cache import Cache from nautilus_trader.common.clock import LiveClock +from nautilus_trader.common.logging import LogColor from nautilus_trader.common.logging import Logger +from nautilus_trader.core.datetime import millis_to_nanos from nautilus_trader.execution.messages import ExecutionReport from nautilus_trader.execution.messages import OrderStatusReport from nautilus_trader.live.execution_client import LiveExecutionClient from nautilus_trader.model.c_enums.account_type import AccountType +from nautilus_trader.model.c_enums.order_side import OrderSideParser +from nautilus_trader.model.c_enums.order_type import OrderType +from nautilus_trader.model.c_enums.time_in_force import TimeInForceParser from nautilus_trader.model.c_enums.venue_type import VenueType from nautilus_trader.model.commands.trading import CancelOrder from nautilus_trader.model.commands.trading import ModifyOrder from nautilus_trader.model.commands.trading import SubmitOrder from nautilus_trader.model.commands.trading import SubmitOrderList +from nautilus_trader.model.enums import LiquiditySide +from nautilus_trader.model.enums import TimeInForce from nautilus_trader.model.identifiers import AccountId from nautilus_trader.model.identifiers import ClientId +from nautilus_trader.model.identifiers import ClientOrderId +from nautilus_trader.model.identifiers import ExecutionId +from nautilus_trader.model.identifiers import InstrumentId +from nautilus_trader.model.identifiers import StrategyId from nautilus_trader.model.identifiers import Symbol from nautilus_trader.model.identifiers import VenueOrderId +from nautilus_trader.model.instruments.base import Instrument +from nautilus_trader.model.objects import Money +from nautilus_trader.model.objects import Price +from nautilus_trader.model.objects import Quantity from nautilus_trader.model.orders.base import Order +from nautilus_trader.model.orders.limit import LimitOrder +from nautilus_trader.model.orders.market import MarketOrder +from nautilus_trader.model.orders.stop_limit import StopLimitOrder from nautilus_trader.msgbus.bus import MessageBus +VALID_TIF = (TimeInForce.GTC, TimeInForce.FOK, TimeInForce.IOC) + + class BinanceSpotExecutionClient(LiveExecutionClient): """ Provides an execution client for Binance SPOT markets. @@ -97,33 +129,106 @@ def __init__( self._client = client - def connect(self): + # Hot caches + self._instrument_ids: Dict[str, InstrumentId] = {} + + # HTTP API + self._account_spot = BinanceSpotAccountHttpAPI(client=self._client) + self._market_spot = BinanceSpotMarketHttpAPI(client=self._client) + self._user = BinanceUserDataHttpAPI(client=self._client) + + # Listen keys + self._ping_listen_keys_interval: int = 60 * 5 # Once every 5 mins (hardcode) + self._ping_listen_keys_task: Optional[asyncio.Task] = None + self._listen_key_spot: Optional[str] = None + self._listen_key_margin: Optional[str] = None + self._listen_key_isolated: Optional[str] = None + + # WebSocket API + self._ws_user_spot = BinanceUserDataWebSocket( + loop=loop, + clock=clock, + logger=logger, + handler=self._handle_user_ws_message, + ) + + def connect(self) -> None: """ Connect the client to Binance. """ self._log.info("Connecting...") self._loop.create_task(self._connect()) - def disconnect(self): + def disconnect(self) -> None: """ Disconnect the client from Binance. """ self._log.info("Disconnecting...") self._loop.create_task(self._disconnect()) - async def _connect(self): + async def _connect(self) -> None: + # Connect HTTP client if not self._client.connected: await self._client.connect() try: await self._instrument_provider.load_all_or_wait_async() except BinanceError as ex: - self._log.ex(ex) + self._log.exception(ex) return + # Authenticate API key and update account(s) + response: Dict[str, Any] = await self._account_spot.account(recv_window=5000) + + self._authenticate_api_key(response=response) + self._update_account_state(response=response) + + # Get listen keys + response = await self._user.create_listen_key_spot() + self._listen_key_spot = response["listenKey"] + self._ping_listen_keys_task = self._loop.create_task(self._ping_listen_keys()) + + # Connect WebSocket clients + self._ws_user_spot.subscribe(key=self._listen_key_spot) + await self._ws_user_spot.connect() + self._set_connected(True) self._log.info("Connected.") - async def _disconnect(self): + def _authenticate_api_key(self, response: Dict[str, Any]) -> None: + if response["canTrade"]: + self._log.info("Binance API key authenticated.", LogColor.GREEN) + self._log.info(f"API key {self._client.api_key} has trading permissions.") + else: + self._log.error("Binance API key does not have trading permissions.") + + def _update_account_state(self, response: Dict[str, Any]) -> None: + self.generate_account_state( + balances=parse_account_balances(raw_balances=response["balances"]), + reported=True, + ts_event=response["updateTime"], + ) + + async def _ping_listen_keys(self): + while True: + self._log.debug( + f"Scheduled `ping_listen_keys` to run in " f"{self._ping_listen_keys_interval}s." + ) + await asyncio.sleep(self._ping_listen_keys_interval) + if self._listen_key_spot: + self._log.debug(f"Pinging WebSocket listen key {self._listen_key_spot}...") + await self._user.ping_listen_key_spot(self._listen_key_spot) + + async def _disconnect(self) -> None: + # Cancel tasks + if self._ping_listen_keys_task: + self._log.debug("Canceling `ping_listen_keys` task...") + self._ping_listen_keys_task.cancel() + + # Disconnect WebSocket clients + if self._ws_user_spot.is_connected: + await self._ws_user_spot.disconnect() + + # Disconnect HTTP client if self._client.connected: await self._client.disconnect() @@ -133,25 +238,133 @@ async def _disconnect(self): # -- COMMAND HANDLERS -------------------------------------------------------------------------- def submit_order(self, command: SubmitOrder) -> None: - self._log.error( # pragma: no cover - "Cannot submit order: not yet implemented.", - ) + if command.order.type == OrderType.STOP_MARKET: + self._log.error( + "Cannot submit order: " + "STOP_MARKET orders not supported by the exchange for SPOT markets. " + "Use any of MARKET, LIMIT, STOP_LIMIT." + ) + return + elif command.order.type == OrderType.STOP_LIMIT: + self._log.warning( + "STOP_LIMIT `post_only` orders not supported by the exchange. " + "This order may become a liquidity TAKER." + ) + if command.order.time_in_force not in VALID_TIF: + self._log.error( + f"Cannot submit order: " + f"{TimeInForceParser.to_str_py(command.order.time_in_force)} " + f"not supported by the exchange. Use any of {VALID_TIF}.", + ) + return + self._loop.create_task(self._submit_order(command)) def submit_order_list(self, command: SubmitOrderList) -> None: - self._log.error( # pragma: no cover - "Cannot submit order list: not yet implemented.", - ) + self._loop.create_task(self._submit_order_list(command)) def modify_order(self, command: ModifyOrder) -> None: self._log.error( # pragma: no cover - "Cannot modify order: not yet implemented.", + "Cannot modify order: Not supported by the exchange.", ) def cancel_order(self, command: CancelOrder) -> None: + self._loop.create_task(self._cancel_order(command)) + + async def _submit_order(self, command: SubmitOrder) -> None: + self._log.debug(f"Submitting {command.order}.") + + # Generate event here to ensure correct ordering of events + self.generate_order_submitted( + strategy_id=command.strategy_id, + instrument_id=command.instrument_id, + client_order_id=command.order.client_order_id, + ts_event=self._clock.timestamp_ns(), + ) + + try: + if command.order.type == OrderType.MARKET: + await self._submit_market_order(command) + elif command.order.type == OrderType.LIMIT: + await self._submit_limit_order(command) + elif command.order.type == OrderType.STOP_LIMIT: + await self._submit_stop_limit_order(command) + except BinanceError as ex: + self.generate_order_rejected( + strategy_id=command.strategy_id, + instrument_id=command.instrument_id, + client_order_id=command.order.client_order_id, + reason=ex.message, # type: ignore # TODO(cs): Improve errors + ts_event=self._clock.timestamp_ns(), # TODO(cs): Parse from response + ) + + async def _submit_market_order(self, command: SubmitOrder): + order: MarketOrder = command.order + await self._account_spot.new_order( + symbol=order.instrument_id.symbol.value, + side=OrderSideParser.to_str_py(order.side), + type="MARKET", + quantity=str(order.quantity), + new_client_order_id=order.client_order_id.value, + recv_window=5000, + ) + + async def _submit_limit_order(self, command: SubmitOrder): + order: LimitOrder = command.order + if order.is_post_only: + time_in_force = None + else: + time_in_force = TimeInForceParser.to_str_py(order.time_in_force) + + await self._account_spot.new_order( + symbol=order.instrument_id.symbol.value, + side=OrderSideParser.to_str_py(order.side), + type=binance_order_type(order=order), + time_in_force=time_in_force, + quantity=str(order.quantity), + price=str(order.price), + iceberg_qty=str(order.display_qty) if order.display_qty is not None else None, + new_client_order_id=order.client_order_id.value, + recv_window=5000, + ) + + async def _submit_stop_limit_order(self, command: SubmitOrder): + order: StopLimitOrder = command.order + + # Get current market price + response: Dict[str, Any] = await self._market_spot.ticker_price( + order.instrument_id.symbol.value + ) + market_price = Decimal(response["price"]) + + await self._account_spot.new_order( + symbol=order.instrument_id.symbol.value, + side=OrderSideParser.to_str_py(order.side), + type=binance_order_type(order=order, market_price=market_price), + time_in_force=TimeInForceParser.to_str_py(order.time_in_force), + quantity=str(order.quantity), + price=str(order.price), + stop_price=str(order.trigger), + iceberg_qty=str(order.display_qty) if order.display_qty is not None else None, + new_client_order_id=order.client_order_id.value, + recv_window=5000, + ) + + async def _submit_order_list(self, command: SubmitOrderList) -> None: self._log.error( # pragma: no cover - "Cannot cancel order: not yet implemented.", + "Cannot submit order list: not yet implemented.", ) + async def _cancel_order(self, command: CancelOrder) -> None: + self._log.debug(f"Canceling order {command.client_order_id.value}.") + + try: + await self._account_spot.cancel_order( + symbol=command.instrument_id.symbol.value, + orig_client_order_id=command.client_order_id.value, + ) + except BinanceError as ex: + self._log.error(ex.message) # type: ignore # TODO(cs): Improve errors + # -- RECONCILIATION ---------------------------------------------------------------------------- async def generate_order_status_report(self, order: Order) -> OrderStatusReport: # type: ignore @@ -204,3 +417,100 @@ async def generate_exec_reports( ) return [] + + def _handle_user_ws_message(self, raw: bytes): + msg: Dict[str, Any] = orjson.loads(raw) + data: Dict[str, Any] = msg.get("data") + + # TODO(cs): Uncomment for development + # self._log.info(str(json.dumps(msg, indent=4)), color=LogColor.GREEN) + + try: + msg_type: str = data.get("e") + if msg_type == "outboundAccountPosition": + self._handle_account_position(data) + elif msg_type == "executionReport": + self._handle_execution_report(data) + except Exception as ex: + self._log.exception(ex) + + def _handle_account_position(self, data: Dict[str, Any]): + self.generate_account_state( + balances=parse_account_balances_ws(raw_balances=data["B"]), + reported=True, + ts_event=millis_to_nanos(data["u"]), + ) + + def _handle_execution_report(self, data: Dict[str, Any]): + execution_type: str = data["x"] + + # Parse instrument ID + symbol: str = data["s"] + instrument_id: Optional[InstrumentId] = self._instrument_ids.get(symbol) + if not instrument_id: + instrument_id = InstrumentId(Symbol(symbol), BINANCE_VENUE) + self._instrument_ids[symbol] = instrument_id + + # Parse client order ID + client_order_id_str: str = data["c"] + if not client_order_id_str or not client_order_id_str.startswith("O"): + client_order_id_str = data["C"] + client_order_id = ClientOrderId(client_order_id_str) + + # Fetch strategy ID + strategy_id: StrategyId = self._cache.strategy_id_for_order(client_order_id) + if strategy_id is None: + # TODO(cs): Implement external order handling + self._log.error( + f"Cannot handle execution report: " f"strategy ID for {client_order_id} not found.", + ) + return + + venue_order_id = VenueOrderId(str(data["i"])) + order_type_str: str = data["o"] + ts_event: int = millis_to_nanos(data["E"]) + + if execution_type == "NEW": + self.generate_order_accepted( + strategy_id=strategy_id, + instrument_id=instrument_id, + client_order_id=client_order_id, + venue_order_id=venue_order_id, + ts_event=ts_event, + ) + elif execution_type == "TRADE": + instrument: Instrument = self._instrument_provider.find(instrument_id=instrument_id) + + # Determine commission + commission_asset: str = data["N"] + commission_amount: str = data["n"] + if commission_asset is not None: + commission = Money.from_str(f"{commission_amount} {commission_asset}") + else: + # Binance typically charges commission as base asset or BNB + commission = Money(0, instrument.base_currency) + + self.generate_order_filled( + strategy_id=strategy_id, + instrument_id=instrument_id, + client_order_id=client_order_id, + venue_order_id=venue_order_id, + venue_position_id=None, # NETTING accounts + execution_id=ExecutionId(str(data["t"])), # Trade ID + order_side=OrderSideParser.from_str_py(data["S"]), + order_type=parse_order_type(order_type_str), + last_qty=Quantity.from_str(data["l"]), + last_px=Price.from_str(data["L"]), + quote_currency=instrument.quote_currency, + commission=commission, + liquidity_side=LiquiditySide.MAKER if data["m"] else LiquiditySide.TAKER, + ts_event=ts_event, + ) + elif execution_type == "CANCELED" or execution_type == "EXPIRED": + self.generate_order_canceled( + strategy_id=strategy_id, + instrument_id=instrument_id, + client_order_id=client_order_id, + venue_order_id=venue_order_id, + ts_event=ts_event, + ) diff --git a/nautilus_trader/adapters/binance/factories.py b/nautilus_trader/adapters/binance/factories.py index ff6e38dd0a0b..6e09d4c9d6e6 100644 --- a/nautilus_trader/adapters/binance/factories.py +++ b/nautilus_trader/adapters/binance/factories.py @@ -44,7 +44,7 @@ def get_cached_binance_http_client( logger: Logger, ) -> BinanceHttpClient: """ - Cache and return a Binance HTTP client with the given key or secret. + Cache and return a Binance HTTP client with the given key and secret. If a cached client with matching key and secret already exists, then that cached client will be returned. diff --git a/nautilus_trader/adapters/binance/http/api/spot_account.py b/nautilus_trader/adapters/binance/http/api/spot_account.py new file mode 100644 index 000000000000..608c248a1a2e --- /dev/null +++ b/nautilus_trader/adapters/binance/http/api/spot_account.py @@ -0,0 +1,871 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2021 Nautech Systems Pty Ltd. All rights reserved. +# https://nautechsystems.io +# +# Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Heavily refactored from MIT licensed github.com/binance/binance-connector-python +# Original author: Jeremy https://github.com/2pd +# ------------------------------------------------------------------------------------------------- + +from typing import Any, Dict, Optional + +from nautilus_trader.adapters.binance.common import format_symbol +from nautilus_trader.adapters.binance.http.client import BinanceHttpClient +from nautilus_trader.adapters.binance.http.enums import NewOrderRespType +from nautilus_trader.core.correctness import PyCondition + + +class BinanceSpotAccountHttpAPI: + """ + Provides access to the `Binance SPOT Account/Trade` HTTP REST API. + """ + + BASE_ENDPOINT = "/api/v3/" + + def __init__(self, client: BinanceHttpClient): + """ + Initialize a new instance of the ``BinanceSpotAccountHttpAPI`` class. + + Parameters + ---------- + client : BinanceHttpClient + The Binance REST API client. + + """ + PyCondition.not_none(client, "client") + + self.client = client + + async def new_order_test( + self, + symbol: str, + side: str, + type: str, + time_in_force: Optional[str] = None, + quantity: Optional[str] = None, + quote_order_qty: Optional[str] = None, + price: Optional[str] = None, + new_client_order_id: Optional[str] = None, + stop_price: Optional[str] = None, + iceberg_qty: Optional[str] = None, + new_order_resp_type: NewOrderRespType = None, + recv_window: Optional[int] = None, + ) -> Dict[str, Any]: + """ + Test new order creation and signature/recvWindow. + + Creates and validates a new order but does not send it into the matching engine. + + Test New Order (TRADE). + `POST /api/v3/order/test`. + + Parameters + ---------- + symbol : str + The symbol for the request. + side : str + The order side for the request. + type : str + The order type for the request. + time_in_force : str, optional + The order time in force for the request. + quantity : str, optional + The order quantity in base asset units for the request. + quote_order_qty : str, optional + The order quantity in quote asset units for the request. + price : str, optional + The order price for the request. + new_client_order_id : str, optional + The client order ID for the request. A unique ID among open orders. + Automatically generated if not provided. + stop_price : str, optional + The order stop price for the request. + Used with STOP_LOSS, STOP_LOSS_LIMIT, TAKE_PROFIT, and TAKE_PROFIT_LIMIT orders. + iceberg_qty : str, optional + The order iceberg (display) quantity for the request. + Used with LIMIT, STOP_LOSS_LIMIT, and TAKE_PROFIT_LIMIT to create an iceberg order. + new_order_resp_type : NewOrderRespType, optional + The response type for the order request. + MARKET and LIMIT order types default to FULL, all other orders default to ACK. + recv_window : int, optional + The response receive window for the request (cannot be greater than 60000). + + Returns + ------- + dict[str, Any] + + References + ---------- + https://binance-docs.github.io/apidocs/spot/en/#test-new-order-trade + + """ + payload: Dict[str, str] = { + "symbol": format_symbol(symbol).upper(), + "side": side, + "type": type, + } + if time_in_force is not None: + payload["timeInForce"] = time_in_force + if quantity is not None: + payload["quantity"] = quantity + if quote_order_qty is not None: + payload["quoteOrderQty"] = quote_order_qty + if price is not None: + payload["price"] = price + if new_client_order_id is not None: + payload["newClientOrderId"] = new_client_order_id + if stop_price is not None: + payload["stopPrice"] = stop_price + if iceberg_qty is not None: + payload["icebergQty"] = iceberg_qty + if new_order_resp_type is not None: + payload["newOrderRespType"] = new_order_resp_type.value + if recv_window is not None: + payload["recvWindow"] = str(recv_window) + + return await self.client.sign_request( + http_method="POST", + url_path=self.BASE_ENDPOINT + "order/test", + payload=payload, + ) + + async def new_order( + self, + symbol: str, + side: str, + type: str, + time_in_force: Optional[str] = None, + quantity: Optional[str] = None, + quote_order_qty: Optional[str] = None, + price: Optional[str] = None, + new_client_order_id: Optional[str] = None, + stop_price: Optional[str] = None, + iceberg_qty: Optional[str] = None, + new_order_resp_type: NewOrderRespType = None, + recv_window: Optional[int] = None, + ) -> Dict[str, Any]: + """ + Submit a new order. + + Submit New Order (TRADE). + `POST /api/v3/order`. + + Parameters + ---------- + symbol : str + The symbol for the request. + side : str + The order side for the request. + type : str + The order type for the request. + time_in_force : str, optional + The order time in force for the request. + quantity : str, optional + The order quantity in base asset units for the request. + quote_order_qty : str, optional + The order quantity in quote asset units for the request. + price : str, optional + The order price for the request. + new_client_order_id : str, optional + The client order ID for the request. A unique ID among open orders. + Automatically generated if not provided. + stop_price : str, optional + The order stop price for the request. + Used with STOP_LOSS, STOP_LOSS_LIMIT, TAKE_PROFIT, and TAKE_PROFIT_LIMIT orders. + iceberg_qty : str, optional + The order iceberg (display) quantity for the request. + Used with LIMIT, STOP_LOSS_LIMIT, and TAKE_PROFIT_LIMIT to create an iceberg order. + new_order_resp_type : NewOrderRespType, optional + The response type for the order request. + MARKET and LIMIT order types default to FULL, all other orders default to ACK. + recv_window : int, optional + The response receive window for the request (cannot be greater than 60000). + + Returns + ------- + dict[str, Any] + + References + ---------- + https://binance-docs.github.io/apidocs/spot/en/#new-order-trade + + """ + payload: Dict[str, str] = { + "symbol": format_symbol(symbol).upper(), + "side": side, + "type": type, + } + if time_in_force is not None: + payload["timeInForce"] = time_in_force + if quantity is not None: + payload["quantity"] = quantity + if quote_order_qty is not None: + payload["quoteOrderQty"] = quote_order_qty + if price is not None: + payload["price"] = price + if new_client_order_id is not None: + payload["newClientOrderId"] = new_client_order_id + if stop_price is not None: + payload["stopPrice"] = stop_price + if iceberg_qty is not None: + payload["icebergQty"] = iceberg_qty + if new_order_resp_type is not None: + payload["newOrderRespType"] = new_order_resp_type.value + if recv_window is not None: + payload["recvWindow"] = str(recv_window) + + return await self.client.sign_request( + http_method="POST", + url_path=self.BASE_ENDPOINT + "order", + payload=payload, + ) + + async def cancel_order( + self, + symbol: str, + order_id: Optional[str] = None, + orig_client_order_id: Optional[str] = None, + new_client_order_id: Optional[str] = None, + recv_window: Optional[int] = None, + ) -> Dict[str, Any]: + """ + Cancel an active order. + + Cancel Order (TRADE). + `DELETE /api/v3/order`. + + Parameters + ---------- + symbol : str + The symbol for the request. + order_id : str, optional + The order ID to cancel. + orig_client_order_id : str, optional + The original client order ID to cancel. + new_client_order_id : str, optional + The new client order ID to uniquely identify this request. + recv_window : int, optional + The response receive window for the request (cannot be greater than 60000). + + Returns + ------- + dict[str, Any] + + References + ---------- + https://binance-docs.github.io/apidocs/spot/en/#cancel-order-trade + + """ + payload: Dict[str, str] = {"symbol": format_symbol(symbol).upper()} + if order_id is not None: + payload["orderId"] = str(order_id) + if orig_client_order_id is not None: + payload["origClientOrderId"] = str(orig_client_order_id) + if new_client_order_id is not None: + payload["newClientOrderId"] = str(new_client_order_id) + if recv_window is not None: + payload["recvWindow"] = str(recv_window) + + return await self.client.sign_request( + http_method="DELETE", + url_path=self.BASE_ENDPOINT + "order", + payload=payload, + ) + + async def cancel_open_orders( + self, + symbol: str, + recv_window: Optional[int] = None, + ) -> Dict[str, Any]: + """ + Cancel all active orders on a symbol. This includes OCO orders. + + Cancel all Open Orders on a Symbol (TRADE). + `DELETE api/v3/openOrders`. + + Parameters + ---------- + symbol : str + The symbol for the request. + recv_window : int, optional + The response receive window for the request (cannot be greater than 60000). + + Returns + ------- + dict[str, Any] + + References + ---------- + https://binance-docs.github.io/apidocs/spot/en/#cancel-all-open-orders-on-a-symbol-trade + + """ + payload: Dict[str, str] = {"symbol": format_symbol(symbol).upper()} + if recv_window is not None: + payload["recvWindow"] = str(recv_window) + + return await self.client.sign_request( + http_method="DELETE", + url_path=self.BASE_ENDPOINT + "openOrders", + payload=payload, + ) + + async def get_order( + self, + symbol: str, + order_id: Optional[str] = None, + orig_client_order_id: Optional[str] = None, + recv_window: Optional[int] = None, + ) -> Dict[str, Any]: + """ + Check an order's status. + + Query Order (USER_DATA). + `GET /api/v3/order`. + + Parameters + ---------- + symbol : str + The symbol for the request. + order_id : str, optional + The order ID for the request. + orig_client_order_id : str, optional + The original client order ID for the request. + recv_window : int, optional + The response receive window for the request (cannot be greater than 60000). + + Returns + ------- + dict[str, Any] + + References + ---------- + https://binance-docs.github.io/apidocs/spot/en/#query-order-user_data + + """ + payload: Dict[str, str] = {"symbol": format_symbol(symbol).upper()} + if order_id is not None: + payload["orderId"] = order_id + if orig_client_order_id is not None: + payload["origClientOrderId"] = orig_client_order_id + if recv_window is not None: + payload["recvWindow"] = str(recv_window) + + return await self.client.sign_request( + http_method="GET", + url_path=self.BASE_ENDPOINT + "order", + payload=payload, + ) + + async def get_open_orders( + self, + symbol: Optional[str] = None, + recv_window: Optional[int] = None, + ) -> Dict[str, Any]: + """ + Get all open orders on a symbol. + + Query Current Open Orders (USER_DATA). + `GET /api/v3/openOrders`. + + Parameters + ---------- + symbol : str, optional + The symbol for the request. + recv_window : int, optional + The response receive window for the request (cannot be greater than 60000). + + Returns + ------- + dict[str, Any] + + References + ---------- + https://binance-docs.github.io/apidocs/spot/en/#current-open-orders-user_data + + """ + payload: Dict[str, str] = {} + if symbol is not None: + payload["symbol"] = format_symbol(symbol).upper() + if recv_window is not None: + payload["recvWindow"] = str(recv_window) + + return await self.client.sign_request( + http_method="GET", + url_path=self.BASE_ENDPOINT + "openOrders", + payload=payload, + ) + + async def get_orders( + self, + symbol: str, + order_id: Optional[str] = None, + start_time: Optional[int] = None, + end_time: Optional[int] = None, + limit: Optional[int] = None, + recv_window: Optional[int] = None, + ) -> Dict[str, Any]: + """ + Get all account orders; active, canceled, or filled. + + All Orders (USER_DATA). + `GET /api/v3/allOrders`. + + Parameters + ---------- + symbol : str + The symbol for the request. + order_id : str, optional + The order ID for the request. + start_time : int, optional + The start time (UNIX milliseconds) filter for the request. + end_time : int, optional + The end time (UNIX milliseconds) filter for the request. + limit : int, optional + The limit for the response. + recv_window : int, optional + The response receive window for the request (cannot be greater than 60000). + + Returns + ------- + dict[str, Any] + + References + ---------- + https://binance-docs.github.io/apidocs/spot/en/#all-orders-user_data + + """ + payload: Dict[str, str] = {"symbol": format_symbol(symbol).upper()} + if order_id is not None: + payload["orderId"] = order_id + if start_time is not None: + payload["startTime"] = str(start_time) + if end_time is not None: + payload["endTime"] = str(end_time) + if limit is not None: + payload["limit"] = str(limit) + if recv_window is not None: + payload["recvWindow"] = str(recv_window) + + return await self.client.sign_request( + http_method="GET", + url_path=self.BASE_ENDPOINT + "allOrders", + payload=payload, + ) + + async def new_oco_order( + self, + symbol: str, + side: str, + quantity: str, + price: str, + stop_price: str, + list_client_order_id: Optional[str] = None, + limit_client_order_id: Optional[str] = None, + limit_iceberg_qty: Optional[str] = None, + stop_client_order_id: Optional[str] = None, + stop_limit_price: Optional[str] = None, + stop_iceberg_qty: Optional[str] = None, + stop_limit_time_in_force: Optional[str] = None, + new_order_resp_type: NewOrderRespType = None, + recv_window: Optional[int] = None, + ) -> Dict[str, Any]: + """ + Submit a new OCO order. + + Submit New OCO (TRADE). + `POST /api/v3/order/oco`. + + Parameters + ---------- + symbol : str + The symbol for the request. + side : str + The order side for the request. + quantity : str + The order quantity for the request. + price : str + The order price for the request. + stop_price : str + The order stop price for the request. + list_client_order_id : str, optional + The list client order ID for the request. + limit_client_order_id : str, optional + The LIMIT client order ID for the request. + limit_iceberg_qty : str, optional + The LIMIT order display quantity for the request. + stop_client_order_id : str, optional + The STOP order client order ID for the request. + stop_limit_price : str, optional + The STOP_LIMIT price for the request. + stop_iceberg_qty : str, optional + The STOP order display quantity for the request. + stop_limit_time_in_force : str, optional + The STOP_LIMIT time_in_force for the request. + new_order_resp_type : NewOrderRespType, optional + The response type for the order request. + MARKET and LIMIT order types default to FULL, all other orders default to ACK. + recv_window : int, optional + The response receive window for the request (cannot be greater than 60000). + + Returns + ------- + dict[str, Any] + + References + ---------- + https://binance-docs.github.io/apidocs/spot/en/#new-oco-trade + + """ + payload: Dict[str, str] = { + "symbol": format_symbol(symbol).upper(), + "side": side, + "quantity": quantity, + "price": price, + "stopPrice": stop_price, + } + if list_client_order_id is not None: + payload["listClientOrderId"] = list_client_order_id + if limit_client_order_id is not None: + payload["limitClientOrderId"] = limit_client_order_id + if limit_iceberg_qty is not None: + payload["limitIcebergQty"] = limit_iceberg_qty + if stop_client_order_id is not None: + payload["stopClientOrderId"] = stop_client_order_id + if stop_limit_price is not None: + payload["stopLimitPrice"] = stop_limit_price + if stop_iceberg_qty is not None: + payload["stopIcebergQty"] = stop_iceberg_qty + if stop_limit_time_in_force is not None: + payload["stopLimitTimeInForce"] = stop_limit_time_in_force + if new_order_resp_type is not None: + payload["new_order_resp_type"] = new_order_resp_type.value + if recv_window is not None: + payload["recvWindow"] = str(recv_window) + + return await self.client.sign_request( + http_method="POST", + url_path=self.BASE_ENDPOINT + "order/oco", + payload=payload, + ) + + async def cancel_oco_order( + self, + symbol: str, + order_list_id: Optional[str] = None, + list_client_order_id: Optional[str] = None, + new_client_order_id: Optional[str] = None, + recv_window: Optional[int] = None, + ) -> Dict[str, Any]: + """ + Cancel an entire Order List. + + Either `order_list_id` or `list_client_order_id` must be provided. + + Cancel OCO (TRADE). + `DELETE /api/v3/orderList`. + + Parameters + ---------- + symbol : str + The symbol for the request. + order_list_id : str, optional + The order list ID for the request. + list_client_order_id : str, optional + The list client order ID for the request. + new_client_order_id : str, optional + The new client order ID to uniquely identify this request. + recv_window : int, optional + The response receive window for the request (cannot be greater than 60000). + + Returns + ------- + dict[str, Any] + + References + ---------- + https://binance-docs.github.io/apidocs/spot/en/#cancel-oco-trade + + """ + payload: Dict[str, str] = {"symbol": format_symbol(symbol).upper()} + if order_list_id is not None: + payload["orderListId"] = order_list_id + if list_client_order_id is not None: + payload["listClientOrderId"] = list_client_order_id + if new_client_order_id is not None: + payload["newClientOrderId"] = new_client_order_id + if recv_window is not None: + payload["recvWindow"] = str(recv_window) + + return await self.client.sign_request( + http_method="DELETE", + url_path=self.BASE_ENDPOINT + "orderList", + payload=payload, + ) + + async def get_oco_order( + self, + order_list_id: Optional[str], + orig_client_order_id: Optional[str], + recv_window: Optional[int] = None, + ) -> Dict[str, Any]: + """ + Retrieve a specific OCO based on provided optional parameters. + + Either `order_list_id` or `orig_client_order_id` must be provided. + + Query OCO (USER_DATA). + `GET /api/v3/orderList`. + + Parameters + ---------- + order_list_id : str, optional + The order list ID for the request. + orig_client_order_id : str, optional + The original client order ID for the request. + recv_window : int, optional + The response receive window for the request (cannot be greater than 60000). + + Returns + ------- + dict[str, Any] + + References + ---------- + https://binance-docs.github.io/apidocs/spot/en/#query-oco-user_data + + """ + payload: Dict[str, str] = {} + if order_list_id is not None: + payload["orderListId"] = order_list_id + if orig_client_order_id is not None: + payload["origClientOrderId"] = orig_client_order_id + if recv_window is not None: + payload["recvWindow"] = str(recv_window) + + return await self.client.sign_request( + http_method="GET", + url_path=self.BASE_ENDPOINT + "orderList", + payload=payload, + ) + + async def get_oco_orders( + self, + from_id: Optional[str] = None, + start_time: Optional[int] = None, + end_time: Optional[int] = None, + limit: Optional[int] = None, + recv_window: Optional[int] = None, + ) -> Dict[str, Any]: + """ + Retrieve all OCO based on provided optional parameters. + + If `from_id` is provided then neither `start_time` nor `end_time` can be + provided. + + Query all OCO (USER_DATA). + `GET /api/v3/allOrderList`. + + Parameters + ---------- + from_id : int, optional + The order ID filter for the request. + start_time : int, optional + The start time (UNIX milliseconds) filter for the request. + end_time : int, optional + The end time (UNIX milliseconds) filter for the request. + limit : int, optional + The limit for the response. + recv_window : int, optional + The response receive window for the request (cannot be greater than 60000). + + Returns + ------- + dict[str, Any] + + References + ---------- + https://binance-docs.github.io/apidocs/spot/en/#query-all-oco-user_data + + """ + payload: Dict[str, str] = {} + if from_id is not None: + payload["fromId"] = from_id + if start_time is not None: + payload["startTime"] = str(start_time) + if end_time is not None: + payload["endTime"] = str(end_time) + if limit is not None: + payload["limit"] = str(limit) + if recv_window is not None: + payload["recvWindow"] = str(recv_window) + + return await self.client.sign_request( + http_method="GET", + url_path=self.BASE_ENDPOINT + "allOrderList", + payload=payload, + ) + + async def get_oco_open_orders(self, recv_window: Optional[int] = None) -> Dict[str, Any]: + """ + Get all open OCO orders. + + Query Open OCO (USER_DATA). + GET /api/v3/openOrderList. + + Parameters + ---------- + recv_window : int, optional + The response receive window for the request (cannot be greater than 60000). + + Returns + ------- + dict[str, Any] + + References + ---------- + https://binance-docs.github.io/apidocs/spot/en/#query-open-oco-user_data + + """ + payload: Dict[str, str] = {} + if recv_window is not None: + payload["recvWindow"] = str(recv_window) + + return await self.client.sign_request( + http_method="GET", + url_path=self.BASE_ENDPOINT + "openOrderList", + payload=payload, + ) + + async def account(self, recv_window: Optional[int] = None) -> Dict[str, Any]: + """ + Get current account information. + + Account Information (USER_DATA). + `GET /api/v3/account`. + + Parameters + ---------- + recv_window : int, optional + The response receive window for the request (cannot be greater than 60000). + + Returns + ------- + dict[str, Any] + + References + ---------- + https://binance-docs.github.io/apidocs/spot/en/#account-information-user_data + + """ + payload: Dict[str, str] = {} + if recv_window is not None: + payload["recvWindow"] = str(recv_window) + + return await self.client.sign_request( + http_method="GET", + url_path=self.BASE_ENDPOINT + "account", + payload=payload, + ) + + async def my_trades( + self, + symbol: str, + from_id: Optional[str] = None, + order_id: Optional[str] = None, + start_time: Optional[int] = None, + end_time: Optional[int] = None, + limit: Optional[int] = None, + recv_window: Optional[int] = None, + ) -> Dict[str, Any]: + """ + Get trades for a specific account and symbol. + + Account Trade List (USER_DATA) + `GET /api/v3/myTrades`. + + Parameters + ---------- + symbol : str + The symbol for the request. + from_id : str, optional + The trade match ID to query from. + order_id : str, optional + The order ID for the trades. This can only be used in combination with symbol. + start_time : int, optional + The start time (UNIX milliseconds) filter for the request. + end_time : int, optional + The end time (UNIX milliseconds) filter for the request. + limit : int, optional + The limit for the response. + recv_window : int, optional + The response receive window for the request (cannot be greater than 60000). + + Returns + ------- + dict[str, Any] + + References + ---------- + https://binance-docs.github.io/apidocs/spot/en/#account-trade-list-user_data + + + """ + payload: Dict[str, str] = {"symbol": format_symbol(symbol).upper()} + if from_id is not None: + payload["fromId"] = from_id + if order_id is not None: + payload["orderId"] = order_id + if start_time is not None: + payload["startTime"] = str(start_time) + if end_time is not None: + payload["endTime"] = str(end_time) + if limit is not None: + payload["limit"] = str(limit) + if recv_window is not None: + payload["recvWindow"] = str(recv_window) + + return await self.client.sign_request( + http_method="GET", + url_path=self.BASE_ENDPOINT + "myTrades", + payload=payload, + ) + + async def get_order_rate_limit(self, recv_window: Optional[int] = None) -> Dict[str, Any]: + """ + Get the user's current order count usage for all intervals. + + Query Current Order Count Usage (TRADE). + `GET /api/v3/rateLimit/order`. + + Parameters + ---------- + recv_window : int, optional + The response receive window for the request (cannot be greater than 60000). + + Returns + ------- + dict[str, Any] + + References + ---------- + https://binance-docs.github.io/apidocs/spot/en/#query-current-order-count-usage-trade + + """ + payload: Dict[str, str] = {} + if recv_window is not None: + payload["recvWindow"] = str(recv_window) + + return await self.client.sign_request( + http_method="GET", + url_path=self.BASE_ENDPOINT + "rateLimit/order", + payload=payload, + ) diff --git a/nautilus_trader/adapters/binance/http/api/spot_market.py b/nautilus_trader/adapters/binance/http/api/spot_market.py index 4b87b546946c..92c05b78ebfc 100644 --- a/nautilus_trader/adapters/binance/http/api/spot_market.py +++ b/nautilus_trader/adapters/binance/http/api/spot_market.py @@ -16,7 +16,7 @@ # Original author: Jeremy https://github.com/2pd # ------------------------------------------------------------------------------------------------- -from typing import Dict, Optional +from typing import Any, Dict, List, Optional from nautilus_trader.adapters.binance.common import format_symbol from nautilus_trader.adapters.binance.http.client import BinanceHttpClient @@ -45,7 +45,7 @@ def __init__(self, client: BinanceHttpClient): self.client = client - async def ping(self) -> bytes: + async def ping(self) -> Dict[str, Any]: """ Test the connectivity to the REST API. @@ -53,8 +53,7 @@ async def ping(self) -> bytes: Returns ------- - bytes - The raw response content. + dict[str, Any] References ---------- @@ -63,18 +62,16 @@ async def ping(self) -> bytes: """ return await self.client.query(url_path=self.BASE_ENDPOINT + "ping") - async def time(self) -> bytes: + async def time(self) -> Dict[str, Any]: """ - Check Server Time. - Test connectivity to the Rest API and get the current server time. + Check Server Time. `GET /api/v3/time` Returns ------- - bytes - The raw response content. + dict[str, Any] References ---------- @@ -83,13 +80,12 @@ async def time(self) -> bytes: """ return await self.client.query(url_path=self.BASE_ENDPOINT + "time") - async def exchange_info(self, symbol: str = None, symbols: list = None) -> bytes: + async def exchange_info(self, symbol: str = None, symbols: list = None) -> Dict[str, Any]: """ - Exchange Information. - - Current exchange trading rules and symbol information. + Get current exchange trading rules and symbol information. Only either `symbol` or `symbols` should be passed. + Exchange Information. `GET /api/v3/exchangeinfo` Parameters @@ -101,8 +97,7 @@ async def exchange_info(self, symbol: str = None, symbols: list = None) -> bytes Returns ------- - bytes - The raw response content. + dict[str, Any] References ---------- @@ -123,7 +118,7 @@ async def exchange_info(self, symbol: str = None, symbols: list = None) -> bytes payload=payload, ) - async def depth(self, symbol: str, limit: Optional[int] = None) -> bytes: + async def depth(self, symbol: str, limit: Optional[int] = None) -> Dict[str, Any]: """ Get orderbook. @@ -139,8 +134,7 @@ async def depth(self, symbol: str, limit: Optional[int] = None) -> bytes: Returns ------- - bytes - The raw response content. + dict[str, Any] References ---------- @@ -156,12 +150,11 @@ async def depth(self, symbol: str, limit: Optional[int] = None) -> bytes: payload=payload, ) - async def trades(self, symbol: str, limit: Optional[int] = None) -> bytes: + async def trades(self, symbol: str, limit: Optional[int] = None) -> List[Dict[str, Any]]: """ - Recent Trades List. - - Get recent trades (up to last 500). + Get recent market trades. + Recent Trades List. `GET /api/v3/trades` Parameters @@ -173,8 +166,7 @@ async def trades(self, symbol: str, limit: Optional[int] = None) -> bytes: Returns ------- - bytes - The raw response content. + list[dict[str, Any]] References ---------- @@ -195,12 +187,11 @@ async def historical_trades( symbol: str, from_id: Optional[int] = None, limit: Optional[int] = None, - ) -> bytes: + ) -> Dict[str, Any]: """ - Old Trade Lookup. - Get older market trades. + Old Trade Lookup. `GET /api/v3/historicalTrades` Parameters @@ -214,8 +205,7 @@ async def historical_trades( Returns ------- - bytes - The raw response content. + dict[str, Any] References ---------- @@ -241,10 +231,11 @@ async def agg_trades( start_time_ms: Optional[int] = None, end_time_ms: Optional[int] = None, limit: Optional[int] = None, - ) -> bytes: + ) -> Dict[str, Any]: """ - Compressed/Aggregate Trades List. + Get recent aggregated market trades. + Compressed/Aggregate Trades List. `GET /api/v3/aggTrades` Parameters @@ -262,8 +253,7 @@ async def agg_trades( Returns ------- - bytes - The raw response content. + dict[str, Any] References ---------- @@ -292,7 +282,7 @@ async def klines( start_time_ms: Optional[int] = None, end_time_ms: Optional[int] = None, limit: Optional[int] = None, - ) -> bytes: + ) -> List[List[Any]]: """ Kline/Candlestick Data. @@ -311,6 +301,10 @@ async def klines( limit : int, optional The limit for the response. Default 500; max 1000. + Returns + ------- + list[list[Any]] + References ---------- https://binance-docs.github.io/apidocs/spot/en/#kline-candlestick-data @@ -332,7 +326,7 @@ async def klines( payload=payload, ) - async def avg_price(self, symbol: str) -> bytes: + async def avg_price(self, symbol: str) -> Dict[str, Any]: """ Get the current average price for the given symbol. @@ -345,8 +339,7 @@ async def avg_price(self, symbol: str) -> bytes: Returns ------- - bytes - The raw response content. + dict[str, Any] References ---------- @@ -360,7 +353,7 @@ async def avg_price(self, symbol: str) -> bytes: payload=payload, ) - async def ticker_24hr(self, symbol: str = None) -> bytes: + async def ticker_24hr(self, symbol: str = None) -> Dict[str, Any]: """ 24hr Ticker Price Change Statistics. @@ -373,8 +366,7 @@ async def ticker_24hr(self, symbol: str = None) -> bytes: Returns ------- - bytes - The raw response content. + dict[str, Any] References ---------- @@ -390,7 +382,7 @@ async def ticker_24hr(self, symbol: str = None) -> bytes: payload=payload, ) - async def ticker_price(self, symbol: str = None) -> bytes: + async def ticker_price(self, symbol: str = None) -> Dict[str, Any]: """ Symbol Price Ticker. @@ -403,8 +395,7 @@ async def ticker_price(self, symbol: str = None) -> bytes: Returns ------- - bytes - The raw response content. + dict[str, Any] References ---------- @@ -420,7 +411,7 @@ async def ticker_price(self, symbol: str = None) -> bytes: payload=payload, ) - async def book_ticker(self, symbol: str = None) -> bytes: + async def book_ticker(self, symbol: str = None) -> Dict[str, Any]: """ Symbol Order Book Ticker. @@ -433,8 +424,7 @@ async def book_ticker(self, symbol: str = None) -> bytes: Returns ------- - bytes - The raw response content. + dict[str, Any] References ---------- diff --git a/nautilus_trader/adapters/binance/http/api/user.py b/nautilus_trader/adapters/binance/http/api/user.py new file mode 100644 index 000000000000..31a5a10cb61e --- /dev/null +++ b/nautilus_trader/adapters/binance/http/api/user.py @@ -0,0 +1,309 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2021 Nautech Systems Pty Ltd. All rights reserved. +# https://nautechsystems.io +# +# Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Heavily refactored from MIT licensed github.com/binance/binance-connector-python +# Original author: Jeremy https://github.com/2pd +# ------------------------------------------------------------------------------------------------- + +from typing import Any, Dict + +from nautilus_trader.adapters.binance.common import format_symbol +from nautilus_trader.adapters.binance.http.client import BinanceHttpClient +from nautilus_trader.core.correctness import PyCondition + + +class BinanceUserDataHttpAPI: + """ + Provides access to the `Binance Wallet` HTTP REST API. + """ + + BASE_ENDPOINT_SPOT = "/api/v3/userDataStream" + BASE_ENDPOINT_MARGIN = "/sapi/v1/userDataStream" + BASE_ENDPOINT_ISOLATED = "/sapi/v1/userDataStream/isolated" + + def __init__(self, client: BinanceHttpClient): + """ + Initialize a new instance of the ``BinanceUserDataHttpAPI`` class. + + Parameters + ---------- + client : BinanceHttpClient + The Binance REST API client. + + """ + PyCondition.not_none(client, "client") + + self.client = client + + async def create_listen_key_spot(self) -> Dict[str, Any]: + """ + Create a new listen key for the SPOT API. + + Start a new user data stream. The stream will close after 60 minutes + unless a keepalive is sent. If the account has an active listenKey, + that listenKey will be returned and its validity will be extended for 60 + minutes. + + Create a ListenKey (USER_STREAM). + `POST /api/v3/userDataStream `. + + Returns + ------- + dict[str, Any] + + References + ---------- + https://binance-docs.github.io/apidocs/spot/en/#listen-key-spot + + """ + return await self.client.send_request( + http_method="POST", + url_path=self.BASE_ENDPOINT_SPOT, + ) + + async def ping_listen_key_spot(self, key: str) -> Dict[str, Any]: + """ + Ping/Keep-alive a listen key for the SPOT API. + + Keep-alive a user data stream to prevent a time-out. User data streams + will close after 60 minutes. It's recommended to send a ping about every + 30 minutes. + + Ping/Keep-alive a ListenKey (USER_STREAM). + `PUT /api/v3/userDataStream ` + + Parameters + ---------- + key : str + The listen key for the request. + + Returns + ------- + dict[str, Any] + + References + ---------- + https://binance-docs.github.io/apidocs/spot/en/#listen-key-spot + + """ + return await self.client.send_request( + http_method="PUT", + url_path=self.BASE_ENDPOINT_SPOT, + payload={"listenKey": key}, + ) + + async def close_listen_key_spot(self, key: str) -> Dict[str, Any]: + """ + Close a listen key for the SPOT API. + + Close a ListenKey (USER_STREAM). + `DELETE /api/v3/userDataStream`. + + Parameters + ---------- + key : str + The listen key for the request. + + Returns + ------- + dict[str, Any] + + References + ---------- + https://binance-docs.github.io/apidocs/spot/en/#listen-key-spot + + """ + return await self.client.send_request( + http_method="DELETE", + url_path=self.BASE_ENDPOINT_SPOT, + payload={"listenKey": key}, + ) + + async def create_listen_key_margin(self) -> Dict[str, Any]: + """ + Create a new listen key for the MARGIN API. + + Start a new user data stream. The stream will close after 60 minutes + unless a keepalive is sent. If the account has an active listenKey, + that listenKey will be returned and its validity will be extended for 60 + minutes. + + Create a ListenKey (USER_STREAM). + `POST /api/v3/userDataStream `. + + Returns + ------- + dict[str, Any] + + References + ---------- + https://binance-docs.github.io/apidocs/spot/en/#listen-key-margin + + """ + return await self.client.send_request( + http_method="POST", + url_path=self.BASE_ENDPOINT_MARGIN, + ) + + async def ping_listen_key_margin(self, key: str) -> Dict[str, Any]: + """ + Ping/Keep-alive a listen key for the MARGIN API. + + Keep-alive a user data stream to prevent a time-out. User data streams + will close after 60 minutes. It's recommended to send a ping about every + 30 minutes. + + Ping/Keep-alive a ListenKey (USER_STREAM). + `PUT /api/v3/userDataStream`. + + Parameters + ---------- + key : str + The listen key for the request. + + Returns + ------- + dict[str, Any] + + References + ---------- + https://binance-docs.github.io/apidocs/spot/en/#listen-key-margin + + """ + return await self.client.send_request( + http_method="PUT", + url_path=self.BASE_ENDPOINT_MARGIN, + payload={"listenKey": key}, + ) + + async def close_listen_key_margin(self, key: str) -> Dict[str, Any]: + """ + Close a listen key for the MARGIN API. + + Close a ListenKey (USER_STREAM). + `DELETE /sapi/v1/userDataStream`. + + Parameters + ---------- + key : str + The listen key for the request. + + Returns + ------- + dict[str, Any] + + References + ---------- + https://binance-docs.github.io/apidocs/spot/en/#listen-key-margin + + """ + return await self.client.send_request( + http_method="DELETE", + url_path=self.BASE_ENDPOINT_MARGIN, + payload={"listenKey": key}, + ) + + async def create_listen_key_isolated_margin(self, symbol: str) -> Dict[str, Any]: + """ + Create a new listen key for the ISOLATED MARGIN API. + + Start a new user data stream. The stream will close after 60 minutes + unless a keepalive is sent. If the account has an active listenKey, + that listenKey will be returned and its validity will be extended for 60 + minutes. + + Create a ListenKey (USER_STREAM). + `POST /api/v3/userDataStream `. + + Parameters + ---------- + symbol : str + The symbol for the listen key request. + + Returns + ------- + dict[str, Any] + + References + ---------- + https://binance-docs.github.io/apidocs/spot/en/#listen-key-isolated-margin + + """ + return await self.client.send_request( + http_method="POST", + url_path=self.BASE_ENDPOINT_ISOLATED, + payload={"symbol": format_symbol(symbol).upper()}, + ) + + async def ping_listen_key_isolated_margin(self, symbol: str, key: str) -> Dict[str, Any]: + """ + Ping/Keep-alive a listen key for the ISOLATED MARGIN API. + + Keep-alive a user data stream to prevent a time-out. User data streams + will close after 60 minutes. It's recommended to send a ping about every + 30 minutes. + + Ping/Keep-alive a ListenKey (USER_STREAM). + `PUT /api/v3/userDataStream`. + + Parameters + ---------- + symbol : str + The symbol for the listen key request. + key : str + The listen key for the request. + + Returns + ------- + dict[str, Any] + + References + ---------- + https://binance-docs.github.io/apidocs/spot/en/#listen-key-isolated-margin + + """ + return await self.client.send_request( + http_method="PUT", + url_path=self.BASE_ENDPOINT_ISOLATED, + payload={"listenKey": key, "symbol": format_symbol(symbol).upper()}, + ) + + async def close_listen_key_isolated_margin(self, symbol: str, key: str) -> Dict[str, Any]: + """ + Close a listen key for the ISOLATED MARGIN API. + + Close a ListenKey (USER_STREAM). + `DELETE /sapi/v1/userDataStream`. + + Parameters + ---------- + symbol : str + The symbol for the listen key request. + key : str + The listen key for the request. + + Returns + ------- + dict[str, Any] + + References + ---------- + https://binance-docs.github.io/apidocs/spot/en/#listen-key-isolated-margin + + """ + return await self.client.send_request( + http_method="DELETE", + url_path=self.BASE_ENDPOINT_ISOLATED, + payload={"listenKey": key, "symbol": format_symbol(symbol).upper()}, + ) diff --git a/nautilus_trader/adapters/binance/http/api/wallet.py b/nautilus_trader/adapters/binance/http/api/wallet.py index fcc8cdfb5a40..41752fafbe20 100644 --- a/nautilus_trader/adapters/binance/http/api/wallet.py +++ b/nautilus_trader/adapters/binance/http/api/wallet.py @@ -16,7 +16,7 @@ # Original author: Jeremy https://github.com/2pd # ------------------------------------------------------------------------------------------------- -from typing import Dict, Optional +from typing import Dict, List, Optional from nautilus_trader.adapters.binance.http.client import BinanceHttpClient from nautilus_trader.core.correctness import PyCondition @@ -47,7 +47,7 @@ async def trade_fee( self, symbol: Optional[str] = None, recv_window: Optional[int] = None, - ) -> bytes: + ) -> List[Dict[str, str]]: """ Fetch trade fee. @@ -62,8 +62,7 @@ async def trade_fee( Returns ------- - bytes - The raw response content. + list[dict[str, str]] References ---------- diff --git a/nautilus_trader/adapters/binance/http/client.py b/nautilus_trader/adapters/binance/http/client.py index 958c0c4322ea..b332cf328243 100644 --- a/nautilus_trader/adapters/binance/http/client.py +++ b/nautilus_trader/adapters/binance/http/client.py @@ -21,6 +21,7 @@ import hmac from typing import Any, Dict +import orjson from aiohttp import ClientResponse from aiohttp import ClientResponseError @@ -74,11 +75,15 @@ def __init__( # TODO(cs): Implement limit usage + @property + def api_key(self) -> str: + return self._key + @property def headers(self): return self._headers - async def query(self, url_path, payload: Dict[str, str] = None) -> bytes: + async def query(self, url_path, payload: Dict[str, str] = None) -> Any: return await self.send_request("GET", url_path, payload=payload) async def limit_request( @@ -86,7 +91,7 @@ async def limit_request( http_method: str, url_path: str, payload: Dict[str, Any] = None, - ) -> bytes: + ) -> Any: """ Limit request is for those endpoints requiring an API key in the header. """ @@ -97,7 +102,7 @@ async def sign_request( http_method: str, url_path: str, payload: Dict[str, str] = None, - ) -> bytes: + ) -> Any: if payload is None: payload = {} payload["timestamp"] = str(self._clock.timestamp_ms()) @@ -111,7 +116,7 @@ async def limited_encoded_sign_request( http_method: str, url_path: str, payload: Dict[str, str] = None, - ) -> bytes: + ) -> Any: """ Limit encoded sign request. @@ -135,9 +140,9 @@ async def send_request( http_method: str, url_path: str, payload: Dict[str, str] = None, - ) -> bytes: + ) -> Any: # TODO(cs): Uncomment for development - # print(f"\nRequest: {http_method}, {url_path}, {self._headers}, {payload}") + # print(f"{http_method} {url_path} {payload}") if payload is None: payload = {} try: @@ -149,6 +154,7 @@ async def send_request( ) except ClientResponseError as ex: await self._handle_exception(ex) + return if self._show_limit_usage: limit_usage = {} @@ -161,7 +167,10 @@ async def send_request( ): limit_usage[key] = resp.headers[key] - return resp.data + try: + return orjson.loads(resp.data) + except orjson.JSONDecodeError: + self._log.error(f"Could not decode data to JSON: {resp.data}.") def _prepare_params(self, params: Dict[str, str]) -> str: return "&".join([k + "=" + v for k, v in params.items()]) diff --git a/nautilus_trader/adapters/binance/http/enums.py b/nautilus_trader/adapters/binance/http/enums.py index 873457f18fcb..6ea0190c39bf 100644 --- a/nautilus_trader/adapters/binance/http/enums.py +++ b/nautilus_trader/adapters/binance/http/enums.py @@ -20,6 +20,16 @@ from enum import auto +class NewOrderRespType(Enum): + """ + Represents a `Binance` newOrderRespType. + """ + + ACK = "ACK" + RESULT = "RESULT" + FULL = "FULL" + + class AutoName(Enum): """ Represents a `Binance` auto name. diff --git a/nautilus_trader/adapters/binance/parsing.py b/nautilus_trader/adapters/binance/parsing.py index 09fc0e29d2ca..95715efe773d 100644 --- a/nautilus_trader/adapters/binance/parsing.py +++ b/nautilus_trader/adapters/binance/parsing.py @@ -19,6 +19,8 @@ from nautilus_trader.adapters.binance.data_types import BinanceBar from nautilus_trader.adapters.binance.data_types import BinanceTicker from nautilus_trader.core.datetime import millis_to_nanos +from nautilus_trader.model.c_enums.order_type import OrderTypeParser +from nautilus_trader.model.currency import Currency from nautilus_trader.model.data.bar import BarSpecification from nautilus_trader.model.data.bar import BarType from nautilus_trader.model.data.tick import QuoteTick @@ -29,14 +31,17 @@ from nautilus_trader.model.enums import BookAction from nautilus_trader.model.enums import BookType from nautilus_trader.model.enums import OrderSide +from nautilus_trader.model.enums import OrderType from nautilus_trader.model.enums import PriceType from nautilus_trader.model.identifiers import InstrumentId +from nautilus_trader.model.objects import AccountBalance +from nautilus_trader.model.objects import Money from nautilus_trader.model.objects import Price from nautilus_trader.model.objects import Quantity -from nautilus_trader.model.orderbook.data import Order from nautilus_trader.model.orderbook.data import OrderBookDelta from nautilus_trader.model.orderbook.data import OrderBookDeltas from nautilus_trader.model.orderbook.data import OrderBookSnapshot +from nautilus_trader.model.orders.base import Order def parse_book_snapshot_ws( @@ -108,7 +113,7 @@ def parse_book_delta_ws( ) -def parse_ticker_ws(instrument_id: InstrumentId, msg: Dict, ts_init: int): +def parse_ticker_ws(instrument_id: InstrumentId, msg: Dict, ts_init: int) -> BinanceTicker: return BinanceTicker( instrument_id=instrument_id, price_change=Decimal(msg["p"]), @@ -134,7 +139,7 @@ def parse_ticker_ws(instrument_id: InstrumentId, msg: Dict, ts_init: int): ) -def parse_quote_tick_ws(instrument_id: InstrumentId, msg: Dict, ts_init: int): +def parse_quote_tick_ws(instrument_id: InstrumentId, msg: Dict, ts_init: int) -> QuoteTick: return QuoteTick( instrument_id=instrument_id, bid=Price.from_str(msg["b"]), @@ -146,7 +151,7 @@ def parse_quote_tick_ws(instrument_id: InstrumentId, msg: Dict, ts_init: int): ) -def parse_trade_tick(instrument_id: InstrumentId, msg: Dict, ts_init: int): +def parse_trade_tick(instrument_id: InstrumentId, msg: Dict, ts_init: int) -> TradeTick: return TradeTick( instrument_id=instrument_id, price=Price.from_str(msg["price"]), @@ -158,7 +163,7 @@ def parse_trade_tick(instrument_id: InstrumentId, msg: Dict, ts_init: int): ) -def parse_trade_tick_ws(instrument_id: InstrumentId, msg: Dict, ts_init: int): +def parse_trade_tick_ws(instrument_id: InstrumentId, msg: Dict, ts_init: int) -> TradeTick: return TradeTick( instrument_id=instrument_id, price=Price.from_str(msg["p"]), @@ -170,7 +175,7 @@ def parse_trade_tick_ws(instrument_id: InstrumentId, msg: Dict, ts_init: int): ) -def parse_bar(bar_type: BarType, values: List, ts_init: int): +def parse_bar(bar_type: BarType, values: List, ts_init: int) -> BinanceBar: return BinanceBar( bar_type=bar_type, open=Price.from_str(values[1]), @@ -187,7 +192,12 @@ def parse_bar(bar_type: BarType, values: List, ts_init: int): ) -def parse_bar_ws(instrument_id: InstrumentId, kline: Dict, ts_event: int, ts_init: int): +def parse_bar_ws( + instrument_id: InstrumentId, + kline: Dict, + ts_event: int, + ts_init: int, +) -> BinanceBar: interval = kline["i"] resolution = interval[1] if resolution == "m": @@ -225,3 +235,87 @@ def parse_bar_ws(instrument_id: InstrumentId, kline: Dict, ts_event: int, ts_ini ts_event=ts_event, ts_init=ts_init, ) + + +def parse_account_balances(raw_balances: List[Dict[str, str]]) -> List[AccountBalance]: + return _parse_balances(raw_balances, "asset", "free", "locked") + + +def parse_account_balances_ws(raw_balances: List[Dict[str, str]]) -> List[AccountBalance]: + return _parse_balances(raw_balances, "a", "f", "l") + + +def _parse_balances( + raw_balances: List[Dict[str, str]], + asset_key: str, + free_key: str, + locked_key: str, +) -> List[AccountBalance]: + parsed_balances: Dict[Currency, Tuple[Decimal, Decimal, Decimal]] = {} + for b in raw_balances: + currency = Currency.from_str(b[asset_key]) + free = Decimal(b[free_key]) + locked = Decimal(b[locked_key]) + total: Decimal = free + locked + parsed_balances[currency] = (total, locked, free) + + balances: List[AccountBalance] = [ + AccountBalance( + currency=currency, + total=Money(values[0], currency), + locked=Money(values[1], currency), + free=Money(values[2], currency), + ) + for currency, values in parsed_balances.items() + ] + + return balances + + +def parse_order_type(order_type: str) -> OrderType: + if order_type == "STOP_LOSS": + return OrderType.STOP_MARKET + elif order_type == "STOP_LOSS_LIMIT": + return OrderType.STOP_LIMIT + elif order_type == "TAKE_PROFIT": + return OrderType.LIMIT + elif order_type == "TAKE_PROFIT_LIMIT": + return OrderType.STOP_LIMIT + elif order_type == "LIMIT_MAKER": + return OrderType.LIMIT + else: + return OrderTypeParser.from_str_py(order_type) + + +def binance_order_type(order: Order, market_price: Decimal = None) -> str: # noqa + if order.type == OrderType.LIMIT: + if order.is_post_only: + return "LIMIT_MAKER" + else: + return "LIMIT" + elif order.type == OrderType.STOP_MARKET: + if order.side == OrderSide.BUY: + if order.price < market_price: + return "TAKE_PROFIT" + else: + return "STOP_LOSS" + else: # OrderSide.SELL + if order.price > market_price: + return "TAKE_PROFIT" + else: + return "STOP_LOSS" + elif order.type == OrderType.STOP_LIMIT: + if order.side == OrderSide.BUY: + if order.trigger < market_price: + return "TAKE_PROFIT_LIMIT" + else: + return "STOP_LOSS_LIMIT" + else: # OrderSide.SELL + if order.trigger > market_price: + return "TAKE_PROFIT_LIMIT" + else: + return "STOP_LOSS_LIMIT" + elif order.type == OrderType.MARKET: + return "MARKET" + else: # pragma: no cover (design-time error) + raise RuntimeError("invalid order type") diff --git a/nautilus_trader/adapters/binance/providers.py b/nautilus_trader/adapters/binance/providers.py index 73b16628cad1..2524d58c6c40 100644 --- a/nautilus_trader/adapters/binance/providers.py +++ b/nautilus_trader/adapters/binance/providers.py @@ -16,14 +16,13 @@ import asyncio import time from decimal import Decimal -from typing import Dict - -import orjson +from typing import Any, Dict, List from nautilus_trader.adapters.binance.common import BINANCE_VENUE from nautilus_trader.adapters.binance.http.api.spot_market import BinanceSpotMarketHttpAPI from nautilus_trader.adapters.binance.http.api.wallet import BinanceWalletHttpAPI from nautilus_trader.adapters.binance.http.client import BinanceHttpClient +from nautilus_trader.adapters.binance.http.error import BinanceClientError from nautilus_trader.common.logging import Logger from nautilus_trader.common.logging import LoggerAdapter from nautilus_trader.common.providers import InstrumentProvider @@ -102,15 +101,21 @@ async def load_all_async(self) -> None: self._loading = True # Get current commission rates - raw: bytes = await self._wallet.trade_fee() - fees: Dict[str, Dict[str, str]] = {s["symbol"]: s for s in orjson.loads(raw)} + try: + fee_res: List[Dict[str, str]] = await self._wallet.trade_fee() + fees: Dict[str, Dict[str, str]] = {s["symbol"]: s for s in fee_res} + except BinanceClientError: + self._log.error( + "Cannot load instruments: API key authentication failed " + "(this is needed to fetch the applicable account fee tier).", + ) + return # Get exchange info for all assets - raw = await self._spot_market.exchange_info() - response = orjson.loads(raw) - server_time_ns: int = millis_to_nanos(response["serverTime"]) + assets_res: Dict[str, Any] = await self._spot_market.exchange_info() + server_time_ns: int = millis_to_nanos(assets_res["serverTime"]) - for info in response["symbols"]: + for info in assets_res["symbols"]: local_symbol = Symbol(info["symbol"]) # Create base asset diff --git a/nautilus_trader/adapters/binance/websocket/futures.py b/nautilus_trader/adapters/binance/websocket/futures.py index 96ad00fbf158..2637883c1744 100644 --- a/nautilus_trader/adapters/binance/websocket/futures.py +++ b/nautilus_trader/adapters/binance/websocket/futures.py @@ -177,9 +177,3 @@ def subscribe_diff_book_depth(self, symbol: str, speed: int): """ self._add_stream(f"{symbol.lower()}@depth@{speed}ms") - - def subscribe_user_data(self, listen_key: str): - """ - Listen to user data by provided `listenkey`. - """ - self._add_stream(listen_key) diff --git a/nautilus_trader/adapters/binance/websocket/spot.py b/nautilus_trader/adapters/binance/websocket/spot.py index c63bc87e2de3..c416c4f097c6 100644 --- a/nautilus_trader/adapters/binance/websocket/spot.py +++ b/nautilus_trader/adapters/binance/websocket/spot.py @@ -163,9 +163,3 @@ def subscribe_diff_book_depth(self, symbol: str, speed: int): """ self._add_stream(f"{format_symbol(symbol)}@depth@{speed}ms") - - def subscribe_user_data(self, listen_key: str): - """ - Listen to user data by provided `listenkey`. - """ - self._add_stream(listen_key) diff --git a/nautilus_trader/adapters/binance/websocket/user.py b/nautilus_trader/adapters/binance/websocket/user.py new file mode 100644 index 000000000000..105cdf2235d3 --- /dev/null +++ b/nautilus_trader/adapters/binance/websocket/user.py @@ -0,0 +1,57 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2021 Nautech Systems Pty Ltd. All rights reserved. +# https://nautechsystems.io +# +# Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Heavily refactored from MIT licensed github.com/binance/binance-connector-python +# Original author: Jeremy https://github.com/2pd +# ------------------------------------------------------------------------------------------------- + +import asyncio +from typing import Callable + +from nautilus_trader.adapters.binance.websocket.client import BinanceWebSocketClient +from nautilus_trader.common.clock import LiveClock +from nautilus_trader.common.logging import Logger + + +class BinanceUserDataWebSocket(BinanceWebSocketClient): + """ + Provides access to the `Binance User Data` streaming WebSocket API. + """ + + def __init__( + self, + loop: asyncio.AbstractEventLoop, + clock: LiveClock, + logger: Logger, + handler: Callable[[bytes], None], + ): + super().__init__( + loop=loop, + clock=clock, + logger=logger, + handler=handler, + base_url="wss://stream.binance.com:9443", + ) + + def subscribe(self, key: str): + """ + Subscribe to the user data stream. + + Parameters + ---------- + key : str + The listen key for the subscription. + + """ + self._add_stream(key) diff --git a/nautilus_trader/adapters/ftx/data.py b/nautilus_trader/adapters/ftx/data.py index 84024dd4b355..30edecb3a518 100644 --- a/nautilus_trader/adapters/ftx/data.py +++ b/nautilus_trader/adapters/ftx/data.py @@ -107,7 +107,7 @@ async def _connect(self): try: await self._instrument_provider.load_all_or_wait_async() except FTXError as ex: - self._log.ex(ex) + self._log.exception(ex) return self._send_all_instruments_to_data_engine() diff --git a/nautilus_trader/adapters/ftx/execution.py b/nautilus_trader/adapters/ftx/execution.py index 0ba1b427692d..3be6d2fade74 100644 --- a/nautilus_trader/adapters/ftx/execution.py +++ b/nautilus_trader/adapters/ftx/execution.py @@ -117,7 +117,7 @@ async def _connect(self): try: await self._instrument_provider.load_all_or_wait_async() except FTXError as ex: - self._log.ex(ex) + self._log.exception(ex) return self._set_connected(True) diff --git a/nautilus_trader/backtest/config.py b/nautilus_trader/backtest/config.py index 1901d2a21e79..822d9ef9fab1 100644 --- a/nautilus_trader/backtest/config.py +++ b/nautilus_trader/backtest/config.py @@ -173,14 +173,13 @@ def load(self, start_time=None, end_time=None): ) catalog = self.catalog() + instruments = catalog.instruments(instrument_ids=self.instrument_id, as_nautilus=True) + if not instruments: + return {"data": [], "instrument": None} return { "type": query["cls"], "data": catalog.query(**query), - "instrument": catalog.instruments(instrument_ids=self.instrument_id, as_nautilus=True)[ - 0 - ] - if self.instrument_id - else None, + "instrument": instruments[0] if self.instrument_id else None, "client_id": ClientId(self.client_id) if self.client_id else None, } diff --git a/nautilus_trader/backtest/data/providers.py b/nautilus_trader/backtest/data/providers.py index d946d9da8eee..d626643936df 100644 --- a/nautilus_trader/backtest/data/providers.py +++ b/nautilus_trader/backtest/data/providers.py @@ -36,6 +36,7 @@ from nautilus_trader.model.currencies import USDT from nautilus_trader.model.currency import Currency from nautilus_trader.model.enums import AssetClass +from nautilus_trader.model.enums import OptionKind from nautilus_trader.model.identifiers import InstrumentId from nautilus_trader.model.identifiers import Symbol from nautilus_trader.model.identifiers import Venue @@ -414,6 +415,7 @@ def aapl_option(): multiplier=Quantity.from_int(100), lot_size=Quantity.from_int(1), underlying="AAPL", + kind=OptionKind.CALL, expiry_date=datetime.date(2021, 12, 17), strike_price=Price.from_str("149.00"), ts_event=0, diff --git a/nautilus_trader/backtest/engine.pyx b/nautilus_trader/backtest/engine.pyx index 88978720bf22..3efa5d7246d7 100644 --- a/nautilus_trader/backtest/engine.pyx +++ b/nautilus_trader/backtest/engine.pyx @@ -28,9 +28,10 @@ from nautilus_trader.backtest.data_client cimport BacktestMarketDataClient from nautilus_trader.backtest.exchange cimport SimulatedExchange from nautilus_trader.backtest.execution_client cimport BacktestExecClient from nautilus_trader.backtest.models cimport FillModel +from nautilus_trader.backtest.models cimport LatencyModel from nautilus_trader.backtest.modules cimport SimulationModule from nautilus_trader.cache.cache cimport Cache -from nautilus_trader.common.actor import Actor +from nautilus_trader.common.actor cimport Actor from nautilus_trader.common.clock cimport LiveClock from nautilus_trader.common.clock cimport TestClock from nautilus_trader.common.logging cimport Logger @@ -242,7 +243,7 @@ cdef class BacktestEngine: Returns ------- - List[Venue] + list[Venue] """ return list(self._exchanges) @@ -513,6 +514,7 @@ cdef class BacktestEngine: bint is_frozen_account=False, list modules=None, FillModel fill_model=None, + LatencyModel latency_model=None, BookType book_type=BookType.L1_TBBO, bar_execution: bool=False, reject_stop_orders: bool=True, @@ -544,7 +546,9 @@ cdef class BacktestEngine: modules : list[SimulationModule, optional The simulation modules to load into the exchange. fill_model : FillModel, optional - The fill model for the exchange (if None then no probabilistic fills). + The fill model for the exchange. + latency_model : LatencyModel, optional + The latency model for the exchange. book_type : BookType The default order book type for fill modelling. bar_execution : bool @@ -583,6 +587,7 @@ cdef class BacktestEngine: modules=modules, cache=self._cache, fill_model=fill_model, + latency_model=latency_model, book_type=book_type, clock=self._test_clock, logger=self._test_logger, @@ -626,13 +631,13 @@ cdef class BacktestEngine: self._exchanges[venue].set_fill_model(model) - def add_component(self, component: Actor) -> None: + def add_actor(self, actor: Actor) -> None: # Checked inside trader - self.trader.add_component(component) + self.trader.add_actor(actor) - def add_components(self, components: List[Actor]) -> None: + def add_actors(self, actors: List[Actor]) -> None: # Checked inside trader - self.trader.add_components(components) + self.trader.add_actors(actors) def add_strategy(self, strategy: TradingStrategy) -> None: # Checked inside trader @@ -867,6 +872,8 @@ cdef class BacktestEngine: # Set clocks self._test_clock.set_time(start_ns) + for actor in self.trader.actors_c(): + actor.clock.set_time(start_ns) for strategy in self.trader.strategies_c(): strategy.clock.set_time(start_ns) @@ -937,9 +944,13 @@ cdef class BacktestEngine: return self._data[cursor] cdef void _advance_time(self, int64_t now_ns) except *: - cdef TradingStrategy strategy - cdef TimeEventHandler event_handler cdef list time_events = [] # type: list[TimeEventHandler] + cdef: + Actor actor + TradingStrategy strategy + cdef TimeEventHandler event_handler + for actor in self.trader.actors_c(): + time_events += actor.clock.advance_time(now_ns) for strategy in self.trader.strategies_c(): time_events += strategy.clock.advance_time(now_ns) for event_handler in sorted(time_events): diff --git a/nautilus_trader/backtest/exchange.pyx b/nautilus_trader/backtest/exchange.pyx index b2fb684aae02..191bfc528f41 100644 --- a/nautilus_trader/backtest/exchange.pyx +++ b/nautilus_trader/backtest/exchange.pyx @@ -92,13 +92,13 @@ cdef class SimulatedExchange: list instruments not None, list modules not None, CacheFacade cache not None, - FillModel fill_model not None, TestClock clock not None, Logger logger not None, + FillModel fill_model not None, + LatencyModel latency_model=None, BookType book_type=BookType.L1_TBBO, bint bar_execution=False, bint reject_stop_orders=True, - LatencyModel latency_model=None ): """ Initialize a new instance of the ``SimulatedExchange`` class. @@ -127,6 +127,8 @@ cdef class SimulatedExchange: The read-only cache for the exchange. fill_model : FillModel The fill model for the exchange. + latency_model : LatencyModel, optional + The latency model for the exchange. clock : TestClock The clock for the exchange. logger : Logger @@ -137,8 +139,6 @@ cdef class SimulatedExchange: If the exchange execution dynamics is based on bar data. reject_stop_orders : bool If stop orders are rejected on submission if in the market. - latency_model : LatencyModel, optional - The latency model for the exchange. Raises ------ diff --git a/nautilus_trader/backtest/models.pyx b/nautilus_trader/backtest/models.pyx index 4b6e68b18a42..eea7ecac6866 100644 --- a/nautilus_trader/backtest/models.pyx +++ b/nautilus_trader/backtest/models.pyx @@ -117,7 +117,7 @@ cdef class FillModel: cdef class LatencyModel: """ - Provides a latency model for messages coming from and going to a simulated exchange. + Provides a latency model for simulated exchange message I/O. """ def __init__( diff --git a/nautilus_trader/backtest/node.py b/nautilus_trader/backtest/node.py index 6758cc000013..bfe625ae984a 100644 --- a/nautilus_trader/backtest/node.py +++ b/nautilus_trader/backtest/node.py @@ -202,7 +202,7 @@ def _run( if actor_configs: actors: List[Actor] = [ActorFactory.create(config) for config in actor_configs] if actors: - engine.add_components(actors) + engine.add_actors(actors) # Create strategies if strategy_configs: @@ -293,6 +293,12 @@ def backtest_runner( # Load data for config in data_configs: d = config.load() + if config.instrument_id and d["instrument"] is None: + print(f"Requested instrument_id={d['instrument']} from data_config not found catalog") + continue + if not d["data"]: + print(f"No data found for {config}") + continue _load_engine_data(engine=engine, data=d) return engine.run(run_config_id=run_config_id) diff --git a/nautilus_trader/common/actor.pxd b/nautilus_trader/common/actor.pxd index cc410512cfe9..e91775f6626f 100644 --- a/nautilus_trader/common/actor.pxd +++ b/nautilus_trader/common/actor.pxd @@ -48,6 +48,8 @@ from nautilus_trader.msgbus.bus cimport MessageBus cdef class Actor(Component): cdef set _warning_events + cdef readonly Clock clock + """The actors clock.\n\n:returns: `Clock`""" cdef readonly MessageBus msgbus """The message bus for the actor (if registered).\n\n:returns: `MessageBus` or ``None``""" cdef readonly CacheFacade cache diff --git a/nautilus_trader/common/actor.pyx b/nautilus_trader/common/actor.pyx index 06d365f96925..491fe10ea929 100644 --- a/nautilus_trader/common/actor.pyx +++ b/nautilus_trader/common/actor.pyx @@ -115,6 +115,7 @@ cdef class Actor(Component): self.trader_id = None # Initialized when registered self.msgbus = None # Initialized when registered self.cache = None # Initialized when registered + self.clock = None # Initialized when registered # -- ABSTRACT METHODS ------------------------------------------------------------------------------ @@ -464,6 +465,7 @@ cdef class Actor(Component): self.trader_id = trader_id self.msgbus = msgbus self.cache = cache + self.clock = self._clock cpdef void register_warning_event(self, type event): """ @@ -1854,7 +1856,7 @@ cdef class Actor(Component): raise cpdef void _handle_data_response(self, DataResponse response) except *: - self.handle_bars(response.data) + self.handle_data(response.data) cpdef void _handle_quote_ticks_response(self, DataResponse response) except *: self.handle_quote_ticks(response.data) diff --git a/nautilus_trader/common/events/risk.pyx b/nautilus_trader/common/events/risk.pyx index 25a6345881aa..594ab923d771 100644 --- a/nautilus_trader/common/events/risk.pyx +++ b/nautilus_trader/common/events/risk.pyx @@ -78,8 +78,10 @@ cdef class TradingStateChanged(RiskEvent): ---------- trader_id : TraderId The trader ID associated with the event. - trader_id : TraderId + state : TradingState The trading state for the event. + config : dict + The configuration of the risk engine. event_id : UUID4 The event ID. ts_event : int64 diff --git a/nautilus_trader/common/factories.pxd b/nautilus_trader/common/factories.pxd index da63d2badc31..0999d6b73e3d 100644 --- a/nautilus_trader/common/factories.pxd +++ b/nautilus_trader/common/factories.pxd @@ -68,7 +68,7 @@ cdef class OrderFactory: datetime expire_time=*, bint post_only=*, bint reduce_only=*, - bint hidden=*, + Quantity display_qty=*, str tags=*, ) @@ -95,7 +95,7 @@ cdef class OrderFactory: datetime expire_time=*, bint post_only=*, bint reduce_only=*, - bint hidden=*, + Quantity display_qty=*, str tags=*, ) diff --git a/nautilus_trader/common/factories.pyx b/nautilus_trader/common/factories.pyx index 5246818d7e19..5a52fd46418d 100644 --- a/nautilus_trader/common/factories.pyx +++ b/nautilus_trader/common/factories.pyx @@ -189,7 +189,7 @@ cdef class OrderFactory: datetime expire_time=None, bint post_only=False, bint reduce_only=False, - bint hidden=False, + Quantity display_qty=None, str tags=None, ): """ @@ -215,8 +215,8 @@ cdef class OrderFactory: If the order will only provide liquidity (make a market). reduce_only : bool, optional If the order carries the 'reduce-only' execution instruction. - hidden : bool, optional - If the order should be hidden from the public book. + display_qty : Quantity, optional + The quantity of the order to display on the public book (iceberg). tags : str, optional The custom user tags for the order. These are optional and can contain any arbitrary delimiter if required. @@ -232,9 +232,7 @@ cdef class OrderFactory: ValueError If `time_in_force` is ``GTD`` and `expire_time` is ``None``. ValueError - If `post_only` and `hidden`. - ValueError - If `hidden` and `post_only`. + If `display_qty` is negative (< 0) or greater than `quantity`. """ return LimitOrder( @@ -251,7 +249,7 @@ cdef class OrderFactory: ts_init=self._clock.timestamp_ns(), post_only=post_only, reduce_only=reduce_only, - hidden=hidden, + display_qty=display_qty, order_list_id=None, parent_order_id=None, child_order_ids=None, @@ -340,7 +338,7 @@ cdef class OrderFactory: datetime expire_time=None, bint post_only=False, bint reduce_only=False, - bint hidden=False, + Quantity display_qty=None, str tags=None, ): """ @@ -368,8 +366,8 @@ cdef class OrderFactory: If the order will only provide liquidity (make a market). reduce_only : bool, optional If the order carries the 'reduce-only' execution instruction. - hidden : bool, optional - If the order should be hidden from the public book. + display_qty : Quantity, optional + The quantity of the order to display on the public book (iceberg). tags : str, optional The custom user tags for the order. These are optional and can contain any arbitrary delimiter if required. @@ -385,9 +383,7 @@ cdef class OrderFactory: ValueError If `time_in_force` is ``GTD`` and `expire_time` is ``None``. ValueError - If `post_only` and `hidden`. - ValueError - If `hidden` and `post_only`. + If `display_qty` is negative (< 0) or greater than `quantity`. """ return StopLimitOrder( @@ -405,7 +401,7 @@ cdef class OrderFactory: ts_init=self._clock.timestamp_ns(), post_only=post_only, reduce_only=reduce_only, - hidden=hidden, + display_qty=display_qty, order_list_id=None, parent_order_id=None, child_order_ids=None, @@ -526,7 +522,6 @@ cdef class OrderFactory: ts_init=self._clock.timestamp_ns(), post_only=True, reduce_only=True, - hidden=False, order_list_id=order_list_id, parent_order_id=entry_client_order_id, child_order_ids=None, @@ -673,10 +668,9 @@ cdef class OrderFactory: ts_init=self._clock.timestamp_ns(), post_only=True, reduce_only=True, - hidden=False, + display_qty=None, order_list_id=order_list_id, parent_order_id=entry_client_order_id, - child_order_ids=None, contingency=ContingencyType.OCO, contingency_ids=[stop_loss_client_order_id], tags="TAKE_PROFIT", diff --git a/nautilus_trader/common/logging.pyx b/nautilus_trader/common/logging.pyx index ed90f2ffc560..180101a513b3 100644 --- a/nautilus_trader/common/logging.pyx +++ b/nautilus_trader/common/logging.pyx @@ -498,7 +498,7 @@ cpdef void nautilus_header(LoggerAdapter logger) except *: Condition.not_none(logger, "logger") print("") # New line to begin logger.info("\033[36m=================================================================") - logger.info(f"\033[36m NAUTILUS TRADER - Algorithmic Trading Platform") + logger.info(f"\033[36m NAUTILUS TRADER - Automated Algorithmic Trading Platform") logger.info(f"\033[36m by Nautech Systems Pty Ltd.") logger.info(f"\033[36m Copyright (C) 2015-2021. All rights reserved.") logger.info("\033[36m=================================================================") diff --git a/nautilus_trader/examples/strategies/ema_cross.py b/nautilus_trader/examples/strategies/ema_cross.py index 9d6666cfbbae..67cd3476df43 100644 --- a/nautilus_trader/examples/strategies/ema_cross.py +++ b/nautilus_trader/examples/strategies/ema_cross.py @@ -114,14 +114,14 @@ def on_start(self): self.register_indicator_for_bars(self.bar_type, self.slow_ema) # Get historical data - # self.request_bars(self.bar_type) + self.request_bars(self.bar_type) # self.request_quote_ticks(self.instrument_id) # self.request_trade_ticks(self.instrument_id) # Subscribe to live data - self.subscribe_bars(self.bar_type) # For debugging + self.subscribe_bars(self.bar_type) + self.subscribe_quote_ticks(self.instrument_id) # self.subscribe_ticker(self.instrument_id) # For debugging - # self.subscribe_quote_ticks(self.instrument_id) # For debugging # self.subscribe_trade_ticks(self.instrument_id) # For debugging # self.subscribe_order_book_deltas(self.instrument_id, depth=20) # For debugging # self.subscribe_order_book_snapshots(self.instrument_id, depth=20) # For debugging @@ -187,7 +187,7 @@ def on_quote_tick(self, tick: QuoteTick): The quote tick received. """ - self.log.info(f"Received {repr(tick)}") # For debugging (must add a subscription) + # self.log.info(f"Received {repr(tick)}") # For debugging (must add a subscription) def on_trade_tick(self, tick: TradeTick): """ @@ -295,11 +295,12 @@ def on_stop(self): self.flatten_all_positions(self.instrument_id) # Unsubscribe from data + self.unsubscribe_bars(self.bar_type) + self.unsubscribe_quote_ticks(self.instrument_id) + # self.unsubscribe_ticker(self.instrument_id) # self.unsubscribe_order_book_deltas(self.instrument_id) - self.unsubscribe_bars(self.bar_type) # self.unsubscribe_order_book_snapshots(self.instrument_id) - # self.unsubscribe_quote_ticks(self.instrument_id) # self.unsubscribe_trade_ticks(self.instrument_id) def on_reset(self): diff --git a/nautilus_trader/examples/strategies/volatility_market_maker.py b/nautilus_trader/examples/strategies/volatility_market_maker.py index 123af2d35f76..6d74f28aeb90 100644 --- a/nautilus_trader/examples/strategies/volatility_market_maker.py +++ b/nautilus_trader/examples/strategies/volatility_market_maker.py @@ -24,7 +24,6 @@ from nautilus_trader.model.data.bar import BarType from nautilus_trader.model.data.tick import QuoteTick from nautilus_trader.model.data.tick import TradeTick -from nautilus_trader.model.enums import BookType from nautilus_trader.model.enums import OrderSide from nautilus_trader.model.enums import TimeInForce from nautilus_trader.model.events.order import OrderFilled @@ -121,13 +120,6 @@ def on_start(self): # Subscribe to live data self.subscribe_bars(self.bar_type) self.subscribe_quote_ticks(self.instrument_id) - self.subscribe_order_book_snapshots( - self.instrument_id, - book_type=BookType.L2_MBP, - depth=10, - interval_ms=1000, - ) # For debugging - # self.subscribe_trade_ticks(self.instrument_id) # For debugging def on_instrument(self, instrument: Instrument): """ @@ -228,7 +220,7 @@ def create_buy_order(self, last: QuoteTick): price=self.instrument.make_price(price), time_in_force=TimeInForce.GTC, post_only=True, # default value is True - hidden=False, # default value is False + display_qty=self.instrument.make_qty(self.trade_size / 2), # iceberg ) self.buy_order = order @@ -246,7 +238,7 @@ def create_sell_order(self, last: QuoteTick): price=self.instrument.make_price(price), time_in_force=TimeInForce.GTC, post_only=True, # default value is True - hidden=False, # default value is False + display_qty=self.instrument.make_qty(self.trade_size / 2), # iceberg ) self.sell_order = order @@ -298,8 +290,6 @@ def on_stop(self): # Unsubscribe from data self.unsubscribe_bars(self.bar_type) self.unsubscribe_quote_ticks(self.instrument_id) - self.unsubscribe_order_book_snapshots(self.instrument_id, interval_ms=1000) - # self.unsubscribe_trade_ticks(self.instrument_id) def on_reset(self): """ diff --git a/nautilus_trader/model/c_enums/option_kind.pxd b/nautilus_trader/model/c_enums/option_kind.pxd new file mode 100644 index 000000000000..2bc815ac8c3b --- /dev/null +++ b/nautilus_trader/model/c_enums/option_kind.pxd @@ -0,0 +1,28 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2021 Nautech Systems Pty Ltd. All rights reserved. +# https://nautechsystems.io +# +# Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ------------------------------------------------------------------------------------------------- + + +cpdef enum OptionKind: + CALL = 1 + PUT = 2 + + +cdef class OptionKindParser: + + @staticmethod + cdef str to_str(int value) + + @staticmethod + cdef OptionKind from_str(str value) except * diff --git a/nautilus_trader/model/c_enums/option_kind.pyx b/nautilus_trader/model/c_enums/option_kind.pyx new file mode 100644 index 000000000000..e43fda8483bf --- /dev/null +++ b/nautilus_trader/model/c_enums/option_kind.pyx @@ -0,0 +1,43 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2021 Nautech Systems Pty Ltd. All rights reserved. +# https://nautechsystems.io +# +# Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ------------------------------------------------------------------------------------------------- + + +cdef class OptionKindParser: + + @staticmethod + cdef str to_str(int value): + if value == 1: + return "CALL" + elif value == 2: + return "PUT" + else: + raise ValueError(f"value was invalid, was {value}") + + @staticmethod + cdef OptionKind from_str(str value) except *: + if value == "CALL": + return OptionKind.CALL + elif value == "PUT": + return OptionKind.PUT + else: + raise ValueError(f"value was invalid, was {value}") + + @staticmethod + def to_str_py(int value): + return OptionKindParser.to_str(value) + + @staticmethod + def from_str_py(str value): + return OptionKindParser.from_str(value) diff --git a/nautilus_trader/model/enums.pyx b/nautilus_trader/model/enums.pyx index cf4e01540372..3bbc31d2f49b 100644 --- a/nautilus_trader/model/enums.pyx +++ b/nautilus_trader/model/enums.pyx @@ -43,6 +43,8 @@ from nautilus_trader.model.c_enums.liquidity_side import LiquiditySide from nautilus_trader.model.c_enums.liquidity_side import LiquiditySideParser # noqa F401 (being used) from nautilus_trader.model.c_enums.oms_type import OMSType # noqa F401 (being used) from nautilus_trader.model.c_enums.oms_type import OMSTypeParser # noqa F401 (being used) +from nautilus_trader.model.c_enums.option_kind import OptionKind # noqa F401 (being used) +from nautilus_trader.model.c_enums.option_kind import OptionKindParser # noqa F401 (being used) from nautilus_trader.model.c_enums.order_side import OrderSide # noqa F401 (being used) from nautilus_trader.model.c_enums.order_side import OrderSideParser # noqa F401 (being used) from nautilus_trader.model.c_enums.order_status import OrderStatus # noqa F401 (being used) diff --git a/nautilus_trader/model/instruments/option.pxd b/nautilus_trader/model/instruments/option.pxd index f878917a872a..a5301d4c9f56 100644 --- a/nautilus_trader/model/instruments/option.pxd +++ b/nautilus_trader/model/instruments/option.pxd @@ -15,6 +15,7 @@ from cpython.datetime cimport date +from nautilus_trader.model.c_enums.option_kind cimport OptionKind from nautilus_trader.model.instruments.base cimport Instrument from nautilus_trader.model.objects cimport Price @@ -23,6 +24,7 @@ cdef class Option(Instrument): cdef readonly str underlying cdef readonly date expiry_date cdef readonly Price strike_price + cdef readonly OptionKind kind @staticmethod cdef Option from_dict_c(dict values) diff --git a/nautilus_trader/model/instruments/option.pyx b/nautilus_trader/model/instruments/option.pyx index d7a0a036aa10..f5c72f5d13dc 100644 --- a/nautilus_trader/model/instruments/option.pyx +++ b/nautilus_trader/model/instruments/option.pyx @@ -22,6 +22,8 @@ from nautilus_trader.core.correctness cimport Condition from nautilus_trader.model.c_enums.asset_class cimport AssetClass from nautilus_trader.model.c_enums.asset_class cimport AssetClassParser from nautilus_trader.model.c_enums.asset_type cimport AssetType +from nautilus_trader.model.c_enums.option_kind cimport OptionKind +from nautilus_trader.model.c_enums.option_kind cimport OptionKindParser from nautilus_trader.model.currency cimport Currency from nautilus_trader.model.identifiers cimport InstrumentId from nautilus_trader.model.identifiers cimport Symbol @@ -48,6 +50,7 @@ cdef class Option(Instrument): Price strike_price not None, str underlying, date expiry_date, + OptionKind kind, int64_t ts_event, int64_t ts_init, ): @@ -123,8 +126,10 @@ cdef class Option(Instrument): ts_init=ts_init, info={}, ) + self.underlying = underlying self.expiry_date = expiry_date self.strike_price = strike_price + self.kind = kind @staticmethod cdef Option from_dict_c(dict values): @@ -133,16 +138,17 @@ cdef class Option(Instrument): instrument_id=InstrumentId.from_str_c(values["id"]), local_symbol=Symbol(values["local_symbol"]), asset_class=AssetClassParser.from_str(values["asset_class"]), - currency=Currency.from_str_c(values['currency']), - price_precision=values['price_precision'], - price_increment=Price.from_str(values['price_increment']), - multiplier=Quantity.from_str(values['multiplier']), - lot_size=Quantity.from_str(values['lot_size']), + currency=Currency.from_str_c(values["currency"]), + price_precision=values["price_precision"], + price_increment=Price.from_str(values["price_increment"]), + multiplier=Quantity.from_str(values["multiplier"]), + lot_size=Quantity.from_str(values["lot_size"]), underlying=values['underlying'], - expiry_date=date.fromisoformat(values['expiry_date']), - strike_price=Price.from_str(values['strike_price']), - ts_event=values['ts_event'], - ts_init=values['ts_init'], + expiry_date=date.fromisoformat(values["expiry_date"]), + strike_price=Price.from_str(values["strike_price"]), + kind=OptionKindParser.from_str(values["kind"]), + ts_event=values["ts_event"], + ts_init=values["ts_init"], ) @staticmethod @@ -165,6 +171,7 @@ cdef class Option(Instrument): "strike_price": str(obj.strike_price), "margin_init": str(obj.margin_init), "margin_maint": str(obj.margin_maint), + "kind": OptionKindParser.to_str(obj.kind), "ts_event": obj.ts_event, "ts_init": obj.ts_init, } diff --git a/nautilus_trader/model/orders/limit.pxd b/nautilus_trader/model/orders/limit.pxd index 0ae999dfd6e9..0697c23b1b7b 100644 --- a/nautilus_trader/model/orders/limit.pxd +++ b/nautilus_trader/model/orders/limit.pxd @@ -14,14 +14,15 @@ # ------------------------------------------------------------------------------------------------- from nautilus_trader.model.events.order cimport OrderInitialized +from nautilus_trader.model.objects cimport Quantity from nautilus_trader.model.orders.base cimport PassiveOrder cdef class LimitOrder(PassiveOrder): cdef readonly bint is_post_only """If the order will only provide liquidity (make a market).\n\n:returns: `bool`""" - cdef readonly bint is_hidden - """If the order should be hidden from the public book.\n\n:returns: `bool`""" + cdef readonly Quantity display_qty + """The quantity of the order to display on the public book (iceberg).\n\n:returns: `Quantity` or ``None``""" @staticmethod cdef LimitOrder create(OrderInitialized init) diff --git a/nautilus_trader/model/orders/limit.pyx b/nautilus_trader/model/orders/limit.pyx index 81913365a2de..c358e8b5a0c0 100644 --- a/nautilus_trader/model/orders/limit.pyx +++ b/nautilus_trader/model/orders/limit.pyx @@ -62,7 +62,7 @@ cdef class LimitOrder(PassiveOrder): int64_t ts_init, bint post_only=False, bint reduce_only=False, - bint hidden=False, + Quantity display_qty=None, OrderListId order_list_id=None, ClientOrderId parent_order_id=None, list child_order_ids=None, @@ -101,8 +101,8 @@ cdef class LimitOrder(PassiveOrder): If the order will only provide liquidity (make a market). reduce_only : bool, optional If the order carries the 'reduce-only' execution instruction. - hidden : bool, optional - If the order should be hidden from the public book. + display_qty : Quantity, optional + The quantity of the order to display on the public book (iceberg). order_list_id : OrderListId, optional The order list ID associated with the order. parent_order_id : ClientOrderId, optional @@ -124,11 +124,10 @@ cdef class LimitOrder(PassiveOrder): ValueError If `time_in_force` is ``GTD`` and expire_time is ``None``. ValueError - If `post_only` and `hidden`. + If `display_qty` is negative (< 0) or greater than `quantity`. """ - if post_only: - Condition.false(hidden, "A post-only order cannot be hidden") + Condition.true(display_qty is None or 0 <= display_qty <= quantity, "display_qty was negative or greater than order quantity") # noqa super().__init__( trader_id=trader_id, strategy_id=strategy_id, @@ -143,7 +142,7 @@ cdef class LimitOrder(PassiveOrder): reduce_only=reduce_only, options={ "post_only": post_only, - "hidden": hidden, + "display_qty": str(display_qty) if display_qty is not None else None, }, order_list_id=order_list_id, parent_order_id=parent_order_id, @@ -156,7 +155,7 @@ cdef class LimitOrder(PassiveOrder): ) self.is_post_only = post_only - self.is_hidden = hidden + self.display_qty = display_qty cpdef dict to_dict(self): """ @@ -189,7 +188,7 @@ cdef class LimitOrder(PassiveOrder): "status": self._fsm.state_string_c(), "is_post_only": self.is_post_only, "is_reduce_only": self.is_reduce_only, - "is_hidden": self.is_hidden, + "display_qty": str(self.display_qty) if self.display_qty is not None else None, "order_list_id": self.order_list_id, "parent_order_id": self.parent_order_id, "child_order_ids": ",".join([o.value for o in self.child_order_ids]) if self.child_order_ids is not None else None, # noqa @@ -223,6 +222,11 @@ cdef class LimitOrder(PassiveOrder): Condition.not_none(init, "init") Condition.equal(init.type, OrderType.LIMIT, "init.type", "OrderType") + # Parse display quantity + cdef str display_qty_str = init.options["display_qty"] + cdef Quantity display_qty = None + if display_qty_str is not None: + display_qty = Quantity.from_str_c(display_qty_str) return LimitOrder( trader_id=init.trader_id, strategy_id=init.strategy_id, @@ -237,7 +241,7 @@ cdef class LimitOrder(PassiveOrder): ts_init=init.ts_init, post_only=init.options["post_only"], reduce_only=init.reduce_only, - hidden=init.options["hidden"], + display_qty=display_qty, order_list_id=init.order_list_id, parent_order_id=init.parent_order_id, child_order_ids=init.child_order_ids, diff --git a/nautilus_trader/model/orders/stop_limit.pxd b/nautilus_trader/model/orders/stop_limit.pxd index e8454fe147c6..d4940f8450ad 100644 --- a/nautilus_trader/model/orders/stop_limit.pxd +++ b/nautilus_trader/model/orders/stop_limit.pxd @@ -15,6 +15,7 @@ from nautilus_trader.model.events.order cimport OrderInitialized from nautilus_trader.model.objects cimport Price +from nautilus_trader.model.objects cimport Quantity from nautilus_trader.model.orders.base cimport PassiveOrder @@ -25,8 +26,8 @@ cdef class StopLimitOrder(PassiveOrder): """If the order has been triggered.\n\n:returns: `bool`""" cdef readonly bint is_post_only """If the order will only provide liquidity (make a market).\n\n:returns: `bool`""" - cdef readonly bint is_hidden - """If the order should be hidden from the public book.\n\n:returns: `bool`""" + cdef readonly Quantity display_qty + """The quantity of the `LIMIT` order to display on the public book (iceberg).\n\n:returns: `Quantity` or ``None``""" # noqa @staticmethod cdef StopLimitOrder create(OrderInitialized init) diff --git a/nautilus_trader/model/orders/stop_limit.pyx b/nautilus_trader/model/orders/stop_limit.pyx index 84694758c874..199c58ab4da9 100644 --- a/nautilus_trader/model/orders/stop_limit.pyx +++ b/nautilus_trader/model/orders/stop_limit.pyx @@ -73,7 +73,7 @@ cdef class StopLimitOrder(PassiveOrder): int64_t ts_init, bint post_only=False, bint reduce_only=False, - bint hidden=False, + Quantity display_qty=None, OrderListId order_list_id=None, ClientOrderId parent_order_id=None, list child_order_ids=None, @@ -114,8 +114,8 @@ cdef class StopLimitOrder(PassiveOrder): If the `LIMIT` order will only provide liquidity (once triggered). reduce_only : bool, optional If the `LIMIT` order carries the 'reduce-only' execution instruction. - hidden : bool, optional - If the `LIMIT` order should be hidden from the public book (once triggered). + display_qty : Quantity, optional + The quantity of the `LIMIT` order to display on the public book (iceberg). order_list_id : OrderListId, optional The order list ID associated with the order. parent_order_id : ClientOrderId, optional @@ -137,11 +137,10 @@ cdef class StopLimitOrder(PassiveOrder): ValueError If `time_in_force` is ``GTD`` and the expire_time is ``None``. ValueError - If `post_only` and `hidden`. + If `display_qty` is negative (< 0) or greater than `quantity`. """ - if post_only: - Condition.false(hidden, "A post-only order cannot be hidden") + Condition.true(display_qty is None or 0 <= display_qty <= quantity, "display_qty was negative or greater than order quantity") # noqa super().__init__( trader_id=trader_id, strategy_id=strategy_id, @@ -157,7 +156,7 @@ cdef class StopLimitOrder(PassiveOrder): options={ "trigger": str(trigger), "post_only": post_only, - "hidden": hidden, + "display_qty": str(display_qty) if display_qty is not None else None, }, order_list_id=order_list_id, parent_order_id=parent_order_id, @@ -172,7 +171,7 @@ cdef class StopLimitOrder(PassiveOrder): self.trigger = trigger self.is_triggered = False self.is_post_only = post_only - self.is_hidden = hidden + self.display_qty = display_qty def __repr__(self) -> str: cdef str id_string = f", id={self.venue_order_id.value})" if self.venue_order_id is not None else ")" @@ -215,7 +214,7 @@ cdef class StopLimitOrder(PassiveOrder): "status": self._fsm.state_string_c(), "is_post_only": self.is_post_only, "is_reduce_only": self.is_reduce_only, - "is_hidden": self.is_hidden, + "display_qty": str(self.display_qty) if self.display_qty is not None else None, "order_list_id": self.order_list_id, "parent_order_id": self.parent_order_id, "child_order_ids": ",".join([o.value for o in self.child_order_ids]) if self.child_order_ids is not None else None, # noqa @@ -249,6 +248,11 @@ cdef class StopLimitOrder(PassiveOrder): Condition.not_none(init, "init") Condition.equal(init.type, OrderType.STOP_LIMIT, "init.type", "OrderType") + # Parse display quantity + cdef str display_qty_str = init.options["display_qty"] + cdef Quantity display_qty = None + if display_qty_str is not None: + display_qty = Quantity.from_str_c(display_qty_str) return StopLimitOrder( trader_id=init.trader_id, strategy_id=init.strategy_id, @@ -264,7 +268,7 @@ cdef class StopLimitOrder(PassiveOrder): ts_init=init.ts_init, post_only=init.options["post_only"], reduce_only=init.reduce_only, - hidden=init.options["hidden"], + display_qty=display_qty, order_list_id=init.order_list_id, parent_order_id=init.parent_order_id, child_order_ids=init.child_order_ids, diff --git a/nautilus_trader/network/websocket.pyx b/nautilus_trader/network/websocket.pyx index 7cbd22571849..6e2ca272b788 100644 --- a/nautilus_trader/network/websocket.pyx +++ b/nautilus_trader/network/websocket.pyx @@ -76,7 +76,7 @@ cdef class WebSocketClient: ) -> None: Condition.valid_string(ws_url, "ws_url") - self._log.debug(f"Connecting to {ws_url}") + self._log.debug(f"Connecting WebSocket to {ws_url}") self._session = aiohttp.ClientSession(loop=self._loop) self._socket = await self._session.ws_connect(url=ws_url, **ws_kwargs) self._ws_url = ws_url @@ -86,7 +86,7 @@ cdef class WebSocketClient: task: Task = self._loop.create_task(self.start()) self._tasks.append(task) self.is_connected = True - self._log.debug("Websocket connected.") + self._log.debug("WebSocket connected.") async def post_connect(self): """ @@ -98,12 +98,13 @@ cdef class WebSocketClient: pass async def disconnect(self) -> None: + self._log.debug("Closing WebSocket...") self._trigger_stop = True await self._socket.close() while not self._stopped: await self._sleep0() self.is_connected = False - self._log.debug("Websocket closed.") + self._log.debug("WebSocket closed.") async def send(self, raw: bytes) -> None: self._log.debug(f"[SEND] {raw}") @@ -120,7 +121,7 @@ cdef class WebSocketClient: if self._trigger_stop is True: return self._log.warning(f"Received closing msg {msg}.") - raise ConnectionAbortedError("Websocket error or closed") + raise ConnectionAbortedError("WebSocket error or closed") else: self._log.warning( f"Received unknown data type: {msg.type} data: {msg.data}.", diff --git a/nautilus_trader/persistence/catalog.py b/nautilus_trader/persistence/catalog.py index 600461f51cec..0759021f5bf2 100644 --- a/nautilus_trader/persistence/catalog.py +++ b/nautilus_trader/persistence/catalog.py @@ -69,10 +69,12 @@ def __init__( The fs storage options. """ + self.path = pathlib.Path(path) + self.fs_protocol = fs_protocol + self.fs_storage_options = fs_storage_options or {} self.fs: fsspec.AbstractFileSystem = fsspec.filesystem( - fs_protocol, **(fs_storage_options or {}) + self.fs_protocol, **self.fs_storage_options ) - self.path = pathlib.Path(path) @classmethod def from_env(cls): diff --git a/nautilus_trader/risk/engine.pyx b/nautilus_trader/risk/engine.pyx index 3a2cfae3abb0..ac1d2a71898f 100644 --- a/nautilus_trader/risk/engine.pyx +++ b/nautilus_trader/risk/engine.pyx @@ -45,6 +45,7 @@ from nautilus_trader.model.commands.trading cimport SubmitOrder from nautilus_trader.model.commands.trading cimport SubmitOrderList from nautilus_trader.model.commands.trading cimport TradingCommand from nautilus_trader.model.events.order cimport OrderDenied +from nautilus_trader.model.identifiers import ComponentId from nautilus_trader.model.identifiers cimport InstrumentId from nautilus_trader.model.instruments.base cimport Instrument from nautilus_trader.model.objects cimport Price @@ -111,6 +112,7 @@ cdef class RiskEngine(Component): super().__init__( clock=clock, logger=logger, + component_id=ComponentId("RiskEngine"), msgbus=msgbus, config=config.dict(), ) diff --git a/nautilus_trader/serialization/arrow/schema.py b/nautilus_trader/serialization/arrow/schema.py index e6863b60e92c..9c897de461cd 100644 --- a/nautilus_trader/serialization/arrow/schema.py +++ b/nautilus_trader/serialization/arrow/schema.py @@ -193,7 +193,7 @@ "reduce_only": pa.bool_(), # -- Options fields -- # "post_only": pa.bool_(), - "hidden": pa.bool_(), + "display_qty": pa.string(), "price": pa.float64(), "trigger": pa.bool_(), # --------------------- # @@ -206,7 +206,7 @@ "event_id": pa.string(), "ts_init": pa.int64(), }, - metadata={"options_fields": orjson.dumps(["post_only", "hidden", "price", "trigger"])}, + metadata={"options_fields": orjson.dumps(["post_only", "display_qty", "price", "trigger"])}, ), OrderDenied: pa.schema( { @@ -569,8 +569,9 @@ "size_increment": pa.dictionary(pa.int8(), pa.string()), "multiplier": pa.dictionary(pa.int8(), pa.string()), "lot_size": pa.dictionary(pa.int8(), pa.string()), - "expiry_date": pa.dictionary(pa.int8(), pa.string()), - "strike_price": pa.dictionary(pa.int16(), pa.string()), + "expiry_date": pa.dictionary(pa.int64(), pa.string()), + "strike_price": pa.dictionary(pa.int64(), pa.string()), + "kind": pa.dictionary(pa.int8(), pa.string()), "ts_init": pa.int64(), "ts_event": pa.int64(), } diff --git a/nautilus_trader/trading/strategy.pxd b/nautilus_trader/trading/strategy.pxd index 01e786913d2a..3b6372646c7e 100644 --- a/nautilus_trader/trading/strategy.pxd +++ b/nautilus_trader/trading/strategy.pxd @@ -43,8 +43,6 @@ cdef class TradingStrategy(Actor): cdef dict _indicators_for_trades cdef dict _indicators_for_bars - cdef readonly Clock clock - """The trading strategies clock.\n\n:returns: `Clock`""" cdef readonly UUIDFactory uuid_factory """The trading strategies UUID4 factory.\n\n:returns: `UUIDFactory`""" cdef readonly LoggerAdapter log diff --git a/nautilus_trader/trading/strategy.pyx b/nautilus_trader/trading/strategy.pyx index 576d6996a9e7..944a6a3ae1ef 100644 --- a/nautilus_trader/trading/strategy.pyx +++ b/nautilus_trader/trading/strategy.pyx @@ -249,7 +249,6 @@ cdef class TradingStrategy(Actor): logger=logger, ) - self.clock = self._clock self.log = self._log self.portfolio = portfolio # Assigned as PortfolioFacade diff --git a/nautilus_trader/trading/trader.pxd b/nautilus_trader/trading/trader.pxd index 9a345e08101e..bdb6cb8c1036 100644 --- a/nautilus_trader/trading/trader.pxd +++ b/nautilus_trader/trading/trader.pxd @@ -32,24 +32,24 @@ cdef class Trader(Component): cdef DataEngine _data_engine cdef RiskEngine _risk_engine cdef ExecutionEngine _exec_engine - cdef list _components + cdef list _actors cdef list _strategies cdef readonly analyzer """The traders performance analyzer.\n\n:returns: `PerformanceAnalyzer`""" - cdef list components_c(self) + cdef list actors_c(self) cdef list strategies_c(self) - cpdef list component_ids(self) + cpdef list actor_ids(self) cpdef list strategy_ids(self) - cpdef dict component_states(self) + cpdef dict actor_states(self) cpdef dict strategy_states(self) - cpdef void add_component(self, Actor component) except * - cpdef void add_components(self, list component) except * + cpdef void add_actor(self, Actor actor) except * + cpdef void add_actors(self, list actors) except * cpdef void add_strategy(self, TradingStrategy strategy) except * cpdef void add_strategies(self, list strategies) except * - cpdef void clear_components(self) except * + cpdef void clear_actors(self) except * cpdef void clear_strategies(self) except * cpdef void subscribe(self, str topic, handler: Callable[[Any], None]) except * cpdef void unsubscribe(self, str topic, handler: Callable[[Any], None]) except * diff --git a/nautilus_trader/trading/trader.pyx b/nautilus_trader/trading/trader.pyx index bf57fb08a811..557fb1db9444 100644 --- a/nautilus_trader/trading/trader.pyx +++ b/nautilus_trader/trading/trader.pyx @@ -115,27 +115,27 @@ cdef class Trader(Component): self._risk_engine = risk_engine self._exec_engine = exec_engine - self._components = [] + self._actors = [] self._strategies = [] self.analyzer = PerformanceAnalyzer() - cdef list components_c(self): - return self._components + cdef list actors_c(self): + return self._actors cdef list strategies_c(self): return self._strategies - cpdef list component_ids(self): + cpdef list actor_ids(self): """ - Return the custom component IDs loaded in the trader. + Return the actor IDs loaded in the trader. Returns ------- list[ComponentId] """ - return sorted([component.id for component in self._components]) + return sorted([actor.id for actor in self._actors]) cpdef list strategy_ids(self): """ @@ -148,21 +148,17 @@ cdef class Trader(Component): """ return sorted([strategy.id for strategy in self._strategies]) - cpdef dict component_states(self): + cpdef dict actor_states(self): """ - Return the traders custom component states. + Return the traders actor states. Returns ------- dict[ComponentId, str] """ - cdef dict states = {} - cdef Actor component - for component in self._components: - states[component.id] = component.state_string_c() - - return states + cdef Actor a + return {a.id: a.state_string_c() for a in self._actors} cpdef dict strategy_states(self): """ @@ -173,12 +169,8 @@ cdef class Trader(Component): dict[StrategyId, str] """ - cdef dict states = {} - cdef TradingStrategy strategy - for strategy in self._strategies: - states[strategy.id] = strategy.state_string_c() - - return states + cdef TradingStrategy s + return {s.id: s.state_string_c() for s in self._strategies} # -- ACTION IMPLEMENTATIONS ------------------------------------------------------------------------ @@ -187,21 +179,21 @@ cdef class Trader(Component): self._log.error(f"No strategies loaded.") return - cdef Actor component - for component in self._components: - component.start() + cdef Actor actor + for actor in self._actors: + actor.start() cdef TradingStrategy strategy for strategy in self._strategies: strategy.start() cpdef void _stop(self) except *: - cdef Actor component - for component in self._components: - if component.is_running_c(): - component.stop() + cdef Actor actor + for actor in self._actors: + if actor.is_running_c(): + actor.stop() else: - self._log.warning(f"{component} already stopped.") + self._log.warning(f"{actor} already stopped.") cdef TradingStrategy strategy for strategy in self._strategies: @@ -211,9 +203,9 @@ cdef class Trader(Component): self._log.warning(f"{strategy} already stopped.") cpdef void _reset(self) except *: - cdef Actor component - for component in self._components: - component.reset() + cdef Actor actor + for actor in self._actors: + actor.reset() cdef TradingStrategy strategy for strategy in self._strategies: @@ -223,9 +215,9 @@ cdef class Trader(Component): self.analyzer.reset() cpdef void _dispose(self) except *: - cdef Actor component - for component in self._components: - component.dispose() + cdef Actor actor + for actor in self._actors: + actor.dispose() cdef TradingStrategy strategy for strategy in self._strategies: @@ -295,14 +287,14 @@ cdef class Trader(Component): for strategy in strategies: self.add_strategy(strategy) - cpdef void add_component(self, Actor component) except *: + cpdef void add_actor(self, Actor actor) except *: """ Add the given custom component to the trader. Parameters ---------- - component : Actor - The custom component to add and register. + actor : Actor + The actor to add and register. Raises ------ @@ -312,16 +304,16 @@ cdef class Trader(Component): If `component.state` is ``RUNNING`` or ``DISPOSED``. """ - Condition.not_in(component, self._components, "component", "components") - Condition.true(not component.is_running_c(), "strategy.state was RUNNING") - Condition.true(not component.is_disposed_c(), "strategy.state was DISPOSED") + Condition.not_in(actor, self._actors, "actor", "actors") + Condition.true(not actor.is_running_c(), "actor.state was RUNNING") + Condition.true(not actor.is_disposed_c(), "actor.state was DISPOSED") if self.is_running_c(): self._log.error("Cannot add component to a running trader.") return # Wire component into trader - component.register_base( + actor.register_base( trader_id=self.id, msgbus=self._msgbus, cache=self._cache, @@ -329,30 +321,30 @@ cdef class Trader(Component): logger=self._log.get_logger(), ) - self._components.append(component) + self._actors.append(actor) - self._log.info(f"Registered Component {component}.") + self._log.info(f"Registered Component {actor}.") - cpdef void add_components(self, list components: [Actor]) except *: + cpdef void add_actors(self, list actors: [Actor]) except *: """ - Add the given custom components to the trader. + Add the given actors to the trader. Parameters ---------- - components : list[TradingStrategies] - The custom components to add and register. + actors : list[TradingStrategies] + The actors to add and register. Raises ------ ValueError - If `components` is ``None`` or empty. + If `actors` is ``None`` or empty. """ - Condition.not_empty(components, "components") + Condition.not_empty(actors, "actors") - cdef Actor component - for component in components: - self.add_component(component) + cdef Actor actor + for actor in actors: + self.add_actor(actor) cpdef void clear_strategies(self) except *: """ @@ -373,9 +365,9 @@ cdef class Trader(Component): self._strategies.clear() - cpdef void clear_components(self) except *: + cpdef void clear_actors(self) except *: """ - Dispose and clear all custom components held by the trader. + Dispose and clear all actors held by the trader. Raises ------ @@ -384,13 +376,13 @@ cdef class Trader(Component): """ if self.is_running_c(): - self._log.error("Cannot clear the components of a running trader.") + self._log.error("Cannot clear the actors of a running trader.") return - for component in self._components: - component.dispose() + for actor in self._actors: + actor.dispose() - self._components.clear() + self._actors.clear() cpdef void subscribe(self, str topic, handler: Callable[[Any], None]) except *: """ diff --git a/poetry.lock b/poetry.lock index 41e8586d9850..28849321c050 100644 --- a/poetry.lock +++ b/poetry.lock @@ -11,7 +11,7 @@ pycares = ">=4.0.0" [[package]] name = "aiohttp" -version = "3.8.0" +version = "3.8.1" description = "Async http client/server framework (asyncio)" category = "main" optional = false @@ -61,7 +61,7 @@ test = ["coverage", "flake8", "pexpect", "wheel"] [[package]] name = "async-timeout" -version = "4.0.0" +version = "4.0.1" description = "Timeout context manager for asyncio programs" category = "main" optional = false @@ -105,7 +105,7 @@ pytz = ">=2015.7" [[package]] name = "backports.entry-points-selectable" -version = "1.1.0" +version = "1.1.1" description = "Compatibility shim providing selectable entry points for older implementations" category = "dev" optional = false @@ -113,7 +113,7 @@ python-versions = ">=2.7" [package.extras] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=4.6)", "pytest-flake8", "pytest-cov", "pytest-black (>=0.3.7)", "pytest-mypy", "pytest-checkdocs (>=2.4)", "pytest-enabler (>=1.0.1)"] +testing = ["pytest", "pytest-flake8", "pytest-cov", "pytest-black (>=0.3.7)", "pytest-mypy", "pytest-checkdocs (>=2.4)", "pytest-enabler (>=1.0.1)"] [[package]] name = "bokeh" @@ -199,7 +199,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "colorlog" -version = "6.5.0" +version = "6.6.0" description = "Add colours to the output of Python's logging module." category = "dev" optional = false @@ -237,7 +237,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "dask" -version = "2021.11.0" +version = "2021.11.2" description = "Parallel PyData with Task Scheduling" category = "main" optional = false @@ -253,12 +253,26 @@ toolz = ">=0.8.2" [package.extras] array = ["numpy (>=1.18)"] -complete = ["bokeh (>=1.0.0,!=2.0.0)", "distributed (==2021.11.0)", "jinja2", "numpy (>=1.18)", "pandas (>=1.0)"] +complete = ["bokeh (>=1.0.0,!=2.0.0)", "distributed (==2021.11.2)", "jinja2", "numpy (>=1.18)", "pandas (>=1.0)"] dataframe = ["numpy (>=1.18)", "pandas (>=1.0)"] diagnostics = ["bokeh (>=1.0.0,!=2.0.0)", "jinja2"] -distributed = ["distributed (==2021.11.0)"] +distributed = ["distributed (==2021.11.2)"] test = ["pytest", "pytest-rerunfailures", "pytest-xdist", "pre-commit"] +[[package]] +name = "deprecated" +version = "1.2.13" +description = "Python @deprecated decorator to deprecate old python classes, functions or methods." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +wrapt = ">=1.10,<2" + +[package.extras] +dev = ["tox", "bump2version (<1)", "sphinx (<2)", "importlib-metadata (<3)", "importlib-resources (<4)", "configparser (<5)", "sphinxcontrib-websupport (<2)", "zipp (<2)", "PyTest (<5)", "PyTest-Cov (<2.6)", "pytest", "pytest-cov"] + [[package]] name = "distlib" version = "0.3.3" @@ -269,7 +283,7 @@ python-versions = "*" [[package]] name = "distributed" -version = "2021.11.0" +version = "2021.11.2" description = "Distributed scheduler for Dask" category = "main" optional = true @@ -278,7 +292,7 @@ python-versions = ">=3.7" [package.dependencies] click = ">=6.6" cloudpickle = ">=1.5.0" -dask = "2021.11.0" +dask = "2021.11.2" jinja2 = "*" msgpack = ">=0.6.0" psutil = ">=5.0" @@ -321,7 +335,7 @@ testing = ["pre-commit"] [[package]] name = "filelock" -version = "3.3.2" +version = "3.4.0" description = "A platform independent file lock." category = "dev" optional = false @@ -331,6 +345,27 @@ python-versions = ">=3.6" docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"] testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"] +[[package]] +name = "fonttools" +version = "4.28.1" +description = "Tools to manipulate font files" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +all = ["fs (>=2.2.0,<3)", "lxml (>=4.0,<5)", "zopfli (>=0.1.4)", "lz4 (>=1.7.4.2)", "matplotlib", "sympy", "skia-pathops (>=0.5.0)", "brotlicffi (>=0.8.0)", "scipy", "brotli (>=1.0.1)", "munkres", "unicodedata2 (>=13.0.0)", "xattr"] +graphite = ["lz4 (>=1.7.4.2)"] +interpolatable = ["scipy", "munkres"] +lxml = ["lxml (>=4.0,<5)"] +pathops = ["skia-pathops (>=0.5.0)"] +plot = ["matplotlib"] +symfont = ["sympy"] +type1 = ["xattr"] +ufo = ["fs (>=2.2.0,<3)"] +unicode = ["unicodedata2 (>=13.0.0)"] +woff = ["zopfli (>=0.1.4)", "brotlicffi (>=0.8.0)", "brotli (>=1.0.1)"] + [[package]] name = "frozenlist" version = "1.2.0" @@ -377,6 +412,14 @@ category = "main" optional = true python-versions = "*" +[[package]] +name = "hiredis" +version = "2.0.0" +description = "Python wrapper for hiredis" +category = "main" +optional = false +python-versions = ">=3.6" + [[package]] name = "ib-insync" version = "0.9.69" @@ -391,14 +434,14 @@ nest-asyncio = "*" [[package]] name = "identify" -version = "2.3.4" +version = "2.4.0" description = "File identification library for Python" category = "dev" optional = false python-versions = ">=3.6.1" [package.extras] -license = ["editdistance-s"] +license = ["ukkonen"] [[package]] name = "idna" @@ -410,7 +453,7 @@ python-versions = ">=3.5" [[package]] name = "imagesize" -version = "1.2.0" +version = "1.3.0" description = "Getting image size from png/jpeg/jpeg2000/gif file" category = "dev" optional = true @@ -426,7 +469,7 @@ python-versions = "*" [[package]] name = "jinja2" -version = "3.0.2" +version = "3.0.3" description = "A very fast and expressive template engine." category = "main" optional = true @@ -478,7 +521,7 @@ python-versions = ">=3.6" [[package]] name = "matplotlib" -version = "3.4.3" +version = "3.5.0" description = "Python plotting package" category = "main" optional = false @@ -486,11 +529,14 @@ python-versions = ">=3.7" [package.dependencies] cycler = ">=0.10" +fonttools = ">=4.22.0" kiwisolver = ">=1.0.1" -numpy = ">=1.16" +numpy = ">=1.17" +packaging = ">=20.0" pillow = ">=6.2.0" pyparsing = ">=2.2.1" python-dateutil = ">=2.7" +setuptools_scm = ">=4" [[package]] name = "msgpack" @@ -510,7 +556,7 @@ python-versions = ">=3.6" [[package]] name = "multitasking" -version = "0.0.9" +version = "0.0.10" description = "Non-blocking Python methods using decorators" category = "main" optional = false @@ -583,14 +629,14 @@ python-versions = ">=3.7" [[package]] name = "packaging" -version = "21.2" +version = "21.3" description = "Core utilities for Python packages" category = "main" optional = false python-versions = ">=3.6" [package.dependencies] -pyparsing = ">=2.0.2,<3" +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" [[package]] name = "pandas" @@ -761,11 +807,14 @@ python-versions = ">=3.5" [[package]] name = "pyparsing" -version = "2.4.7" +version = "3.0.6" description = "Python parsing module" category = "main" optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +python-versions = ">=3.6" + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pytest" @@ -908,7 +957,7 @@ python-versions = ">=3.6" [[package]] name = "quantstats" -version = "0.0.45" +version = "0.0.46" description = "Portfolio analytics for quants" category = "main" optional = false @@ -925,14 +974,17 @@ yfinance = ">=0.1.63" [[package]] name = "redis" -version = "3.5.3" -description = "Python client for Redis key-value store" +version = "4.0.1" +description = "Python client for Redis database and key-value store" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.6" + +[package.dependencies] +deprecated = "*" [package.extras] -hiredis = ["hiredis (>=0.1.3)"] +hiredis = ["hiredis (>=1.0.0)"] [[package]] name = "requests" @@ -977,6 +1029,21 @@ numpy = ">=1.15" pandas = ">=0.23" scipy = ">=1.0" +[[package]] +name = "setuptools-scm" +version = "6.3.2" +description = "the blessed package to manage your versions by scm tags" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +packaging = ">=20.0" +tomli = ">=1.0.0" + +[package.extras] +toml = ["setuptools (>=42)", "tomli (>=1.0.0)"] + [[package]] name = "six" version = "1.16.0" @@ -987,7 +1054,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "snowballstemmer" -version = "2.1.0" +version = "2.2.0" description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." category = "dev" optional = true @@ -1003,7 +1070,7 @@ python-versions = "*" [[package]] name = "sphinx" -version = "4.2.0" +version = "4.3.0" description = "Python documentation generator" category = "dev" optional = true @@ -1145,6 +1212,14 @@ category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +[[package]] +name = "tomli" +version = "1.2.2" +description = "A lil' TOML parser" +category = "main" +optional = false +python-versions = ">=3.6" + [[package]] name = "toolz" version = "0.11.2" @@ -1179,11 +1254,11 @@ telegram = ["requests"] [[package]] name = "typing-extensions" -version = "3.10.0.2" -description = "Backported and Experimental Type Hints for Python 3.5+" +version = "4.0.0" +description = "Backported and Experimental Type Hints for Python 3.6+" category = "main" optional = false -python-versions = "*" +python-versions = ">=3.6" [[package]] name = "urllib3" @@ -1230,6 +1305,14 @@ six = ">=1.9.0,<2" docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"] +[[package]] +name = "wrapt" +version = "1.13.3" +description = "Module for decorators, wrappers and monkey patching." +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + [[package]] name = "yarl" version = "1.7.2" @@ -1244,8 +1327,8 @@ multidict = ">=4.0" [[package]] name = "yfinance" -version = "0.1.64" -description = "Yahoo! Finance market data downloader" +version = "0.1.67" +description = "Download market data from Yahoo! Finance API" category = "main" optional = false python-versions = "*" @@ -1276,7 +1359,7 @@ ib = ["ib_insync"] [metadata] lock-version = "1.1" python-versions = ">=3.8,<3.11" -content-hash = "c66d3cd1547f54b854d29f2b7de08a3e37cf934f0f738d4bf476185fabe75586" +content-hash = "a68c2337f90ce866029cd5eef91befa2bd70c02220f9c277670acc538e912f61" [metadata.files] aiodns = [ @@ -1284,78 +1367,78 @@ aiodns = [ {file = "aiodns-3.0.0.tar.gz", hash = "sha256:946bdfabe743fceeeb093c8a010f5d1645f708a241be849e17edfb0e49e08cd6"}, ] aiohttp = [ - {file = "aiohttp-3.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:48f218a5257b6bc16bcf26a91d97ecea0c7d29c811a90d965f3dd97c20f016d6"}, - {file = "aiohttp-3.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a2fee4d656a7cc9ab47771b2a9e8fad8a9a33331c1b59c3057ecf0ac858f5bfe"}, - {file = "aiohttp-3.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:688a1eb8c1a5f7e795c7cb67e0fe600194e6723ba35f138dfae0db20c0cb8f94"}, - {file = "aiohttp-3.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ba09bb3dcb0b7ec936a485db2b64be44fe14cdce0a5eac56f50e55da3627385"}, - {file = "aiohttp-3.8.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d7715daf84f10bcebc083ad137e3eced3e1c8e7fa1f096ade9a8d02b08f0d91c"}, - {file = "aiohttp-3.8.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e3f81fbbc170418e22918a9585fd7281bbc11d027064d62aa4b507552c92671"}, - {file = "aiohttp-3.8.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1fa9f50aa1f114249b7963c98e20dc35c51be64096a85bc92433185f331de9cc"}, - {file = "aiohttp-3.8.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8a50150419b741ee048b53146c39c47053f060cb9d98e78be08fdbe942eaa3c4"}, - {file = "aiohttp-3.8.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a84c335337b676d832c1e2bc47c3a97531b46b82de9f959dafb315cbcbe0dfcd"}, - {file = "aiohttp-3.8.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:88d4917c30fcd7f6404fb1dc713fa21de59d3063dcc048f4a8a1a90e6bbbd739"}, - {file = "aiohttp-3.8.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b76669b7c058b8020b11008283c3b8e9c61bfd978807c45862956119b77ece45"}, - {file = "aiohttp-3.8.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:84fe1732648c1bc303a70faa67cbc2f7f2e810c8a5bca94f6db7818e722e4c0a"}, - {file = "aiohttp-3.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:730b7c2b7382194d9985ffdc32ab317e893bca21e0665cb1186bdfbb4089d990"}, - {file = "aiohttp-3.8.0-cp310-cp310-win32.whl", hash = "sha256:0a96473a1f61d7920a9099bc8e729dc8282539d25f79c12573ee0fdb9c8b66a8"}, - {file = "aiohttp-3.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:764c7c6aa1f78bd77bd9674fc07d1ec44654da1818d0eef9fb48aa8371a3c847"}, - {file = "aiohttp-3.8.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9951c2696c4357703001e1fe6edc6ae8e97553ac630492ea1bf64b429cb712a3"}, - {file = "aiohttp-3.8.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0af379221975054162959e00daf21159ff69a712fc42ed0052caddbd70d52ff4"}, - {file = "aiohttp-3.8.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9689af0f0a89e5032426c143fa3683b0451f06c83bf3b1e27902bd33acfae769"}, - {file = "aiohttp-3.8.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe4a327da0c6b6e59f2e474ae79d6ee7745ac3279fd15f200044602fa31e3d79"}, - {file = "aiohttp-3.8.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ecb314e59bedb77188017f26e6b684b1f6d0465e724c3122a726359fa62ca1ba"}, - {file = "aiohttp-3.8.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a5399a44a529083951b55521cf4ecbf6ad79fd54b9df57dbf01699ffa0549fc9"}, - {file = "aiohttp-3.8.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:09754a0d5eaab66c37591f2f8fac8f9781a5f61d51aa852a3261c4805ca6b984"}, - {file = "aiohttp-3.8.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:adf0cb251b1b842c9dee5cfcdf880ba0aae32e841b8d0e6b6feeaef002a267c5"}, - {file = "aiohttp-3.8.0-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:a4759e85a191de58e0ea468ab6fd9c03941986eee436e0518d7a9291fab122c8"}, - {file = "aiohttp-3.8.0-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:28369fe331a59d80393ec82df3d43307c7461bfaf9217999e33e2acc7984ff7c"}, - {file = "aiohttp-3.8.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:2f44d1b1c740a9e2275160d77c73a11f61e8a916191c572876baa7b282bcc934"}, - {file = "aiohttp-3.8.0-cp36-cp36m-win32.whl", hash = "sha256:e27cde1e8d17b09730801ce97b6e0c444ba2a1f06348b169fd931b51d3402f0d"}, - {file = "aiohttp-3.8.0-cp36-cp36m-win_amd64.whl", hash = "sha256:15a660d06092b7c92ed17c1dbe6c1eab0a02963992d60e3e8b9d5fa7fa81f01e"}, - {file = "aiohttp-3.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:257f4fad1714d26d562572095c8c5cd271d5a333252795cb7a002dca41fdbad7"}, - {file = "aiohttp-3.8.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6074a3b2fa2d0c9bf0963f8dfc85e1e54a26114cc8594126bc52d3fa061c40e"}, - {file = "aiohttp-3.8.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7a315ceb813208ef32bdd6ec3a85cbe3cb3be9bbda5fd030c234592fa9116993"}, - {file = "aiohttp-3.8.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9a52b141ff3b923a9166595de6e3768a027546e75052ffba267d95b54267f4ab"}, - {file = "aiohttp-3.8.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6a038cb1e6e55b26bb5520ccffab7f539b3786f5553af2ee47eb2ec5cbd7084e"}, - {file = "aiohttp-3.8.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98b1ea2763b33559dd9ec621d67fc17b583484cb90735bfb0ec3614c17b210e4"}, - {file = "aiohttp-3.8.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:9e8723c3256641e141cd18f6ce478d54a004138b9f1a36e41083b36d9ecc5fc5"}, - {file = "aiohttp-3.8.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:14a6f026eca80dfa3d52e86be89feb5cd878f6f4a6adb34457e2c689fd85229b"}, - {file = "aiohttp-3.8.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:c62d4791a8212c885b97a63ef5f3974b2cd41930f0cd224ada9c6ee6654f8150"}, - {file = "aiohttp-3.8.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:90a97c2ed2830e7974cbe45f0838de0aefc1c123313f7c402e21c29ec063fbb4"}, - {file = "aiohttp-3.8.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:dcc4d5dd5fba3affaf4fd08f00ef156407573de8c63338787614ccc64f96b321"}, - {file = "aiohttp-3.8.0-cp37-cp37m-win32.whl", hash = "sha256:de42f513ed7a997bc821bddab356b72e55e8396b1b7ba1bf39926d538a76a90f"}, - {file = "aiohttp-3.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:7d76e8a83396e06abe3df569b25bd3fc88bf78b7baa2c8e4cf4aaf5983af66a3"}, - {file = "aiohttp-3.8.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5d79174d96446a02664e2bffc95e7b6fa93b9e6d8314536c5840dff130d0878b"}, - {file = "aiohttp-3.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4a6551057a846bf72c7a04f73de3fcaca269c0bd85afe475ceb59d261c6a938c"}, - {file = "aiohttp-3.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:871d4fdc56288caa58b1094c20f2364215f7400411f76783ea19ad13be7c8e19"}, - {file = "aiohttp-3.8.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ba08a71caa42eef64357257878fb17f3fba3fba6e81a51d170e32321569e079"}, - {file = "aiohttp-3.8.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:51f90dabd9933b1621260b32c2f0d05d36923c7a5a909eb823e429dba0fd2f3e"}, - {file = "aiohttp-3.8.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f348ebd20554e8bc26e8ef3ed8a134110c0f4bf015b3b4da6a4ddf34e0515b19"}, - {file = "aiohttp-3.8.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d5f8c04574efa814a24510122810e3a3c77c0552f9f6ff65c9862f1f046be2c3"}, - {file = "aiohttp-3.8.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5ecffdc748d3b40dd3618ede0170e4f5e1d3c9647cfb410d235d19e62cb54ee0"}, - {file = "aiohttp-3.8.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:577cc2c7b807b174814dac2d02e673728f2e46c7f90ceda3a70ea4bb6d90b769"}, - {file = "aiohttp-3.8.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6b79f6c31e68b6dafc0317ec453c83c86dd8db1f8f0c6f28e97186563fca87a0"}, - {file = "aiohttp-3.8.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:2bdd655732e38b40f8a8344d330cfae3c727fb257585df923316aabbd489ccb8"}, - {file = "aiohttp-3.8.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:63fa57a0708573d3c059f7b5527617bd0c291e4559298473df238d502e4ab98c"}, - {file = "aiohttp-3.8.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d3f90ee275b1d7c942e65b5c44c8fb52d55502a0b9a679837d71be2bd8927661"}, - {file = "aiohttp-3.8.0-cp38-cp38-win32.whl", hash = "sha256:fa818609357dde5c4a94a64c097c6404ad996b1d38ca977a72834b682830a722"}, - {file = "aiohttp-3.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:097ecf52f6b9859b025c1e36401f8aa4573552e887d1b91b4b999d68d0b5a3b3"}, - {file = "aiohttp-3.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:be03a7483ad9ea60388f930160bb3728467dd0af538aa5edc60962ee700a0bdc"}, - {file = "aiohttp-3.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:78d51e35ed163783d721b6f2ce8ce3f82fccfe471e8e50a10fba13a766d31f5a"}, - {file = "aiohttp-3.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bda75d73e7400e81077b0910c9a60bf9771f715420d7e35fa7739ae95555f195"}, - {file = "aiohttp-3.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:707adc30ea6918fba725c3cb3fe782d271ba352b22d7ae54a7f9f2e8a8488c41"}, - {file = "aiohttp-3.8.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f58aa995b905ab82fe228acd38538e7dc1509e01508dcf307dad5046399130f"}, - {file = "aiohttp-3.8.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c996eb91bfbdab1e01e2c02e7ff678c51e2b28e3a04e26e41691991cc55795"}, - {file = "aiohttp-3.8.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d6a1a66bb8bac9bc2892c2674ea363486bfb748b86504966a390345a11b1680e"}, - {file = "aiohttp-3.8.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dafc01a32b4a1d7d3ef8bfd3699406bb44f7b2e0d3eb8906d574846e1019b12f"}, - {file = "aiohttp-3.8.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:949a605ef3907254b122f845baa0920407080cdb1f73aa64f8d47df4a7f4c4f9"}, - {file = "aiohttp-3.8.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0d7b056fd3972d353cb4bc305c03f9381583766b7f8c7f1c44478dba69099e33"}, - {file = "aiohttp-3.8.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:6f1d39a744101bf4043fa0926b3ead616607578192d0a169974fb5265ab1e9d2"}, - {file = "aiohttp-3.8.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:67ca7032dfac8d001023fadafc812d9f48bf8a8c3bb15412d9cdcf92267593f4"}, - {file = "aiohttp-3.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cb751ef712570d3bda9a73fd765ff3e1aba943ec5d52a54a0c2e89c7eef9da1e"}, - {file = "aiohttp-3.8.0-cp39-cp39-win32.whl", hash = "sha256:6d3e027fe291b77f6be9630114a0200b2c52004ef20b94dc50ca59849cd623b3"}, - {file = "aiohttp-3.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:3c5e9981e449d54308c6824f172ec8ab63eb9c5f922920970249efee83f7e919"}, - {file = "aiohttp-3.8.0.tar.gz", hash = "sha256:d3b19d8d183bcfd68b25beebab8dc3308282fe2ca3d6ea3cb4cd101b3c279f8d"}, + {file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1ed0b6477896559f17b9eaeb6d38e07f7f9ffe40b9f0f9627ae8b9926ae260a8"}, + {file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7dadf3c307b31e0e61689cbf9e06be7a867c563d5a63ce9dca578f956609abf8"}, + {file = "aiohttp-3.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a79004bb58748f31ae1cbe9fa891054baaa46fb106c2dc7af9f8e3304dc30316"}, + {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12de6add4038df8f72fac606dff775791a60f113a725c960f2bab01d8b8e6b15"}, + {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f0d5f33feb5f69ddd57a4a4bd3d56c719a141080b445cbf18f238973c5c9923"}, + {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eaba923151d9deea315be1f3e2b31cc39a6d1d2f682f942905951f4e40200922"}, + {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:099ebd2c37ac74cce10a3527d2b49af80243e2a4fa39e7bce41617fbc35fa3c1"}, + {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2e5d962cf7e1d426aa0e528a7e198658cdc8aa4fe87f781d039ad75dcd52c516"}, + {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fa0ffcace9b3aa34d205d8130f7873fcfefcb6a4dd3dd705b0dab69af6712642"}, + {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:61bfc23df345d8c9716d03717c2ed5e27374e0fe6f659ea64edcd27b4b044cf7"}, + {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:31560d268ff62143e92423ef183680b9829b1b482c011713ae941997921eebc8"}, + {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:01d7bdb774a9acc838e6b8f1d114f45303841b89b95984cbb7d80ea41172a9e3"}, + {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:97ef77eb6b044134c0b3a96e16abcb05ecce892965a2124c566af0fd60f717e2"}, + {file = "aiohttp-3.8.1-cp310-cp310-win32.whl", hash = "sha256:c2aef4703f1f2ddc6df17519885dbfa3514929149d3ff900b73f45998f2532fa"}, + {file = "aiohttp-3.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:713ac174a629d39b7c6a3aa757b337599798da4c1157114a314e4e391cd28e32"}, + {file = "aiohttp-3.8.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:473d93d4450880fe278696549f2e7aed8cd23708c3c1997981464475f32137db"}, + {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b5eeae8e019e7aad8af8bb314fb908dd2e028b3cdaad87ec05095394cce632"}, + {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3af642b43ce56c24d063325dd2cf20ee012d2b9ba4c3c008755a301aaea720ad"}, + {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3630c3ef435c0a7c549ba170a0633a56e92629aeed0e707fec832dee313fb7a"}, + {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4a4a4e30bf1edcad13fb0804300557aedd07a92cabc74382fdd0ba6ca2661091"}, + {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6f8b01295e26c68b3a1b90efb7a89029110d3a4139270b24fda961893216c440"}, + {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a25fa703a527158aaf10dafd956f7d42ac6d30ec80e9a70846253dd13e2f067b"}, + {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:5bfde62d1d2641a1f5173b8c8c2d96ceb4854f54a44c23102e2ccc7e02f003ec"}, + {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:51467000f3647d519272392f484126aa716f747859794ac9924a7aafa86cd411"}, + {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:03a6d5349c9ee8f79ab3ff3694d6ce1cfc3ced1c9d36200cb8f08ba06bd3b782"}, + {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:102e487eeb82afac440581e5d7f8f44560b36cf0bdd11abc51a46c1cd88914d4"}, + {file = "aiohttp-3.8.1-cp36-cp36m-win32.whl", hash = "sha256:4aed991a28ea3ce320dc8ce655875e1e00a11bdd29fe9444dd4f88c30d558602"}, + {file = "aiohttp-3.8.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b0e20cddbd676ab8a64c774fefa0ad787cc506afd844de95da56060348021e96"}, + {file = "aiohttp-3.8.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:37951ad2f4a6df6506750a23f7cbabad24c73c65f23f72e95897bb2cecbae676"}, + {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c23b1ad869653bc818e972b7a3a79852d0e494e9ab7e1a701a3decc49c20d51"}, + {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15b09b06dae900777833fe7fc4b4aa426556ce95847a3e8d7548e2d19e34edb8"}, + {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:477c3ea0ba410b2b56b7efb072c36fa91b1e6fc331761798fa3f28bb224830dd"}, + {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2f2f69dca064926e79997f45b2f34e202b320fd3782f17a91941f7eb85502ee2"}, + {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ef9612483cb35171d51d9173647eed5d0069eaa2ee812793a75373447d487aa4"}, + {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6d69f36d445c45cda7b3b26afef2fc34ef5ac0cdc75584a87ef307ee3c8c6d00"}, + {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:55c3d1072704d27401c92339144d199d9de7b52627f724a949fc7d5fc56d8b93"}, + {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b9d00268fcb9f66fbcc7cd9fe423741d90c75ee029a1d15c09b22d23253c0a44"}, + {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:07b05cd3305e8a73112103c834e91cd27ce5b4bd07850c4b4dbd1877d3f45be7"}, + {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c34dc4958b232ef6188c4318cb7b2c2d80521c9a56c52449f8f93ab7bc2a8a1c"}, + {file = "aiohttp-3.8.1-cp37-cp37m-win32.whl", hash = "sha256:d2f9b69293c33aaa53d923032fe227feac867f81682f002ce33ffae978f0a9a9"}, + {file = "aiohttp-3.8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:6ae828d3a003f03ae31915c31fa684b9890ea44c9c989056fea96e3d12a9fa17"}, + {file = "aiohttp-3.8.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0c7ebbbde809ff4e970824b2b6cb7e4222be6b95a296e46c03cf050878fc1785"}, + {file = "aiohttp-3.8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b7ef7cbd4fec9a1e811a5de813311ed4f7ac7d93e0fda233c9b3e1428f7dd7b"}, + {file = "aiohttp-3.8.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c3d6a4d0619e09dcd61021debf7059955c2004fa29f48788a3dfaf9c9901a7cd"}, + {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:718626a174e7e467f0558954f94af117b7d4695d48eb980146016afa4b580b2e"}, + {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:589c72667a5febd36f1315aa6e5f56dd4aa4862df295cb51c769d16142ddd7cd"}, + {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2ed076098b171573161eb146afcb9129b5ff63308960aeca4b676d9d3c35e700"}, + {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:086f92daf51a032d062ec5f58af5ca6a44d082c35299c96376a41cbb33034675"}, + {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:11691cf4dc5b94236ccc609b70fec991234e7ef8d4c02dd0c9668d1e486f5abf"}, + {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:31d1e1c0dbf19ebccbfd62eff461518dcb1e307b195e93bba60c965a4dcf1ba0"}, + {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:11a67c0d562e07067c4e86bffc1553f2cf5b664d6111c894671b2b8712f3aba5"}, + {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:bb01ba6b0d3f6c68b89fce7305080145d4877ad3acaed424bae4d4ee75faa950"}, + {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:44db35a9e15d6fe5c40d74952e803b1d96e964f683b5a78c3cc64eb177878155"}, + {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:844a9b460871ee0a0b0b68a64890dae9c415e513db0f4a7e3cab41a0f2fedf33"}, + {file = "aiohttp-3.8.1-cp38-cp38-win32.whl", hash = "sha256:7d08744e9bae2ca9c382581f7dce1273fe3c9bae94ff572c3626e8da5b193c6a"}, + {file = "aiohttp-3.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:04d48b8ce6ab3cf2097b1855e1505181bdd05586ca275f2505514a6e274e8e75"}, + {file = "aiohttp-3.8.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f5315a2eb0239185af1bddb1abf472d877fede3cc8d143c6cddad37678293237"}, + {file = "aiohttp-3.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a996d01ca39b8dfe77440f3cd600825d05841088fd6bc0144cc6c2ec14cc5f74"}, + {file = "aiohttp-3.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:13487abd2f761d4be7c8ff9080de2671e53fff69711d46de703c310c4c9317ca"}, + {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea302f34477fda3f85560a06d9ebdc7fa41e82420e892fc50b577e35fc6a50b2"}, + {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2f635ce61a89c5732537a7896b6319a8fcfa23ba09bec36e1b1ac0ab31270d2"}, + {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e999f2d0e12eea01caeecb17b653f3713d758f6dcc770417cf29ef08d3931421"}, + {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0770e2806a30e744b4e21c9d73b7bee18a1cfa3c47991ee2e5a65b887c49d5cf"}, + {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d15367ce87c8e9e09b0f989bfd72dc641bcd04ba091c68cd305312d00962addd"}, + {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6c7cefb4b0640703eb1069835c02486669312bf2f12b48a748e0a7756d0de33d"}, + {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:71927042ed6365a09a98a6377501af5c9f0a4d38083652bcd2281a06a5976724"}, + {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:28d490af82bc6b7ce53ff31337a18a10498303fe66f701ab65ef27e143c3b0ef"}, + {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:b6613280ccedf24354406caf785db748bebbddcf31408b20c0b48cb86af76866"}, + {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:81e3d8c34c623ca4e36c46524a3530e99c0bc95ed068fd6e9b55cb721d408fb2"}, + {file = "aiohttp-3.8.1-cp39-cp39-win32.whl", hash = "sha256:7187a76598bdb895af0adbd2fb7474d7f6025d170bc0a1130242da817ce9e7d1"}, + {file = "aiohttp-3.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:1c182cb873bc91b411e184dab7a2b664d4fea2743df0e4d57402f7f3fa644bac"}, + {file = "aiohttp-3.8.1.tar.gz", hash = "sha256:fc5471e1a54de15ef71c1bc6ebe80d4dc681ea600e68bfd1cbce40427f0b7578"}, ] aiosignal = [ {file = "aiosignal-1.2.0-py3-none-any.whl", hash = "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a"}, @@ -1370,8 +1453,8 @@ argcomplete = [ {file = "argcomplete-1.12.3.tar.gz", hash = "sha256:2c7dbffd8c045ea534921e63b0be6fe65e88599990d8dc408ac8c542b72a5445"}, ] async-timeout = [ - {file = "async-timeout-4.0.0.tar.gz", hash = "sha256:7d87a4e8adba8ededb52e579ce6bc8276985888913620c935094c2276fd83382"}, - {file = "async_timeout-4.0.0-py3-none-any.whl", hash = "sha256:f3303dddf6cafa748a92747ab6c2ecf60e0aeca769aee4c151adfce243a05d9b"}, + {file = "async-timeout-4.0.1.tar.gz", hash = "sha256:b930cb161a39042f9222f6efb7301399c87eeab394727ec5437924a36d6eef51"}, + {file = "async_timeout-4.0.1-py3-none-any.whl", hash = "sha256:a22c0b311af23337eb05fcf05a8b51c3ea53729d46fb5460af62bee033cec690"}, ] atomicwrites = [ {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, @@ -1386,8 +1469,8 @@ babel = [ {file = "Babel-2.9.1.tar.gz", hash = "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0"}, ] "backports.entry-points-selectable" = [ - {file = "backports.entry_points_selectable-1.1.0-py2.py3-none-any.whl", hash = "sha256:a6d9a871cde5e15b4c4a53e3d43ba890cc6861ec1332c9c2428c92f977192acc"}, - {file = "backports.entry_points_selectable-1.1.0.tar.gz", hash = "sha256:988468260ec1c196dab6ae1149260e2f5472c9110334e5d51adcb77867361f6a"}, + {file = "backports.entry_points_selectable-1.1.1-py2.py3-none-any.whl", hash = "sha256:7fceed9532a7aa2bd888654a7314f864a3c16a4e710b34a58cfc0f08114c663b"}, + {file = "backports.entry_points_selectable-1.1.1.tar.gz", hash = "sha256:914b21a479fde881635f7af5adc7f6e38d6b274be32269070c53b698c60d5386"}, ] bokeh = [ {file = "bokeh-2.4.1-py3-none-any.whl", hash = "sha256:b270d6ef899598fe26e64b6ae08e30f8d67a177baa1f5bfe18e1979a81bb7c4d"}, @@ -1470,8 +1553,8 @@ colorama = [ {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] colorlog = [ - {file = "colorlog-6.5.0-py2.py3-none-any.whl", hash = "sha256:d334b1b8dae5989b786232f05586a7a0111feb24ff9cfc8310c3347a91388717"}, - {file = "colorlog-6.5.0.tar.gz", hash = "sha256:cf62a8e389d5660d0d22be17937b25b9abef9497ddc940197d1773aa1f604339"}, + {file = "colorlog-6.6.0-py2.py3-none-any.whl", hash = "sha256:351c51e866c86c3217f08e4b067a7974a678be78f07f85fc2d55b8babde6d94e"}, + {file = "colorlog-6.6.0.tar.gz", hash = "sha256:344f73204009e4c83c5b6beb00b3c45dc70fcdae3c80db919e0a4171d006fde8"}, ] coverage = [ {file = "coverage-4.5.4-cp26-cp26m-macosx_10_12_x86_64.whl", hash = "sha256:eee64c616adeff7db37cc37da4180a3a5b6177f5c46b187894e633f088fb5b28"}, @@ -1553,16 +1636,20 @@ cython = [ {file = "Cython-3.0.0a9.tar.gz", hash = "sha256:23931c45877432097cef9de2db2dc66322cbc4fc3ebbb42c476bb2c768cecff0"}, ] dask = [ - {file = "dask-2021.11.0-py3-none-any.whl", hash = "sha256:0244678de0bf52175aa75cedd7187dc6b55f8dbe0cc0f14b94065761cf4b4b59"}, - {file = "dask-2021.11.0.tar.gz", hash = "sha256:007c400eab2b032717cd8d589351abfb0e79688fcc6cd033a12e506cb01066d7"}, + {file = "dask-2021.11.2-py3-none-any.whl", hash = "sha256:2b0ad7beba8950add4fdc7c5cb94fa9444915ddb00c711d5743e2c4bb0a95ef5"}, + {file = "dask-2021.11.2.tar.gz", hash = "sha256:e12bfe272928d62fa99623d98d0e0b0c045b33a47509ef31a22175aa5fd10917"}, +] +deprecated = [ + {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"}, + {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"}, ] distlib = [ {file = "distlib-0.3.3-py2.py3-none-any.whl", hash = "sha256:c8b54e8454e5bf6237cc84c20e8264c3e991e824ef27e8f1e81049867d861e31"}, {file = "distlib-0.3.3.zip", hash = "sha256:d982d0751ff6eaaab5e2ec8e691d949ee80eddf01a62eaa96ddb11531fe16b05"}, ] distributed = [ - {file = "distributed-2021.11.0-py3-none-any.whl", hash = "sha256:a170e35c9a67315ebaf14d80bdcb34fefe9bf508b7343e65a7beed4fdd2e68c4"}, - {file = "distributed-2021.11.0.tar.gz", hash = "sha256:4ecbb1e1d61a55d226a67146076686d5cb6091645732d5e14f7330820dcc682d"}, + {file = "distributed-2021.11.2-py3-none-any.whl", hash = "sha256:af1f7b98d85d43886fefe2354379c848c7a5aa6ae4d2313a7aca9ab9081a7e56"}, + {file = "distributed-2021.11.2.tar.gz", hash = "sha256:f86a01a2e1e678865d2e42300c47552b5012cd81a2d354e47827a1fd074cc302"}, ] docutils = [ {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, @@ -1577,8 +1664,12 @@ execnet = [ {file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"}, ] filelock = [ - {file = "filelock-3.3.2-py3-none-any.whl", hash = "sha256:bb2a1c717df74c48a2d00ed625e5a66f8572a3a30baacb7657add1d7bac4097b"}, - {file = "filelock-3.3.2.tar.gz", hash = "sha256:7afc856f74fa7006a289fd10fa840e1eebd8bbff6bffb69c26c54a0512ea8cf8"}, + {file = "filelock-3.4.0-py3-none-any.whl", hash = "sha256:2e139a228bcf56dd8b2274a65174d005c4a6b68540ee0bdbb92c76f43f29f7e8"}, + {file = "filelock-3.4.0.tar.gz", hash = "sha256:93d512b32a23baf4cac44ffd72ccf70732aeff7b8050fcaf6d3ec406d954baf4"}, +] +fonttools = [ + {file = "fonttools-4.28.1-py3-none-any.whl", hash = "sha256:68071406009e7ef6a5fdcd85d95975cd6963867bb226f2b786bfffe15d1959ef"}, + {file = "fonttools-4.28.1.zip", hash = "sha256:8c8f84131bf04f3b1dcf99b9763cec35c347164ab6ad006e18d2f99fcab05529"}, ] frozenlist = [ {file = "frozenlist-1.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:977a1438d0e0d96573fd679d291a1542097ea9f4918a8b6494b06610dfeefbf9"}, @@ -1662,29 +1753,72 @@ heapdict = [ {file = "HeapDict-1.0.1-py3-none-any.whl", hash = "sha256:6065f90933ab1bb7e50db403b90cab653c853690c5992e69294c2de2b253fc92"}, {file = "HeapDict-1.0.1.tar.gz", hash = "sha256:8495f57b3e03d8e46d5f1b2cc62ca881aca392fd5cc048dc0aa2e1a6d23ecdb6"}, ] +hiredis = [ + {file = "hiredis-2.0.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b4c8b0bc5841e578d5fb32a16e0c305359b987b850a06964bd5a62739d688048"}, + {file = "hiredis-2.0.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0adea425b764a08270820531ec2218d0508f8ae15a448568109ffcae050fee26"}, + {file = "hiredis-2.0.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:3d55e36715ff06cdc0ab62f9591607c4324297b6b6ce5b58cb9928b3defe30ea"}, + {file = "hiredis-2.0.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:5d2a48c80cf5a338d58aae3c16872f4d452345e18350143b3bf7216d33ba7b99"}, + {file = "hiredis-2.0.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:240ce6dc19835971f38caf94b5738092cb1e641f8150a9ef9251b7825506cb05"}, + {file = "hiredis-2.0.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:5dc7a94bb11096bc4bffd41a3c4f2b958257085c01522aa81140c68b8bf1630a"}, + {file = "hiredis-2.0.0-cp36-cp36m-win32.whl", hash = "sha256:139705ce59d94eef2ceae9fd2ad58710b02aee91e7fa0ccb485665ca0ecbec63"}, + {file = "hiredis-2.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:c39c46d9e44447181cd502a35aad2bb178dbf1b1f86cf4db639d7b9614f837c6"}, + {file = "hiredis-2.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:adf4dd19d8875ac147bf926c727215a0faf21490b22c053db464e0bf0deb0485"}, + {file = "hiredis-2.0.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0f41827028901814c709e744060843c77e78a3aca1e0d6875d2562372fcb405a"}, + {file = "hiredis-2.0.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:508999bec4422e646b05c95c598b64bdbef1edf0d2b715450a078ba21b385bcc"}, + {file = "hiredis-2.0.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:0d5109337e1db373a892fdcf78eb145ffb6bbd66bb51989ec36117b9f7f9b579"}, + {file = "hiredis-2.0.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:04026461eae67fdefa1949b7332e488224eac9e8f2b5c58c98b54d29af22093e"}, + {file = "hiredis-2.0.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:a00514362df15af041cc06e97aebabf2895e0a7c42c83c21894be12b84402d79"}, + {file = "hiredis-2.0.0-cp37-cp37m-win32.whl", hash = "sha256:09004096e953d7ebd508cded79f6b21e05dff5d7361771f59269425108e703bc"}, + {file = "hiredis-2.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:f8196f739092a78e4f6b1b2172679ed3343c39c61a3e9d722ce6fcf1dac2824a"}, + {file = "hiredis-2.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:294a6697dfa41a8cba4c365dd3715abc54d29a86a40ec6405d677ca853307cfb"}, + {file = "hiredis-2.0.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:3dddf681284fe16d047d3ad37415b2e9ccdc6c8986c8062dbe51ab9a358b50a5"}, + {file = "hiredis-2.0.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:dcef843f8de4e2ff5e35e96ec2a4abbdf403bd0f732ead127bd27e51f38ac298"}, + {file = "hiredis-2.0.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:87c7c10d186f1743a8fd6a971ab6525d60abd5d5d200f31e073cd5e94d7e7a9d"}, + {file = "hiredis-2.0.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:7f0055f1809b911ab347a25d786deff5e10e9cf083c3c3fd2dd04e8612e8d9db"}, + {file = "hiredis-2.0.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:11d119507bb54e81f375e638225a2c057dda748f2b1deef05c2b1a5d42686048"}, + {file = "hiredis-2.0.0-cp38-cp38-win32.whl", hash = "sha256:7492af15f71f75ee93d2a618ca53fea8be85e7b625e323315169977fae752426"}, + {file = "hiredis-2.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:65d653df249a2f95673976e4e9dd7ce10de61cfc6e64fa7eeaa6891a9559c581"}, + {file = "hiredis-2.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ae8427a5e9062ba66fc2c62fb19a72276cf12c780e8db2b0956ea909c48acff5"}, + {file = "hiredis-2.0.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:3f5f7e3a4ab824e3de1e1700f05ad76ee465f5f11f5db61c4b297ec29e692b2e"}, + {file = "hiredis-2.0.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:e3447d9e074abf0e3cd85aef8131e01ab93f9f0e86654db7ac8a3f73c63706ce"}, + {file = "hiredis-2.0.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:8b42c0dc927b8d7c0eb59f97e6e34408e53bc489f9f90e66e568f329bff3e443"}, + {file = "hiredis-2.0.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:b84f29971f0ad4adaee391c6364e6f780d5aae7e9226d41964b26b49376071d0"}, + {file = "hiredis-2.0.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:0b39ec237459922c6544d071cdcf92cbb5bc6685a30e7c6d985d8a3e3a75326e"}, + {file = "hiredis-2.0.0-cp39-cp39-win32.whl", hash = "sha256:a7928283143a401e72a4fad43ecc85b35c27ae699cf5d54d39e1e72d97460e1d"}, + {file = "hiredis-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:a4ee8000454ad4486fb9f28b0cab7fa1cd796fc36d639882d0b34109b5b3aec9"}, + {file = "hiredis-2.0.0-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1f03d4dadd595f7a69a75709bc81902673fa31964c75f93af74feac2f134cc54"}, + {file = "hiredis-2.0.0-pp36-pypy36_pp73-manylinux1_x86_64.whl", hash = "sha256:04927a4c651a0e9ec11c68e4427d917e44ff101f761cd3b5bc76f86aaa431d27"}, + {file = "hiredis-2.0.0-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:a39efc3ade8c1fb27c097fd112baf09d7fd70b8cb10ef1de4da6efbe066d381d"}, + {file = "hiredis-2.0.0-pp36-pypy36_pp73-win32.whl", hash = "sha256:07bbf9bdcb82239f319b1f09e8ef4bdfaec50ed7d7ea51a56438f39193271163"}, + {file = "hiredis-2.0.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:807b3096205c7cec861c8803a6738e33ed86c9aae76cac0e19454245a6bbbc0a"}, + {file = "hiredis-2.0.0-pp37-pypy37_pp73-manylinux1_x86_64.whl", hash = "sha256:1233e303645f468e399ec906b6b48ab7cd8391aae2d08daadbb5cad6ace4bd87"}, + {file = "hiredis-2.0.0-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:cb2126603091902767d96bcb74093bd8b14982f41809f85c9b96e519c7e1dc41"}, + {file = "hiredis-2.0.0-pp37-pypy37_pp73-win32.whl", hash = "sha256:f52010e0a44e3d8530437e7da38d11fb822acfb0d5b12e9cd5ba655509937ca0"}, + {file = "hiredis-2.0.0.tar.gz", hash = "sha256:81d6d8e39695f2c37954d1011c0480ef7cf444d4e3ae24bc5e89ee5de360139a"}, +] ib-insync = [ {file = "ib_insync-0.9.69-py3-none-any.whl", hash = "sha256:744d3ef8b5ce7608614ecd90246c6a6be84924e8276cbd9ad23cdfff6aae9ba9"}, {file = "ib_insync-0.9.69.tar.gz", hash = "sha256:b218c602dc9844c1d867448d14d29f12caa66657f56c48f0e96165b2edd07672"}, ] identify = [ - {file = "identify-2.3.4-py2.py3-none-any.whl", hash = "sha256:4de55a93e0ba72bf917c840b3794eb1055a67272a1732351c557c88ec42011b1"}, - {file = "identify-2.3.4.tar.gz", hash = "sha256:595283a1c3a078ac5774ad4dc4d1bdd0c1602f60bcf11ae673b64cb2b1945762"}, + {file = "identify-2.4.0-py2.py3-none-any.whl", hash = "sha256:eba31ca80258de6bb51453084bff4a923187cd2193b9c13710f2516ab30732cc"}, + {file = "identify-2.4.0.tar.gz", hash = "sha256:a33ae873287e81651c7800ca309dc1f84679b763c9c8b30680e16fbfa82f0107"}, ] idna = [ {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, ] imagesize = [ - {file = "imagesize-1.2.0-py2.py3-none-any.whl", hash = "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1"}, - {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"}, + {file = "imagesize-1.3.0-py2.py3-none-any.whl", hash = "sha256:1db2f82529e53c3e929e8926a1fa9235aa82d0bd0c580359c67ec31b2fddaa8c"}, + {file = "imagesize-1.3.0.tar.gz", hash = "sha256:cd1750d452385ca327479d45b64d9c7729ecf0b3969a58148298c77092261f9d"}, ] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] jinja2 = [ - {file = "Jinja2-3.0.2-py3-none-any.whl", hash = "sha256:8569982d3f0889eed11dd620c706d39b60c36d6d25843961f33f77fb6bc6b20c"}, - {file = "Jinja2-3.0.2.tar.gz", hash = "sha256:827a0e32839ab1600d4eb1c4c33ec5a8edfbc5cb42dafa13b81f182f97784b45"}, + {file = "Jinja2-3.0.3-py3-none-any.whl", hash = "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8"}, + {file = "Jinja2-3.0.3.tar.gz", hash = "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7"}, ] kiwisolver = [ {file = "kiwisolver-1.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1d819553730d3c2724582124aee8a03c846ec4362ded1034c16fb3ef309264e6"}, @@ -1870,27 +2004,41 @@ markupsafe = [ {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, ] matplotlib = [ - {file = "matplotlib-3.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5c988bb43414c7c2b0a31bd5187b4d27fd625c080371b463a6d422047df78913"}, - {file = "matplotlib-3.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f1c5efc278d996af8a251b2ce0b07bbeccb821f25c8c9846bdcb00ffc7f158aa"}, - {file = "matplotlib-3.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:eeb1859efe7754b1460e1d4991bbd4a60a56f366bc422ef3a9c5ae05f0bc70b5"}, - {file = "matplotlib-3.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:844a7b0233e4ff7fba57e90b8799edaa40b9e31e300b8d5efc350937fa8b1bea"}, - {file = "matplotlib-3.4.3-cp37-cp37m-win32.whl", hash = "sha256:85f0c9cf724715e75243a7b3087cf4a3de056b55e05d4d76cc58d610d62894f3"}, - {file = "matplotlib-3.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:c70b6311dda3e27672f1bf48851a0de816d1ca6aaf3d49365fbdd8e959b33d2b"}, - {file = "matplotlib-3.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b884715a59fec9ad3b6048ecf3860f3b2ce965e676ef52593d6fa29abcf7d330"}, - {file = "matplotlib-3.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:a78a3b51f29448c7f4d4575e561f6b0dbb8d01c13c2046ab6c5220eb25c06506"}, - {file = "matplotlib-3.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6a724e3a48a54b8b6e7c4ae38cd3d07084508fa47c410c8757e9db9791421838"}, - {file = "matplotlib-3.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:48e1e0859b54d5f2e29bb78ca179fd59b971c6ceb29977fb52735bfd280eb0f5"}, - {file = "matplotlib-3.4.3-cp38-cp38-win32.whl", hash = "sha256:01c9de93a2ca0d128c9064f23709362e7fefb34910c7c9e0b8ab0de8258d5eda"}, - {file = "matplotlib-3.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:ebfb01a65c3f5d53a8c2a8133fec2b5221281c053d944ae81ff5822a68266617"}, - {file = "matplotlib-3.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b8b53f336a4688cfce615887505d7e41fd79b3594bf21dd300531a4f5b4f746a"}, - {file = "matplotlib-3.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:fcd6f1954943c0c192bfbebbac263f839d7055409f1173f80d8b11a224d236da"}, - {file = "matplotlib-3.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:6be8df61b1626e1a142c57e065405e869e9429b4a6dab4a324757d0dc4d42235"}, - {file = "matplotlib-3.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:41b6e307458988891fcdea2d8ecf84a8c92d53f84190aa32da65f9505546e684"}, - {file = "matplotlib-3.4.3-cp39-cp39-win32.whl", hash = "sha256:f72657f1596199dc1e4e7a10f52a4784ead8a711f4e5b59bea95bdb97cf0e4fd"}, - {file = "matplotlib-3.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:f15edcb0629a0801738925fe27070480f446fcaa15de65946ff946ad99a59a40"}, - {file = "matplotlib-3.4.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:556965514b259204637c360d213de28d43a1f4aed1eca15596ce83f768c5a56f"}, - {file = "matplotlib-3.4.3-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:54a026055d5f8614f184e588f6e29064019a0aa8448450214c0b60926d62d919"}, - {file = "matplotlib-3.4.3.tar.gz", hash = "sha256:fc4f526dfdb31c9bd6b8ca06bf9fab663ca12f3ec9cdf4496fb44bc680140318"}, + {file = "matplotlib-3.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4b018ea6f26424a0852eb60eb406420d9f0d34f65736ea7bbfbb104946a66d86"}, + {file = "matplotlib-3.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a07ff2565da72a7b384a9e000b15b6b8270d81370af8a3531a16f6fbcee023cc"}, + {file = "matplotlib-3.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2eea16883aa7724c95eea0eb473ab585c6cf66f0e28f7f13e63deb38f4fd6d0f"}, + {file = "matplotlib-3.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e020a42f3338823a393dd2f80e39a2c07b9f941dfe2c778eb104eeb33d60bb5"}, + {file = "matplotlib-3.5.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bac8eb1eccef540d7f4e844b6313d9f7722efd48c07e1b4bfec1056132127fd"}, + {file = "matplotlib-3.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a7cb59ebd63a8ac4542ec1c61dd08724f82ec3aa7bb6b4b9e212d43c611ce3d"}, + {file = "matplotlib-3.5.0-cp310-cp310-win32.whl", hash = "sha256:6e0e6b2111165522ad336705499b1f968c34a9e84d05d498ee5af0b5697d1efe"}, + {file = "matplotlib-3.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:ff5d9fe518ad2de14ce82ab906b6ab5c2b0c7f4f984400ff8a7a905daa580a0a"}, + {file = "matplotlib-3.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:66b172610db0ececebebb09d146f54205f87c7b841454e408fba854764f91bdd"}, + {file = "matplotlib-3.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee3d9ff16d749a9aa521bd7d86f0dbf256b2d2ac8ce31b19e4d2c86d2f2ff0b6"}, + {file = "matplotlib-3.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970aa97297537540369d05fe0fd1bb952593f9ab696c9b427c06990a83e2418b"}, + {file = "matplotlib-3.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:153a0cf6a6ff4f406a0600d2034710c49988bacc6313d193b32716f98a697580"}, + {file = "matplotlib-3.5.0-cp37-cp37m-win32.whl", hash = "sha256:6db02c5605f063b67780f4d5753476b6a4944343284aa4e93c5e8ff6e9ec7f76"}, + {file = "matplotlib-3.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:df0042cab69f4d246f4cb8fc297770ac4ae6ec2983f61836b04a117722037dcd"}, + {file = "matplotlib-3.5.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a7bf8b05c214d32fb7ca7c001fde70b9b426378e897b0adbf77b85ea3569d56a"}, + {file = "matplotlib-3.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0abf8b51cc6d3ba34d1b15b26e329f23879848a0cf1216954c1f432ffc7e1af7"}, + {file = "matplotlib-3.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:13930a0c9bec0fd25f43c448b047a21af1353328b946f044a8fc3be077c6b1a8"}, + {file = "matplotlib-3.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18f6e52386300db5cc4d1e9019ad9da2e80658bab018834d963ebb0aa5355095"}, + {file = "matplotlib-3.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba107add08e12600b072cf3c47aaa1ab85dd4d3c48107a5d3377d1bf80f8b235"}, + {file = "matplotlib-3.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2089b9014792dcc87bb1d620cde847913338abf7d957ef05587382b0cb76d44e"}, + {file = "matplotlib-3.5.0-cp38-cp38-win32.whl", hash = "sha256:f23fbf70d2e80f4e03a83fc1206a8306d9bc50482fee4239f10676ce7e470c83"}, + {file = "matplotlib-3.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:71a1851111f23f82fc43d2b6b2bfdd3f760579a664ebc939576fe21cc6133d01"}, + {file = "matplotlib-3.5.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d092b7ba63182d2dd427904e3eb58dd5c46ec67c5968de14a4b5007010a3a4cc"}, + {file = "matplotlib-3.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ac17a7e7b06ee426a4989f0b7f24ab1a592e39cdf56353a90f4e998bc0bf44d6"}, + {file = "matplotlib-3.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a5b62d1805cc83d755972033c05cea78a1e177a159fc84da5c9c4ab6303ccbd9"}, + {file = "matplotlib-3.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:666d717a4798eb9c5d3ae83fe80c7bc6ed696b93e879cb01cb24a74155c73612"}, + {file = "matplotlib-3.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65f877882b7ddede7090c7d87be27a0f4720fe7fc6fddd4409c06e1aa0f1ae8d"}, + {file = "matplotlib-3.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7baf23adb698d8c6ca7339c9dde00931bc47b2dd82fa912827fef9f93db77f5e"}, + {file = "matplotlib-3.5.0-cp39-cp39-win32.whl", hash = "sha256:b3b687e905da32e5f2e5f16efa713f5d1fcd9fb8b8c697895de35c91fedeb086"}, + {file = "matplotlib-3.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6cef5b31e27c31253c0f852b629a38d550ae66ec6850129c49d872f9ee428cb"}, + {file = "matplotlib-3.5.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a0dcaf5648cecddc328e81a0421821a1f65a1d517b20746c94a1f0f5c36fb51a"}, + {file = "matplotlib-3.5.0-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b5e439d9e55d645f2a4dca63e2f66d68fe974c405053b132d61c7e98c25dfeb2"}, + {file = "matplotlib-3.5.0-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dc8c5c23e7056e126275dbf29efba817b3d94196690930d0968873ac3a94ab82"}, + {file = "matplotlib-3.5.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:a0ea10faa3bab0714d3a19c7e0921279a68d57552414d6eceaea99f97d7735db"}, + {file = "matplotlib-3.5.0.tar.gz", hash = "sha256:38892a254420d95594285077276162a5e9e9c30b6da08bdc2a4d53331ad9a6fa"}, ] msgpack = [ {file = "msgpack-1.0.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:b6d9e2dae081aa35c44af9c4298de4ee72991305503442a5c74656d82b581fe9"}, @@ -1997,7 +2145,7 @@ multidict = [ {file = "multidict-5.2.0.tar.gz", hash = "sha256:0dd1c93edb444b33ba2274b66f63def8a327d607c6c790772f448a53b6ea59ce"}, ] multitasking = [ - {file = "multitasking-0.0.9.tar.gz", hash = "sha256:b59d99f709d2e17d60ccaa2be09771b6e9ed9391c63f083c0701e724f624d2e0"}, + {file = "multitasking-0.0.10.tar.gz", hash = "sha256:810640fa6670be41f4a712b287d9307a14ad849d966f06a17d2cf1593b66c3cd"}, ] nest-asyncio = [ {file = "nest_asyncio-1.5.1-py3-none-any.whl", hash = "sha256:76d6e972265063fe92a90b9cc4fb82616e07d586b346ed9d2c89a4187acea39c"}, @@ -2073,8 +2221,8 @@ orjson = [ {file = "orjson-3.6.4.tar.gz", hash = "sha256:f8dbc428fc6d7420f231a7133d8dff4c882e64acb585dcf2fda74bdcfe1a6d9d"}, ] packaging = [ - {file = "packaging-21.2-py3-none-any.whl", hash = "sha256:14317396d1e8cdb122989b916fa2c7e9ca8e2be9e8060a6eff75b6b7b4d8a7e0"}, - {file = "packaging-21.2.tar.gz", hash = "sha256:096d689d78ca690e4cd8a89568ba06d07ca097e3306a4381635073ca91479966"}, + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, ] pandas = [ {file = "pandas-1.3.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9707bdc1ea9639c886b4d3be6e2a45812c1ac0c2080f94c31b71c9fa35556f9b"}, @@ -2294,8 +2442,8 @@ pygments = [ {file = "Pygments-2.10.0.tar.gz", hash = "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"}, ] pyparsing = [ - {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, - {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, + {file = "pyparsing-3.0.6-py3-none-any.whl", hash = "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4"}, + {file = "pyparsing-3.0.6.tar.gz", hash = "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"}, ] pytest = [ {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, @@ -2369,11 +2517,11 @@ pyyaml = [ {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, ] quantstats = [ - {file = "QuantStats-0.0.45.tar.gz", hash = "sha256:9f6f5bb2e799d216572d82311b0a97dd8dda8be72672bf980aeff48b96f39519"}, + {file = "QuantStats-0.0.46.tar.gz", hash = "sha256:72b9348b201506e05bc890f8962ea9897c25d3aaff7abfce631730a7df4a289a"}, ] redis = [ - {file = "redis-3.5.3-py2.py3-none-any.whl", hash = "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"}, - {file = "redis-3.5.3.tar.gz", hash = "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2"}, + {file = "redis-4.0.1-py3-none-any.whl", hash = "sha256:bc6832367d60e1a5f94d75314fc46e8ce6f07fee8e532ee1bfafaf4887f8b4bb"}, + {file = "redis-4.0.1.tar.gz", hash = "sha256:cc642f70e0ebddce960818ba35776af6a18487cc38f66deace68d55b97e6e3cf"}, ] requests = [ {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, @@ -2408,21 +2556,25 @@ seaborn = [ {file = "seaborn-0.11.2-py3-none-any.whl", hash = "sha256:85a6baa9b55f81a0623abddc4a26b334653ff4c6b18c418361de19dbba0ef283"}, {file = "seaborn-0.11.2.tar.gz", hash = "sha256:cf45e9286d40826864be0e3c066f98536982baf701a7caa386511792d61ff4f6"}, ] +setuptools-scm = [ + {file = "setuptools_scm-6.3.2-py3-none-any.whl", hash = "sha256:4c64444b1d49c4063ae60bfe1680f611c8b13833d556fd1d6050c0023162a119"}, + {file = "setuptools_scm-6.3.2.tar.gz", hash = "sha256:a49aa8081eeb3514eb9728fa5040f2eaa962d6c6f4ec9c32f6c1fba88f88a0f2"}, +] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] snowballstemmer = [ - {file = "snowballstemmer-2.1.0-py2.py3-none-any.whl", hash = "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2"}, - {file = "snowballstemmer-2.1.0.tar.gz", hash = "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914"}, + {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, + {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, ] sortedcontainers = [ {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, ] sphinx = [ - {file = "Sphinx-4.2.0-py3-none-any.whl", hash = "sha256:98a535c62a4fcfcc362528592f69b26f7caec587d32cd55688db580be0287ae0"}, - {file = "Sphinx-4.2.0.tar.gz", hash = "sha256:94078db9184491e15bce0a56d9186e0aec95f16ac20b12d00e06d4e36f1058a6"}, + {file = "Sphinx-4.3.0-py3-none-any.whl", hash = "sha256:7e2b30da5f39170efcd95c6270f07669d623c276521fee27ad6c380f49d2bf5b"}, + {file = "Sphinx-4.3.0.tar.gz", hash = "sha256:6d051ab6e0d06cba786c4656b0fe67ba259fe058410f49e95bee6e49c4052cbf"}, ] sphinx-rtd-theme = [ {file = "sphinx_rtd_theme-1.0.0-py2.py3-none-any.whl", hash = "sha256:4d35a56f4508cfee4c4fb604373ede6feae2a306731d533f409ef5c3496fdbd8"}, @@ -2464,6 +2616,10 @@ toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] +tomli = [ + {file = "tomli-1.2.2-py3-none-any.whl", hash = "sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade"}, + {file = "tomli-1.2.2.tar.gz", hash = "sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee"}, +] toolz = [ {file = "toolz-0.11.2-py3-none-any.whl", hash = "sha256:a5700ce83414c64514d82d60bcda8aabfde092d1c1a8663f9200c07fdcc6da8f"}, {file = "toolz-0.11.2.tar.gz", hash = "sha256:6b312d5e15138552f1bda8a4e66c30e236c831b612b2bf0005f8a1df10a4bc33"}, @@ -2516,9 +2672,7 @@ tqdm = [ {file = "tqdm-4.62.3.tar.gz", hash = "sha256:d359de7217506c9851b7869f3708d8ee53ed70a1b8edbba4dbcb47442592920d"}, ] typing-extensions = [ - {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, - {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"}, - {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, + {file = "typing_extensions-4.0.0-py3-none-any.whl", hash = "sha256:829704698b22e13ec9eaf959122315eabb370b0884400e9818334d8b677023d9"}, ] urllib3 = [ {file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"}, @@ -2546,6 +2700,59 @@ virtualenv = [ {file = "virtualenv-20.10.0-py2.py3-none-any.whl", hash = "sha256:4b02e52a624336eece99c96e3ab7111f469c24ba226a53ec474e8e787b365814"}, {file = "virtualenv-20.10.0.tar.gz", hash = "sha256:576d05b46eace16a9c348085f7d0dc8ef28713a2cabaa1cf0aea41e8f12c9218"}, ] +wrapt = [ + {file = "wrapt-1.13.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:e05e60ff3b2b0342153be4d1b597bbcfd8330890056b9619f4ad6b8d5c96a81a"}, + {file = "wrapt-1.13.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:85148f4225287b6a0665eef08a178c15097366d46b210574a658c1ff5b377489"}, + {file = "wrapt-1.13.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:2dded5496e8f1592ec27079b28b6ad2a1ef0b9296d270f77b8e4a3a796cf6909"}, + {file = "wrapt-1.13.3-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:e94b7d9deaa4cc7bac9198a58a7240aaf87fe56c6277ee25fa5b3aa1edebd229"}, + {file = "wrapt-1.13.3-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:498e6217523111d07cd67e87a791f5e9ee769f9241fcf8a379696e25806965af"}, + {file = "wrapt-1.13.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ec7e20258ecc5174029a0f391e1b948bf2906cd64c198a9b8b281b811cbc04de"}, + {file = "wrapt-1.13.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:87883690cae293541e08ba2da22cacaae0a092e0ed56bbba8d018cc486fbafbb"}, + {file = "wrapt-1.13.3-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:f99c0489258086308aad4ae57da9e8ecf9e1f3f30fa35d5e170b4d4896554d80"}, + {file = "wrapt-1.13.3-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6a03d9917aee887690aa3f1747ce634e610f6db6f6b332b35c2dd89412912bca"}, + {file = "wrapt-1.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:936503cb0a6ed28dbfa87e8fcd0a56458822144e9d11a49ccee6d9a8adb2ac44"}, + {file = "wrapt-1.13.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f9c51d9af9abb899bd34ace878fbec8bf357b3194a10c4e8e0a25512826ef056"}, + {file = "wrapt-1.13.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:220a869982ea9023e163ba915077816ca439489de6d2c09089b219f4e11b6785"}, + {file = "wrapt-1.13.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0877fe981fd76b183711d767500e6b3111378ed2043c145e21816ee589d91096"}, + {file = "wrapt-1.13.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:43e69ffe47e3609a6aec0fe723001c60c65305784d964f5007d5b4fb1bc6bf33"}, + {file = "wrapt-1.13.3-cp310-cp310-win32.whl", hash = "sha256:78dea98c81915bbf510eb6a3c9c24915e4660302937b9ae05a0947164248020f"}, + {file = "wrapt-1.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:ea3e746e29d4000cd98d572f3ee2a6050a4f784bb536f4ac1f035987fc1ed83e"}, + {file = "wrapt-1.13.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:8c73c1a2ec7c98d7eaded149f6d225a692caa1bd7b2401a14125446e9e90410d"}, + {file = "wrapt-1.13.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:086218a72ec7d986a3eddb7707c8c4526d677c7b35e355875a0fe2918b059179"}, + {file = "wrapt-1.13.3-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:e92d0d4fa68ea0c02d39f1e2f9cb5bc4b4a71e8c442207433d8db47ee79d7aa3"}, + {file = "wrapt-1.13.3-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:d4a5f6146cfa5c7ba0134249665acd322a70d1ea61732723c7d3e8cc0fa80755"}, + {file = "wrapt-1.13.3-cp35-cp35m-win32.whl", hash = "sha256:8aab36778fa9bba1a8f06a4919556f9f8c7b33102bd71b3ab307bb3fecb21851"}, + {file = "wrapt-1.13.3-cp35-cp35m-win_amd64.whl", hash = "sha256:944b180f61f5e36c0634d3202ba8509b986b5fbaf57db3e94df11abee244ba13"}, + {file = "wrapt-1.13.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:2ebdde19cd3c8cdf8df3fc165bc7827334bc4e353465048b36f7deeae8ee0918"}, + {file = "wrapt-1.13.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:610f5f83dd1e0ad40254c306f4764fcdc846641f120c3cf424ff57a19d5f7ade"}, + {file = "wrapt-1.13.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5601f44a0f38fed36cc07db004f0eedeaadbdcec90e4e90509480e7e6060a5bc"}, + {file = "wrapt-1.13.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:e6906d6f48437dfd80464f7d7af1740eadc572b9f7a4301e7dd3d65db285cacf"}, + {file = "wrapt-1.13.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:766b32c762e07e26f50d8a3468e3b4228b3736c805018e4b0ec8cc01ecd88125"}, + {file = "wrapt-1.13.3-cp36-cp36m-win32.whl", hash = "sha256:5f223101f21cfd41deec8ce3889dc59f88a59b409db028c469c9b20cfeefbe36"}, + {file = "wrapt-1.13.3-cp36-cp36m-win_amd64.whl", hash = "sha256:f122ccd12fdc69628786d0c947bdd9cb2733be8f800d88b5a37c57f1f1d73c10"}, + {file = "wrapt-1.13.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:46f7f3af321a573fc0c3586612db4decb7eb37172af1bc6173d81f5b66c2e068"}, + {file = "wrapt-1.13.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:778fd096ee96890c10ce96187c76b3e99b2da44e08c9e24d5652f356873f6709"}, + {file = "wrapt-1.13.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0cb23d36ed03bf46b894cfec777eec754146d68429c30431c99ef28482b5c1df"}, + {file = "wrapt-1.13.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:96b81ae75591a795d8c90edc0bfaab44d3d41ffc1aae4d994c5aa21d9b8e19a2"}, + {file = "wrapt-1.13.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7dd215e4e8514004c8d810a73e342c536547038fb130205ec4bba9f5de35d45b"}, + {file = "wrapt-1.13.3-cp37-cp37m-win32.whl", hash = "sha256:47f0a183743e7f71f29e4e21574ad3fa95676136f45b91afcf83f6a050914829"}, + {file = "wrapt-1.13.3-cp37-cp37m-win_amd64.whl", hash = "sha256:fd76c47f20984b43d93de9a82011bb6e5f8325df6c9ed4d8310029a55fa361ea"}, + {file = "wrapt-1.13.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b73d4b78807bd299b38e4598b8e7bd34ed55d480160d2e7fdaabd9931afa65f9"}, + {file = "wrapt-1.13.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ec9465dd69d5657b5d2fa6133b3e1e989ae27d29471a672416fd729b429eb554"}, + {file = "wrapt-1.13.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dd91006848eb55af2159375134d724032a2d1d13bcc6f81cd8d3ed9f2b8e846c"}, + {file = "wrapt-1.13.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ae9de71eb60940e58207f8e71fe113c639da42adb02fb2bcbcaccc1ccecd092b"}, + {file = "wrapt-1.13.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:51799ca950cfee9396a87f4a1240622ac38973b6df5ef7a41e7f0b98797099ce"}, + {file = "wrapt-1.13.3-cp38-cp38-win32.whl", hash = "sha256:4b9c458732450ec42578b5642ac53e312092acf8c0bfce140ada5ca1ac556f79"}, + {file = "wrapt-1.13.3-cp38-cp38-win_amd64.whl", hash = "sha256:7dde79d007cd6dfa65afe404766057c2409316135cb892be4b1c768e3f3a11cb"}, + {file = "wrapt-1.13.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:981da26722bebb9247a0601e2922cedf8bb7a600e89c852d063313102de6f2cb"}, + {file = "wrapt-1.13.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:705e2af1f7be4707e49ced9153f8d72131090e52be9278b5dbb1498c749a1e32"}, + {file = "wrapt-1.13.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:25b1b1d5df495d82be1c9d2fad408f7ce5ca8a38085e2da41bb63c914baadff7"}, + {file = "wrapt-1.13.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:77416e6b17926d953b5c666a3cb718d5945df63ecf922af0ee576206d7033b5e"}, + {file = "wrapt-1.13.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:865c0b50003616f05858b22174c40ffc27a38e67359fa1495605f96125f76640"}, + {file = "wrapt-1.13.3-cp39-cp39-win32.whl", hash = "sha256:0a017a667d1f7411816e4bf214646d0ad5b1da2c1ea13dec6c162736ff25a374"}, + {file = "wrapt-1.13.3-cp39-cp39-win_amd64.whl", hash = "sha256:81bd7c90d28a4b2e1df135bfbd7c23aee3050078ca6441bead44c42483f9ebfb"}, + {file = "wrapt-1.13.3.tar.gz", hash = "sha256:1fea9cd438686e6682271d36f3481a9f3636195578bab9ca3382e2f5f01fc185"}, +] yarl = [ {file = "yarl-1.7.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2a8508f7350512434e41065684076f640ecce176d262a7d54f0da41d99c5a95"}, {file = "yarl-1.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da6df107b9ccfe52d3a48165e48d72db0eca3e3029b5b8cb4fe6ee3cb870ba8b"}, @@ -2621,7 +2828,7 @@ yarl = [ {file = "yarl-1.7.2.tar.gz", hash = "sha256:45399b46d60c253327a460e99856752009fcee5f5d3c80b2f7c0cae1c38d56dd"}, ] yfinance = [ - {file = "yfinance-0.1.64.tar.gz", hash = "sha256:bde7ff6c04b7179881c15753460c600c4bd877dc9f33cdc98da68e7e1ebbc5a2"}, + {file = "yfinance-0.1.67-py2.py3-none-any.whl", hash = "sha256:597a3e83804726f45acceb3d56bcd16317c718cc62876234deb4c2b561e65b42"}, ] zict = [ {file = "zict-2.0.0-py3-none-any.whl", hash = "sha256:26aa1adda8250a78dfc6a78d200bfb2ea43a34752cf58980bca75dde0ba0c6e9"}, diff --git a/pyproject.toml b/pyproject.toml index b1249a2d616f..592057233aa3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "nautilus_trader" -version = "1.133.0" +version = "1.134.0" description = "A high-performance algorithmic trading platform and event-driven backtester" authors = ["Nautech Systems "] license = "LGPL-3.0-or-later" @@ -33,7 +33,7 @@ include = [ requires = [ "setuptools", "poetry-core>=1.0.7", - "numpy>=1.21.3", + "numpy>=1.21.4", "Cython>=3.0.0a9", ] build-backend = "poetry.core.masonry.api" @@ -46,25 +46,26 @@ generate-setup-file = false python = ">=3.8,<3.11" cython = "^3.0.0a9" aiodns = "^3.0.0" -aiohttp = "^3.8.0" -dask = "^2021.11.0" +aiohttp = "^3.8.1" +hiredis = "^2.0.0" +dask = "^2021.11.2" fsspec = "^2021.11.0" msgpack = "^1.0.2" -numpy = "^1.21.3" +numpy = "^1.21.4" orjson = "^3.6.4" pandas = "^1.3.4" psutil = "^5.8.0" pyarrow = ">=4.0.0,<6.0.0" pydantic = "^1.8.2" pytz = "^2021.3" -quantstats = "^0.0.45" -redis = "^3.5.3" +quantstats = "^0.0.46" +redis = "^4.0.1" tabulate = "^0.8.9" toml = "^0.10.2" tqdm = "^4.62.3" uvloop = { version = "^0.16.0", markers = "sys_platform != 'win32'" } bokeh = { version = "^2.4.1", optional = true } -distributed = { version = "^2021.8.0", optional = true } +distributed = { version = "^2021.11.2", optional = true } ib_insync = { version = "^0.9.66", optional = true } [tool.poetry.dev-dependencies] diff --git a/tests/integration_tests/adapters/betfair/test_kit.py b/tests/integration_tests/adapters/betfair/test_kit.py index f2ab8fc96181..9bdb31812b0a 100644 --- a/tests/integration_tests/adapters/betfair/test_kit.py +++ b/tests/integration_tests/adapters/betfair/test_kit.py @@ -302,7 +302,6 @@ def make_order( ts_init=0, post_only=False, reduce_only=False, - hidden=False, ) @staticmethod diff --git a/tests/integration_tests/adapters/binance/resources/http_spot_account_sandbox.py b/tests/integration_tests/adapters/binance/resources/http_spot_account_sandbox.py new file mode 100644 index 000000000000..e779aebc4712 --- /dev/null +++ b/tests/integration_tests/adapters/binance/resources/http_spot_account_sandbox.py @@ -0,0 +1,67 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2021 Nautech Systems Pty Ltd. All rights reserved. +# https://nautechsystems.io +# +# Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ------------------------------------------------------------------------------------------------- + +import asyncio +import json +import os + +import pytest + +from nautilus_trader.adapters.binance.factories import get_cached_binance_http_client +from nautilus_trader.adapters.binance.http.api.spot_account import BinanceSpotAccountHttpAPI +from nautilus_trader.common.clock import LiveClock +from nautilus_trader.common.logging import Logger + + +@pytest.mark.asyncio +async def test_binance_spot_account_http_client(): + loop = asyncio.get_event_loop() + clock = LiveClock() + + client = get_cached_binance_http_client( + loop=loop, + clock=clock, + logger=Logger(clock=clock), + key=os.getenv("BINANCE_API_KEY"), + secret=os.getenv("BINANCE_API_SECRET"), + ) + await client.connect() + + print(os.getenv("BINANCE_API_KEY")) + account = BinanceSpotAccountHttpAPI(client=client) + # response = await account.account(recv_window=5000) + # print(json.dumps(response, indent=4)) + + response = await account.new_order( + symbol="ETHUSDT", + side="BUY", + type="LIMIT", + quantity="0.01", + time_in_force="GTC", + price="4300", + iceberg_qty="0.005", + # stop_price="4200", + # new_client_order_id="O-20211120-021300-001-001-1", + recv_window=5000, + ) + # response = await account.cancel_order( + # symbol="ETHUSDT", + # orig_client_order_id="oN6xVmPpaUe0Awk7KFGIv3", + # #new_client_order_id=str(uuid.uuid4()), + # recv_window=5000, + # ) + print(json.dumps(json.loads(response), indent=4)) + + await client.disconnect() diff --git a/tests/integration_tests/adapters/binance/resources/http_spot_market_sandbox.py b/tests/integration_tests/adapters/binance/resources/http_spot_market_sandbox.py index f037564e695e..4dec7d8ce6da 100644 --- a/tests/integration_tests/adapters/binance/resources/http_spot_market_sandbox.py +++ b/tests/integration_tests/adapters/binance/resources/http_spot_market_sandbox.py @@ -42,7 +42,7 @@ async def test_binance_spot_market_http_client(): market = BinanceSpotMarketHttpAPI(client=client) response = await market.exchange_info(symbols=["BTCUSDT", "ETHUSDT"]) - print(json.dumps(json.loads(response), indent=4)) + print(json.dumps(response, indent=4)) provider = BinanceInstrumentProvider( client=client, diff --git a/tests/integration_tests/adapters/binance/resources/http_user_sandbox.py b/tests/integration_tests/adapters/binance/resources/http_user_sandbox.py new file mode 100644 index 000000000000..034f6713c9d0 --- /dev/null +++ b/tests/integration_tests/adapters/binance/resources/http_user_sandbox.py @@ -0,0 +1,47 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2021 Nautech Systems Pty Ltd. All rights reserved. +# https://nautechsystems.io +# +# Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ------------------------------------------------------------------------------------------------- + +import asyncio +import json +import os + +import pytest + +from nautilus_trader.adapters.binance.factories import get_cached_binance_http_client +from nautilus_trader.adapters.binance.http.api.user import BinanceUserDataHttpAPI +from nautilus_trader.common.clock import LiveClock +from nautilus_trader.common.logging import Logger + + +@pytest.mark.asyncio +async def test_binance_spot_account_http_client(): + loop = asyncio.get_event_loop() + clock = LiveClock() + + client = get_cached_binance_http_client( + loop=loop, + clock=clock, + logger=Logger(clock=clock), + key=os.getenv("BINANCE_API_KEY"), + secret=os.getenv("BINANCE_API_SECRET"), + ) + await client.connect() + + user = BinanceUserDataHttpAPI(client=client) + response = await user.create_listen_key_spot() + + print(json.dumps(response, indent=4)) + + await client.disconnect() diff --git a/tests/integration_tests/adapters/binance/resources/http_wallet_sandbox.py b/tests/integration_tests/adapters/binance/resources/http_wallet_sandbox.py index 7075f504231e..fcb440697667 100644 --- a/tests/integration_tests/adapters/binance/resources/http_wallet_sandbox.py +++ b/tests/integration_tests/adapters/binance/resources/http_wallet_sandbox.py @@ -41,6 +41,6 @@ async def test_binance_spot_wallet_http_client(): wallet = BinanceWalletHttpAPI(client=client) await client.connect() response = await wallet.trade_fee(symbol="BTCUSDT") - print(json.dumps(json.loads(response), indent=4)) + print(json.dumps(response, indent=4)) await client.disconnect() diff --git a/tests/integration_tests/adapters/binance/resources/ws_user_sandbox.py b/tests/integration_tests/adapters/binance/resources/ws_user_sandbox.py new file mode 100644 index 000000000000..0b2ae87d3844 --- /dev/null +++ b/tests/integration_tests/adapters/binance/resources/ws_user_sandbox.py @@ -0,0 +1,59 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2021 Nautech Systems Pty Ltd. All rights reserved. +# https://nautechsystems.io +# +# Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ------------------------------------------------------------------------------------------------- + +import asyncio +import os + +import pytest + +from nautilus_trader.adapters.binance.factories import get_cached_binance_http_client +from nautilus_trader.adapters.binance.http.api.user import BinanceUserDataHttpAPI +from nautilus_trader.adapters.binance.websocket.user import BinanceUserDataWebSocket +from nautilus_trader.common.clock import LiveClock +from nautilus_trader.common.logging import LiveLogger +from nautilus_trader.common.logging import Logger + + +@pytest.mark.asyncio +async def test_binance_websocket_client(): + loop = asyncio.get_event_loop() + clock = LiveClock() + + client = get_cached_binance_http_client( + loop=loop, + clock=clock, + logger=Logger(clock=clock), + key=os.getenv("BINANCE_API_KEY"), + secret=os.getenv("BINANCE_API_SECRET"), + ) + await client.connect() + + user = BinanceUserDataHttpAPI(client=client) + response = await user.create_listen_key_spot() + key = response["listenKey"] + + ws = BinanceUserDataWebSocket( + loop=loop, + clock=clock, + logger=LiveLogger(loop=loop, clock=clock), + handler=print, + ) + + ws.subscribe(key=key) + + await ws.connect(start=True) + await asyncio.sleep(4) + await ws.close() + await client.disconnect() diff --git a/tests/integration_tests/adapters/binance/test_http_spot_account.py b/tests/integration_tests/adapters/binance/test_http_spot_account.py new file mode 100644 index 000000000000..29c544ee9c1a --- /dev/null +++ b/tests/integration_tests/adapters/binance/test_http_spot_account.py @@ -0,0 +1,334 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2021 Nautech Systems Pty Ltd. All rights reserved. +# https://nautechsystems.io +# +# Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ------------------------------------------------------------------------------------------------- + +import asyncio + +import pytest + +from nautilus_trader.adapters.binance.http.api.spot_account import BinanceSpotAccountHttpAPI +from nautilus_trader.adapters.binance.http.client import BinanceHttpClient +from nautilus_trader.common.clock import LiveClock +from nautilus_trader.common.logging import Logger + + +class TestBinanceSpotAccountHttpAPI: + def setup(self): + # Fixture Setup + clock = LiveClock() + logger = Logger(clock=clock) + self.client = BinanceHttpClient( # noqa: S106 (no hardcoded password) + loop=asyncio.get_event_loop(), + clock=clock, + logger=logger, + key="SOME_BINANCE_API_KEY", + secret="SOME_BINANCE_API_SECRET", + ) + + self.api = BinanceSpotAccountHttpAPI(self.client) + + @pytest.mark.asyncio + async def test_new_order_test_sends_expected_request(self, mocker): + # Arrange + await self.client.connect() + mock_send_request = mocker.patch(target="aiohttp.client.ClientSession.request") + + # Act + await self.api.new_order_test( + symbol="ETHUSDT", + side="SELL", + type="LIMIT", + time_in_force="GTC", + quantity="0.01", + price="5000", + recv_window=5000, + ) + + # Assert + request = mock_send_request.call_args.kwargs + assert request["method"] == "POST" + assert request["url"] == "https://api.binance.com/api/v3/order/test" + assert request["params"].startswith( + "symbol=ETHUSDT&side=SELL&type=LIMIT&timeInForce=GTC&quantity=0.01&price=5000&recvWindow=5000×tamp=" + ) + + @pytest.mark.asyncio + async def test_order_test_sends_expected_request(self, mocker): + # Arrange + await self.client.connect() + mock_send_request = mocker.patch(target="aiohttp.client.ClientSession.request") + + # Act + await self.api.new_order( + symbol="ETHUSDT", + side="SELL", + type="LIMIT", + time_in_force="GTC", + quantity="0.01", + price="5000", + recv_window=5000, + ) + + # Assert + request = mock_send_request.call_args.kwargs + assert request["method"] == "POST" + assert request["url"] == "https://api.binance.com/api/v3/order" + assert request["params"].startswith( + "symbol=ETHUSDT&side=SELL&type=LIMIT&timeInForce=GTC&quantity=0.01&price=5000&recvWindow=5000×tamp=" + ) + + @pytest.mark.asyncio + async def test_cancel_order_sends_expected_request(self, mocker): + # Arrange + await self.client.connect() + mock_send_request = mocker.patch(target="aiohttp.client.ClientSession.request") + + # Act + await self.api.cancel_order( + symbol="ETHUSDT", + order_id="1", + recv_window=5000, + ) + + # Assert + request = mock_send_request.call_args.kwargs + assert request["method"] == "DELETE" + assert request["url"] == "https://api.binance.com/api/v3/order" + assert request["params"].startswith("symbol=ETHUSDT&orderId=1&recvWindow=5000×tamp=") + + @pytest.mark.asyncio + async def test_cancel_open_orders_sends_expected_request(self, mocker): + # Arrange + await self.client.connect() + mock_send_request = mocker.patch(target="aiohttp.client.ClientSession.request") + + # Act + await self.api.cancel_open_orders( + symbol="ETHUSDT", + recv_window=5000, + ) + + # Assert + request = mock_send_request.call_args.kwargs + assert request["method"] == "DELETE" + assert request["url"] == "https://api.binance.com/api/v3/openOrders" + assert request["params"].startswith("symbol=ETHUSDT&recvWindow=5000×tamp=") + + @pytest.mark.asyncio + async def test_get_order_sends_expected_request(self, mocker): + # Arrange + await self.client.connect() + mock_send_request = mocker.patch(target="aiohttp.client.ClientSession.request") + + # Act + await self.api.get_order( + symbol="ETHUSDT", + order_id="1", + recv_window=5000, + ) + + # Assert + request = mock_send_request.call_args.kwargs + assert request["method"] == "GET" + assert request["url"] == "https://api.binance.com/api/v3/order" + assert request["params"].startswith("symbol=ETHUSDT&orderId=1&recvWindow=5000×tamp=") + + @pytest.mark.asyncio + async def test_get_open_orders_sends_expected_request(self, mocker): + # Arrange + await self.client.connect() + mock_send_request = mocker.patch(target="aiohttp.client.ClientSession.request") + + # Act + await self.api.get_open_orders( + symbol="ETHUSDT", + recv_window=5000, + ) + + # Assert + request = mock_send_request.call_args.kwargs + assert request["method"] == "GET" + assert request["url"] == "https://api.binance.com/api/v3/openOrders" + assert request["params"].startswith("symbol=ETHUSDT&recvWindow=5000×tamp=") + + @pytest.mark.asyncio + async def test_get_orders_sends_expected_request(self, mocker): + # Arrange + await self.client.connect() + mock_send_request = mocker.patch(target="aiohttp.client.ClientSession.request") + + # Act + await self.api.get_orders( + symbol="ETHUSDT", + recv_window=5000, + ) + + # Assert + request = mock_send_request.call_args.kwargs + assert request["method"] == "GET" + assert request["url"] == "https://api.binance.com/api/v3/allOrders" + assert request["params"].startswith("symbol=ETHUSDT&recvWindow=5000×tamp=") + + @pytest.mark.asyncio + async def test_new_oco_order_sends_expected_request(self, mocker): + # Arrange + await self.client.connect() + mock_send_request = mocker.patch(target="aiohttp.client.ClientSession.request") + + # Act + await self.api.new_oco_order( + symbol="ETHUSDT", + side="BUY", + quantity="100", + price="5000.00", + stop_price="4000.00", + list_client_order_id="1", + limit_client_order_id="O-001", + limit_iceberg_qty="50", + stop_client_order_id="O-002", + stop_limit_price="3500.00", + stop_iceberg_qty="50", + stop_limit_time_in_force="GTC", + recv_window=5000, + ) + + # Assert + request = mock_send_request.call_args.kwargs + assert request["method"] == "POST" + assert request["url"] == "https://api.binance.com/api/v3/order/oco" + assert request["params"].startswith( + "symbol=ETHUSDT&side=BUY&quantity=100&price=5000.00&stopPrice=4000.00&listClientOrderId=1&limitClientOrderId=O-001&limitIcebergQty=50&stopClientOrderId=O-002&stopLimitPrice=3500.00&stopIcebergQty=50&stopLimitTimeInForce=GTC&recvWindow=5000×tamp=" # noqa + ) + + @pytest.mark.asyncio + async def test_cancel_oco_order_sends_expected_request(self, mocker): + # Arrange + await self.client.connect() + mock_send_request = mocker.patch(target="aiohttp.client.ClientSession.request") + + # Act + await self.api.cancel_oco_order( + symbol="ETHUSDT", + order_list_id="1", + list_client_order_id="1", + new_client_order_id="2", + recv_window=5000, + ) + + # Assert + request = mock_send_request.call_args.kwargs + assert request["method"] == "DELETE" + assert request["url"] == "https://api.binance.com/api/v3/orderList" + assert request["params"].startswith( + "symbol=ETHUSDT&orderListId=1&listClientOrderId=1&newClientOrderId=2&recvWindow=5000×tamp=" + ) + + @pytest.mark.asyncio + async def test_get_oco_order_sends_expected_request(self, mocker): + # Arrange + await self.client.connect() + mock_send_request = mocker.patch(target="aiohttp.client.ClientSession.request") + + # Act + await self.api.get_oco_order( + order_list_id="1", + orig_client_order_id="1", + recv_window=5000, + ) + + # Assert + request = mock_send_request.call_args.kwargs + assert request["method"] == "GET" + assert request["url"] == "https://api.binance.com/api/v3/orderList" + assert request["params"].startswith( + "orderListId=1&origClientOrderId=1&recvWindow=5000×tamp=" + ) + + @pytest.mark.asyncio + async def test_get_oco_orders_sends_expected_request(self, mocker): + # Arrange + await self.client.connect() + mock_send_request = mocker.patch(target="aiohttp.client.ClientSession.request") + + # Act + await self.api.get_oco_orders( + from_id="1", + start_time=1600000000, + end_time=1637355823, + limit=10, + recv_window=5000, + ) + + # Assert + request = mock_send_request.call_args.kwargs + assert request["method"] == "GET" + assert request["url"] == "https://api.binance.com/api/v3/allOrderList" + assert request["params"].startswith( + "fromId=1&startTime=1600000000&endTime=1637355823&limit=10&recvWindow=5000×tamp=" + ) + + @pytest.mark.asyncio + async def test_get_open_oco_orders_sends_expected_request(self, mocker): + # Arrange + await self.client.connect() + mock_send_request = mocker.patch(target="aiohttp.client.ClientSession.request") + + # Act + await self.api.get_oco_open_orders(recv_window=5000) + + # Assert + request = mock_send_request.call_args.kwargs + assert request["method"] == "GET" + assert request["url"] == "https://api.binance.com/api/v3/openOrderList" + assert request["params"].startswith("recvWindow=5000×tamp=") + + @pytest.mark.asyncio + async def test_account_sends_expected_request(self, mocker): + # Arrange + await self.client.connect() + mock_send_request = mocker.patch(target="aiohttp.client.ClientSession.request") + + # Act + await self.api.account(recv_window=5000) + + # Assert + request = mock_send_request.call_args.kwargs + assert request["method"] == "GET" + assert request["url"] == "https://api.binance.com/api/v3/account" + assert request["params"].startswith("recvWindow=5000×tamp=") + + @pytest.mark.asyncio + async def test_my_trades_sends_expected_request(self, mocker): + # Arrange + await self.client.connect() + mock_send_request = mocker.patch(target="aiohttp.client.ClientSession.request") + + # Act + await self.api.my_trades( + symbol="ETHUSDT", + from_id="1", + order_id="1", + start_time=1600000000, + end_time=1637355823, + limit=1000, + recv_window=5000, + ) + + # Assert + request = mock_send_request.call_args.kwargs + assert request["method"] == "GET" + assert request["url"] == "https://api.binance.com/api/v3/myTrades" + assert request["params"].startswith( + "symbol=ETHUSDT&fromId=1&orderId=1&startTime=1600000000&endTime=1637355823&limit=1000&recvWindow=5000×tamp=" + ) diff --git a/tests/integration_tests/adapters/binance/test_http_user.py b/tests/integration_tests/adapters/binance/test_http_user.py new file mode 100644 index 000000000000..bc4600d7d747 --- /dev/null +++ b/tests/integration_tests/adapters/binance/test_http_user.py @@ -0,0 +1,204 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2021 Nautech Systems Pty Ltd. All rights reserved. +# https://nautechsystems.io +# +# Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ------------------------------------------------------------------------------------------------- + +import asyncio + +import pytest + +from nautilus_trader.adapters.binance.http.api.user import BinanceUserDataHttpAPI +from nautilus_trader.adapters.binance.http.client import BinanceHttpClient +from nautilus_trader.common.clock import LiveClock +from nautilus_trader.common.logging import Logger + + +class TestBinanceUserHttpAPI: + def setup(self): + # Fixture Setup + clock = LiveClock() + logger = Logger(clock=clock) + self.client = BinanceHttpClient( # noqa: S106 (no hardcoded password) + loop=asyncio.get_event_loop(), + clock=clock, + logger=logger, + key="SOME_BINANCE_API_KEY", + secret="SOME_BINANCE_API_SECRET", + ) + + self.api = BinanceUserDataHttpAPI(self.client) + + @pytest.mark.asyncio + async def test_create_listen_key_spot(self, mocker): + # Arrange + await self.client.connect() + mock_send_request = mocker.patch(target="aiohttp.client.ClientSession.request") + + # Act + await self.api.create_listen_key_spot() + + # Assert + request = mock_send_request.call_args.kwargs + assert request["method"] == "POST" + assert request["url"] == "https://api.binance.com/api/v3/userDataStream" + + @pytest.mark.asyncio + async def test_ping_listen_key_spot(self, mocker): + # Arrange + await self.client.connect() + mock_send_request = mocker.patch(target="aiohttp.client.ClientSession.request") + + # Act + await self.api.ping_listen_key_spot( + key="JUdsZc8CSmMUxg1wJha23RogrT3EuC8eV5UTbAOVTkF3XWofMzWoXtWmDAhy" + ) + + # Assert + request = mock_send_request.call_args.kwargs + assert request["method"] == "PUT" + assert request["url"] == "https://api.binance.com/api/v3/userDataStream" + assert ( + request["params"] + == "listenKey=JUdsZc8CSmMUxg1wJha23RogrT3EuC8eV5UTbAOVTkF3XWofMzWoXtWmDAhy" + ) + + @pytest.mark.asyncio + async def test_close_listen_key_spot(self, mocker): + # Arrange + await self.client.connect() + mock_send_request = mocker.patch(target="aiohttp.client.ClientSession.request") + + # Act + await self.api.close_listen_key_spot( + key="JUdsZc8CSmMUxg1wJha23RogrT3EuC8eV5UTbAOVTkF3XWofMzWoXtWmDAhy" + ) + + # Assert + request = mock_send_request.call_args.kwargs + assert request["method"] == "DELETE" + assert request["url"] == "https://api.binance.com/api/v3/userDataStream" + assert ( + request["params"] + == "listenKey=JUdsZc8CSmMUxg1wJha23RogrT3EuC8eV5UTbAOVTkF3XWofMzWoXtWmDAhy" + ) + + @pytest.mark.asyncio + async def test_create_listen_key_margin(self, mocker): + # Arrange + await self.client.connect() + mock_send_request = mocker.patch(target="aiohttp.client.ClientSession.request") + + # Act + await self.api.create_listen_key_margin() + + # Assert + request = mock_send_request.call_args.kwargs + assert request["method"] == "POST" + assert request["url"] == "https://api.binance.com/sapi/v1/userDataStream" + + @pytest.mark.asyncio + async def test_ping_listen_key_margin(self, mocker): + # Arrange + await self.client.connect() + mock_send_request = mocker.patch(target="aiohttp.client.ClientSession.request") + + # Act + await self.api.ping_listen_key_margin( + key="JUdsZc8CSmMUxg1wJha23RogrT3EuC8eV5UTbAOVTkF3XWofMzWoXtWmDAhy" + ) + + # Assert + request = mock_send_request.call_args.kwargs + assert request["method"] == "PUT" + assert request["url"] == "https://api.binance.com/sapi/v1/userDataStream" + assert ( + request["params"] + == "listenKey=JUdsZc8CSmMUxg1wJha23RogrT3EuC8eV5UTbAOVTkF3XWofMzWoXtWmDAhy" + ) + + @pytest.mark.asyncio + async def test_close_listen_key_margin(self, mocker): + # Arrange + await self.client.connect() + mock_send_request = mocker.patch(target="aiohttp.client.ClientSession.request") + + # Act + await self.api.close_listen_key_margin( + key="JUdsZc8CSmMUxg1wJha23RogrT3EuC8eV5UTbAOVTkF3XWofMzWoXtWmDAhy" + ) + + # Assert + request = mock_send_request.call_args.kwargs + assert request["method"] == "DELETE" + assert request["url"] == "https://api.binance.com/sapi/v1/userDataStream" + assert ( + request["params"] + == "listenKey=JUdsZc8CSmMUxg1wJha23RogrT3EuC8eV5UTbAOVTkF3XWofMzWoXtWmDAhy" + ) + + @pytest.mark.asyncio + async def test_create_listen_key_isolated_margin(self, mocker): + # Arrange + await self.client.connect() + mock_send_request = mocker.patch(target="aiohttp.client.ClientSession.request") + + # Act + await self.api.create_listen_key_isolated_margin(symbol="ETHUSDT") + + # Assert + request = mock_send_request.call_args.kwargs + assert request["method"] == "POST" + assert request["url"] == "https://api.binance.com/sapi/v1/userDataStream/isolated" + assert request["params"] == "symbol=ETHUSDT" + + @pytest.mark.asyncio + async def test_ping_listen_key_isolated_margin(self, mocker): + # Arrange + await self.client.connect() + mock_send_request = mocker.patch(target="aiohttp.client.ClientSession.request") + + # Act + await self.api.ping_listen_key_isolated_margin( + symbol="ETHUSDT", + key="JUdsZc8CSmMUxg1wJha23RogrT3EuC8eV5UTbAOVTkF3XWofMzWoXtWmDAhy", + ) + + # Assert + request = mock_send_request.call_args.kwargs + assert request["method"] == "PUT" + assert request["url"] == "https://api.binance.com/sapi/v1/userDataStream/isolated" + assert ( + request["params"] + == "listenKey=JUdsZc8CSmMUxg1wJha23RogrT3EuC8eV5UTbAOVTkF3XWofMzWoXtWmDAhy&symbol=ETHUSDT" + ) + + @pytest.mark.asyncio + async def test_close_listen_key_isolated_margin(self, mocker): + # Arrange + await self.client.connect() + mock_send_request = mocker.patch(target="aiohttp.client.ClientSession.request") + + # Act + await self.api.close_listen_key_isolated_margin( + symbol="ETHUSDT", + key="JUdsZc8CSmMUxg1wJha23RogrT3EuC8eV5UTbAOVTkF3XWofMzWoXtWmDAhy", + ) + + # Assert + request = mock_send_request.call_args.kwargs + assert request["method"] == "DELETE" + assert request["url"] == "https://api.binance.com/sapi/v1/userDataStream/isolated" + assert ( + request["params"] + == "listenKey=JUdsZc8CSmMUxg1wJha23RogrT3EuC8eV5UTbAOVTkF3XWofMzWoXtWmDAhy&symbol=ETHUSDT" + ) diff --git a/tests/integration_tests/adapters/binance/test_http_wallet.py b/tests/integration_tests/adapters/binance/test_http_wallet.py new file mode 100644 index 000000000000..445f6b545323 --- /dev/null +++ b/tests/integration_tests/adapters/binance/test_http_wallet.py @@ -0,0 +1,53 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2021 Nautech Systems Pty Ltd. All rights reserved. +# https://nautechsystems.io +# +# Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ------------------------------------------------------------------------------------------------- + +import asyncio + +import pytest + +from nautilus_trader.adapters.binance.http.api.wallet import BinanceWalletHttpAPI +from nautilus_trader.adapters.binance.http.client import BinanceHttpClient +from nautilus_trader.common.clock import LiveClock +from nautilus_trader.common.logging import Logger + + +class TestBinanceUserHttpAPI: + def setup(self): + # Fixture Setup + clock = LiveClock() + logger = Logger(clock=clock) + self.client = BinanceHttpClient( # noqa: S106 (no hardcoded password) + loop=asyncio.get_event_loop(), + clock=clock, + logger=logger, + key="SOME_BINANCE_API_KEY", + secret="SOME_BINANCE_API_SECRET", + ) + + self.api = BinanceWalletHttpAPI(self.client) + + @pytest.mark.asyncio + async def test_trade_fee(self, mocker): + # Arrange + await self.client.connect() + mock_send_request = mocker.patch(target="aiohttp.client.ClientSession.request") + + # Act + await self.api.trade_fee() + + # Assert + request = mock_send_request.call_args.kwargs + assert request["method"] == "GET" + assert request["url"] == "https://api.binance.com/sapi/v1/asset/tradeFee" diff --git a/tests/integration_tests/adapters/binance/test_providers.py b/tests/integration_tests/adapters/binance/test_providers.py index 4f7b9ad19af6..a6c05960a7a0 100644 --- a/tests/integration_tests/adapters/binance/test_providers.py +++ b/tests/integration_tests/adapters/binance/test_providers.py @@ -16,6 +16,7 @@ import pkgutil from typing import Dict +import orjson import pytest from nautilus_trader.adapters.binance.http.client import BinanceHttpClient @@ -53,7 +54,7 @@ async def mock_send_request( url_path: str, # noqa (needed for mock) payload: Dict[str, str], # noqa (needed for mock) ) -> bytes: - return responses.pop() + return orjson.loads(responses.pop()) # Apply mock coroutine to client monkeypatch.setattr( diff --git a/tests/unit_tests/common/test_common_logging.py b/tests/unit_tests/common/test_common_logging.py index af6f266536f4..90ed76c78d3e 100644 --- a/tests/unit_tests/common/test_common_logging.py +++ b/tests/unit_tests/common/test_common_logging.py @@ -252,8 +252,8 @@ async def test_log_when_queue_over_maxsize_blocks(self): logger_adapter.info("A log message.") # <-- blocks await asyncio.sleep(0.3) # <-- processes all log messages - self.logger.stop() + logger.stop() await asyncio.sleep(0.3) # Assert - assert not self.logger.is_running + assert not logger.is_running diff --git a/tests/unit_tests/model/test_model_enums.py b/tests/unit_tests/model/test_model_enums.py index 1dce97113026..b9c3eedd12d1 100644 --- a/tests/unit_tests/model/test_model_enums.py +++ b/tests/unit_tests/model/test_model_enums.py @@ -45,6 +45,8 @@ from nautilus_trader.model.enums import LiquiditySideParser from nautilus_trader.model.enums import OMSType from nautilus_trader.model.enums import OMSTypeParser +from nautilus_trader.model.enums import OptionKind +from nautilus_trader.model.enums import OptionKindParser from nautilus_trader.model.enums import OrderSide from nautilus_trader.model.enums import OrderSideParser from nautilus_trader.model.enums import OrderStatus @@ -457,6 +459,44 @@ def test_depth_type_from_str(self, string, expected): assert expected == result +class TestOptionKind: + def test_option_kind_parser_given_invalid_value_raises_value_error(self): + # Arrange, Act, Assert + with pytest.raises(ValueError): + OptionKindParser.to_str_py(0) + + with pytest.raises(ValueError): + OptionKindParser.from_str_py("") + + @pytest.mark.parametrize( + "enum, expected", + [ + [OptionKind.CALL, "CALL"], + [OptionKind.PUT, "PUT"], + ], + ) + def test_option_kind_to_str(self, enum, expected): + # Arrange, Act + result = OptionKindParser.to_str_py(enum) + + # Assert + assert expected == result + + @pytest.mark.parametrize( + "string, expected", + [ + ["CALL", OptionKind.CALL], + ["PUT", OptionKind.PUT], + ], + ) + def test_option_kind_from_str(self, string, expected): + # Arrange, Act + result = OptionKindParser.from_str_py(string) + + # Assert + assert expected == result + + class TestInstrumentCloseType: def test_instrument_close_type_parser_given_invalid_value_raises_value_error(self): # Arrange, Act, Assert diff --git a/tests/unit_tests/model/test_model_instrument.py b/tests/unit_tests/model/test_model_instrument.py index f32a1f09f83d..3722f126f945 100644 --- a/tests/unit_tests/model/test_model_instrument.py +++ b/tests/unit_tests/model/test_model_instrument.py @@ -24,6 +24,7 @@ from nautilus_trader.model.currencies import ETH from nautilus_trader.model.currencies import USD from nautilus_trader.model.currencies import USDT +from nautilus_trader.model.enums import OptionKindParser from nautilus_trader.model.instruments.base import Instrument from nautilus_trader.model.instruments.crypto_swap import CryptoSwap from nautilus_trader.model.objects import Money @@ -319,6 +320,10 @@ def test_next_bid_price(self, instrument, tick_scheme_name, value, n, expected): expected = Price.from_str(expected) assert result == expected + def test_option_attributes(self): + assert AAPL_OPTION.underlying == "AAPL" + assert AAPL_OPTION.kind == OptionKindParser.from_str_py("CALL") + class TestBettingInstrument: def setup(self): diff --git a/tests/unit_tests/model/test_model_orders.py b/tests/unit_tests/model/test_model_orders.py index 4aa2f75a265d..9a85bda2d91c 100644 --- a/tests/unit_tests/model/test_model_orders.py +++ b/tests/unit_tests/model/test_model_orders.py @@ -370,6 +370,7 @@ def test_limit_order_to_dict(self): OrderSide.BUY, Quantity.from_int(100000), Price.from_str("1.00000"), + display_qty=Quantity.from_int(20000), ) # Act @@ -398,7 +399,7 @@ def test_limit_order_to_dict(self): "status": "INITIALIZED", "is_post_only": False, "is_reduce_only": False, - "is_hidden": False, + "display_qty": "20000", "order_list_id": None, "parent_order_id": None, "child_order_ids": None, @@ -567,7 +568,7 @@ def test_stop_limit_order_to_dict(self): "status": "INITIALIZED", "is_post_only": False, "is_reduce_only": False, - "is_hidden": False, + "display_qty": None, "order_list_id": None, "parent_order_id": None, "child_order_ids": None, diff --git a/tests/unit_tests/serialization/test_serialization_msgpack.py b/tests/unit_tests/serialization/test_serialization_msgpack.py index 932f019e615b..ad8045e9a4d4 100644 --- a/tests/unit_tests/serialization/test_serialization_msgpack.py +++ b/tests/unit_tests/serialization/test_serialization_msgpack.py @@ -161,6 +161,7 @@ def test_pack_and_unpack_limit_orders(self): Quantity(100000, precision=0), Price(1.00000, precision=5), TimeInForce.DAY, + display_qty=Quantity(50000, precision=0), ) # Act diff --git a/tests/unit_tests/trading/test_trading_trader.py b/tests/unit_tests/trading/test_trading_trader.py index 0e500b56dbf7..5519c122c689 100644 --- a/tests/unit_tests/trading/test_trading_trader.py +++ b/tests/unit_tests/trading/test_trading_trader.py @@ -190,46 +190,46 @@ def test_clear_strategies(self): # Assert assert self.trader.strategy_states() == {} - def test_add_component(self): + def test_add_actor(self): # Arrange config = ActorConfig(component_id="MyPlugin-01") - component = Actor(config) + actor = Actor(config) # Act - self.trader.add_component(component) + self.trader.add_actor(actor) # Assert - assert self.trader.component_ids() == [ComponentId("MyPlugin-01")] + assert self.trader.actor_ids() == [ComponentId("MyPlugin-01")] - def test_add_plugins(self): + def test_add_actors(self): # Arrange - plugins = [ + actors = [ Actor(ActorConfig(component_id="MyPlugin-01")), Actor(ActorConfig(component_id="MyPlugin-02")), ] # Act - self.trader.add_components(plugins) + self.trader.add_actors(actors) # Assert - assert self.trader.component_ids() == [ + assert self.trader.actor_ids() == [ ComponentId("MyPlugin-01"), ComponentId("MyPlugin-02"), ] - def test_clear_plugins(self): + def test_clear_actors(self): # Arrange - plugins = [ + actors = [ Actor(ActorConfig(component_id="MyPlugin-01")), Actor(ActorConfig(component_id="MyPlugin-02")), ] - self.trader.add_components(plugins) + self.trader.add_actors(actors) # Act - self.trader.clear_components() + self.trader.clear_actors() # Assert - assert self.trader.component_ids() == [] + assert self.trader.actor_ids() == [] def test_get_strategy_states(self): # Arrange diff --git a/version.json b/version.json index 245d24057cbf..f481ba1f06f8 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "schemaVersion": 1, "label": "", - "message": "v1.133.0", + "message": "v1.134.0", "color": "blue" }