/** * DSL plugin types: config, state, events, DslTick result. */ import type { CloseOrderType, FeeOptimizedLimitOptions } from "../../senpi/types.js"; export interface DslTier { triggerPct: number; /** % of current high-water ROE to lock as floor at this tier (0–100). Floor trails every tick in phase 2. */ lockHwPct: number; } export interface HardTimeout { enabled: boolean; intervalInMinutes: number; } export interface WeakPeakCut { enabled: boolean; intervalInMinutes: number; /** ROE % threshold (absolute, e.g. 3.0 = 3%). */ minValue: number; } export interface DeadWeightCut { enabled: boolean; intervalInMinutes: number; } export interface DslPhase1Config { enabled?: boolean; retraceThreshold: number; absoluteFloor: number; consecutiveBreachesRequired: number; } export interface DslPhase2Config { enabled?: boolean; } export interface DslPresetConfig { tiers: DslTier[]; maxLossPct: number; /** Parsed from `phase1.enabled`; default true. */ phase1Enabled: boolean; /** Parsed from `phase2.enabled`; default true. */ phase2Enabled: boolean; /** Phase 1: ROE % retrace from high-water for trailing floor. */ phase1RetraceThreshold: number; /** Phase 1 consecutive breach ticks before exit (no tier yet). */ phase1BreachesRequired: number; hardTimeout?: HardTimeout; weakPeakCut?: WeakPeakCut; deadWeightCut?: DeadWeightCut; } export interface DslPluginConfig { intervalMs: number; preset: string; presets: Record; closeOrderType?: CloseOrderType; feeOptimizedLimitOptions?: FeeOptimizedLimitOptions; } export interface DslState { version: number; /** * UUID v4 generated at creation; persisted across the DSL lifecycle as the * stable per-position identity (the `positionId ?? correlationId` join fallback). */ correlationId: string; /** * Cross-flow position identifier from the shared PositionIdRegistry. Stable across the * whole trade (open → DSL → close) and across DSL teardown/recreate, so every `dsl.*` * telemetry event can join to the open decision and the position. Lazy-filled from * `correlationId` for legacy state files written before this field existed. */ positionId?: string; /** Lifecycle event that produced this DSL row. Persisted so closed/tier/sl events can include it. */ triggerReason: DslCreatedTriggerReason; asset: string; address: string; direction: "LONG" | "SHORT"; entryPrice: number; size: number; leverage: number; wallet: string; dex: string; highWaterPrice: number; hwTimestamp: string; /** ROE % at high-water (computed every tick). */ highWaterRoe: number; phase: 1 | 2; currentBreachCount: number; currentTierIndex: number | null; tierFloorPrice: number; floorPrice: number; tiers: DslTier[]; phase1: DslPhase1Config; phase2: DslPhase2Config; /** Global time-based cuts (copied from preset at init). */ hardTimeout?: HardTimeout; weakPeakCut?: WeakPeakCut; deadWeightCut?: DeadWeightCut; /** ISO; set at init; never auto-resets. */ hardTimeoutStartedAt: string; /** ISO; resets when currentROE >= weakPeakCut.minValue. */ weakPeakCutStartedAt: string; /** ISO; resets when position first crosses to profitable. */ deadWeightCutStartedAt: string; slOrderId: number | null; lastSyncedFloorPrice: number | null; slOrderIdUpdatedAt: string | null; lastSlSyncError: string | null; pendingClose: boolean; active: boolean; createdAt: string; lastCheck: string; lastPrice: number; /** Consecutive monitor ticks without a usable price (independent of close API failures). */ consecutiveFetchFailures: number; /** * Latch so the `price_fetch_stale` event fires once per stall (edge-triggered) even across a * restart mid-stall: set when the streak first reaches the stale threshold, cleared on the next * priced tick that re-arms it. Persisted, so a rehydrated at/above-threshold counter can't re-emit. */ priceFetchStaleEmitted?: boolean; /** Consecutive closePosition failures after a breach or while pendingClose. */ consecutiveCloseFailures?: number; /** Exchange SL sync failures; when positive, monitor retries SL sync each tick. */ slSyncFailureCount?: number; handoffStatus: HandoffStatus | null; backendDslPositionId: string | null; lastBackendDslTick: string | null; tickCount: number; peakROE: number; lastError: string | null; lastErrorAt: string | null; closedAt: string | null; closeReason: string | null; /** ROE % at current price (computed every tick). */ currentROE: number; /** Phase 1: trailing / base floor telemetry. Phase 2: tier lock floor (or 0 before first tier). */ trailingFloor: number; /** Price distance to effective floor (LONG: lastPrice - floorPrice; SHORT: floorPrice - lastPrice). */ distanceToFloor: number; /** distanceToFloor / floorPrice * 100. */ distancePct: number; /** Retrace from HW: LONG (hw-price)/hw*100; SHORT (price-hw)/hw*100. */ retracePct: number; /** Minutes since createdAt. */ elapsedMinutes: number; /** Effective consecutive breaches required (phase 1 only; tier overrides removed in v2). */ breachesRequired: number; /** ROE % to next tier trigger; null at max tier. */ distanceToNextTierPct: number | null; /** Tier floor as ROE %; 0 when no tier. */ lockedProfitPct: number; /** ISO timestamp when current phase started. */ phaseSince: string; /** ISO timestamp when current tier was entered; null until first tier. */ tierSince: string | null; /** ISO: last periodic heartbeat notification emitted (optional; persisted). */ lastHeartbeatAt?: string | null; /** ISO: last tier-advanced notification (for heartbeat suppression; optional; persisted). */ lastTierNotificationAt?: string | null; /** * Most-favorable exchange-SL floor (price) already ANNOUNCED to the user via a * "Stop raised" / "Profit locked" notification. Stamped when such a notification * is actually sent, advanced favorably only (higher LONG / lower SHORT). Unlike * the in-memory ratchet (keyed on the per-rebuild correlationId, so it resets on * every increase/decrease/flip/resize and on restart), this baseline is * PERSISTED and carried across rebuilds, so a lower stop can never re-fire as a * fresh raise after a rebuild or a process restart. Optional: absent until the * position has announced its first raise, and reset on a direction flip (the * favorable polarity inverts). See {@link ../dsl/notifications/index.ts}. */ lastNotifiedStopFloor?: number; /** * Wall-clock (epoch ms) of the last "Stop raised" / "Profit locked" * announcement — the cooldown anchor for the stop-trail debounce * (notifications 2.6). Stamped in the same locked RMW as the floor. */ lastNotifiedStopAtMs?: number; /** * Breakeven latch (notifications 2.6): true once a stop at/above entry (below, * for a SHORT) has been announced for this position, so the "you can no longer * lose" moment fires exactly once. A rebuilt position already past breakeven * never re-crosses, so the latch needs no explicit reset; a direction flip * resets it alongside the floor. */ stopBreakevenAnnounced?: boolean; } export type HandoffStatus = "pending" | "ok" | "aborted"; export type DslTickAction = "none" | "close"; export interface DslTickResult { tierChanged: boolean; phaseChanged: boolean; action: DslTickAction; newFloorPrice?: number; newTierIndex?: number; newPhase?: 1 | 2; state: DslState; /** True when floorPrice moved this tick (used by monitor for SL sync in HW mode). */ floorChanged?: boolean; /** Set when action === "close" (e.g. hard_timeout, weak_peak_cut). */ closeReason?: string; } export interface PositionOpenedEvent { address: string; wallet: string; dex: string; preset?: string; coin: string; szi: string; entryPx: string; leverage: number; /** Cross-flow position.id minted by the tracker (stamped as `__positionId`). */ positionId?: string; } export interface PositionClosedEvent { address: string; asset: string; wallet: string; /** When set (e.g. from hook `data.dex`), used with {@link asset} to resolve DSL state on the xyz book. */ dex?: string; } export interface PositionFlippedEvent { address: string; asset: string; wallet: string; dex: string; coin: string; szi: string; entryPx: string; leverage: number; /** When set, used for the new direction after flip (same as position.opened preset). */ preset?: string; /** Cross-flow position.id reused across the flip (stamped as `__positionId`). */ positionId?: string; } export interface PositionIncreasedEvent { address: string; wallet: string; dex: string; coin: string; szi: string; entryPx: string; leverage: number; preset?: string; } export interface PositionDecreasedEvent { address: string; wallet: string; dex: string; coin: string; szi: string; entryPx: string; leverage: number; preset?: string; } /** * Who acted on a DSL transition: this runtime's own monitor tick, or the backend DSL service whose * transition we relayed. Absent on a transition we cannot attribute — never defaulted. */ export type DslEventSource = "runtime" | "backend"; export type DslCreatedTriggerReason = "position_opened" | "position_increased" | "position_decreased" | "flipped" | "adopted"; export interface DslCreatedEvent { address: string; asset: string; preset: string; tiers: DslTier[]; floorPrice: number; direction: "LONG" | "SHORT"; entryPrice: number; /** Empty string when main/default book. */ dex: string; /** Position leverage multiplier (e.g. 10). */ leverage: number; /** Absolute position size in asset units (always > 0; sign derived from direction for display). */ size: number; /** Collateral = (size * entryPrice) / leverage. */ margin: number; /** Lifecycle event that produced this DSL row (`"position_opened"` for fresh opens). */ triggerReason: DslCreatedTriggerReason; /** Cross-flow position.id (stamped as `__positionId`); falls back to correlationId for legacy rows. */ __positionId?: string; timestamp: string; } export interface DslPhaseChangedEvent { address: string; asset: string; phase: 1 | 2; tierIndex: number; triggerReason: DslCreatedTriggerReason; /** Cross-flow position.id (DslState.positionId); telemetry handler reads this. */ __positionId?: string; timestamp: string; } export interface DslTierAdvancedEvent { address: string; asset: string; /** Empty string when main/default book. */ dex: string; tier: number; lockHwPct: number; triggerPct: number; newFloorPrice: number; triggerReason: DslCreatedTriggerReason; /** Cross-flow position.id (DslState.positionId); telemetry handler reads this. */ __positionId?: string; timestamp: string; direction?: "LONG" | "SHORT"; entryPrice?: number; leverage?: number; margin?: number; currentROE?: number; highWaterRoe?: number; lastPrice?: number; /** Tier floor as ROE % (from state). */ lockedProfitPct?: number; } export interface DslSlUpdatedEvent { address: string; asset: string; newSLPrice: number; slOrderId: number; triggerReason: DslCreatedTriggerReason; /** Cross-flow position.id (DslState.positionId); telemetry handler reads this. */ __positionId?: string; /** * Trade direction — powers the direction-aware favorable-floor compare in the * stop-raise gate (higher = raise for LONG, lower = raise for SHORT). Optional * defensively; a missing value makes the gate suppress rather than misjudge. */ direction?: "LONG" | "SHORT"; /** * Instance-scoped id (DslState.correlationId), fresh per DSL rebuild. Keys the * per-instance favorable-floor baseline so a rebuild auto-resets it. MUST be * present on both emit sites; if omitted, events key under a fallback and can * collide across positions. */ correlationId?: string; /** * Position leverage multiplier (e.g. 10). Optional defensively for legacy * emitters; when absent the stop-raise copy omits the `N×` fragment. */ leverage?: number; /** * High-water price at the raise (DslState.highWaterPrice). Optional defensively * for legacy emitters; when absent the stop-raise copy omits the "new high" clause. */ highWaterPrice?: number; /** * Entry price (DslState.entryPrice). Optional defensively for legacy emitters; * when absent the copy omits the max-loss clause (it needs entry + leverage to * compute the leverage-adjusted ROE at the new stop). */ entryPrice?: number; timestamp: string; } export interface DslClosedEvent { address: string; asset: string; dex: string; reason: string; closeReason: string; triggerReason: DslCreatedTriggerReason; /** Cross-flow position.id (DslState.positionId); telemetry handler reads this. */ __positionId?: string; /** The monitor tick that emitted this close; absent when the close came from outside a tick. */ __tickId?: string; /** Who moved the stop; absent when the close cannot be attributed to a DSL actor. */ __dslSource?: DslEventSource; /** The backend's own stamp of when it acted; only on a backend-sourced close. */ __backendEventAt?: string; /** Close time (ISO); used for trade-history matching. */ timestamp: string; direction?: "LONG" | "SHORT"; entryPrice?: number; leverage?: number; margin?: number; lastPrice?: number; currentROE?: number; peakROE?: number; /** Position open time (ISO). */ createdAt?: string; currentTierIndex?: number | null; size?: number; } /** * Periodic status for open positions, emitted by the monitor on a long interval. */ export interface DslHeartbeatEvent { address: string; asset: string; dex: string; direction: "LONG" | "SHORT"; entryPrice: number; lastPrice: number; floorPrice: number; currentROE: number; highWaterRoe: number; lockedProfitPct: number; currentTierIndex: number | null; elapsedMinutes: number; triggerReason: DslCreatedTriggerReason; /** Cross-flow position.id (DslState.positionId); telemetry handler reads this. */ __positionId?: string; /** Collateral = (size * entryPrice) / leverage; used for PnL computation in heartbeat notification. */ margin?: number; timestamp: string; } export interface DslSettingsUpdatedEvent { address: string; asset?: string; updated?: string[]; timestamp: string; } /** Active position row for CLI/RPC (no runtimeId — gateway adds it when merging). */ export interface DslPositionSummary { address: string; wallet: string; asset: string; dex: string; direction: "LONG" | "SHORT"; entryPrice: number; lastPrice: number; currentROE: number; phase: 1 | 2; currentTierIndex: number | null; floorPrice: number; currentBreachCount: number; breachesRequired: number; elapsedMinutes: number; pendingClose: boolean; } /** Archived close row for CLI/RPC (no runtimeId — gateway adds it when merging). */ export interface DslArchivedClose { address: string; wallet: string; asset: string; dex: string; direction: "LONG" | "SHORT"; entryPrice: number; lastPrice: number; closeReason: string; closedAt: string; createdAt: string; peakROE: number; currentROE: number; elapsedMinutes: number; phase: 1 | 2; currentTierIndex: number | null; } export type { EditPositionResult } from "../../senpi/types.js"; //# sourceMappingURL=index.d.ts.map