전체적인 리팩토링

This commit is contained in:
2026-03-12 09:26:27 +09:00
parent 406af7408a
commit e51d767878
97 changed files with 13651 additions and 363 deletions

View File

@@ -0,0 +1,217 @@
/**
* [파일 역할]
* 자동매매 프론트엔드가 호출하는 API 클라이언트 모음입니다.
*
* [주요 책임]
* - compile/validate/session/signal 관련 Next API 호출을 캡슐화합니다.
* - 공통 응답 파싱/오류 메시지 처리를 제공합니다.
*/
import type { KisRuntimeCredentials } from "@/features/settings/store/use-kis-runtime-store";
import { buildKisRequestHeaders } from "@/features/settings/apis/kis-api-utils";
import type {
AutotradeAiMode,
AutotradeCompileResponse,
AutotradeCompiledStrategy,
AutotradeMarketSnapshot,
AutotradeSessionInfo,
AutotradeSessionResponse,
AutotradeSignalResponse,
AutotradeStopReason,
AutotradeValidateResponse,
} from "@/features/autotrade/types/autotrade.types";
interface AutotradeErrorPayload {
ok?: boolean;
message?: string;
errorCode?: string;
}
// [목적] UI 설정값을 서버 compile 라우트로 전달해 실행 전략(JSON)을 받습니다.
export async function compileAutotradeStrategy(payload: {
aiMode: AutotradeAiMode;
subscriptionCliVendor?: "auto" | "codex" | "gemini";
subscriptionCliModel?: string;
prompt: string;
selectedTechniques: AutotradeCompiledStrategy["selectedTechniques"];
confidenceThreshold: number;
}) {
const response = await fetch("/api/autotrade/strategies/compile", {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify(payload),
cache: "no-store",
});
return parseAutotradeResponse<AutotradeCompileResponse>(
response,
"자동매매 전략 컴파일 중 오류가 발생했습니다.",
);
}
// [목적] 가용자산/손실한도를 서버에서 동일 규칙으로 계산해 검증 결과를 받습니다.
export async function validateAutotradeStrategy(payload: {
cashBalance: number;
allocationPercent: number;
allocationAmount: number;
dailyLossPercent: number;
dailyLossAmount: number;
}) {
const response = await fetch("/api/autotrade/strategies/validate", {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify(payload),
cache: "no-store",
});
return parseAutotradeResponse<AutotradeValidateResponse>(
response,
"자동매매 리스크 검증 중 오류가 발생했습니다.",
);
}
// [목적] 자동매매 실행 세션을 서버에 등록합니다.
export async function startAutotradeSession(
payload: {
symbol: string;
leaderTabId: string;
effectiveAllocationAmount: number;
effectiveDailyLossLimit: number;
strategySummary: string;
},
credentials: KisRuntimeCredentials,
) {
const response = await fetch("/api/autotrade/sessions/start", {
method: "POST",
headers: {
...buildKisRequestHeaders(credentials, {
jsonContentType: true,
includeAccountNo: true,
}),
},
body: JSON.stringify(payload),
cache: "no-store",
});
return parseAutotradeResponse<AutotradeSessionResponse>(
response,
"자동매매 세션 시작 중 오류가 발생했습니다.",
);
}
// [목적] 실행 중 세션 생존 신호를 주기적으로 갱신합니다.
export async function heartbeatAutotradeSession(payload: {
sessionId: string;
leaderTabId: string;
}) {
const response = await fetch("/api/autotrade/sessions/heartbeat", {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify(payload),
cache: "no-store",
});
return parseAutotradeResponse<AutotradeSessionResponse>(
response,
"자동매매 heartbeat 전송 중 오류가 발생했습니다.",
);
}
// [목적] 수동/비상/종료 등 중지 사유를 서버 세션에 반영합니다.
export async function stopAutotradeSession(payload: {
sessionId?: string;
reason?: AutotradeStopReason;
}) {
const response = await fetch("/api/autotrade/sessions/stop", {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify(payload),
cache: "no-store",
});
return parseAutotradeResponse<{
ok: boolean;
session: AutotradeSessionInfo | null;
}>(response, "자동매매 세션 종료 중 오류가 발생했습니다.");
}
// [목적] 현재 사용자의 실행 중 세션 존재 여부를 조회합니다.
export async function fetchActiveAutotradeSession() {
const response = await fetch("/api/autotrade/sessions/active", {
method: "GET",
cache: "no-store",
});
return parseAutotradeResponse<{
ok: boolean;
session: AutotradeSessionInfo | null;
}>(response, "자동매매 세션 조회 중 오류가 발생했습니다.");
}
// [목적] 시세 스냅샷 + 전략을 서버에 보내 매수/매도/대기 신호를 생성합니다.
export async function generateAutotradeSignal(payload: {
aiMode: AutotradeAiMode;
subscriptionCliVendor?: "auto" | "codex" | "gemini";
subscriptionCliModel?: string;
prompt: string;
strategy: AutotradeCompiledStrategy;
snapshot: AutotradeMarketSnapshot;
}) {
const response = await fetch("/api/autotrade/signals/generate", {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify(payload),
cache: "no-store",
});
return parseAutotradeResponse<AutotradeSignalResponse>(
response,
"자동매매 신호 생성 중 오류가 발생했습니다.",
);
}
// [목적] 브라우저 종료 직전 stop 요청을 보내기 위한 비동기 beacon 경로입니다.
export function sendAutotradeStopBeacon(payload: {
sessionId?: string;
reason: AutotradeStopReason;
}) {
if (typeof navigator === "undefined") return false;
try {
const body = JSON.stringify(payload);
const blob = new Blob([body], { type: "application/json" });
return navigator.sendBeacon("/api/autotrade/sessions/stop", blob);
} catch {
return false;
}
}
async function parseAutotradeResponse<T>(
response: Response,
fallbackMessage: string,
): Promise<T> {
let payload: unknown = null;
try {
payload = (await response.json()) as unknown;
} catch {
throw new Error(fallbackMessage);
}
if (!response.ok) {
const errorPayload = payload as AutotradeErrorPayload;
throw new Error(errorPayload.message || fallbackMessage);
}
return payload as T;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,37 @@
import { AlertTriangle } from "lucide-react";
import { Button } from "@/components/ui/button";
interface AutotradeWarningBannerProps {
visible: boolean;
isStopping?: boolean;
onStop: () => void;
}
export function AutotradeWarningBanner({
visible,
isStopping = false,
onStop,
}: AutotradeWarningBannerProps) {
if (!visible) return null;
return (
<div className="border-b border-red-300/60 bg-red-600/90 px-3 py-2 text-white shadow-[0_2px_10px_rgba(220,38,38,0.35)] sm:px-4">
<div className="mx-auto flex w-full max-w-[1800px] items-center gap-3">
<AlertTriangle className="h-4 w-4 shrink-0" />
<p className="text-xs font-semibold sm:text-sm">
: 브라우저/ .
</p>
<Button
type="button"
variant="secondary"
size="sm"
className="ml-auto h-7 bg-white text-red-700 hover:bg-red-50"
disabled={isStopping}
onClick={onStop}
>
{isStopping ? "중지 중..." : "비상 중지"}
</Button>
</div>
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,212 @@
"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;
}
}

View File

@@ -0,0 +1,482 @@
/**
* @file features/autotrade/types/autotrade.types.ts
* @description 자동매매 기능에서 공통으로 사용하는 타입 정의입니다.
*/
export const AUTOTRADE_TECHNIQUE_IDS = [
"orb",
"vwap_reversion",
"volume_breakout",
"ma_crossover",
"gap_breakout",
"intraday_box_reversion",
"intraday_breakout_scalp",
] as const;
export type AutotradeTechniqueId = (typeof AUTOTRADE_TECHNIQUE_IDS)[number];
export interface AutotradeTechniqueOption {
id: AutotradeTechniqueId;
label: string;
description: string;
}
export const AUTOTRADE_TECHNIQUE_OPTIONS: AutotradeTechniqueOption[] = [
{
id: "orb",
label: "ORB(시가 범위 돌파)",
description: "시가 근처 범위를 돌파할 때 추세 진입 신호를 확인합니다.",
},
{
id: "vwap_reversion",
label: "VWAP 되돌림",
description: "VWAP에서 과하게 이탈한 가격이 평균으로 복귀하는 구간을 봅니다.",
},
{
id: "volume_breakout",
label: "거래량 돌파",
description: "거래량 급증과 함께 방향성이 생기는 순간을 포착합니다.",
},
{
id: "ma_crossover",
label: "이동평균 교차",
description: "단기/중기 평균선 교차로 추세 전환 여부를 확인합니다.",
},
{
id: "gap_breakout",
label: "갭 돌파",
description: "갭 상승/하락 이후 추가 돌파 또는 되돌림을 판단합니다.",
},
{
id: "intraday_box_reversion",
label: "상승 후 박스권 단타",
description:
"당일 상승 이후 박스권 횡보 구간에서 상단/하단 왕복(오르락내리락) 단타를 노립니다.",
},
{
id: "intraday_breakout_scalp",
label: "상승구간 눌림-재돌파 단타",
description:
"1분봉 상승 추세에서 저거래량 눌림 후 고점 재돌파(거래량 재유입) 구간을 노립니다.",
},
];
export const AUTOTRADE_DEFAULT_TECHNIQUES: AutotradeTechniqueId[] = [
"ma_crossover",
"vwap_reversion",
"intraday_box_reversion",
"intraday_breakout_scalp",
];
export type AutotradeEngineState =
| "IDLE"
| "RUNNING"
| "STOPPING"
| "STOPPED"
| "ERROR";
export const AUTOTRADE_AI_MODE_IDS = [
"auto",
"openai_api",
"subscription_cli",
"rule_fallback",
] as const;
export type AutotradeAiMode = (typeof AUTOTRADE_AI_MODE_IDS)[number];
export const AUTOTRADE_SUBSCRIPTION_CLI_VENDOR_IDS = [
"auto",
"codex",
"gemini",
] as const;
export type AutotradeSubscriptionCliVendor =
(typeof AUTOTRADE_SUBSCRIPTION_CLI_VENDOR_IDS)[number];
export interface AutotradeSubscriptionCliVendorOption {
id: AutotradeSubscriptionCliVendor;
label: string;
description: string;
}
export const AUTOTRADE_SUBSCRIPTION_CLI_VENDOR_OPTIONS: AutotradeSubscriptionCliVendorOption[] =
[
{
id: "auto",
label: "자동 선택",
description: "Codex -> Gemini 순서로 시도합니다.",
},
{
id: "codex",
label: "Codex CLI",
description: "OpenAI Codex CLI만 사용합니다.",
},
{
id: "gemini",
label: "Gemini CLI",
description: "Google Gemini CLI만 사용합니다.",
},
];
export interface AutotradeSubscriptionCliModelOption {
value: string;
label: string;
description: string;
}
// [출처] 공식 문서 기준 추천 프리셋
// - Codex Models: https://developers.openai.com/codex/models
// - Gemini CLI model command: https://github.com/google-gemini/gemini-cli/blob/main/docs/cli/model.md
export const AUTOTRADE_SUBSCRIPTION_CLI_MODEL_OPTIONS = {
codex: [
{
value: "gpt-5.4",
label: "gpt-5.4",
description: "Codex 추천 기본형",
},
{
value: "gpt-5.3-codex",
label: "gpt-5.3-codex",
description: "Codex 5.3 고성능 라인",
},
{
value: "gpt-5.3-codex-spark",
label: "gpt-5.3-codex-spark",
description: "Codex 5.3 경량형",
},
{
value: "gpt-5.2-codex",
label: "gpt-5.2-codex",
description: "Codex 5.2 균형형",
},
{
value: "gpt-5.2",
label: "gpt-5.2",
description: "Codex 5.2 범용형",
},
{
value: "gpt-5.1-codex-max",
label: "gpt-5.1-codex-max",
description: "문맥 확장형 Codex 5.1",
},
{
value: "gpt-5.1",
label: "gpt-5.1",
description: "Codex 5.1 범용형",
},
{
value: "gpt-5.1-codex",
label: "gpt-5.1-codex",
description: "Codex 5.1 기본형",
},
{
value: "gpt-5-codex",
label: "gpt-5-codex (안정형)",
description: "Codex 안정형",
},
{
value: "gpt-5-codex-mini",
label: "gpt-5-codex-mini",
description: "Codex 경량형",
},
{
value: "gpt-5",
label: "gpt-5",
description: "Codex 범용 경량 라인",
},
] satisfies AutotradeSubscriptionCliModelOption[],
gemini: [
{
value: "auto",
label: "auto (권장)",
description: "상황에 따라 Pro/Flash 계열을 자동 선택",
},
{
value: "gemini-3.1-pro-preview",
label: "gemini-3.1-pro-preview (신규)",
description: "Gemini 3.1 고성능 추론/코딩 프리뷰",
},
{
value: "gemini-3.1-flash-lite-preview",
label: "gemini-3.1-flash-lite-preview",
description: "Gemini 3.1 경량 고속 프리뷰",
},
{
value: "gemini-3-flash-preview",
label: "gemini-3-flash-preview",
description: "Gemini 3 고속 프리뷰",
},
{
value: "gemini-2.5-pro",
label: "gemini-2.5-pro",
description: "고난도 추론 중심",
},
{
value: "gemini-2.5-flash",
label: "gemini-2.5-flash",
description: "속도/품질 균형형",
},
{
value: "gemini-2.5-flash-lite",
label: "gemini-2.5-flash-lite",
description: "가벼운 작업용 고속 모델",
},
{
value: "gemini-3-pro-preview",
label: "gemini-3-pro-preview (종료예정)",
description: "공식 문서 기준 2026-03-09 종료 예정 프리뷰",
},
] satisfies AutotradeSubscriptionCliModelOption[],
} as const;
export interface AutotradeAiModeOption {
id: AutotradeAiMode;
label: string;
description: string;
}
export const AUTOTRADE_AI_MODE_OPTIONS: AutotradeAiModeOption[] = [
{
id: "auto",
label: "자동(권장)",
description:
"OpenAI API 키가 있으면 OpenAI를 사용하고, 없으면 구독형 CLI를 시도합니다. 둘 다 실패하면 규칙 기반으로 전환합니다.",
},
{
id: "openai_api",
label: "OpenAI API",
description: "서버에서 OpenAI API를 직접 호출해 판단합니다.",
},
{
id: "subscription_cli",
label: "구독형 CLI 자동판단",
description: "서버에 설치된 Codex/Gemini CLI로 자동 판단을 생성합니다.",
},
{
id: "rule_fallback",
label: "규칙 기반",
description: "AI 호출 없이 내부 규칙 엔진으로만 판단합니다.",
},
];
export type AutotradeStopReason =
| "browser_exit"
| "external_leave"
| "manual"
| "emergency"
| "heartbeat_timeout";
export interface AutotradeSetupFormValues {
aiMode: AutotradeAiMode;
subscriptionCliVendor: AutotradeSubscriptionCliVendor;
subscriptionCliModel: string;
prompt: string;
selectedTechniques: AutotradeTechniqueId[];
allocationPercent: number;
allocationAmount: number;
dailyLossPercent: number;
dailyLossAmount: number;
confidenceThreshold: number;
agreeStopOnExit: boolean;
}
export interface AutotradeCompiledStrategy {
provider: "openai" | "fallback" | "subscription_cli";
providerVendor?: "codex" | "gemini";
providerModel?: string;
summary: string;
selectedTechniques: AutotradeTechniqueId[];
confidenceThreshold: number;
maxDailyOrders: number;
cooldownSec: number;
maxOrderAmountRatio: number;
createdAt: string;
}
export interface AutotradeValidationResult {
isValid: boolean;
blockedReasons: string[];
warnings: string[];
cashBalance: number;
effectiveAllocationAmount: number;
effectiveDailyLossLimit: number;
}
export interface AutotradeMinuteCandle {
time: string;
open: number;
high: number;
low: number;
close: number;
volume: number;
timestamp?: number;
}
export interface AutotradeMinutePatternContext {
timeframe: "1m";
candleCount: number;
impulseDirection: "up" | "down" | "flat";
impulseBarCount: number;
consolidationBarCount: number;
impulseChangeRate?: number;
impulseRangePercent?: number;
consolidationRangePercent?: number;
consolidationCloseClusterPercent?: number;
consolidationVolumeRatio?: number;
breakoutUpper?: number;
breakoutLower?: number;
}
export interface AutotradeBudgetContext {
setupAllocationPercent: number;
setupAllocationAmount: number;
effectiveAllocationAmount: number;
strategyMaxOrderAmountRatio: number;
effectiveOrderBudgetAmount: number;
estimatedBuyUnitCost: number;
estimatedBuyableQuantity: number;
}
export interface AutotradePortfolioContext {
holdingQuantity: number;
sellableQuantity: number;
averagePrice: number;
estimatedSellableNetAmount?: number;
}
export interface AutotradeExecutionCostProfileSnapshot {
buyFeeRate: number;
sellFeeRate: number;
sellTaxRate: number;
}
export interface AutotradeSessionInfo {
sessionId: string;
symbol: string;
runtimeState: "RUNNING" | "STOPPED";
leaderTabId: string;
startedAt: string;
lastHeartbeatAt: string;
endedAt: string | null;
stopReason: AutotradeStopReason | null;
effectiveAllocationAmount: number;
effectiveDailyLossLimit: number;
}
export interface AutotradeMarketSnapshot {
symbol: string;
stockName?: string;
market?: "KOSPI" | "KOSDAQ";
requestAtIso?: string;
requestAtKst?: string;
tickTime?: string;
executionClassCode?: string;
isExpected?: boolean;
trId?: string;
currentPrice: number;
prevClose?: number;
changeRate: number;
open: number;
high: number;
low: number;
tradeVolume: number;
accumulatedVolume: number;
tradeStrength?: number;
askPrice1?: number;
bidPrice1?: number;
askSize1?: number;
bidSize1?: number;
totalAskSize?: number;
totalBidSize?: number;
buyExecutionCount?: number;
sellExecutionCount?: number;
netBuyExecutionCount?: number;
spread?: number;
spreadRate?: number;
dayRangePercent?: number;
dayRangePosition?: number;
volumeRatio?: number;
recentTradeCount?: number;
recentTradeVolumeSum?: number;
recentAverageTradeVolume?: number;
accumulatedVolumeDelta?: number;
netBuyExecutionDelta?: number;
orderBookImbalance?: number;
liquidityDepth?: number;
topLevelOrderBookImbalance?: number;
buySellExecutionRatio?: number;
recentPriceHigh?: number;
recentPriceLow?: number;
recentPriceRangePercent?: number;
recentTradeVolumes?: number[];
recentNetBuyTrail?: number[];
recentTickAgesSec?: number[];
intradayMomentum?: number;
recentReturns?: number[];
recentPrices: number[];
marketDataLatencySec?: number;
recentMinuteCandles?: AutotradeMinuteCandle[];
minutePatternContext?: AutotradeMinutePatternContext;
budgetContext?: AutotradeBudgetContext;
portfolioContext?: AutotradePortfolioContext;
executionCostProfile?: AutotradeExecutionCostProfileSnapshot;
}
export interface AutotradeProposedOrder {
symbol: string;
side: "buy" | "sell";
orderType: "limit" | "market";
price?: number;
quantity?: number;
}
export interface AutotradeSignalCandidate {
signal: "buy" | "sell" | "hold";
confidence: number;
reason: string;
ttlSec: number;
riskFlags: string[];
proposedOrder?: AutotradeProposedOrder;
source: "openai" | "fallback" | "subscription_cli";
providerVendor?: "codex" | "gemini";
providerModel?: string;
}
export interface AutotradeRuntimeLog {
id: string;
level: "info" | "warning" | "error";
stage?:
| "session"
| "strategy_compile"
| "strategy_validate"
| "signal_request"
| "signal_response"
| "risk_gate"
| "order_execution"
| "order_blocked"
| "provider_fallback"
| "engine_error";
message: string;
detail?: string;
createdAt: string;
}
export interface AutotradeCompileResponse {
ok: boolean;
compiledStrategy: AutotradeCompiledStrategy;
}
export interface AutotradeValidateResponse {
ok: boolean;
validation: AutotradeValidationResult;
}
export interface AutotradeSessionResponse {
ok: boolean;
session: AutotradeSessionInfo;
}
export interface AutotradeSignalResponse {
ok: boolean;
signal: AutotradeSignalCandidate;
}