/* * 예측을 얼마나 잘게 굴릴 것인가 — **한 회차의 틱 수를 묶는다.** * * ── 왜 필요한가 (2026-08-17 실측) ─────────────────────────────────────────── * 예측 비용의 거의 전부가 틱이다. 트윈 하나의 예측 한 번이 30회차 × 300틱 = 9,000틱, 한 틱 3.2ms → * 28.5초였다. 그런데 몬테카를로는 지평선 **끝에서** 지표를 한 번 읽는다 — 중간을 1초 단위로 볼 이유가 * 없다면 성글게 굴려도 답이 같다. * * 정말 같은지는 재서 확인했다(`operato-twin/test/fork-cost.test.ts`, 10회차·지평선 5분): * * step 1s → 3,048ms · 재고 p50 325 · 완료작업 8 · 완료오더 0 * step 5s → 628ms · 재고 p50 325 · 완료작업 8 · 완료오더 0 ← 답이 같고 5배 싸다 * step 10s → 286ms · 재고 p50 326 · … ← 여기서부터 흔들린다 * * ── 왜 고정 step 이 아니라 틱 수 상한인가 ─────────────────────────────────── * step 을 5초로 못 박으면 1분짜리 지평선은 12틱이 되어 너무 성글고, 2시간짜리는 1,440틱이라 여전히 * 비싸다. 묶어야 하는 것은 step 이 아니라 **한 회차가 도는 횟수**다. * * 상한(10초)을 따로 두는 이유: 지평선이 길다고 무한정 성글게 굴리면 짧은 작업(초 단위 피킹)이 통째로 * 한 틱에 삼켜진다. 그 지점을 넘지 않는다. */ /** 한 회차가 도는 틱 수의 목표 — 이보다 잘게 굴려야 할 이유가 생기면 여기를 고친다. */ export const TARGET_TICKS_PER_RUN = 60 /** 아무리 지평선이 길어도 이보다 성글게 굴리지 않는다 — 짧은 작업이 한 틱에 삼켜진다. */ export const MAX_STEP_MS = 10_000 /** 커널의 시각 단위 — 이보다 잘게 굴릴 이유는 없다. */ export const MIN_STEP_MS = 1_000 /** * 이 지평선을 굴릴 틱 크기. * * 초 단위로 떨어뜨린다 — 3,333ms 같은 값은 사람이 읽을 수도, 화면이 설명할 수도 없다. */ export function forecastStepMs(horizonMs: number): number { if (!Number.isFinite(horizonMs) || horizonMs <= 0) return MIN_STEP_MS const raw = horizonMs / TARGET_TICKS_PER_RUN const rounded = Math.round(raw / MIN_STEP_MS) * MIN_STEP_MS return Math.min(MAX_STEP_MS, Math.max(MIN_STEP_MS, rounded)) }