From 4be4016e53499ce9206c5bc1450e18418ed1d390 Mon Sep 17 00:00:00 2001 From: Graeme Holliday Date: Thu, 10 Oct 2024 12:48:52 -0500 Subject: [PATCH] Move from price effect to +/- for numbers (#169) * begin remove price effects * finish change, update tests * fix bug in tests --- .github/pull_request_template.md | 4 +- .github/workflows/python-app.yml | 7 +- .gitignore | 3 - .python-version | 1 + Makefile | 6 +- pyproject.toml | 2 +- tastytrade/account.py | 108 +++++++++++++++++++------- tastytrade/order.py | 128 +++++++++++++++++++++---------- tastytrade/streamer.py | 21 +++-- tastytrade/utils.py | 36 +++++++++ tests/test_account.py | 28 ++++--- tests/test_streamer.py | 2 +- uv.lock | 64 +++++----------- 13 files changed, 262 insertions(+), 148 deletions(-) create mode 100644 .python-version diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index fd45965..34c25d0 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -4,9 +4,9 @@ Fixes ... ## Pre-merge checklist -- [ ] Code formatted correctly with `uv run ruff format .` +- [ ] Code formatted correctly (check with `make lint`) - [ ] Code implemented for both sync and async -- [ ] Passing tests locally +- [ ] Passing tests locally (check with `make test`, make sure you have `TT_USERNAME`, `TT_PASSWORD`, and `TT_ACCOUNT` environment variables set) - [ ] New tests added (if applicable) Please note that, in order to pass the tests, you'll need to set up your Tastytrade credentials as repository secrets on your local fork. Read more at CONTRIBUTING.md. diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index c524206..7bc850c 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -21,11 +21,10 @@ jobs: uv sync - name: Lint with ruff run: | - uv run ruff check . - - name: Type check with mypy + uv run ruff check tastytrade/ tests/ + - name: Type check with pyright run: | - uv run mypy -p tastytrade - uv run mypy -p tests + uv run pyright tastytrade/ tests/ - name: Test with pytest run: | uv run pytest --cov=tastytrade --cov-report=term-missing tests/ --cov-fail-under=95 diff --git a/.gitignore b/.gitignore index de8a4c3..e4179d2 100644 --- a/.gitignore +++ b/.gitignore @@ -74,9 +74,6 @@ target/ .ipynb_checkpoints *.ipynb -# pyenv -.python-version - # celery beat schedule file celerybeat-schedule diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..cc1923a --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.8 diff --git a/Makefile b/Makefile index e60cee4..1e0c4cd 100644 --- a/Makefile +++ b/Makefile @@ -4,9 +4,9 @@ install: uv sync lint: - uv run ruff check --fix . - uv run mypy -p tastytrade - uv run mypy -p tests + uv run ruff format tastytrade/ tests/ + uv run ruff check tastytrade/ tests/ + uv run pyright tastytrade/ tests/ test: uv run pytest --cov=tastytrade --cov-report=term-missing tests/ --cov-fail-under=95 diff --git a/pyproject.toml b/pyproject.toml index 5f158c5..19ca258 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,12 +25,12 @@ Documentation = "https://tastyworks-api.readthedocs.io/en/latest" [tool.uv] dev-dependencies = [ - "mypy>=1.11.2", "pytest>=8.3.3", "pytest-aio>=1.5.0", "pytest-cov>=5.0.0", "ruff>=0.6.9", "types-pytz>=2024.2.0.20241003", + "pyright>=1.1.384", ] [tool.setuptools.package-data] diff --git a/tastytrade/account.py b/tastytrade/account.py index aa3c274..036afda 100644 --- a/tastytrade/account.py +++ b/tastytrade/account.py @@ -2,7 +2,7 @@ from decimal import Decimal from typing import Any, Dict, List, Literal, Optional, Union -from pydantic import BaseModel +from pydantic import BaseModel, model_validator from tastytrade.order import ( InstrumentType, @@ -13,12 +13,13 @@ PlacedComplexOrder, PlacedOrder, PlacedOrderResponse, - PriceEffect, ) from tastytrade.session import Session from tastytrade.utils import ( + PriceEffect, TastytradeError, TastytradeJsonDataclass, + _set_sign_for, today_in_new_york, validate_response, ) @@ -61,12 +62,10 @@ class AccountBalance(TastytradeJsonDataclass): cash_available_to_withdraw: Decimal day_trade_excess: Decimal pending_cash: Decimal - pending_cash_effect: PriceEffect long_cryptocurrency_value: Decimal short_cryptocurrency_value: Decimal cryptocurrency_margin_requirement: Decimal unsettled_cryptocurrency_fiat_amount: Decimal - unsettled_cryptocurrency_fiat_effect: PriceEffect closed_loop_available_balance: Decimal equity_offering_margin_requirement: Decimal long_bond_value: Decimal @@ -81,9 +80,18 @@ class AccountBalance(TastytradeJsonDataclass): updated_at: datetime apex_starting_day_margin_equity: Optional[Decimal] = None buying_power_adjustment: Optional[Decimal] = None - buying_power_adjustment_effect: Optional[PriceEffect] = None time_of_day: Optional[str] = None + @model_validator(mode="before") + @classmethod + def validate_price_effects(cls, data: Any) -> Any: + if isinstance(data, dict): + key = "unsettled-cryptocurrency-fiat-amount" + effect = data.get("unsettled-cryptocurrency-fiat-effect") + if effect == PriceEffect.DEBIT: + data[key] = -abs(Decimal(data[key])) + return _set_sign_for(data, ["pending_cash", "buying_power_adjustment"]) + class AccountBalanceSnapshot(TastytradeJsonDataclass): """ @@ -117,19 +125,27 @@ class AccountBalanceSnapshot(TastytradeJsonDataclass): cash_available_to_withdraw: Decimal day_trade_excess: Decimal pending_cash: Decimal - pending_cash_effect: PriceEffect snapshot_date: date time_of_day: Optional[str] = None long_cryptocurrency_value: Optional[Decimal] = None short_cryptocurrency_value: Optional[Decimal] = None cryptocurrency_margin_requirement: Optional[Decimal] = None unsettled_cryptocurrency_fiat_amount: Optional[Decimal] = None - unsettled_cryptocurrency_fiat_effect: Optional[PriceEffect] = None closed_loop_available_balance: Optional[Decimal] = None equity_offering_margin_requirement: Optional[Decimal] = None long_bond_value: Optional[Decimal] = None bond_margin_requirement: Optional[Decimal] = None + @model_validator(mode="before") + @classmethod + def validate_price_effects(cls, data: Any) -> Any: + if isinstance(data, dict): + key = "unsettled-cryptocurrency-fiat-amount" + effect = data.get("unsettled-cryptocurrency-fiat-effect") + if effect == PriceEffect.DEBIT: + data[key] = -abs(Decimal(data[key])) + return _set_sign_for(data, ["pending_cash"]) + class CurrentPosition(TastytradeJsonDataclass): """ @@ -161,15 +177,22 @@ class CurrentPosition(TastytradeJsonDataclass): deliverable_type: Optional[str] = None average_yearly_market_close_price: Optional[Decimal] = None average_daily_market_close_price: Optional[Decimal] = None - realized_day_gain_effect: Optional[PriceEffect] = None realized_day_gain_date: Optional[date] = None - realized_today_effect: Optional[PriceEffect] = None realized_today_date: Optional[date] = None + @model_validator(mode="before") + @classmethod + def validate_price_effects(cls, data: Any) -> Any: + return _set_sign_for(data, ["realized_day_gain", "realized_today"]) + class FeesInfo(TastytradeJsonDataclass): total_fees: Decimal - total_fees_effect: PriceEffect + + @model_validator(mode="before") + @classmethod + def validate_price_effects(cls, data: Any) -> Any: + return _set_sign_for(data, ["total_fees"]) class Lot(TastytradeJsonDataclass): @@ -195,23 +218,32 @@ class MarginReportEntry(TastytradeJsonDataclass): description: str code: str buying_power: Decimal - buying_power_effect: PriceEffect margin_calculation_type: str margin_requirement: Decimal - margin_requirement_effect: PriceEffect expected_price_range_up_percent: Optional[Decimal] = None expected_price_range_down_percent: Optional[Decimal] = None groups: Optional[List[Dict[str, Any]]] = None initial_requirement: Optional[Decimal] = None - initial_requirement_effect: Optional[PriceEffect] = None maintenance_requirement: Optional[Decimal] = None - maintenance_requirement_effect: Optional[PriceEffect] = None point_of_no_return_percent: Optional[Decimal] = None price_increase_percent: Optional[Decimal] = None price_decrease_percent: Optional[Decimal] = None underlying_symbol: Optional[str] = None underlying_type: Optional[str] = None + @model_validator(mode="before") + @classmethod + def validate_price_effects(cls, data: Any) -> Any: + return _set_sign_for( + data, + [ + "buying_power", + "margin_requirement", + "initial_requirement", + "maintenance_requirement", + ], + ) + class MarginReport(TastytradeJsonDataclass): """ @@ -223,23 +255,32 @@ class MarginReport(TastytradeJsonDataclass): margin_calculation_type: str option_level: str margin_requirement: Decimal - margin_requirement_effect: PriceEffect maintenance_requirement: Decimal - maintenance_requirement_effect: PriceEffect margin_equity: Decimal - margin_equity_effect: PriceEffect option_buying_power: Decimal - option_buying_power_effect: PriceEffect reg_t_margin_requirement: Decimal - reg_t_margin_requirement_effect: PriceEffect reg_t_option_buying_power: Decimal - reg_t_option_buying_power_effect: PriceEffect maintenance_excess: Decimal - maintenance_excess_effect: PriceEffect last_state_timestamp: int groups: List[Union[MarginReportEntry, EmptyDict]] initial_requirement: Optional[Decimal] = None - initial_requirement_effect: Optional[PriceEffect] = None + + @model_validator(mode="before") + @classmethod + def validate_price_effects(cls, data: Any) -> Any: + return _set_sign_for( + data, + [ + "maintenance_requirement", + "margin_requirement", + "margin_equity", + "maintenance_excess", + "option_buying_power", + "reg_t_margin_requirement", + "reg_t_option_buying_power", + "initial_requirement", + ], + ) class MarginRequirement(TastytradeJsonDataclass): @@ -355,9 +396,7 @@ class Transaction(TastytradeJsonDataclass): executed_at: datetime transaction_date: date value: Decimal - value_effect: PriceEffect net_value: Decimal - net_value_effect: PriceEffect is_estimated_fee: bool symbol: Optional[str] = None instrument_type: Optional[InstrumentType] = None @@ -366,13 +405,9 @@ class Transaction(TastytradeJsonDataclass): quantity: Optional[Decimal] = None price: Optional[Decimal] = None regulatory_fees: Optional[Decimal] = None - regulatory_fees_effect: Optional[PriceEffect] = None clearing_fees: Optional[Decimal] = None - clearing_fees_effect: Optional[PriceEffect] = None commission: Optional[Decimal] = None - commission_effect: Optional[PriceEffect] = None proprietary_index_option_fees: Optional[Decimal] = None - proprietary_index_option_fees_effect: Optional[PriceEffect] = None ext_exchange_order_number: Optional[str] = None ext_global_order_number: Optional[int] = None ext_group_id: Optional[str] = None @@ -385,7 +420,6 @@ class Transaction(TastytradeJsonDataclass): leg_count: Optional[int] = None destination_venue: Optional[str] = None other_charge: Optional[Decimal] = None - other_charge_effect: Optional[PriceEffect] = None other_charge_description: Optional[str] = None reverses_id: Optional[int] = None cost_basis_reconciliation_date: Optional[date] = None @@ -393,6 +427,22 @@ class Transaction(TastytradeJsonDataclass): agency_price: Optional[Decimal] = None principal_price: Optional[Decimal] = None + @model_validator(mode="before") + @classmethod + def validate_price_effects(cls, data: Any) -> Any: + return _set_sign_for( + data, + [ + "value", + "net_value", + "regulatory_fees", + "clearing_fees", + "proprietary_index_option_fees", + "commission", + "other_charge", + ], + ) + class Account(TastytradeJsonDataclass): """ diff --git a/tastytrade/order.py b/tastytrade/order.py index 2e89e73..3637cc5 100644 --- a/tastytrade/order.py +++ b/tastytrade/order.py @@ -1,10 +1,17 @@ from datetime import date, datetime from decimal import Decimal from enum import Enum -from typing import Dict, List, Optional, Union +from typing import Any, Dict, List, Optional, Union + +from pydantic import computed_field, field_serializer, model_validator from tastytrade import VERSION -from tastytrade.utils import TastytradeJsonDataclass +from tastytrade.utils import ( + PriceEffect, + TastytradeJsonDataclass, + _get_sign, + _set_sign_for, +) class InstrumentType(str, Enum): @@ -101,17 +108,6 @@ class ComplexOrderType(str, Enum): OTOCO = "OTOCO" -class PriceEffect(str, Enum): - """ - This is an :class:`~enum.Enum` that shows the sign of a price effect, since - Tastytrade is apparently against negative numbers. - """ - - CREDIT = "Credit" - DEBIT = "Debit" - NONE = "None" - - class FillInfo(TastytradeJsonDataclass): """ Dataclass that contains information about an order fill. @@ -236,15 +232,30 @@ class NewOrder(TastytradeJsonDataclass): source: str = f"tastyware/tastytrade:v{VERSION}" legs: List[Leg] gtc_date: Optional[date] = None + #: For a stop/stop limit order. If the latter, use price for the limit price stop_trigger: Optional[Decimal] = None - price: Optional[Decimal] = None # optional for market orders - price_effect: Optional[PriceEffect] = None + #: The price of the order; negative = debit, positive = credit + price: Optional[Decimal] = None + #: The actual notional value of the order. Only for notional market orders! value: Optional[Decimal] = None - value_effect: Optional[PriceEffect] = None partition_key: Optional[str] = None preflight_id: Optional[str] = None rules: Optional[OrderRule] = None + @computed_field + @property + def price_effect(self) -> Optional[PriceEffect]: + return _get_sign(self.price) + + @computed_field + @property + def value_effect(self) -> Optional[PriceEffect]: + return _get_sign(self.value) + + @field_serializer("price", "value") + def serialize_fields(self, field: Optional[Decimal]) -> Optional[Decimal]: + return abs(field) if field else None + class NewComplexOrder(TastytradeJsonDataclass): """ @@ -283,10 +294,8 @@ class PlacedOrder(TastytradeJsonDataclass): size: Optional[Decimal] = None id: Optional[int] = None price: Optional[Decimal] = None - price_effect: Optional[PriceEffect] = None gtc_date: Optional[date] = None value: Optional[Decimal] = None - value_effect: Optional[PriceEffect] = None stop_trigger: Optional[str] = None contingent_status: Optional[str] = None confirmation_status: Optional[str] = None @@ -307,6 +316,11 @@ class PlacedOrder(TastytradeJsonDataclass): preflight_id: Optional[Union[str, int]] = None order_rule: Optional[OrderRule] = None + @model_validator(mode="before") + @classmethod + def validate_price_effects(cls, data: Any) -> Any: + return _set_sign_for(data, ["price", "value"]) + class PlacedComplexOrder(TastytradeJsonDataclass): """ @@ -332,19 +346,28 @@ class BuyingPowerEffect(TastytradeJsonDataclass): """ change_in_margin_requirement: Decimal - change_in_margin_requirement_effect: PriceEffect change_in_buying_power: Decimal - change_in_buying_power_effect: PriceEffect current_buying_power: Decimal - current_buying_power_effect: PriceEffect new_buying_power: Decimal - new_buying_power_effect: PriceEffect isolated_order_margin_requirement: Decimal - isolated_order_margin_requirement_effect: PriceEffect is_spread: bool impact: Decimal effect: PriceEffect + @model_validator(mode="before") + @classmethod + def validate_price_effects(cls, data: Any) -> Any: + return _set_sign_for( + data, + [ + "change_in_margin_requirement", + "change_in_buying_power", + "current_buying_power", + "new_buying_power", + "isolated_order_margin_requirement", + ], + ) + class FeeCalculation(TastytradeJsonDataclass): """ @@ -352,15 +375,24 @@ class FeeCalculation(TastytradeJsonDataclass): """ regulatory_fees: Decimal - regulatory_fees_effect: PriceEffect clearing_fees: Decimal - clearing_fees_effect: PriceEffect commission: Decimal - commission_effect: PriceEffect proprietary_index_option_fees: Decimal - proprietary_index_option_fees_effect: PriceEffect total_fees: Decimal - total_fees_effect: PriceEffect + + @model_validator(mode="before") + @classmethod + def validate_price_effects(cls, data: Any) -> Any: + return _set_sign_for( + data, + [ + "regulatory_fees", + "clearing_fees", + "commission", + "proprietary_index_option_fees", + "total_fees", + ], + ) class PlacedOrderResponse(TastytradeJsonDataclass): @@ -411,17 +443,26 @@ class OrderChainNode(TastytradeJsonDataclass): description: str occurred_at: Optional[datetime] = None total_fees: Optional[Decimal] = None - total_fees_effect: Optional[PriceEffect] = None total_fill_cost: Optional[Decimal] = None - total_fill_cost_effect: Optional[PriceEffect] = None gcd_quantity: Optional[Decimal] = None fill_cost_per_quantity: Optional[Decimal] = None - fill_cost_per_quantity_effect: Optional[PriceEffect] = None order_fill_count: Optional[int] = None roll: Optional[bool] = None legs: Optional[List[OrderChainLeg]] = None entries: Optional[List[OrderChainEntry]] = None + @model_validator(mode="before") + @classmethod + def validate_price_effects(cls, data: Any) -> Any: + return _set_sign_for( + data, + [ + "total_fees", + "total_fill_cost", + "fill_cost_per_quantity", + ], + ) + class ComputedData(TastytradeJsonDataclass): """ @@ -431,13 +472,9 @@ class ComputedData(TastytradeJsonDataclass): open: bool updated_at: datetime total_fees: Decimal - total_fees_effect: PriceEffect total_commissions: Decimal - total_commissions_effect: PriceEffect realized_gain: Decimal - realized_gain_effect: PriceEffect realized_gain_with_fees: Decimal - realized_gain_with_fees_effect: PriceEffect winner_realized_and_closed: bool winner_realized: bool winner_realized_with_fees: bool @@ -447,16 +484,29 @@ class ComputedData(TastytradeJsonDataclass): started_at_days_to_expiration: int duration: int total_opening_cost: Decimal - total_opening_cost_effect: PriceEffect total_closing_cost: Decimal - total_closing_cost_effect: PriceEffect total_cost: Decimal - total_cost_effect: PriceEffect gcd_open_quantity: Decimal fees_missing: bool open_entries: List[OrderChainEntry] total_cost_per_unit: Optional[Decimal] = None - total_cost_per_unit_effect: Optional[PriceEffect] = None + + @model_validator(mode="before") + @classmethod + def validate_price_effects(cls, data: Any) -> Any: + return _set_sign_for( + data, + [ + "total_fees", + "total_commissions", + "realized_gain", + "realized_gain_with_fees", + "total_opening_cost", + "total_closing_cost", + "total_cost", + "total_cost_per_unit", + ], + ) class OrderChain(TastytradeJsonDataclass): diff --git a/tastytrade/streamer.py b/tastytrade/streamer.py index 516849d..939f701 100644 --- a/tastytrade/streamer.py +++ b/tastytrade/streamer.py @@ -9,6 +9,7 @@ from typing import Any, AsyncIterator, Dict, List, Optional, Union import websockets +from pydantic import model_validator from websockets import WebSocketClientProtocol from tastytrade import logger @@ -31,10 +32,9 @@ OrderChain, PlacedComplexOrder, PlacedOrder, - PriceEffect, ) from tastytrade.session import Session -from tastytrade.utils import TastytradeError, TastytradeJsonDataclass +from tastytrade.utils import TastytradeError, TastytradeJsonDataclass, _set_sign_for from tastytrade.watchlists import Watchlist CERT_STREAMER_URL = "wss://streamer.cert.tastyworks.com" @@ -73,13 +73,22 @@ class UnderlyingYearGainSummary(TastytradeJsonDataclass): symbol: str instrument_type: InstrumentType fees: Decimal - fees_effect: PriceEffect commissions: Decimal - commissions_effect: PriceEffect yearly_realized_gain: Decimal - yearly_realized_gain_effect: PriceEffect realized_lot_gain: Decimal - realized_lot_gain_effect: PriceEffect + + @model_validator(mode="before") + @classmethod + def validate_price_effects(cls, data: Any) -> Any: + return _set_sign_for( + data, + [ + "fees", + "commissions", + "yearly_realized_gain", + "realized_lot_gain", + ], + ) class SubscriptionType(str, Enum): diff --git a/tastytrade/utils.py b/tastytrade/utils.py index 34c5824..f3c6bb1 100644 --- a/tastytrade/utils.py +++ b/tastytrade/utils.py @@ -1,4 +1,7 @@ from datetime import date, datetime, timedelta +from decimal import Decimal +from enum import Enum +from typing import Any, List, Optional import pandas_market_calendars as mcal # type: ignore import pytz @@ -9,6 +12,17 @@ TZ = pytz.timezone("US/Eastern") +class PriceEffect(str, Enum): + """ + This is an :class:`~enum.Enum` that shows the sign of a price effect, since + Tastytrade is apparently against negative numbers. + """ + + CREDIT = "Credit" + DEBIT = "Debit" + NONE = "None" + + def now_in_new_york() -> datetime: """ Gets the current time in the New York timezone. @@ -226,3 +240,25 @@ def validate_response(response: Response) -> None: error_message += f"\n{error['domain']}: {error['reason']}" raise TastytradeError(error_message) + + +def _get_sign(value: Optional[Decimal]) -> Optional[PriceEffect]: + if not value: + return None + return PriceEffect.DEBIT if value < 0 else PriceEffect.CREDIT + + +def _set_sign_for(data: Any, properties: List[str]) -> Any: + """ + Handles setting the sign of a number using the associated "-effect" field. + + :param data: the raw, unprocessed model object + :param properties: the name of the number fields to set + """ + if isinstance(data, dict): + for property in properties: + key = _dasherize(property) + effect = data.get(f"{key}-effect") + if effect == PriceEffect.DEBIT: + data[key] = -abs(Decimal(data[key])) + return data diff --git a/tests/test_account.py b/tests/test_account.py index 2956422..6bfc48b 100644 --- a/tests/test_account.py +++ b/tests/test_account.py @@ -12,7 +12,6 @@ OrderAction, OrderTimeInForce, OrderType, - PriceEffect, ) @@ -157,8 +156,7 @@ def new_order(session): time_in_force=OrderTimeInForce.DAY, order_type=OrderType.LIMIT, legs=[leg], - price=Decimal(2), - price_effect=PriceEffect.DEBIT, + price=Decimal(-2), ) @@ -167,6 +165,10 @@ def placed_order(session, account, new_order): return account.place_order(session, new_order, dry_run=False).order +def test_place_order(placed_order): + pass + + def test_get_order(session, account, placed_order): sleep(3) assert account.get_order(session, placed_order.id).id == placed_order.id @@ -174,7 +176,7 @@ def test_get_order(session, account, placed_order): def test_replace_and_delete_order(session, account, new_order, placed_order): modified_order = new_order.model_copy() - modified_order.price = Decimal("2.01") + modified_order.price = Decimal("-2.01") replaced = account.replace_order(session, placed_order.id, modified_order) sleep(3) account.delete_order(session, replaced.id) @@ -191,14 +193,12 @@ def test_place_oco_order(session, account): order_type=OrderType.LIMIT, legs=[closing], price=Decimal("100"), # will never fill - price_effect=PriceEffect.CREDIT, ), NewOrder( time_in_force=OrderTimeInForce.GTC, order_type=OrderType.STOP, legs=[closing], stop_trigger=Decimal("1.5"), # will never fill - price_effect=PriceEffect.CREDIT, ), ] ) @@ -218,8 +218,7 @@ def test_place_otoco_order(session, account): time_in_force=OrderTimeInForce.DAY, order_type=OrderType.LIMIT, legs=[opening], - price=Decimal("2"), # won't fill - price_effect=PriceEffect.DEBIT, + price=Decimal("-2"), # won't fill ), orders=[ NewOrder( @@ -227,14 +226,12 @@ def test_place_otoco_order(session, account): order_type=OrderType.LIMIT, legs=[closing], price=Decimal("400"), # won't fill - price_effect=PriceEffect.CREDIT, ), NewOrder( time_in_force=OrderTimeInForce.GTC, order_type=OrderType.STOP, legs=[closing], stop_trigger=Decimal("1.5"), # won't fill - price_effect=PriceEffect.CREDIT, ), ], ) @@ -254,6 +251,10 @@ async def placed_order_async(session, account, new_order): return res.order +async def test_place_order_async(placed_order_async): + pass + + async def test_get_order_async(session, account, placed_order_async): sleep(3) placed = await account.a_get_order(session, placed_order_async.id) @@ -264,7 +265,7 @@ async def test_replace_and_delete_order_async( session, account, new_order, placed_order_async ): modified_order = new_order.model_copy() - modified_order.price = Decimal("2.01") + modified_order.price = Decimal("-2.01") replaced = await account.a_replace_order( session, placed_order_async.id, modified_order ) @@ -282,8 +283,7 @@ async def test_place_complex_order_async(session, account): time_in_force=OrderTimeInForce.DAY, order_type=OrderType.LIMIT, legs=[opening], - price=Decimal("2"), # won't fill - price_effect=PriceEffect.DEBIT, + price=Decimal("-2"), # won't fill ), orders=[ NewOrder( @@ -291,14 +291,12 @@ async def test_place_complex_order_async(session, account): order_type=OrderType.LIMIT, legs=[closing], price=Decimal("400"), # won't fill - price_effect=PriceEffect.CREDIT, ), NewOrder( time_in_force=OrderTimeInForce.GTC, order_type=OrderType.STOP, legs=[closing], stop_trigger=Decimal("1.5"), # won't fill - price_effect=PriceEffect.CREDIT, ), ], ) diff --git a/tests/test_streamer.py b/tests/test_streamer.py index ea6279a..75a4c37 100644 --- a/tests/test_streamer.py +++ b/tests/test_streamer.py @@ -27,4 +27,4 @@ async def test_dxlink_streamer(session): async for _ in streamer.listen(EventType.QUOTE): break await streamer.unsubscribe_candle(subs[0], "1d") - await streamer.unsubscribe(EventType.QUOTE, subs[1]) + await streamer.unsubscribe(EventType.QUOTE, subs) diff --git a/uv.lock b/uv.lock index ae0ec42..cc5cf22 100644 --- a/uv.lock +++ b/uv.lock @@ -229,51 +229,12 @@ wheels = [ ] [[package]] -name = "mypy" -version = "1.11.2" +name = "nodeenv" +version = "1.9.1" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mypy-extensions" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5c/86/5d7cbc4974fd564550b80fbb8103c05501ea11aa7835edf3351d90095896/mypy-1.11.2.tar.gz", hash = "sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79", size = 3078806 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/cd/815368cd83c3a31873e5e55b317551500b12f2d1d7549720632f32630333/mypy-1.11.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a", size = 10939401 }, - { url = "https://files.pythonhosted.org/packages/f1/27/e18c93a195d2fad75eb96e1f1cbc431842c332e8eba2e2b77eaf7313c6b7/mypy-1.11.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef", size = 10111697 }, - { url = "https://files.pythonhosted.org/packages/dc/08/cdc1fc6d0d5a67d354741344cc4aa7d53f7128902ebcbe699ddd4f15a61c/mypy-1.11.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41ea707d036a5307ac674ea172875f40c9d55c5394f888b168033177fce47383", size = 12500508 }, - { url = "https://files.pythonhosted.org/packages/64/12/aad3af008c92c2d5d0720ea3b6674ba94a98cdb86888d389acdb5f218c30/mypy-1.11.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6e658bd2d20565ea86da7d91331b0eed6d2eee22dc031579e6297f3e12c758c8", size = 13020712 }, - { url = "https://files.pythonhosted.org/packages/03/e6/a7d97cc124a565be5e9b7d5c2a6ebf082379ffba99646e4863ed5bbcb3c3/mypy-1.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:478db5f5036817fe45adb7332d927daa62417159d49783041338921dcf646fc7", size = 9567319 }, - { url = "https://files.pythonhosted.org/packages/e2/aa/cc56fb53ebe14c64f1fe91d32d838d6f4db948b9494e200d2f61b820b85d/mypy-1.11.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385", size = 10859630 }, - { url = "https://files.pythonhosted.org/packages/04/c8/b19a760fab491c22c51975cf74e3d253b8c8ce2be7afaa2490fbf95a8c59/mypy-1.11.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca", size = 10037973 }, - { url = "https://files.pythonhosted.org/packages/88/57/7e7e39f2619c8f74a22efb9a4c4eff32b09d3798335625a124436d121d89/mypy-1.11.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd953f221ac1379050a8a646585a29574488974f79d8082cedef62744f0a0104", size = 12416659 }, - { url = "https://files.pythonhosted.org/packages/fc/a6/37f7544666b63a27e46c48f49caeee388bf3ce95f9c570eb5cfba5234405/mypy-1.11.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:57555a7715c0a34421013144a33d280e73c08df70f3a18a552938587ce9274f4", size = 12897010 }, - { url = "https://files.pythonhosted.org/packages/84/8b/459a513badc4d34acb31c736a0101c22d2bd0697b969796ad93294165cfb/mypy-1.11.2-cp311-cp311-win_amd64.whl", hash = "sha256:36383a4fcbad95f2657642a07ba22ff797de26277158f1cc7bd234821468b1b6", size = 9562873 }, - { url = "https://files.pythonhosted.org/packages/35/3a/ed7b12ecc3f6db2f664ccf85cb2e004d3e90bec928e9d7be6aa2f16b7cdf/mypy-1.11.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318", size = 10990335 }, - { url = "https://files.pythonhosted.org/packages/04/e4/1a9051e2ef10296d206519f1df13d2cc896aea39e8683302f89bf5792a59/mypy-1.11.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36", size = 10007119 }, - { url = "https://files.pythonhosted.org/packages/f3/3c/350a9da895f8a7e87ade0028b962be0252d152e0c2fbaafa6f0658b4d0d4/mypy-1.11.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e7184632d89d677973a14d00ae4d03214c8bc301ceefcdaf5c474866814c987", size = 12506856 }, - { url = "https://files.pythonhosted.org/packages/b6/49/ee5adf6a49ff13f4202d949544d3d08abb0ea1f3e7f2a6d5b4c10ba0360a/mypy-1.11.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca", size = 12952066 }, - { url = "https://files.pythonhosted.org/packages/27/c0/b19d709a42b24004d720db37446a42abadf844d5c46a2c442e2a074d70d9/mypy-1.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70", size = 9664000 }, - { url = "https://files.pythonhosted.org/packages/42/ad/5a8567700410f8aa7c755b0ebd4cacff22468cbc5517588773d65075c0cb/mypy-1.11.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:37c7fa6121c1cdfcaac97ce3d3b5588e847aa79b580c1e922bb5d5d2902df19b", size = 10876550 }, - { url = "https://files.pythonhosted.org/packages/1b/bc/9fc16ea7a27ceb93e123d300f1cfe27a6dd1eac9a8beea4f4d401e737e9d/mypy-1.11.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a8a53bc3ffbd161b5b2a4fff2f0f1e23a33b0168f1c0778ec70e1a3d66deb86", size = 10068086 }, - { url = "https://files.pythonhosted.org/packages/cd/8f/a1e460f1288405a13352dad16b24aba6dce4f850fc76510c540faa96eda3/mypy-1.11.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ff93107f01968ed834f4256bc1fc4475e2fecf6c661260066a985b52741ddce", size = 12459214 }, - { url = "https://files.pythonhosted.org/packages/c7/74/746b31aef7cc7512dab8bdc2311ef88d63fadc1c453a09c8cab7e57e59bf/mypy-1.11.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:edb91dded4df17eae4537668b23f0ff6baf3707683734b6a818d5b9d0c0c31a1", size = 12962942 }, - { url = "https://files.pythonhosted.org/packages/28/a4/7fae712240b640d75bb859294ad4776b9960b3216ccb7fa747f578e6c632/mypy-1.11.2-cp38-cp38-win_amd64.whl", hash = "sha256:ee23de8530d99b6db0573c4ef4bd8f39a2a6f9b60655bf7a1357e585a3486f2b", size = 9545616 }, - { url = "https://files.pythonhosted.org/packages/16/64/bb5ed751487e2bea0dfaa6f640a7e3bb88083648f522e766d5ef4a76f578/mypy-1.11.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:801ca29f43d5acce85f8e999b1e431fb479cb02d0e11deb7d2abb56bdaf24fd6", size = 10937294 }, - { url = "https://files.pythonhosted.org/packages/a9/a3/67a0069abed93c3bf3b0bebb8857e2979a02828a4a3fd82f107f8f1143e8/mypy-1.11.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af8d155170fcf87a2afb55b35dc1a0ac21df4431e7d96717621962e4b9192e70", size = 10107707 }, - { url = "https://files.pythonhosted.org/packages/2f/4d/0379daf4258b454b1f9ed589a9dabd072c17f97496daea7b72fdacf7c248/mypy-1.11.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7821776e5c4286b6a13138cc935e2e9b6fde05e081bdebf5cdb2bb97c9df81d", size = 12498367 }, - { url = "https://files.pythonhosted.org/packages/3b/dc/3976a988c280b3571b8eb6928882dc4b723a403b21735a6d8ae6ed20e82b/mypy-1.11.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:539c570477a96a4e6fb718b8d5c3e0c0eba1f485df13f86d2970c91f0673148d", size = 13018014 }, - { url = "https://files.pythonhosted.org/packages/83/84/adffc7138fb970e7e2a167bd20b33bb78958370179853a4ebe9008139342/mypy-1.11.2-cp39-cp39-win_amd64.whl", hash = "sha256:3f14cd3d386ac4d05c5a39a51b84387403dadbd936e17cb35882134d4f8f0d24", size = 9568056 }, - { url = "https://files.pythonhosted.org/packages/42/3a/bdf730640ac523229dd6578e8a581795720a9321399de494374afc437ec5/mypy-1.11.2-py3-none-any.whl", hash = "sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12", size = 2619625 }, -] - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, ] [[package]] @@ -504,6 +465,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4f/83/2e585d06d49e0320050b3d7d8ae0dfbd1459e976ff9f4b4d8bcca983d474/pyluach-2.2.0-py3-none-any.whl", hash = "sha256:d1eb49d6292087e9290f4661ae01b60c8c933704ec8c9cef82673b349ff96adf", size = 25037 }, ] +[[package]] +name = "pyright" +version = "1.1.384" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/84/00/a23114619f9d005f4b0f35e037c76cee029174d090a6f73a355749c74f4a/pyright-1.1.384.tar.gz", hash = "sha256:25e54d61f55cbb45f1195ff89c488832d7a45d59f3e132f178fdf9ef6cafc706", size = 21956 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/4a/e7f4d71d194ba675f3577d11eebe4e17a592c4d1c3f9986d4b321ba3c809/pyright-1.1.384-py3-none-any.whl", hash = "sha256:f0b6f4db2da38f27aeb7035c26192f034587875f751b847e9ad42ed0c704ac9e", size = 18578 }, +] + [[package]] name = "pytest" version = "8.3.3" @@ -623,7 +597,7 @@ dependencies = [ [package.dev-dependencies] dev = [ - { name = "mypy" }, + { name = "pyright" }, { name = "pytest" }, { name = "pytest-aio" }, { name = "pytest-cov" }, @@ -641,7 +615,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ - { name = "mypy", specifier = ">=1.11.2" }, + { name = "pyright", specifier = ">=1.1.384" }, { name = "pytest", specifier = ">=8.3.3" }, { name = "pytest-aio", specifier = ">=1.5.0" }, { name = "pytest-cov", specifier = ">=5.0.0" },