전체적인 리팩토링
This commit is contained in:
217
features/autotrade/apis/autotrade.api.ts
Normal file
217
features/autotrade/apis/autotrade.api.ts
Normal 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;
|
||||
}
|
||||
1343
features/autotrade/components/AutotradeControlPanel.tsx
Normal file
1343
features/autotrade/components/AutotradeControlPanel.tsx
Normal file
File diff suppressed because it is too large
Load Diff
37
features/autotrade/components/AutotradeWarningBanner.tsx
Normal file
37
features/autotrade/components/AutotradeWarningBanner.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
2166
features/autotrade/hooks/useAutotradeEngine.ts
Normal file
2166
features/autotrade/hooks/useAutotradeEngine.ts
Normal file
File diff suppressed because it is too large
Load Diff
212
features/autotrade/stores/use-autotrade-engine-store.ts
Normal file
212
features/autotrade/stores/use-autotrade-engine-store.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
482
features/autotrade/types/autotrade.types.ts
Normal file
482
features/autotrade/types/autotrade.types.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user