Open positions
A Position is the central business object Kraite manages. One row represents one attempt to enter the market for an account and exchange symbol. The row carries the position from selection to close and is the 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 |
cancelled | Failure cleanup completed and no exchange residual remains | cancelling | Final verified cancel step |
failed | Cleanup could not prove a safe terminal state (or terminal exchange error like Binance -2022) | cancelling, closing (on -2022) | Side-effects: notification + auto-block of symbol |
Statuses new, opening, active, syncing, waping, closing, cancelling are non-terminal (treated as "open" for the duplicate-open guard). closed, cancelled, failed are terminal.
Duplicate-open invariant
The product allows only one bot-owned position per account and trading pair, regardless of direction. Hedge mode can represent LONG and SHORT together at the exchange, but Kraite deliberately does not assign that shape. Enforcement is layered:
- Selection-level — locally-open positions, exchange position snapshots, exchange open-order snapshots, and symbols already selected in the current batch are removed before either LONG or SHORT assignment.
- Pre-entry exchange check —
VerifyTradingPairNotOpenJobblocks the pair when anyLONG,SHORT, or one-wayBOTHposition key exists, independent of the requested slot direction. - DB-level — virtual
is_openis1for non-terminal statuses andNULLotherwise. The unique index rejects a second non-terminal row with the same account and exchange symbol, regardless of direction. - Orchestrator-level — parent election and child creation commit together under a parent-row lock. A retry sees the populated child block and becomes a no-op instead of appending another opening chain.
All layers are intentional: selection prevents bad intent, the exchange check catches stale local state, the DB rejects duplicate storage, and orchestration prevents retry duplication.
Slot caps
Each account carries configured LONG and SHORT maximums. BSCS derives a lower effective cap during elevated and fragile regimes without rewriting those saved settings. The selection phase will not assign a position above the effective cap, even when the test-only symbol override is configured; an override never creates capacity.
The read-only mobile dashboard shows effective versus configured caps and the selected account's latest clean close. Only closed history qualifies; cancelled and failed rows remain operational history and never become the trader's last successful close.
Exchange-truth safety
Position existence is matched by exact symbol and logical direction. Hedge LONG / SHORT rows remain directional; one-way BOTH rows derive direction from signed quantity. A same-symbol opposite-side row therefore cannot stand in for the bot-owned exposure.
Bitget Classic and Unified responses are normalized into the same position shape. If an external hedge account contains both sides of one pair, recovery rejects the whole pair instead of adopting one side and risking the other.
Before a REST response can drive replacement, WAP, quantity sync, drift follow-up, or recovery, its vendor envelope and normalized rows must validate. HTTP success with an exchange error is unknown state, not an empty account.
One valid missing result only schedules a 20-second high-priority confirmation. The second valid flat snapshot may cancel Kraite-owned opening LIMITs and let the owning workflow continue. Reappearance or invalid data preserves every order. A direct User Data Stream zero-quantity event remains immediate because it is exchange-pushed position truth rather than a REST absence inference.
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
An opening failure triggers the cancel workflow. When cleanup verifies that no exchange exposure or open orders remain, the position ends cancelled with no failure alert and no symbol block. Only cleanup that cannot prove a safe terminal state ends failed, which triggers two side effects (decision detail):
position_opening_failedPushover notification fires (priority high)- a separate automatic system block stops the next selection tick from re-picking the same broken symbol without changing the sysadmin-owned manual switch
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