218 lines
6.2 KiB
TypeScript
218 lines
6.2 KiB
TypeScript
/**
|
|
* [파일 역할]
|
|
* 자동매매 프론트엔드가 호출하는 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;
|
|
}
|