Repository navigation
fix(risk): measure executor exposure in quote, not base units - #260
Conversation
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.
|
Pending order is forgotten when checking the position limit ❌
|
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.
|
@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:
Tests: two added to Notes: I couldn't run |
| if row.get("amount", 0) > 0: | ||
| continue |
There was a problem hiding this comment.
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
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'samounttocfg.total_amount_quote or cfg.amount. For position/order executorsamountis base currency (asrisk._planned_amount_quotealready documents).ExecutorsProvidersums that intototal_exposure, which is:JournalManager.get_drawdown_pct()→ soft pause + hard kill switch;RiskEngine.check_executor_actionadds a new create to → position cap.The error scales with the asset price, in both directions:
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
amountis now always quote (new_quote_sizehelper):total_amount_quote(unchanged)amounts_quote(was read as 0 → exposure silently dropped)amount× entry price (then the order's limitprice, 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 approvedfilled_amount_quote, else 0 — never the raw base figurecustom_info.total_value_quote(unchanged)Consumers checked: the provider's
total_exposureand the session report's$Amount column both expect quote. The webExecutorInfowire model dropsamount, and the frontend never renders the row'samount, so there is no UI change.Out of scope (follow-up)
get_drawdown_pctdivides 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 whatshutdown_drawdown_pctmeans 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 (providertotal_exposure→ journal drawdown = 0.35%, not 24%). 7 of the 9 fail onmain.