(fix) sync pair-derived connector state (throttler limits, trading rules) on dynamic pair registration - #209
(fix) sync pair-derived connector state (throttler limits, trading rules) on dynamic pair registration#209fengtality wants to merge 4 commits into
Conversation
Connectors are created with trading_pairs=[], and state built from that list at init was never refreshed when pairs were registered later: - AsyncThrottler pair-templated rate limits (#207): pair-scoped requests crash with AttributeError 'NoneType' has no attribute 'weight' - Per-pair trading rules, e.g. XRPL (#208): executors die at startup with KeyError and register as silent RUNNING zombies that never place orders Adds UnifiedConnectorService.sync_pair_derived_state(), which appends the pair, mutates the existing throttler in place via add_rate_limits() (the instance WebAssistantsFactory captured at init), and refreshes trading rules only when the pair has no rule yet. Wired into all dynamic registration sites: TradingService.add_market, ensure_data_connector_started, and _add_trading_pair_to_tracker (covers the data-connector bootstrap path). Fixes #207 Fixes #208 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Greptile SummaryThe PR synchronizes connector state derived from dynamically registered trading pairs and extends pair registration to leverage, position-mode, order, and market-data paths.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| services/unified_connector_service.py | Adds centralized, idempotent synchronization of dynamically registered pairs, throttler limits, and per-pair trading rules. |
| services/trading_service.py | Moves connector pair-state synchronization before market-registration early returns and awaits the new asynchronous helper. |
| services/perpetual_trading_service.py | Synchronizes pairs before leverage changes and requires a registered pair before changing position mode. |
| services/accounts_service.py | Synchronizes pair-derived connector state before validating trading rules and forwards position-mode pair input. |
| routers/trading.py | Passes the optional trading pair from position-mode requests into the service layer. |
| models/accounts.py | Extends position-mode requests with an optional trading-pair field. |
| test/test_sync_pair_derived_state.py | Covers pair registration, throttler mutation, rule refreshes, idempotency, missing pair lists, and degraded refresh failures. |
Sequence Diagram
sequenceDiagram
participant API as API service
participant Sync as sync_pair_derived_state
participant Connector as Connector
participant Throttler as AsyncThrottler
API->>Sync: Register trading pair
Sync->>Connector: Append pair if missing
Sync->>Throttler: Add pair-derived rate limits
alt Trading connector lacks pair rule
Sync->>Connector: update_trading_rules()
end
Sync-->>API: Continue registration or operation
Reviews (4): Last reviewed commit: "(fix) set_position_mode: register pair a..." | Re-trigger Greptile
…aths
- Gate trading-rules refresh on is_trading_required: data connectors never
place orders, and a refresh on per-pair-rules connectors is a real
(possibly on-chain) fetch that added latency and spurious warnings to
every order-book bootstrap
- Wire sync into set_leverage: pair-scoped leverage endpoints (bybit's
v5/position/set-leverage-{PAIR}) were #207's headline symptom and did
not route through any registered path
- Wire sync into place_trade: per-pair-rules connectors 503'd forever on
the empty-rules check and 400'd "pair not supported" after it
- Guard rate_limits_rules with hasattr instead of catching AttributeError
(removes warning noise on connectors without the property)
- accounts_service.py: mechanical flake8 cleanup (trailing whitespace,
continuation indents, long lines) — pre-existing violations surfaced by
the pre-commit hook once the file was touched; no behavior change beyond
the place_trade sync above
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Skeptical self-review of the first commit found four defects, fixed in 62fcb9e:
Also verified during review: XRPL's Note on diff size: New test: data connector skips the rules fetch but still gets the throttler limit. Suite: 92 passed, same 7 pre-existing failures as clean main. |
… lint noise - Move pair registration before add_market's early return: a transiently failed rules sync (node outage) previously stuck until restart because an already-tracked market with a healthy book returned before the sync could retry. The sync is a no-op when state is already consistent. - Revert the mechanical flake8 cleanup in accounts_service.py so the diff carries only the functional place_trade change (flake8 skipped for this commit; violations are pre-existing). Known gap, documented not fixed: set_position_mode iterates connector.trading_pairs, so with connectors created as trading_pairs=[] it reports success while never telling the exchange anything — needs a pair-aware API or eager registration, out of scope here. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Second skeptical pass (3b7f63c) — two more findings:
Also reverted the mechanical flake8 cleanup in Diff is now: unified_connector_service (+helper, 3 sites), trading_service (registration hoisted), perpetual_trading_service (set_leverage), accounts_service (place_trade, 7 lines), tests. Suite: 92 passed, same 7 pre-existing failures as |
…ent no-op Position-mode implementations apply the switch through the connector's trading pairs (the py-base default uses trading_pairs[0]; bybit loops over all of them) and both log a warning and return when the list is empty. With API connectors created as trading_pairs=[], the endpoint reported success while the exchange was never called. - PositionModeRequest gains optional trading_pair (backwards compatible) - The pair is registered via sync_pair_derived_state before switching (also picks up pair-templated throttler limits, e.g. bybit switch-mode) - With no pair provided and none registered, the service now returns 400 with instructions instead of a false success flake8 skipped: remaining violations in routers/trading.py are pre-existing on main. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
1351798 closes the
New test covers both behaviors (400 on empty + exchange untouched; register-then-switch with a provided pair). Suite: 93 passed, same 7 pre-existing failures. With this, all three silent-failure paths from the dynamic-pair root cause are closed: executors (KeyError zombies), set_leverage (throttler crash), and set_position_mode (silent no-op). |
|
Third skeptical pass, on the
Why the 4-layer param threading is the minimal correct shape, for the record: the switch is applied through the connector's pair list, so with an empty list the only honest options are "refuse" or "accept a pair to register". Refuse-only makes the endpoint unusable standalone; inferring a pair would be magic. Optional-pair + loud 400 is the smallest surface that is both usable and truthful. |
|
Split by issue per review: #211 fixes #207 (throttler rate limits), #212 fixes #208 (per-pair trading rules, stacked on #211), #213 fixes #210 (position-mode truth: exchange-mode adoption at init + loud-failure switch endpoint, stacked on #212). All commits from this PR are preserved across the three, including every self-review fix discussed here. Branch kept for reference. |
Fixes #207
Fixes #208
Problem
Trading connectors are created with
trading_pairs=[]and pairs are registered dynamically — but several pieces of connector state are built from that list during__init__and never refreshed afterwards. Two independently reported failures share this root cause:bybit_perpetualet al.): a pair added later has no rate limit, so pair-scoped endpoints resolvelimit_idtoNoneand crash withAttributeError: 'NoneType' object has no attribute 'weight'.xrplet al.): XRPL builds trading rules by querying the ledger for each pair in_trading_pairs, so a dynamically added pair has no rule. Executors then die at startup withKeyErrorinvalidate_sufficient_balance— but register asRUNNING,order_id: None: silent zombies that report 201 Created and never place an order. Reproduced live 3/3 onxrpl/USDC-XRP.Point-patching either symptom leaves the other (and the next pair-derived component) broken, so this PR fixes the registration path itself.
Fix
New
UnifiedConnectorService.sync_pair_derived_state(connector, trading_pair)— idempotent, called on every dynamic registration:connector._trading_pairs(handles theNonecase).throttler.add_rate_limits(connector.rate_limits_rules), mutating the instanceWebAssistantsFactorycaptured at init (reassigning_throttlerprovably doesn't work).add_rate_limitsskips knownlimit_ids, so repeat calls are free.await connector._update_trading_rules(). Guarded so it's a no-op for connectors whose rules already cover all pairs (one exchange-info call), and only triggers a real (possibly on-chain) fetch when the rule is genuinely missing.Wired into every dynamic registration site:
TradingService.add_market→_register_trading_pair_with_connector(the executor-creation path)UnifiedConnectorService.ensure_data_connector_startedUnifiedConnectorService._add_trading_pair_to_tracker— covers the data-connector bootstrap path AsyncThrottler missing pair-specific rate limits for dynamically registered trading pairs (AttributeError: 'NoneType' object has no attribute 'weight') #207 flagged as bypassing registration helpersAll sync failures degrade to warnings; registration never hard-fails on a sync step.
Tests
test/test_sync_pair_derived_state.py(6 tests, using the realAsyncThrottler):_trading_pairs=Noneinitialization (XRPL allowsNone)Full suite: 91 passed, 7 failed — the 7 failures are identical on clean
main(5 ×test_gateway_lp_executor, 2 ×test_controller_config_class_loading), i.e. pre-existing and unrelated.Verified the deployed image's hummingbot build has
AsyncThrottlerBase.add_rate_limits(also confirmed end-to-end by the #207 reporter on two pairs).🤖 Generated with Claude Code