213 lines
4.9 KiB
TypeScript
213 lines
4.9 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
import { create } from "zustand";
|
||
|
|
import type {
|
||
|
|
AutotradeCompiledStrategy,
|
||
|
|
AutotradeEngineState,
|
||
|
|
AutotradeRuntimeLog,
|
||
|
|
AutotradeSessionInfo,
|
||
|
|
AutotradeSetupFormValues,
|
||
|
|
AutotradeSignalCandidate,
|
||
|
|
AutotradeValidationResult,
|
||
|
|
} from "@/features/autotrade/types/autotrade.types";
|
||
|
|
import { resolveSetupDefaults } from "@/lib/autotrade/strategy";
|
||
|
|
|
||
|
|
interface AutotradeEngineStoreState {
|
||
|
|
panelOpen: boolean;
|
||
|
|
setupForm: AutotradeSetupFormValues;
|
||
|
|
engineState: AutotradeEngineState;
|
||
|
|
isWorking: boolean;
|
||
|
|
|
||
|
|
activeSession: AutotradeSessionInfo | null;
|
||
|
|
compiledStrategy: AutotradeCompiledStrategy | null;
|
||
|
|
validation: AutotradeValidationResult | null;
|
||
|
|
lastSignal: AutotradeSignalCandidate | null;
|
||
|
|
|
||
|
|
orderCountToday: number;
|
||
|
|
cumulativeLossAmount: number;
|
||
|
|
consecutiveFailures: number;
|
||
|
|
lastOrderAtBySymbol: Record<string, number>;
|
||
|
|
|
||
|
|
logs: AutotradeRuntimeLog[];
|
||
|
|
}
|
||
|
|
|
||
|
|
interface AutotradeEngineStoreActions {
|
||
|
|
setPanelOpen: (open: boolean) => void;
|
||
|
|
patchSetupForm: (patch: Partial<AutotradeSetupFormValues>) => void;
|
||
|
|
setEngineState: (state: AutotradeEngineState) => void;
|
||
|
|
setWorking: (working: boolean) => void;
|
||
|
|
|
||
|
|
setActiveSession: (session: AutotradeSessionInfo | null) => void;
|
||
|
|
setCompiledStrategy: (strategy: AutotradeCompiledStrategy | null) => void;
|
||
|
|
setValidation: (validation: AutotradeValidationResult | null) => void;
|
||
|
|
setLastSignal: (signal: AutotradeSignalCandidate | null) => void;
|
||
|
|
|
||
|
|
increaseOrderCount: (count?: number) => void;
|
||
|
|
addLossAmount: (lossAmount: number) => void;
|
||
|
|
setLastOrderAt: (symbol: string, timestampMs: number) => void;
|
||
|
|
increaseFailure: () => void;
|
||
|
|
resetFailure: () => void;
|
||
|
|
|
||
|
|
appendLog: (
|
||
|
|
level: AutotradeRuntimeLog["level"],
|
||
|
|
message: string,
|
||
|
|
options?: {
|
||
|
|
stage?: AutotradeRuntimeLog["stage"];
|
||
|
|
detail?: string | Record<string, unknown>;
|
||
|
|
},
|
||
|
|
) => void;
|
||
|
|
clearRuntime: () => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
const INITIAL_FORM = resolveSetupDefaults();
|
||
|
|
|
||
|
|
const INITIAL_STATE: AutotradeEngineStoreState = {
|
||
|
|
panelOpen: false,
|
||
|
|
setupForm: INITIAL_FORM,
|
||
|
|
engineState: "IDLE",
|
||
|
|
isWorking: false,
|
||
|
|
|
||
|
|
activeSession: null,
|
||
|
|
compiledStrategy: null,
|
||
|
|
validation: null,
|
||
|
|
lastSignal: null,
|
||
|
|
|
||
|
|
orderCountToday: 0,
|
||
|
|
cumulativeLossAmount: 0,
|
||
|
|
consecutiveFailures: 0,
|
||
|
|
lastOrderAtBySymbol: {},
|
||
|
|
|
||
|
|
logs: [],
|
||
|
|
};
|
||
|
|
|
||
|
|
export const useAutotradeEngineStore = create<
|
||
|
|
AutotradeEngineStoreState & AutotradeEngineStoreActions
|
||
|
|
>((set) => ({
|
||
|
|
...INITIAL_STATE,
|
||
|
|
|
||
|
|
setPanelOpen: (open) => {
|
||
|
|
set({ panelOpen: open });
|
||
|
|
},
|
||
|
|
|
||
|
|
patchSetupForm: (patch) => {
|
||
|
|
set((state) => ({
|
||
|
|
setupForm: {
|
||
|
|
...state.setupForm,
|
||
|
|
...patch,
|
||
|
|
},
|
||
|
|
}));
|
||
|
|
},
|
||
|
|
|
||
|
|
setEngineState: (engineState) => {
|
||
|
|
set({ engineState });
|
||
|
|
},
|
||
|
|
|
||
|
|
setWorking: (isWorking) => {
|
||
|
|
set({ isWorking });
|
||
|
|
},
|
||
|
|
|
||
|
|
setActiveSession: (activeSession) => {
|
||
|
|
set({ activeSession });
|
||
|
|
},
|
||
|
|
|
||
|
|
setCompiledStrategy: (compiledStrategy) => {
|
||
|
|
set({ compiledStrategy });
|
||
|
|
},
|
||
|
|
|
||
|
|
setValidation: (validation) => {
|
||
|
|
set({ validation });
|
||
|
|
},
|
||
|
|
|
||
|
|
setLastSignal: (lastSignal) => {
|
||
|
|
set({ lastSignal });
|
||
|
|
},
|
||
|
|
|
||
|
|
increaseOrderCount: (count = 1) => {
|
||
|
|
set((state) => ({
|
||
|
|
orderCountToday: state.orderCountToday + Math.max(1, count),
|
||
|
|
}));
|
||
|
|
},
|
||
|
|
|
||
|
|
addLossAmount: (lossAmount) => {
|
||
|
|
set((state) => ({
|
||
|
|
cumulativeLossAmount:
|
||
|
|
state.cumulativeLossAmount + Math.max(0, Math.floor(lossAmount)),
|
||
|
|
}));
|
||
|
|
},
|
||
|
|
|
||
|
|
setLastOrderAt: (symbol, timestampMs) => {
|
||
|
|
set((state) => ({
|
||
|
|
lastOrderAtBySymbol: {
|
||
|
|
...state.lastOrderAtBySymbol,
|
||
|
|
[symbol]: timestampMs,
|
||
|
|
},
|
||
|
|
}));
|
||
|
|
},
|
||
|
|
|
||
|
|
increaseFailure: () => {
|
||
|
|
set((state) => ({
|
||
|
|
consecutiveFailures: state.consecutiveFailures + 1,
|
||
|
|
}));
|
||
|
|
},
|
||
|
|
|
||
|
|
resetFailure: () => {
|
||
|
|
set({ consecutiveFailures: 0 });
|
||
|
|
},
|
||
|
|
|
||
|
|
appendLog: (level, message, options) => {
|
||
|
|
const entry: AutotradeRuntimeLog = {
|
||
|
|
id: safeLogId(),
|
||
|
|
level,
|
||
|
|
stage: options?.stage,
|
||
|
|
message,
|
||
|
|
detail: normalizeLogDetail(options?.detail),
|
||
|
|
createdAt: new Date().toISOString(),
|
||
|
|
};
|
||
|
|
|
||
|
|
set((state) => ({
|
||
|
|
logs: [entry, ...state.logs].slice(0, 80),
|
||
|
|
}));
|
||
|
|
},
|
||
|
|
|
||
|
|
clearRuntime: () => {
|
||
|
|
set((state) => ({
|
||
|
|
...state,
|
||
|
|
engineState: "IDLE",
|
||
|
|
isWorking: false,
|
||
|
|
activeSession: null,
|
||
|
|
compiledStrategy: null,
|
||
|
|
validation: null,
|
||
|
|
lastSignal: null,
|
||
|
|
orderCountToday: 0,
|
||
|
|
cumulativeLossAmount: 0,
|
||
|
|
consecutiveFailures: 0,
|
||
|
|
lastOrderAtBySymbol: {},
|
||
|
|
}));
|
||
|
|
},
|
||
|
|
}));
|
||
|
|
|
||
|
|
function safeLogId() {
|
||
|
|
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
||
|
|
return crypto.randomUUID();
|
||
|
|
}
|
||
|
|
|
||
|
|
return `autotrade-log-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function normalizeLogDetail(detail: string | Record<string, unknown> | undefined) {
|
||
|
|
if (!detail) {
|
||
|
|
return undefined;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (typeof detail === "string") {
|
||
|
|
const cleaned = detail.trim();
|
||
|
|
return cleaned.length > 0 ? cleaned : undefined;
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
return JSON.stringify(detail, null, 2);
|
||
|
|
} catch {
|
||
|
|
return undefined;
|
||
|
|
}
|
||
|
|
}
|