전략평가 MA 참조에러 수정
This commit is contained in:
@@ -463,6 +463,7 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
||||
barSignalHighlight={barSignalHighlight}
|
||||
hasStrategy={selectedStrategyId != null}
|
||||
className="seval-signal-panel"
|
||||
getParams={getEvalParams}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -24,6 +24,11 @@ import {
|
||||
candleRangeWindowBars,
|
||||
formatCandleRangeClause,
|
||||
} from '../utils/strategyTypes';
|
||||
import {
|
||||
appendLegacyMaFieldOption,
|
||||
buildEmaFieldOptions,
|
||||
buildMaFieldOptions,
|
||||
} from '../utils/maDslField';
|
||||
|
||||
// ─── 타입 ─────────────────────────────────────────────────────────────────────
|
||||
type LogicNodeType = 'AND' | 'OR' | 'NOT' | 'CONDITION' | 'TIMEFRAME';
|
||||
@@ -298,7 +303,7 @@ const ICHIMOKU_CONDS = [
|
||||
type Opt = { value: string; label: string };
|
||||
const NONE: Opt = { value: 'NONE', label: '선택안함' };
|
||||
|
||||
const getFieldOpts = (ind: string, signalType: 'buy'|'sell', DEF: DefType): Opt[] => {
|
||||
const getFieldOpts = (ind: string, signalType: 'buy'|'sell', DEF: DefType, cond?: ConditionDSL): Opt[] => {
|
||||
const overTerm = signalType === 'buy' ? '과열진입' : '과열이탈';
|
||||
const underTerm = signalType === 'buy' ? '침체이탈' : '침체진입';
|
||||
const condOpts = (conds: Opt[]): Opt[] => [NONE, ...conds];
|
||||
@@ -377,16 +382,26 @@ const getFieldOpts = (ind: string, signalType: 'buy'|'sell', DEF: DefType): Opt[
|
||||
{ value:'VOLUME_VALUE', label:'거래량' },
|
||||
{ value:'VOLUME_MA', label:'거래량 이동평균' },
|
||||
]);
|
||||
case 'MA': return [
|
||||
NONE,
|
||||
...DEF.maLines.map((p, i) => ({ value:`MA${p}`, label:`MA${i+1}(${p}일)` })),
|
||||
{ value:'CLOSE_PRICE', label:'종가' },
|
||||
];
|
||||
case 'EMA': return [
|
||||
NONE,
|
||||
...DEF.maLines.slice(0,4).map((p, i) => ({ value:`EMA${p}`, label:`EMA${i+1}(${p}일)` })),
|
||||
{ value:'CLOSE_PRICE', label:'종가' },
|
||||
];
|
||||
case 'MA': {
|
||||
let opts = [
|
||||
NONE,
|
||||
...buildMaFieldOptions(DEF.maLines),
|
||||
{ value: 'CLOSE_PRICE', label: '종가' },
|
||||
];
|
||||
if (cond?.leftField) opts = appendLegacyMaFieldOption(opts, cond.leftField);
|
||||
if (cond?.rightField) opts = appendLegacyMaFieldOption(opts, cond.rightField);
|
||||
return opts;
|
||||
}
|
||||
case 'EMA': {
|
||||
let opts = [
|
||||
NONE,
|
||||
...buildEmaFieldOptions(DEF.maLines, 4),
|
||||
{ value: 'CLOSE_PRICE', label: '종가' },
|
||||
];
|
||||
if (cond?.leftField) opts = appendLegacyMaFieldOption(opts, cond.leftField);
|
||||
if (cond?.rightField) opts = appendLegacyMaFieldOption(opts, cond.rightField);
|
||||
return opts;
|
||||
}
|
||||
case 'DISPARITY': return condOpts([
|
||||
{ value:`DISPARITY${DEF.dispUltra}`, label:`초단기 이격도(${DEF.dispUltra}일)` },
|
||||
{ value:`DISPARITY${DEF.dispShort}`, label:`단기 이격도(${DEF.dispShort}일)` },
|
||||
@@ -515,8 +530,8 @@ const getDefaultFields = (ind: string, signalType: 'buy'|'sell', DEF: DefType):
|
||||
DMI: { l:'PDI', r:'MDI' },
|
||||
OBV: { l:'OBV_LINE', r:'OBV_SIGNAL' },
|
||||
VOLUME: { l:'VOLUME_VALUE', r:'VOLUME_MA' },
|
||||
MA: { l:`MA${DEF.maLines[0]}`, r:`MA${DEF.maLines[1]}` },
|
||||
EMA: { l:`EMA${DEF.maLines[0]}`, r:`EMA${DEF.maLines[1]}` },
|
||||
MA: { l: 'MA1', r: 'MA2' },
|
||||
EMA: { l: 'EMA1', r: 'EMA2' },
|
||||
DISPARITY: { l:`DISPARITY${DEF.dispUltra}`, r: mid(100) },
|
||||
PSYCHOLOGICAL: { l:'PSY_VALUE', r: over(75) },
|
||||
NEW_PSYCHOLOGICAL: { l:'NEW_PSY_VALUE', r: over(50) },
|
||||
@@ -538,7 +553,7 @@ const getDefaultFields = (ind: string, signalType: 'buy'|'sell', DEF: DefType):
|
||||
// conditionType 변경 시 자동 필드 조정
|
||||
const applyCondTypeDefaults = (cond: ConditionDSL, newCondType: string, DEF: DefType): ConditionDSL => {
|
||||
const updated = { ...cond, conditionType: newCondType };
|
||||
const fieldOpts = getFieldOpts(cond.indicatorType, 'buy', DEF);
|
||||
const fieldOpts = getFieldOpts(cond.indicatorType, 'buy', DEF, cond);
|
||||
const isValid = (v: string|undefined) => !!v && fieldOpts.some(o => o.value === v);
|
||||
const def = getDefaultFields(cond.indicatorType, 'buy', DEF);
|
||||
|
||||
@@ -575,7 +590,7 @@ const nodeToText = (node: LogicNode, DEF: DefType = DEF_DEFAULTS): string => {
|
||||
if (!node) return '';
|
||||
if (node.type === 'CONDITION' && node.condition) {
|
||||
const c = node.condition;
|
||||
const opts = getFieldOpts(c.indicatorType, 'buy', DEF);
|
||||
const opts = getFieldOpts(c.indicatorType, 'buy', DEF, c);
|
||||
const L = opts.find(o => o.value === c.leftField)?.label ?? c.leftField ?? c.indicatorType;
|
||||
const R = opts.find(o => o.value === c.rightField)?.label ?? c.rightField ?? '';
|
||||
const C = condLabel(c.conditionType);
|
||||
@@ -680,7 +695,7 @@ interface CondEditorProps {
|
||||
}
|
||||
|
||||
const CondEditor: React.FC<CondEditorProps> = ({ cond, signalType, onChange, def }) => {
|
||||
const fieldOpts = getFieldOpts(cond.indicatorType, signalType, def);
|
||||
const fieldOpts = getFieldOpts(cond.indicatorType, signalType, def, cond);
|
||||
const condOpts = cond.indicatorType === 'ICHIMOKU' ? ICHIMOKU_CONDS : COND_OPTIONS;
|
||||
|
||||
const isValid = (v: string|undefined) => !!v && fieldOpts.some(o => o.value === v);
|
||||
|
||||
@@ -23,6 +23,8 @@ interface Props {
|
||||
loadingMessage?: string;
|
||||
hasStrategy?: boolean;
|
||||
className?: string;
|
||||
/** 보조지표 SMA period1~11 — MA 슬롯 라벨·해석용 */
|
||||
getParams?: (type: string, defaults?: Record<string, number | string | boolean>) => Record<string, number | string | boolean>;
|
||||
}
|
||||
|
||||
interface SideCardProps {
|
||||
@@ -32,6 +34,7 @@ interface SideCardProps {
|
||||
signalActive?: boolean;
|
||||
strategy?: StrategyDto | null;
|
||||
barTimeSec?: number | null;
|
||||
getParams?: Props['getParams'];
|
||||
}
|
||||
|
||||
const SideCard: React.FC<SideCardProps> = ({
|
||||
@@ -41,6 +44,7 @@ const SideCard: React.FC<SideCardProps> = ({
|
||||
signalActive = false,
|
||||
strategy,
|
||||
barTimeSec,
|
||||
getParams,
|
||||
}) => {
|
||||
const [interpretOpen, setInterpretOpen] = useState(false);
|
||||
const sideLabel = side === 'buy' ? '매수' : '매도';
|
||||
@@ -48,7 +52,7 @@ const SideCard: React.FC<SideCardProps> = ({
|
||||
const overallLabel =
|
||||
overallMet === true ? '충족' : overallMet === false ? '미충족' : null;
|
||||
|
||||
const contextMap = useMemo(() => buildConditionContextMap(strategy), [strategy]);
|
||||
const contextMap = useMemo(() => buildConditionContextMap(strategy, getParams), [strategy, getParams]);
|
||||
const interpretation = useMemo(
|
||||
() => buildSideInterpretation(side, metrics, overallMet, contextMap),
|
||||
[side, metrics, overallMet, contextMap],
|
||||
@@ -114,6 +118,7 @@ const StrategyEvaluationSignalPanel: React.FC<Props> = ({
|
||||
loadingMessage,
|
||||
hasStrategy = true,
|
||||
className = '',
|
||||
getParams,
|
||||
}) => {
|
||||
const rows = snapshot?.rows ?? [];
|
||||
const metrics = useMemo(() => buildConditionMetrics(rows), [rows]);
|
||||
@@ -143,6 +148,7 @@ const StrategyEvaluationSignalPanel: React.FC<Props> = ({
|
||||
signalActive={barSignalHighlight?.sell ?? false}
|
||||
strategy={strategy}
|
||||
barTimeSec={barTimeSec}
|
||||
getParams={getParams}
|
||||
/>
|
||||
<SideCard
|
||||
side="buy"
|
||||
@@ -151,6 +157,7 @@ const StrategyEvaluationSignalPanel: React.FC<Props> = ({
|
||||
signalActive={barSignalHighlight?.buy ?? false}
|
||||
strategy={strategy}
|
||||
barTimeSec={barTimeSec}
|
||||
getParams={getParams}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user