가상투자 설정 수정

This commit is contained in:
Macbook
2026-05-25 22:30:54 +09:00
parent cae1c3a624
commit 1e950c7db4
24 changed files with 1015 additions and 362 deletions
+117
View File
@@ -0,0 +1,117 @@
/**
* 가상투자 Match(전략 조건 100% 충족) 시 자동 모의 체결
* 설정 · 가상투자 · 자동매매 ON + 세션 running 일 때만 동작
*/
import { useEffect, useRef } from 'react';
import type { TickerData } from './useMarketTicker';
import type { VirtualIndicatorSnapshot } from './useVirtualIndicatorSnapshots';
import type { VirtualSessionConfig, VirtualTargetItem } from '../utils/virtualTradingStorage';
import { placePaperOrder, type PaperSummaryDto } from '../utils/backendApi';
import {
buildConditionMetrics,
resolveVirtualTradeTiming,
type VirtualTradeTiming,
} from '../utils/virtualSignalMetrics';
import { coerceFiniteNumber } from '../utils/safeFormat';
const EXEC_COOLDOWN_MS = 15_000;
interface Options {
enabled: boolean;
session: VirtualSessionConfig;
targets: VirtualTargetItem[];
snapshots: Record<string, VirtualIndicatorSnapshot | undefined>;
tickers?: Map<string, TickerData>;
paperAutoTradeBudgetPct: number;
positions?: PaperSummaryDto['positions'];
onFilled?: () => void;
}
export function useVirtualAutoTrade({
enabled,
session,
targets,
snapshots,
tickers,
paperAutoTradeBudgetPct,
positions = [],
onFilled,
}: Options): void {
const prevTimingRef = useRef<Record<string, VirtualTradeTiming>>({});
const inflightRef = useRef<Set<string>>(new Set());
const lastExecRef = useRef<Record<string, number>>({});
useEffect(() => {
if (!enabled || !session.running) {
prevTimingRef.current = {};
return;
}
for (const target of targets) {
const snap = snapshots[target.market];
if (!snap?.rows?.length) continue;
const metrics = buildConditionMetrics(snap.rows);
const timing = resolveVirtualTradeTiming(metrics);
const prev = prevTimingRef.current[target.market] ?? 'neutral';
prevTimingRef.current[target.market] = timing;
if (timing !== 'buy' && timing !== 'sell') continue;
if (timing === prev) continue;
const price = coerceFiniteNumber(tickers?.get(target.market)?.tradePrice);
if (price == null || price <= 0) continue;
const heldQty = coerceFiniteNumber(
positions.find(p => p.symbol === target.market)?.quantity,
) ?? 0;
if (session.positionMode === 'LONG_ONLY') {
if (timing === 'buy' && heldQty > 0) continue;
if (timing === 'sell' && heldQty <= 0) continue;
}
const side = timing === 'buy' ? 'BUY' : 'SELL';
const execKey = `${target.market}:${side}`;
if (inflightRef.current.has(execKey)) continue;
if (Date.now() - (lastExecRef.current[execKey] ?? 0) < EXEC_COOLDOWN_MS) continue;
const strategyId = target.strategyId ?? session.globalStrategyId ?? null;
inflightRef.current.add(execKey);
void placePaperOrder({
market: target.market,
side,
orderKind: 'market',
price,
quantity: 0,
budgetPct: timing === 'buy' ? paperAutoTradeBudgetPct : 100,
source: 'STRATEGY',
strategyId,
})
.then(trade => {
if (trade) {
lastExecRef.current[execKey] = Date.now();
onFilled?.();
}
})
.catch(err => {
console.warn('[VirtualAutoTrade]', target.market, side, err);
})
.finally(() => {
inflightRef.current.delete(execKey);
});
}
}, [
enabled,
session.running,
session.globalStrategyId,
session.positionMode,
targets,
snapshots,
tickers,
paperAutoTradeBudgetPct,
positions,
onFilled,
]);
}