Open positions
A Position is the central business object Kraite manages. One row per attempt to enter the market on a given (account_id, exchange_symbol_id, direction) tuple. The row carries the position from selection to close (or to failure) and is the single source of truth that reconciles against the exchange.
This is the business-domain lens view. For the step-by-step flow that drives every transition, jump to position lifecycle.
State machine
| Status | Meaning | Reached from | Reached how |
|---|---|---|---|
new | Slot assigned, no on-exchange action yet | — (created by AssignBestTokensToPositionSlotsJob) | Selection picked this symbol for this slot |
opening | Open block running on a worker | new | PreparePositionData step entry |
active | Live on the exchange, fully wired (market + limits + TP + SL) | opening, syncing, waping | ActivatePositionJob |
syncing | Reconciling DB orders against exchange (transient) | active | PrepareSyncOrdersJob (only if current = active) |
waping | TP being recalculated against new weighted average entry (transient) | active | DCA LIMIT fill triggers ApplyWapJob |
closing | Close block running | active | TP or SL reached FILLED |
closed | Closed cleanly | closing | UpdatePositionStatus final step |
cancelling | Cancel workflow running (failure path) | any open status | CancelPositionJob |
failed | Cancel workflow finished (or terminal exchange error like Binance -2022) | cancelling, closing (on -2022) | side-effects: notification + auto-block of symbol |
Statuses new, opening, active, syncing, closing, cancelling are non-terminal (treated as "open" for the duplicate-open guard). closed, cancelled, failed are terminal.
Duplicate-open invariant
Only one non-terminal position can exist for the same (account_id, exchange_symbol_id, direction) tuple. Enforced two ways:
- DB-level — virtual
is_opencolumn is1for non-terminal statuses andNULLotherwise. Unique indexux_positions_open_sloton(account_id, exchange_symbol_id, direction, is_open)rejects any second non-terminal row. NULL rows (closed / cancelled / failed) never collide so the constraint plays nicely with re-trades on the same symbol. - Orchestrator-level —
PreparePositionsOpeningJob::compute()is a no-op on retry if any child step already exists in its block.
Both layers are intentional — the DB constraint is the last line of defence; the orchestrator guard prevents the cascade-then-fail-on-DB-violation pattern.
Slot caps
Each account carries total_positions_long and total_positions_short integers. The selection phase will not assign a position to a slot above the cap, even when the symbol-override priority-0 is configured (override does not raise caps). Live config on the main Binance account: total_positions_long=6, total_positions_short=6 since 2026-04-23.
Selection priority order
When a slot is open, HasTokenDiscovery::assignTokensToPositions walks four priorities top-down and stops at the first hit:
| # | Priority | Notes |
|---|---|---|
| 0 | Symbol override (test-only) | Pin a specific symbol on a configured account, bypassing scoring, correlation, BTC-bias, and eligibility flags. Used for rehearsing WAP / close / drift flows on a known token. |
| 1 | Fast-tracked symbols | Recently-closed-and-profitable repeats; direction match only, no scoring |
| 2 | BTC-bias scoring | When BTC has a concluded direction: same timeframe, correlation-sign filter, score = `elasticity × |
| 3 | Fallback scoring | When BTC has no direction: iterate all configured timeframes, no correlation-sign filter, same score formula |
Priorities 2 and 3 also apply the S/R proximity gate as a soft penalty multiplier — never a hard filter.
Failure semantics
A failure during opening triggers the cancel workflow → status='failed' → two side effects (decision detail):
position_opening_failedPushover notification fires (priority high)exchange_symbol.is_manually_enabledflips tofalseso the next selection tick won't re-pick the same broken symbol
A -2022 from Binance during close → status='failed' with no retry, with the position_residual_detected notification routed to the operator since exchange state may diverge from DB.
Cross-lens links
- Position lifecycle — the step-by-step flow that drives every transition above
- Orders — the rows owned by a Position (1 MARKET + N LIMITs + 1 TP + 1 SL)
- Token selection — the four-priority pipeline that decides which symbol fills a slot