Skip to content

fix: keep HTX spot frozen balance in the owned base inventory - #246

Merged
brokermr810 merged 2 commits into
OpenByteInc:mainfrom
Dev-next-gen:fix/htx-spot-frozen-base-inventory
Sep 16, 2026
Merged

brokermr810 merged 2 commits into
OpenByteInc:mainfrom
Dev-next-gen:fix/htx-spot-frozen-base-inventory

Conversation

@Dev-next-gen

Copy link
Copy Markdown
Contributor

Summary

While reading get_spot_base_holding I noticed the HTX branch returns as soon as it finds a row whose currency matches the base asset. HTX's GET /v1/account/accounts/{id}/balance does not return one row per currency — it returns one row per (currency, type): a trade row for the sellable part and a separate frozen row for whatever is locked by resting orders. So only the first of the two rows ever reached the holding.

That matters because of the contract the same file states: get_spot_total_base_balance says ownership and drift checks must use the whole account inventory, "open limit orders can move quantity from available to locked without changing ownership, so using the sellable balance here would create a false negative drift". position_query.py repeats it for spot. On HTX that is exactly what happens: with 0.6 BTC free and 0.4 BTC sitting in a resting sell order, the total came back as 0.6 instead of 1.0, and the drift check sees a position that has not actually gone anywhere.

The row order is not guaranteed either. If the frozen row comes first, the locked quantity was reported as both the total and the available balance, so the close-sizing clamp in clamp_spot_close_quantity would size against quantity that cannot be sold.

Binance already gets this right (free + locked), and there is a test pinning it — test_spot_ownership_total_includes_locked_while_close_uses_free. This brings HTX in line.

Changes

  • spot_sizing.get_spot_base_holding: accumulate over every row matching the base asset instead of returning on the first one. A frozen row adds to the total only; anything else adds to both the total and the available part.
  • Two tests in tests/test_spot_sizing.py, mirroring the existing Binance one: trade + frozen in the documented order, and the reversed order.

Test plan

  • Relevant pytest tests pass

Both new tests fail on main and pass with the change:

tests/test_spot_sizing.py::test_htx_spot_ownership_includes_frozen_rows FAILED
E       AssertionError: assert 0.6 == 1.0
tests/test_spot_sizing.py::test_htx_spot_frozen_row_first_is_not_taken_for_the_whole_holding FAILED
E       AssertionError: assert 2.0 == 5.0
2 failed, 6 passed

After:

tests/test_spot_sizing.py ........                                       [100%]
8 passed

Whole backend suite, plus the two checks basic-ci runs:

$ python -m pytest tests -q
2547 passed, 30 skipped, 44 warnings in 99.51s

$ ruff check app scripts tests
All checks passed!

$ python scripts/backend_quality_check.py
Backend quality check passed

API documentation (if routes/schemas changed)

No route or schema change, so nothing to regenerate.

Found by a defect-hunting pipeline I build and run (Dev-next-gen), using Claude Code with Anthropic's Claude Opus 5.

Dev-next-gen and others added 2 commits September 16, 2026 03:28
HTX /v1/account/accounts/{id}/balance returns one row per (currency, type):
a "trade" row for the sellable part and a "frozen" row for whatever is
locked by resting orders. get_spot_base_holding returned on the first row
matching the base asset, so the frozen quantity never reached the holding.

get_spot_total_base_balance documents that ownership must use the whole
account inventory, precisely because open limit orders move quantity from
available to locked without changing ownership. With a resting sell order
on HTX spot, the total came back short and the drift check saw a position
that is not actually gone. Row order is not guaranteed either, so a
"frozen"-first payload reported the locked amount as available.

Accumulate over every row for the base asset instead: "frozen" adds to
the total only, anything else adds to both the total and the available
part.
Only trade balances are sellable; keep frozen balances in owned trading inventory and exclude unrelated balance types. Cover non-trading rows in both orders, close sizing, and fully frozen holdings.
@brokermr810
brokermr810 merged commit ad2ce27 into OpenByteInc:main Sep 16, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants