Skip to content

fix(risk): measure executor exposure in quote, not base units - #260

Merged
david-hummingbot merged 2 commits into
mainfrom
fix/executor-row-amount-quote
Oct 2, 2026
Merged

david-hummingbot merged 2 commits into
mainfrom
fix/executor-row-amount-quote

Conversation

@cardosofede

Copy link
Copy Markdown
Contributor

Problem

User report: Engine 2's shutdown kill switch fired on an LTC trade. Condor measured the position as 2.9 (coins) instead of ~$200, so a real $0.70 loss read as a 24% drawdown.

Root cause: build_executor_row (condor/fetchers/executors.py) set the row's amount to cfg.total_amount_quote or cfg.amount. For position/order executors amount is base currency (as risk._planned_amount_quote already documents). ExecutorsProvider sums that into total_exposure, which is:

  1. the denominator of JournalManager.get_drawdown_pct() → soft pause + hard kill switch;
  2. the existing book RiskEngine.check_executor_action adds a new create to → position cap.

The error scales with the asset price, in both directions:

Asset Real size Counted as Kill switch Position cap
LTC (~$69) $200 2.9 ~69× too sensitive → false shutdown understated
BTC (~$100k) $300 0.003 fires on cents effectively bypassed
low-priced coins $400 2,000+ may never fire false refusals

Grid (total_amount_quote), LP (total_value_quote) and bot-mode rows were already quote, which is why some engines behaved and others didn't.

Fix

The row's amount is now always quote (new _quote_size helper):

  • grid → total_amount_quote (unchanged)
  • dca → sum of amounts_quote (was read as 0 → exposure silently dropped)
  • position / order → base amount × entry price (then the order's limit price, then the live/fill price) — the same base×price rule the risk gate uses to price a create, so next tick's book agrees with what was approved
  • unpriceable base amount → filled_amount_quote, else 0 — never the raw base figure
  • lp → custom_info.total_value_quote (unchanged)

Consumers checked: the provider's total_exposure and the session report's $ Amount column both expect quote. The web ExecutorInfo wire model drops amount, and the frontend never renders the row's amount, so there is no UI change.

Out of scope (follow-up)

get_drawdown_pct divides the peak-to-current drop in cumulative session PnL by the exposure open right now. So a realized loss followed by a small new position inflates the %, and a flat book reads 0% regardless of losses. Changing the denominator changes what shutdown_drawdown_pct means for existing configs, so it is left for a separate PR.

Tests

  • tests/test_executor_row_amount_quote.py (new): per-type units, the no-price fallback, and the incident end to end (provider total_exposure → journal drawdown = 0.35%, not 24%). 7 of the 9 fail on main.
  • Full suite: 6404 passed, 23 skipped (frontend-dist web routes); black/isort clean.

A position/order executor's config `amount` is in BASE currency, but
build_executor_row returned it unpriced as the row's `amount`, which the
executors provider sums into `total_exposure`. That figure is the
drawdown's denominator and the book the position cap adds new creates to.

A ~$200 LTC position counted as $2.90, so a $0.70 loss read as a 24%
drawdown and fired the shutdown kill switch. The error scales with price:
a $300 BTC position counted as $0.003 (cap effectively bypassed), and
low-priced coins inflate exposure (kill switch too insensitive, false
cap refusals).

The row's `amount` is now always quote: base amounts are priced at entry
(or the order's limit price, or the fill price for market orders) — the
same rule the risk gate prices a create with — falling back to the
filled quote and never to the raw base figure. DCA's `amounts_quote`
ladder, previously read as 0, is summed.
@greptile-apps

greptile-apps Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5

[Critical risk] Changes how executor exposure is measured for risk limits.

The PR is not yet safe to merge because a partially filled pending order can leave the position-cap book understated.

Findings

  1. P1 Partial fills hide pending exposure ▶

Summary

The PR changes executor-row amounts to quote units and adds live pricing and a fail-closed signal for pending orders that the row builder cannot price.

  • Position and order sizes now feed quote-valued exposure, drawdown, and position-cap calculations.
  • The new pending-order fallback still understates exposure after a partial fill when no price is reported.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Running executor] --> B[Build quote-sized row]
  B --> C{Row amount positive?}
  C -->|Yes| D[Use row amount as exposure]
  C -->|No| E[Fetch live price]
  E -->|Available| F[Price configured base amount]
  E -->|Unavailable| G[Mark book untrusted]
  D --> H[Position-cap check]
  F --> H
Loading

Reviews (2) · Last reviewed commit: "fix(risk): keep a pending market order's..."

Comment thread condor/fetchers/executors.py
@rapcmia rapcmia self-assigned this Sep 30, 2026
@rapcmia rapcmia moved this to Condor in Pull Request Board Sep 30, 2026
@rapcmia

rapcmia commented Sep 30, 2026

Copy link
Copy Markdown
Contributor

595fc57

  • Tested with HAPI and utiize condor to build custom strategies
  • Use hyperlioquid perpetual and HYPE-USD for the tests
  • Position quote exposure, opened a live HYPE-USD position and confirmed the report and journal showed $10.37, matching base amount × entry price. ✅
  • Unfilled DCA exposure, created two $50 limit levels and confirmed $100 exposure remained counted with no fills. ✅
  • Drawdown calculation, observed a $0.01 retreat from peak PnL against $12.81 exposure. Condor displayed 0.1% drawdown and correctly stayed below both risk limits. ✅

Pending order is forgotten when checking the position limit ❌

  • Tested with simulated exchange responses and Condor’s real exposure and risk calculations. No live orders were placed.
  • Set a $150 position limit. Condor initially counted a pending order as $100.
  • On the next tick, the order was still pending without price or fill information. Condor counted it as $0 and allowed another $60 order, bringing total commitments to $160—above the $150 limit.
  • Expected: keep counting the pending $100 and reject the extra $60.
  • Confirmed the extra order was correctly rejected within the same tick and when the pending order had a reported price.
#### Automated tests used
PYTHONPATH=. .venv/bin/pytest -q -s data/code_runs/pr260_pending_market_cap_qa.py tests/test_executor_row_amount_quote.py

Result: **1 failed, 10 passed.**

A market order still pending at the next tick reports no entry, no limit
price and no fill, so its row was sized at 0 and the position cap approved
a further create the two together exceed.

The executors provider now prices such a row at the live price, the same
rule the risk gate priced the create with. When no price is available the
executor is reported as unpriced and the tick marks the book untrusted, so
creates are refused while stops still pass.
@cardosofede

Copy link
Copy Markdown
Contributor Author

@rapcmia thanks for the repro — confirmed and fixed in 756e90b.

Cause: the row builder has no client, so a pending market order with no entry, limit price or fill was sized at $0 on the next tick and dropped out of the position-cap book.

Fix:

  • The executors provider now prices such a row at the live price — the same rule the risk gate uses when it approves the create — so the pending $100 stays counted and the extra $60 is rejected.
  • If no live price is available either, the executor is reported as unpriced and the tick marks the book untrusted: new creates are refused, stops still pass, and the agent sees a warning in its executors summary.

Tests: two added to tests/test_executor_row_amount_quote.py — your scenario ($150 cap, $100 pending, +$60 rejected) and the no-price case. Both fail on 595fc57 and pass now; full suite green.

Notes: I couldn't run data/code_runs/pr260_pending_market_cap_qa.py since it isn't in the repo, so a re-run on your side would be appreciated. The dashboard still shows amount 0 for such a pending order — only the agent's exposure path is priced here.

Comment on lines +41 to +42
if row.get("amount", 0) > 0:
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Partial fills hide pending exposure

If a running market order is partially filled but has no reported entry, limit, or current price, its row uses the positive filled quote amount. This check then skips both live pricing and the unpriced warning. The position cap counts only the fill, not the full pending order, so a later create can be approved even when the two orders exceed the cap.

Knowledge Base Used: Agent execution and risk controls

@david-hummingbot
david-hummingbot merged commit 45e60d6 into main Oct 2, 2026
5 checks passed
@david-hummingbot
david-hummingbot deleted the fix/executor-row-amount-quote branch October 2, 2026 05:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Condor

Development

Successfully merging this pull request may close these issues.

3 participants