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

StatusMeaningReached fromReached how
newSlot assigned, no on-exchange action yet— (created by AssignBestTokensToPositionSlotsJob)Selection picked this symbol for this slot
openingOpen block running on a workernewPreparePositionData step entry
activeLive on the exchange, fully wired (market + limits + TP + SL)opening, syncing, wapingActivatePositionJob
syncingReconciling DB orders against exchange (transient)activePrepareSyncOrdersJob (only if current = active)
wapingTP being recalculated against new weighted average entry (transient)activeDCA LIMIT fill triggers ApplyWapJob
closingClose block runningactiveTP or SL reached FILLED
closedClosed cleanlyclosingUpdatePositionStatus final step
cancellingCancel workflow running (failure path)any open statusCancelPositionJob
failedCancel 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:

  1. DB-level — virtual is_open column is 1 for non-terminal statuses and NULL otherwise. Unique index ux_positions_open_slot on (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.
  2. Orchestrator-levelPreparePositionsOpeningJob::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:

#PriorityNotes
0Symbol 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.
1Fast-tracked symbolsRecently-closed-and-profitable repeats; direction match only, no scoring
2BTC-bias scoringWhen BTC has a concluded direction: same timeframe, correlation-sign filter, score = `elasticity ×
3Fallback scoringWhen 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_failed Pushover notification fires (priority high)
  • exchange_symbol.is_manually_enabled flips to false so 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.


  • 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