Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ MODEL_MICROSTRUCTURE_ANALYST=gpt-5.4-mini
MODEL_SKEPTIC_ANALYST=gpt-5.4-mini
MODEL_KOREA_TRANSLATOR=gpt-5.4
MODEL_FINAL_JUDGE=gpt-5.4
MODEL_REPLAY_CALIBRATOR=gpt-5.4-mini
ALPHA_VANTAGE_API_KEY=
POLYGON_API_KEY=

235 changes: 226 additions & 9 deletions apps/web/components/replay/weekly-replay-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const API_BASE_URL =
const REPLAY_TIMEOUT_MS = 90000;

function outcomeVariant(label: string): "ok" | "warn" | "danger" {
if (label === "강한 반응" || label === "양호") {
if (label === "강한 적중" || label === "양호") {
return "ok";
}
if (label === "보통") {
Expand All @@ -23,6 +23,16 @@ function outcomeVariant(label: string): "ok" | "warn" | "danger" {
return "danger";
}

function deviationVariant(score: number): "ok" | "warn" | "danger" {
if (score >= 0.75) {
return "danger";
}
if (score >= 0.45) {
return "warn";
}
return "ok";
}

function tierLabel(tier: string) {
return tier === "leader" ? "주도주" : "2등주";
}
Expand Down Expand Up @@ -84,7 +94,8 @@ export function WeeklyReplayClient() {
<Card className="rounded-[20px] bg-[color:var(--bg-muted)] p-5">
<p className="section-eyebrow">Loading</p>
<p className="keep-korean mt-2 text-[14px] leading-6 text-[color:var(--text-muted)]">
리플레이 데이터를 불러오는 중입니다. 최근 7거래일 미국 이벤트와 한국장 결과를 정리하고 있습니다.
리플레이 데이터를 불러오는 중입니다. 최근 7거래일 미국 이벤트, 실제 한국장 반응,
보정 분석을 함께 계산하고 있습니다.
</p>
</Card>
);
Expand All @@ -107,12 +118,12 @@ export function WeeklyReplayClient() {

return (
<>
<div className="grid gap-4 lg:grid-cols-3">
<div className="grid gap-4 xl:grid-cols-4">
<Card className="rounded-[20px] bg-[color:var(--bg-muted)] p-4">
<p className="section-eyebrow">Window</p>
<p className="keep-korean mt-2 text-[20px] font-semibold">{data.windowLabel}</p>
<p className="mt-2 text-[13px] leading-6 text-[color:var(--text-muted)]">
현재 운영 중인 추론 규칙과 동일한 방식으로 과거 구간을 재구성한 결과입니다.
현재 추론 규칙과 동일한 방식으로 최근 구간을 다시 돌린 결과입니다.
</p>
</Card>
<Card className="rounded-[20px] bg-[color:var(--bg-muted)] p-4">
Expand All @@ -133,8 +144,106 @@ export function WeeklyReplayClient() {
전체 리플레이 기준 주도주의 평균 종가 수익률입니다.
</p>
</Card>
<Card className="rounded-[20px] bg-[color:var(--bg-muted)] p-4">
<p className="section-eyebrow">Weekly Drift</p>
<p className="mt-2 text-[20px] font-semibold">
{formatScore(data.calibrationSummary.averageDeviationScore)}
</p>
<p className="mt-2 text-[13px] leading-6 text-[color:var(--text-muted)]">
의미 있는 오차 {data.calibrationSummary.daysWithMeaningfulMismatch}일 / 분석 모드{" "}
{data.calibrationSummary.analysisMode === "model_augmented" ? "모델 보강" : "규칙 기반"}
</p>
</Card>
</div>

<Card className="rounded-[24px] border border-[color:var(--border)] bg-[color:var(--bg-muted)] p-5">
<div className="flex flex-wrap items-start justify-between gap-4">
<div className="max-w-3xl">
<p className="section-eyebrow">Calibration Summary</p>
<h2 className="keep-korean mt-2 text-[22px] font-semibold leading-[1.3]">
왜 빗나갔는지와 다음 분석에서 조정할 포인트
</h2>
<p className="keep-korean mt-3 text-[14px] leading-7 text-[color:var(--text-muted)]">
{data.calibrationSummary.summary}
</p>
</div>
<div className="grid gap-2 rounded-[18px] bg-[color:var(--surface-white)] px-4 py-3 text-[13px] text-[color:var(--text-muted)]">
<p>역할 {data.calibrationSummary.role}</p>
<p>모델 {data.calibrationSummary.model}</p>
<p>신뢰도 {formatScore(data.calibrationSummary.confidence)}</p>
</div>
</div>

<div className="mt-4 flex flex-wrap gap-2">
{data.calibrationSummary.topDriverLabels.map((label) => (
<Badge key={label} variant="warn">
{label}
</Badge>
))}
</div>

<div className="mt-5 grid gap-4 xl:grid-cols-2">
<div className="rounded-[20px] bg-[color:var(--surface-white)] p-4">
<p className="text-[14px] font-semibold">권장 보정</p>
<div className="mt-3 space-y-3">
{data.calibrationSummary.suggestedAdjustments.map((action) => (
<div
key={`${action.target}-${action.adjustment}`}
className="rounded-[16px] border border-[color:var(--border)] px-4 py-3"
>
<p className="text-[13px] font-semibold">
{action.target} · {action.adjustment}
</p>
<p className="keep-korean mt-2 text-[13px] leading-6 text-[color:var(--text-muted)]">
{action.rationale}
</p>
</div>
))}
</div>
</div>

<div className="rounded-[20px] bg-[color:var(--surface-white)] p-4">
<p className="text-[14px] font-semibold">다음 확인 포인트</p>
<div className="mt-3 space-y-2">
{data.calibrationSummary.watchouts.map((item) => (
<p
key={item}
className="keep-korean rounded-[16px] border border-[color:var(--border)] px-4 py-3 text-[13px] leading-6 text-[color:var(--text-muted)]"
>
{item}
</p>
))}
</div>
</div>
</div>
</Card>

<Card className="rounded-[24px] border border-[color:var(--border)] bg-[color:var(--bg-muted)] p-5">
<div className="flex flex-wrap items-start justify-between gap-4">
<div className="max-w-3xl">
<p className="section-eyebrow">Exit Timing</p>
<h2 className="keep-korean mt-2 text-[22px] font-semibold leading-[1.3]">
시가 진입 가정 시 언제 파는 것이 가장 유리했는가
</h2>
<p className="keep-korean mt-3 text-[14px] leading-7 text-[color:var(--text-muted)]">
{data.tradeTimingSummary.summary}
</p>
</div>
<div className="grid gap-2 rounded-[18px] bg-[color:var(--surface-white)] px-4 py-3 text-[13px] text-[color:var(--text-muted)]">
<p>평균 추가 수익 {formatPct(data.tradeTimingSummary.averageExtraReturnPct)}</p>
<p>기준 5분봉 / 시가 진입 가정</p>
</div>
</div>

<div className="mt-4 flex flex-wrap gap-2">
{data.tradeTimingSummary.topSignalLabels.map((label) => (
<Badge key={label} variant="ok">
{label}
</Badge>
))}
</div>
</Card>

<div className="space-y-5">
{orderedDays.map((day) => (
<Card key={day.marketDateLabel}>
Expand All @@ -159,7 +268,7 @@ export function WeeklyReplayClient() {
<p className="keep-korean text-[14px] leading-6 text-[color:var(--text-muted)]">{day.summary}</p>
</div>

<div className="mt-5 grid gap-6 xl:grid-cols-[minmax(0,1.2fr)_minmax(340px,0.8fr)]">
<div className="mt-5 grid gap-6 xl:grid-cols-[minmax(0,1.1fr)_minmax(360px,0.9fr)]">
<div className="space-y-4">
{day.predictedThemes.length > 0 ? (
day.predictedThemes.map((theme) => (
Expand Down Expand Up @@ -197,13 +306,13 @@ export function WeeklyReplayClient() {
<div className="rounded-[18px] bg-[color:var(--bg-muted)] p-4">
<p className="text-[14px] font-semibold">예상 주도주 / 2등주</p>
<p className="keep-korean mt-2 text-[13px] leading-6 text-[color:var(--text-muted)]">
주도주: {theme.leaders.map((item) => `${item.ticker} ${item.name}`).join(", ") || "-"}
주도주 {theme.leaders.map((item) => `${item.ticker} ${item.name}`).join(", ") || "-"}
</p>
<p className="keep-korean mt-1 text-[13px] leading-6 text-[color:var(--text-muted)]">
2등주: {theme.secondTier.map((item) => `${item.ticker} ${item.name}`).join(", ") || "-"}
2등주 {theme.secondTier.map((item) => `${item.ticker} ${item.name}`).join(", ") || "-"}
</p>
<p className="keep-korean mt-3 text-[13px] leading-6 text-[color:var(--text-muted)]">
무효화 조건: {theme.invalidationCondition}
무효화 조건 {theme.invalidationCondition}
</p>
</div>
<div className="rounded-[18px] bg-[color:var(--bg-muted)] p-4">
Expand Down Expand Up @@ -241,12 +350,120 @@ export function WeeklyReplayClient() {
))
) : (
<div className="rounded-[20px] border border-dashed border-[color:var(--border)] px-4 py-6 text-[13px] text-[color:var(--text-muted)]">
해당 날짜에는 현재 규칙 기준으로 의미 있는 한국 테마 후보가 생성되지 않았습니다.
해당 날짜에는 현재 규칙 기준으로 의미 있는 한국장 테마 후보가 생성되지 않았습니다.
</div>
)}
</div>

<div className="space-y-4">
<Card className="rounded-[20px] bg-[color:var(--bg-muted)] p-4">
<div className="flex flex-wrap items-start justify-between gap-3">
<div>
<p className="text-[14px] font-semibold">오차 원인 분석</p>
<p className="keep-korean mt-2 text-[13px] leading-6 text-[color:var(--text-muted)]">
{day.calibrationReview.summary}
</p>
</div>
<Badge variant={deviationVariant(day.calibrationReview.deviationScore)}>
오차 {formatScore(day.calibrationReview.deviationScore)}
</Badge>
</div>

<div className="mt-3 flex flex-wrap gap-2">
<Badge variant={deviationVariant(day.calibrationReview.deviationScore)}>
{day.calibrationReview.mismatchLabel}
</Badge>
<Badge variant="warn">
분석 신뢰도 {formatScore(day.calibrationReview.calibrationConfidence)}
</Badge>
</div>

<div className="mt-4 space-y-3">
{day.calibrationReview.drivers.map((driver) => (
<div
key={`${day.marketDateLabel}-${driver.code}`}
className="rounded-[18px] border border-[color:var(--border)] bg-[color:var(--surface-white)] px-4 py-4"
>
<div className="flex flex-wrap items-center justify-between gap-2">
<p className="text-[13px] font-semibold">{driver.label}</p>
<Badge variant={driver.impactLabel === "높음" ? "danger" : "warn"}>
{driver.impactLabel}
</Badge>
</div>
<p className="keep-korean mt-2 text-[13px] leading-6 text-[color:var(--text-muted)]">
{driver.reason}
</p>
<p className="mt-2 text-[12px] leading-6 text-[color:var(--text-muted)]">
근거 {driver.evidence}
</p>
</div>
))}
</div>

<div className="mt-4 rounded-[18px] bg-[color:var(--surface-white)] p-4">
<p className="text-[14px] font-semibold">다음 분석에서의 보정 제안</p>
<div className="mt-3 space-y-3">
{day.calibrationReview.suggestedActions.map((action) => (
<div
key={`${day.marketDateLabel}-${action.target}-${action.adjustment}`}
className="rounded-[16px] border border-[color:var(--border)] px-4 py-3"
>
<p className="text-[13px] font-semibold">
{action.target} · {action.adjustment}
</p>
<p className="keep-korean mt-2 text-[13px] leading-6 text-[color:var(--text-muted)]">
{action.rationale}
</p>
</div>
))}
</div>
</div>
</Card>

<Card className="rounded-[20px] bg-[color:var(--bg-muted)] p-4">
<div className="flex flex-wrap items-start justify-between gap-3">
<div>
<p className="text-[14px] font-semibold">최적 매도 시점 분석</p>
<p className="keep-korean mt-2 text-[13px] leading-6 text-[color:var(--text-muted)]">
{day.tradeOptimization.executionNote}
</p>
</div>
<Badge
variant={
day.tradeOptimization.extraReturnPct !== null &&
day.tradeOptimization.extraReturnPct > 0.5
? "ok"
: "warn"
}
>
{day.tradeOptimization.chartSignal}
</Badge>
</div>

<div className="mt-4 grid gap-3 rounded-[18px] bg-[color:var(--surface-white)] p-4 text-[13px] text-[color:var(--text-muted)]">
<p>
분석 종목 {day.tradeOptimization.ticker} {day.tradeOptimization.name}
</p>
<p>{day.tradeOptimization.entryAssumption}</p>
<p>
최적 매도 시점 {day.tradeOptimization.bestExitTimeLabel ?? "-"} / 최고 수익률{" "}
{day.tradeOptimization.bestExitReturnPct !== null
? formatPct(day.tradeOptimization.bestExitReturnPct)
: "-"}
</p>
<p>
종가 보유 수익률{" "}
{day.tradeOptimization.closeReturnPct !== null
? formatPct(day.tradeOptimization.closeReturnPct)
: "-"}{" "}
/ 추가 확보 가능 수익{" "}
{day.tradeOptimization.extraReturnPct !== null
? formatPct(day.tradeOptimization.extraReturnPct)
: "-"}
</p>
</div>
</Card>

<Card className="rounded-[20px] bg-[color:var(--bg-muted)] p-4">
<p className="text-[14px] font-semibold">근거 이벤트</p>
<div className="mt-3 space-y-3">
Expand Down
60 changes: 59 additions & 1 deletion packages/contracts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,29 @@ export const WeeklyReplayPayloadSchema = z.object({
positiveHitDays: z.number(),
avgLeaderCloseReturnPct: z.number()
}),
calibrationSummary: z.object({
analysisMode: z.string(),
role: z.string(),
model: z.string(),
summary: z.string(),
daysWithMeaningfulMismatch: z.number(),
averageDeviationScore: z.number(),
topDriverLabels: z.array(z.string()),
suggestedAdjustments: z.array(
z.object({
target: z.string(),
adjustment: z.string(),
rationale: z.string()
})
),
watchouts: z.array(z.string()),
confidence: z.number()
}),
tradeTimingSummary: z.object({
summary: z.string(),
averageExtraReturnPct: z.number(),
topSignalLabels: z.array(z.string())
}),
days: z.array(
z.object({
marketDateLabel: z.string(),
Expand Down Expand Up @@ -212,7 +235,42 @@ export const WeeklyReplayPayloadSchema = z.object({
url: z.string()
})
),
summary: z.string()
summary: z.string(),
calibrationReview: z.object({
mismatchLabel: z.string(),
deviationScore: z.number(),
calibrationConfidence: z.number(),
summary: z.string(),
drivers: z.array(
z.object({
code: z.string(),
label: z.string(),
impactLabel: z.string(),
reason: z.string(),
evidence: z.string()
})
),
suggestedActions: z.array(
z.object({
target: z.string(),
adjustment: z.string(),
rationale: z.string()
})
)
}),
tradeOptimization: z.object({
availabilityLabel: z.string(),
ticker: z.string(),
name: z.string(),
barInterval: z.string(),
entryAssumption: z.string(),
bestExitTimeLabel: z.string().nullable(),
bestExitReturnPct: z.number().nullable(),
closeReturnPct: z.number().nullable(),
extraReturnPct: z.number().nullable(),
chartSignal: z.string(),
executionNote: z.string()
})
})
)
});
Expand Down
1 change: 1 addition & 0 deletions scripts/seed_reference_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
("skeptic_analyst", "gpt-5.4-mini"),
("korea_translator", "gpt-5.4"),
("final_judge", "gpt-5.4"),
("replay_calibrator", "gpt-5.4-mini"),
]

THEME_MAPS = [
Expand Down
Loading