Fix string errors in `.ib.broker` relay
Normalize structured and raw IB errors without indexing strings as message dictionaries. Deats, - match each error form before request-id handling; - parse `[code N]` and `[Errno N]` prefixes without crashing; - cover raw errors through `deliver_trade_events()`. Prompt-IO: ai/prompt-io/opencode/20260828T063126Z_1606122b_prompt_io.md (this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))wkt/fsp_backfill_sync
parent
1606122b45
commit
9115d24da0
|
|
@ -0,0 +1,35 @@
|
||||||
|
---
|
||||||
|
model: openai/gpt-5.6-sol
|
||||||
|
service: opencode
|
||||||
|
session: pid-469264
|
||||||
|
timestamp: 2026-08-28T06:31:26Z
|
||||||
|
git_ref: 1606122b
|
||||||
|
scope: code
|
||||||
|
substantive: true
|
||||||
|
raw_file: 20260828T063126Z_1606122b_prompt_io.raw.md
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prompt
|
||||||
|
|
||||||
|
Review the staged IB broker patch before committing it. After the review
|
||||||
|
identified a deterministic error-parser crash, continue with the available
|
||||||
|
next steps while leaving the typed EMS worktree and Ruff change untouched.
|
||||||
|
|
||||||
|
## Response summary
|
||||||
|
|
||||||
|
Repaired bracketed IB API error parsing by splitting on the matched error
|
||||||
|
prefix. Added a deterministic regression that routes bracketed and plain
|
||||||
|
strings through the real trade-event relay and verifies their EMS error
|
||||||
|
messages.
|
||||||
|
|
||||||
|
## Files changed
|
||||||
|
|
||||||
|
- `piker/brokers/ib/broker.py` - safely extracts bracketed API error codes.
|
||||||
|
- `tests/test_ib_broker.py` - covers bracketed and plain error relay inputs.
|
||||||
|
|
||||||
|
## Human edits
|
||||||
|
|
||||||
|
The human required review before the root commit, accepted the identified
|
||||||
|
parser defect as the next work item, and explicitly authorized continuation.
|
||||||
|
The pre-existing staged IB refactor is human-authored; the agent applied only
|
||||||
|
the parser repair and focused regression described above.
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
---
|
||||||
|
model: openai/gpt-5.6-sol
|
||||||
|
service: opencode
|
||||||
|
timestamp: 2026-08-28T06:31:26Z
|
||||||
|
git_ref: fix_symcache_search
|
||||||
|
diff_cmd: git diff HEAD~1..HEAD
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prompt
|
||||||
|
|
||||||
|
The human asked for a review of the root checkout's staged IB patch before
|
||||||
|
committing it. The review found that bracketed API error strings call
|
||||||
|
`str.partition()` without its required separator. The human then instructed
|
||||||
|
the agent to continue with available next steps, or ask only if unsure.
|
||||||
|
|
||||||
|
## Response
|
||||||
|
|
||||||
|
> `git diff HEAD~1..HEAD -- piker/brokers/ib/broker.py`
|
||||||
|
|
||||||
|
The generated parser repair uses the recognized `[code ` or `[Errno ` prefix
|
||||||
|
to extract the numeric code without changing the surrounding event relay.
|
||||||
|
|
||||||
|
> `git diff HEAD~1..HEAD -- tests/test_ib_broker.py`
|
||||||
|
|
||||||
|
The generated regression drives bracketed and plain strings through
|
||||||
|
`deliver_trade_events()` and verifies that one `BrokerdError` reaches EMS
|
||||||
|
with the expected reason and unknown request ID.
|
||||||
|
|
||||||
|
The typed EMS worktree and the unrelated Ruff policy change were left
|
||||||
|
untouched. No commit was created.
|
||||||
|
|
@ -52,7 +52,6 @@ from ib_async.objects import (
|
||||||
from piker import config
|
from piker import config
|
||||||
from piker.log import (
|
from piker.log import (
|
||||||
get_logger,
|
get_logger,
|
||||||
get_console_log,
|
|
||||||
)
|
)
|
||||||
from piker.types import Struct
|
from piker.types import Struct
|
||||||
from piker.accounting import (
|
from piker.accounting import (
|
||||||
|
|
@ -1429,27 +1428,35 @@ async def deliver_trade_events(
|
||||||
# NOTE: see impl deats in
|
# NOTE: see impl deats in
|
||||||
# `Client.inline_errors()::push_err()`
|
# `Client.inline_errors()::push_err()`
|
||||||
err: dict|str = item
|
err: dict|str = item
|
||||||
|
is_msg: bool = False
|
||||||
|
|
||||||
# std case, never relay errors for non-order-control
|
# std case, never relay errors for non-order-control
|
||||||
# related issues.
|
# related issues.
|
||||||
# https://interactivebrokers.github.io/tws-api/message_codes.html
|
# https://interactivebrokers.github.io/tws-api/message_codes.html
|
||||||
if isinstance(err, dict):
|
match err:
|
||||||
|
case dict():
|
||||||
|
# if isinstance(err, dict):
|
||||||
code: int = err['error_code']
|
code: int = err['error_code']
|
||||||
reason: str = err['reason']
|
reason: str = err['reason']
|
||||||
reqid: str = str(err['reqid'])
|
reqid: str = str(err['reqid'])
|
||||||
|
is_msg = True
|
||||||
|
|
||||||
|
case str():
|
||||||
# XXX, sometimes you'll get just a `str` of the form,
|
# XXX, sometimes you'll get just a `str` of the form,
|
||||||
# '[code 104] connection failed' or something..
|
# '[code 104] connection failed' or something..
|
||||||
elif isinstance(err, str):
|
# elif isinstance(err, str):
|
||||||
|
code: int|str = '<NA-err-code>'
|
||||||
code_part, _, reason = err.rpartition(']')
|
code_part, _, reason = err.rpartition(']')
|
||||||
if code_part:
|
if code_part:
|
||||||
for prefix_patt in [
|
for prefix_patt in [
|
||||||
'[Errno ',
|
'[Errno ',
|
||||||
'[code ',
|
'[code ',
|
||||||
]:
|
]:
|
||||||
code_part, _, code = code_part.partition()
|
_, prefix, code_str = (
|
||||||
if code:
|
code_part.partition(prefix_patt)
|
||||||
code = int(code)
|
)
|
||||||
|
if prefix:
|
||||||
|
code = int(code_str)
|
||||||
break
|
break
|
||||||
|
|
||||||
reqid: str = '<unknown>'
|
reqid: str = '<unknown>'
|
||||||
|
|
@ -1497,16 +1504,17 @@ async def deliver_trade_events(
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if err['reqid'] == -1:
|
if (
|
||||||
|
is_msg
|
||||||
|
and
|
||||||
|
err['reqid'] == -1
|
||||||
|
):
|
||||||
log.error(
|
log.error(
|
||||||
f'TWS external order error ??\n'
|
f'TWS external order error ??\n'
|
||||||
f'{pformat(err)}\n'
|
f'{pformat(err)}\n'
|
||||||
)
|
)
|
||||||
|
|
||||||
flow: dict = dict(
|
flow: dict = dict(flows.get(reqid) or {})
|
||||||
flows.get(reqid)
|
|
||||||
or {}
|
|
||||||
)
|
|
||||||
|
|
||||||
# TODO: we don't want to relay data feed / lookup errors
|
# TODO: we don't want to relay data feed / lookup errors
|
||||||
# so we need some further filtering logic here..
|
# so we need some further filtering logic here..
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
'''
|
||||||
|
IB broker event-relay regressions.
|
||||||
|
|
||||||
|
'''
|
||||||
|
from collections.abc import AsyncIterator
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import trio
|
||||||
|
|
||||||
|
from piker.brokers.ib.broker import deliver_trade_events
|
||||||
|
from piker.clearing import OrderDialogs
|
||||||
|
from piker.clearing._messages import BrokerdError
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
('error', 'expected_reason'),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
'[code 104] connection failed',
|
||||||
|
' connection failed',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'[Errno 111] Connect call failed',
|
||||||
|
' Connect call failed',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'plain API error',
|
||||||
|
'plain API error',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_trade_error_strings_are_relayed(
|
||||||
|
error: str,
|
||||||
|
expected_reason: str,
|
||||||
|
) -> None:
|
||||||
|
'''
|
||||||
|
Raw IB API error strings must not kill the trade-event relay.
|
||||||
|
|
||||||
|
``Client.inline_errors()`` can emit bracketed ``[code N]`` and
|
||||||
|
``[Errno N]`` strings instead of structured error dictionaries.
|
||||||
|
The string branch called ``str.partition()`` without a separator,
|
||||||
|
raising ``TypeError`` before EMS received the error and
|
||||||
|
terminating all later trade-event delivery. Feed both documented
|
||||||
|
bracket forms and an unprefixed control through
|
||||||
|
``deliver_trade_events()``. The exact emitted reason and unknown
|
||||||
|
request ID prove each event crossed the real relay path without
|
||||||
|
crashing or being misclassified.
|
||||||
|
|
||||||
|
'''
|
||||||
|
class FakeEmsStream:
|
||||||
|
'''
|
||||||
|
Collect messages sent to the EMS stream.
|
||||||
|
|
||||||
|
'''
|
||||||
|
def __init__(self) -> None:
|
||||||
|
'''
|
||||||
|
Initialize the captured message sequence.
|
||||||
|
|
||||||
|
'''
|
||||||
|
self.messages: list[Any] = []
|
||||||
|
|
||||||
|
async def send(self, msg: Any) -> None:
|
||||||
|
'''
|
||||||
|
Capture one relayed broker message.
|
||||||
|
|
||||||
|
'''
|
||||||
|
self.messages.append(msg)
|
||||||
|
|
||||||
|
async def events() -> AsyncIterator[tuple[str, str]]:
|
||||||
|
'''
|
||||||
|
Emit the raw IB error under test.
|
||||||
|
|
||||||
|
'''
|
||||||
|
yield 'error', error
|
||||||
|
|
||||||
|
async def main() -> list[Any]:
|
||||||
|
'''
|
||||||
|
Drive the broker relay to input exhaustion.
|
||||||
|
|
||||||
|
'''
|
||||||
|
ems_stream = FakeEmsStream()
|
||||||
|
await deliver_trade_events(
|
||||||
|
events(),
|
||||||
|
ems_stream,
|
||||||
|
accounts_def={},
|
||||||
|
proxies={},
|
||||||
|
ledgers={},
|
||||||
|
tables={},
|
||||||
|
flows=OrderDialogs(_flows={}),
|
||||||
|
)
|
||||||
|
return ems_stream.messages
|
||||||
|
|
||||||
|
messages = trio.run(main)
|
||||||
|
assert len(messages) == 1
|
||||||
|
msg = messages[0]
|
||||||
|
assert isinstance(msg, BrokerdError)
|
||||||
|
assert msg.reqid == '<unknown>'
|
||||||
|
assert msg.reason == expected_reason
|
||||||
Loading…
Reference in New Issue