All checks were successful
deploy / deploy (push) Successful in 7s
Live HL data → in-browser TA → 4-agent committee → LONG/SHORT/SKIP verdict. Terminal grid command bar + interactive agent-workflow graph. Paper-trading worker reuses the browser brain (dry-run + Telegram). Forgejo static deploy. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
103 lines
2.5 KiB
TypeScript
103 lines
2.5 KiB
TypeScript
/**
|
|
* Persistence — a single JSON file written atomically (tmp + rename). No native
|
|
* deps, survives restarts, and the exact shape a future `/api/*` endpoint would
|
|
* serve to make the site's Performance + fills honest. Swap for SQLite later
|
|
* without touching callers (the engine only talks to getState()/save()).
|
|
*/
|
|
import { readFileSync, writeFileSync, mkdirSync, renameSync, existsSync } from 'node:fs'
|
|
import { dirname } from 'node:path'
|
|
import { CONFIG } from './config.ts'
|
|
import type { Direction } from '../src/lib/signals.ts'
|
|
|
|
export type Side = Exclude<Direction, 'SKIP'>
|
|
|
|
export type Position = {
|
|
id: string
|
|
coin: string
|
|
dir: Side
|
|
entry: number
|
|
notional: number // USD exposure (= equity * size% * leverage)
|
|
tp: number
|
|
sl: number
|
|
tpPct: number
|
|
slPct: number
|
|
confidence: number
|
|
reason: string
|
|
openedAt: number
|
|
}
|
|
|
|
export type Trade = Position & {
|
|
exit: number
|
|
closedAt: number
|
|
pnlPct: number // direction-adjusted price move, %
|
|
pnlUsd: number // notional * pnlPct
|
|
outcome: 'tp' | 'sl'
|
|
holdMin: number
|
|
}
|
|
|
|
export type SignalLog = {
|
|
t: number
|
|
coin: string
|
|
decision: Direction
|
|
confidence: number
|
|
price: number
|
|
net: number
|
|
reason: string
|
|
}
|
|
|
|
export type State = {
|
|
startedAt: number
|
|
mode: string
|
|
equity: number
|
|
positions: Position[]
|
|
closed: Trade[]
|
|
signals: SignalLog[]
|
|
equityCurve: { t: number; equity: number }[]
|
|
}
|
|
|
|
let state: State | null = null
|
|
|
|
function fresh(): State {
|
|
return {
|
|
startedAt: Date.now(),
|
|
mode: CONFIG.mode,
|
|
equity: CONFIG.equityStart,
|
|
positions: [],
|
|
closed: [],
|
|
signals: [],
|
|
equityCurve: [{ t: Date.now(), equity: CONFIG.equityStart }],
|
|
}
|
|
}
|
|
|
|
/** Load persisted state, or start fresh. Idempotent. */
|
|
export function load(): State {
|
|
if (state) return state
|
|
try {
|
|
if (existsSync(CONFIG.dataFile)) {
|
|
const raw = JSON.parse(readFileSync(CONFIG.dataFile, 'utf8')) as State
|
|
state = { ...fresh(), ...raw }
|
|
return state
|
|
}
|
|
} catch (e) {
|
|
console.error('[store] load failed, starting fresh:', (e as Error).message)
|
|
}
|
|
state = fresh()
|
|
return state
|
|
}
|
|
|
|
export function getState(): State {
|
|
return state ?? load()
|
|
}
|
|
|
|
/** Atomic write: serialise to `<file>.tmp`, then rename over the real file. */
|
|
export function save(): void {
|
|
const st = getState()
|
|
try {
|
|
mkdirSync(dirname(CONFIG.dataFile), { recursive: true })
|
|
const tmp = `${CONFIG.dataFile}.tmp`
|
|
writeFileSync(tmp, JSON.stringify(st, null, 2))
|
|
renameSync(tmp, CONFIG.dataFile)
|
|
} catch (e) {
|
|
console.error('[store] save failed:', (e as Error).message)
|
|
}
|
|
}
|