위젯항목 추가
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
loadBacktestSettings,
|
||||
runBacktest,
|
||||
type BacktestSignal,
|
||||
type StrategyDto,
|
||||
} from '../../utils/backendApi';
|
||||
import { loadAnalysisCandles } from '../../utils/analysisChartData';
|
||||
import { normalizeChartTimeframe } from '../../utils/backtestUiUtils';
|
||||
import { resolveStrategyPrimaryTimeframe } from '../../utils/strategyToChartIndicators';
|
||||
import { resolveEvaluationFromLoadedBars } from '../../utils/backtestWarmup';
|
||||
import type { OHLCVBar } from '../../types';
|
||||
import { useIndicatorSettings } from '../../hooks/useIndicatorSettings';
|
||||
|
||||
const BAR_COUNT = 300;
|
||||
|
||||
/** 선택 종목·전략에 대한 백테스트 매매 시그널 목록 */
|
||||
export function useWidgetBacktestSignals(
|
||||
market: string,
|
||||
strategyId: number | null,
|
||||
strategy: StrategyDto | undefined,
|
||||
) {
|
||||
const { getParams } = useIndicatorSettings();
|
||||
const [signals, setSignals] = useState<BacktestSignal[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const genRef = useRef(0);
|
||||
const toTimeSec = useMemo(() => Math.floor(Date.now() / 1000), [market, strategyId]);
|
||||
|
||||
const runSignals = useCallback(async (
|
||||
bars: OHLCVBar[],
|
||||
tf: string,
|
||||
strat: StrategyDto,
|
||||
sid: number,
|
||||
) => {
|
||||
const sym = market.startsWith('KRW-') ? market : `KRW-${market}`;
|
||||
if (bars.length < 10) {
|
||||
setSignals([]);
|
||||
setError('캔들 데이터가 부족합니다.');
|
||||
return;
|
||||
}
|
||||
const gen = ++genRef.current;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const btSettings = await loadBacktestSettings();
|
||||
const indicatorParams = Object.fromEntries(
|
||||
['RSI', 'MACD', 'CCI', 'SMA', 'EMA', 'IchimokuCloud', 'Stochastic', 'ADX', 'MFI', 'NewPsychological'].map(t => [
|
||||
t,
|
||||
getParams(t) as Record<string, unknown>,
|
||||
]),
|
||||
);
|
||||
const { evaluationBarCount } = resolveEvaluationFromLoadedBars(
|
||||
bars.length,
|
||||
strat.buyCondition,
|
||||
strat.sellCondition,
|
||||
indicatorParams,
|
||||
);
|
||||
const res = await runBacktest({
|
||||
strategyId: sid,
|
||||
bars: bars.map(b => ({
|
||||
time: b.time,
|
||||
open: b.open,
|
||||
high: b.high,
|
||||
low: b.low,
|
||||
close: b.close,
|
||||
volume: b.volume,
|
||||
})),
|
||||
timeframe: tf,
|
||||
symbol: sym,
|
||||
strategyName: strat.name,
|
||||
settings: btSettings,
|
||||
indicatorParams,
|
||||
evaluationBarCount,
|
||||
});
|
||||
if (gen !== genRef.current) return;
|
||||
if (!res) {
|
||||
setSignals([]);
|
||||
setError('시그널 계산에 실패했습니다.');
|
||||
return;
|
||||
}
|
||||
setSignals(res.signals);
|
||||
} catch (e) {
|
||||
if (gen !== genRef.current) return;
|
||||
setSignals([]);
|
||||
setError(e instanceof Error ? e.message : '시그널 계산 실패');
|
||||
} finally {
|
||||
if (gen === genRef.current) setLoading(false);
|
||||
}
|
||||
}, [market, getParams]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!strategyId || !strategy) {
|
||||
setSignals([]);
|
||||
setError(strategyId ? null : '전략을 선택하세요.');
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
const tf = normalizeChartTimeframe(resolveStrategyPrimaryTimeframe(strategy));
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
const sym = market.startsWith('KRW-') ? market : `KRW-${market}`;
|
||||
const bars = await loadAnalysisCandles(sym, tf, toTimeSec, BAR_COUNT);
|
||||
if (cancelled) return;
|
||||
await runSignals(bars, tf, strategy, strategyId);
|
||||
} catch (e) {
|
||||
if (!cancelled) {
|
||||
setSignals([]);
|
||||
setError(e instanceof Error ? e.message : '시그널 계산 실패');
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
++genRef.current;
|
||||
};
|
||||
}, [market, strategyId, strategy, toTimeSec, runSignals]);
|
||||
|
||||
return { signals, loading, error };
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { loadStrategy, type StrategyDto } from '../../utils/backendApi';
|
||||
import { useWidgetDashboard } from '../WidgetDashboardContext';
|
||||
|
||||
function strategyHasConditions(s: StrategyDto | undefined): boolean {
|
||||
return Boolean(s?.buyCondition || s?.sellCondition);
|
||||
}
|
||||
|
||||
/** 위젯 대시보드 — config·컨텍스트에서 전략 ID·전체 DTO 해석 */
|
||||
export function useWidgetSelectedStrategy(configStrategyId?: unknown) {
|
||||
const { strategies, selectedStrategyId: ctxStrategyId } = useWidgetDashboard();
|
||||
|
||||
const strategyId = useMemo(() => {
|
||||
if (typeof configStrategyId === 'number' && configStrategyId > 0) return configStrategyId;
|
||||
if (ctxStrategyId != null && ctxStrategyId > 0) return ctxStrategyId;
|
||||
return strategies[0]?.id ?? null;
|
||||
}, [configStrategyId, ctxStrategyId, strategies]);
|
||||
|
||||
const listStrategy = useMemo(
|
||||
() => (strategyId != null ? strategies.find(s => s.id === strategyId) : undefined),
|
||||
[strategies, strategyId],
|
||||
);
|
||||
|
||||
const [strategy, setStrategy] = useState<StrategyDto | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
if (strategyId == null) {
|
||||
setStrategy(undefined);
|
||||
return;
|
||||
}
|
||||
if (listStrategy && strategyHasConditions(listStrategy)) {
|
||||
setStrategy(listStrategy);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
void loadStrategy(strategyId).then(s => {
|
||||
if (!cancelled) setStrategy(s ?? listStrategy);
|
||||
});
|
||||
return () => { cancelled = true; };
|
||||
}, [strategyId, listStrategy]);
|
||||
|
||||
const strategyLabel = strategy?.name ?? listStrategy?.name;
|
||||
|
||||
return { strategyId, strategy, strategies, strategyLabel };
|
||||
}
|
||||
Reference in New Issue
Block a user