-
Notifications
You must be signed in to change notification settings - Fork 0
Use confirmed sell release for Firstrade buys #219
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ | |
| from dataclasses import dataclass | ||
| from typing import Any | ||
|
|
||
| from quant_platform_kit.common.order_status import compute_confirmed_sell_release_value | ||
| from quant_platform_kit.common.models import OrderIntent | ||
| from quant_platform_kit.common.ports import ExecutionPort, MarketDataPort | ||
| try: | ||
|
|
@@ -573,6 +574,7 @@ def execute_value_target_plan( | |
| safe_haven_cash_substitute_threshold_usd: float = DEFAULT_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD, | ||
| cash_only_execution: bool = True, | ||
| notional_buy_execution: bool = False, | ||
| fetch_order_status=None, | ||
| ) -> ExecutionCycleResult: | ||
| del dry_run_only # ExecutionPort owns preview vs live submission. | ||
| plan = substitute_small_safe_haven_targets_with_cash( | ||
|
|
@@ -628,6 +630,7 @@ def execute_value_target_plan( | |
| skipped: list[dict[str, Any]] = [] | ||
| reference_prices: dict[str, float] = {} | ||
| pending_sell_release_symbols: list[str] = [] | ||
| submitted_sell_orders: list[dict[str, Any]] = [] | ||
| sell_submitted = False | ||
|
|
||
| tradable_deltas: list[tuple[str, float, float]] = [] | ||
|
|
@@ -687,21 +690,35 @@ def execute_value_target_plan( | |
| max_notional_usd=max_order_notional_usd, | ||
| ) | ||
| ) | ||
| submitted_sell_orders.append(submitted[-1]) | ||
| pending_sell_release_symbols.append(symbol) | ||
| sell_submitted = True | ||
| investable_cash += quantity * sell_limit_price | ||
| continue | ||
|
|
||
| confirmed_sell_release_value = compute_confirmed_sell_release_value( | ||
| submitted_sell_orders=submitted_sell_orders, | ||
| fetch_order_status=fetch_order_status, | ||
| ) | ||
| investable_cash = max( | ||
| 0.0, | ||
| float(execution.get("investable_cash") or portfolio.get("liquid_cash") or 0.0) | ||
| + confirmed_sell_release_value, | ||
| ) | ||
|
|
||
| buy_deltas = [item for item in tradable_deltas if item[1] > 0] | ||
| _buys_blocked_reason: str | None = None | ||
| if cash_only_execution and buy_deltas and pending_sell_release_symbols: | ||
| estimated_buy_cost = 0.0 | ||
| requires_pending_sell_release = False | ||
| for symbol, delta_value, price in buy_deltas: | ||
| buy_budget = min(float(delta_value), investable_cash) | ||
| if order_notional_cap is not None: | ||
| buy_budget = min(buy_budget, order_notional_cap) | ||
| if notional_buy_execution: | ||
| if buy_budget >= MIN_NOTIONAL_BUY_USD: | ||
| estimated_buy_cost += buy_budget | ||
| elif float(delta_value) >= MIN_NOTIONAL_BUY_USD: | ||
| requires_pending_sell_release = True | ||
| else: | ||
| limit_price = _limit_buy_price( | ||
| symbol, price, limit_buy_premium, limit_buy_premium_by_symbol | ||
|
|
@@ -718,7 +735,9 @@ def execute_value_target_plan( | |
| ) | ||
| if quantity > 0: | ||
| estimated_buy_cost += quantity * limit_price | ||
| if estimated_buy_cost > investable_cash: | ||
| elif float(delta_value) > 0.0 and limit_price > max(0.0, buy_budget): | ||
| requires_pending_sell_release = True | ||
| if estimated_buy_cost > investable_cash or requires_pending_sell_release: | ||
|
Comment on lines
+738
to
+740
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a negative delta floors to zero shares, this function still adds that symbol to Useful? React with 👍 / 👎. |
||
| _buys_blocked_reason = "pending_sell_release" | ||
| for symbol, _delta_value, _price in buy_deltas: | ||
| skipped.append( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,7 @@ | |
| from time import time | ||
| from typing import Any, Callable | ||
|
|
||
| from application.account_payload_utils import flatten_values, float_or_none | ||
| from application.state_persistence import GcsStateStore | ||
|
|
||
|
|
||
|
|
@@ -447,6 +448,68 @@ def get_positions(self, account: str) -> dict[str, Any]: | |
| _, account_data = self.require_connected() | ||
| return dict(account_data.get_positions(account)) | ||
|
|
||
| def get_orders(self, account: str, *, per_page: int = 0) -> list[dict[str, Any]]: | ||
| _, account_data = self.require_connected() | ||
| payload = account_data.get_orders(account, per_page=per_page) | ||
| if isinstance(payload, list): | ||
| return [dict(row) for row in payload if isinstance(row, dict)] | ||
| if isinstance(payload, dict): | ||
| for key in ("items", "orders", "data", "result"): | ||
| value = payload.get(key) | ||
| if isinstance(value, list): | ||
| return [dict(row) for row in value if isinstance(row, dict)] | ||
| return [] | ||
|
|
||
| def get_order_status(self, account: str, order_id: str) -> dict[str, Any] | None: | ||
| normalized_order_id = str(order_id or "").strip() | ||
| if not normalized_order_id: | ||
| return None | ||
| for row in self.get_orders(account): | ||
| if not _payload_contains_order_id(row, normalized_order_id): | ||
| continue | ||
| status = _first_text_from_payload( | ||
| row, | ||
| "status", | ||
| "order_status", | ||
| "state", | ||
| "status_description", | ||
| "description", | ||
| ) | ||
| executed_qty = _first_numeric_from_payload( | ||
| row, | ||
| "executed_qty", | ||
| "executed_quantity", | ||
| "filled_quantity", | ||
| "filled_qty", | ||
| "filled", | ||
| "filled_shares", | ||
| "executed_shares", | ||
| "quantity_filled", | ||
| "quantity", | ||
| "shares", | ||
| "qty", | ||
|
Comment on lines
+488
to
+490
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Even when Useful? React with 👍 / 👎. |
||
| ) | ||
| executed_price = _first_numeric_from_payload( | ||
| row, | ||
| "executed_price", | ||
| "average_fill_price", | ||
| "avg_fill_price", | ||
| "avg_price", | ||
| "average_price", | ||
| "fill_price", | ||
| "filled_price", | ||
| "price", | ||
| "limit_price", | ||
| ) | ||
| return { | ||
| "status": status or "", | ||
| "executed_qty": max(0.0, float(executed_qty or 0.0)), | ||
| "executed_price": max(0.0, float(executed_price or 0.0)), | ||
| "broker_order_id": normalized_order_id, | ||
| "raw_payload": dict(row), | ||
| } | ||
| return None | ||
|
|
||
| def get_quote(self, account: str, symbol: str) -> dict[str, Any]: | ||
| session, _ = self.require_connected() | ||
| quote_factory = self._quote_factory | ||
|
|
@@ -533,3 +596,41 @@ def place_stock_order( | |
| notional=notional, | ||
| ) | ||
| ) | ||
|
|
||
|
|
||
| def _sanitize_payload_key(value: Any) -> str: | ||
| return "".join(ch for ch in str(value or "").lower() if ch.isalnum()) | ||
|
|
||
|
|
||
| def _first_payload_value(payload: Any, *candidate_keys: str) -> Any: | ||
| flattened = flatten_values(payload) | ||
| candidates = {_sanitize_payload_key(key) for key in candidate_keys} | ||
| for key, value in flattened.items(): | ||
| if _sanitize_payload_key(key.rsplit(".", 1)[-1]) in candidates: | ||
| return value | ||
|
Comment on lines
+608
to
+610
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This helper receives candidates in priority order, but it returns whichever matching key appears first in Firstrade's payload. When an order-status row includes both a generic field such as Useful? React with 👍 / 👎. |
||
| return None | ||
|
|
||
|
|
||
| def _first_text_from_payload(payload: Any, *candidate_keys: str) -> str | None: | ||
| value = _first_payload_value(payload, *candidate_keys) | ||
| text = str(value or "").strip() | ||
| return text or None | ||
|
|
||
|
|
||
| def _first_numeric_from_payload(payload: Any, *candidate_keys: str) -> float | None: | ||
| return float_or_none(_first_payload_value(payload, *candidate_keys)) | ||
|
|
||
|
|
||
| def _payload_contains_order_id(payload: Any, order_id: str) -> bool: | ||
| normalized_order_id = str(order_id or "").strip() | ||
| if not normalized_order_id: | ||
| return False | ||
| for key, value in flatten_values(payload).items(): | ||
| key_normalized = _sanitize_payload_key(key) | ||
| if "order" not in key_normalized: | ||
| continue | ||
| if not any(token in key_normalized for token in ("id", "number", "orderno")): | ||
| continue | ||
| if str(value or "").strip() == normalized_order_id: | ||
| return True | ||
| return False | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -618,6 +618,7 @@ def log_message(message: str) -> None: | |
| safe_haven_cash_substitute_threshold_usd=settings.safe_haven_cash_substitute_threshold_usd, | ||
| cash_only_execution=settings.cash_only_execution, | ||
| notional_buy_execution=notional_buy_execution_enabled(settings.strategy_profile), | ||
| fetch_order_status=lambda broker_order_id: client.get_order_status(account, broker_order_id), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Useful? React with 👍 / 👎. |
||
| ) | ||
| submitted_orders = list(execution_result.submitted_orders) | ||
| skipped_orders = list(execution_result.skipped_orders) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here the release calculation receives the just-submitted sell order, so when
get_order_statuscannot find the order yet (common immediately after submitting a live limit sell, and also for preview/dry-run payloads), the pinned helper falls back to the submission payload/order dict. Firstrade submission payloads contain the requestedshares/limit_pricewhile stillpending, which can be credited as released cash and allow the later buy loop to spend proceeds from an unfilled sell; only fetched fill data (or a status explicitly confirmed as filled) should be used for this value.Useful? React with 👍 / 👎.