전략 설명 추가

This commit is contained in:
Macbook
2026-06-16 21:35:59 +09:00
parent 26bfca7b4c
commit 822d99f6b0
6 changed files with 363 additions and 11 deletions
+29 -10
View File
@@ -107,6 +107,7 @@ import {
type StrategyFlowLayoutStore,
} from '../utils/strategyEditorLayoutStorage';
import LogicExpressionPreview from './strategyEditor/LogicExpressionPreview';
import LogicExpressionNarrative from './strategyEditor/LogicExpressionNarrative';
import StrategyEditorCanvas, { type FlowLayoutSeed } from './strategyEditor/StrategyEditorCanvas';
import StrategyListEditor from './strategyEditor/StrategyListEditor';
import StartCombineOpControl from './strategyEditor/StartCombineOpControl';
@@ -178,7 +179,7 @@ const RIGHT_PANEL_MAX = 560;
const RIGHT_PANEL_DEFAULT = 380;
const TERMINAL_MIN = 88;
const TERMINAL_MAX = 420;
const TERMINAL_DEFAULT = 140;
const TERMINAL_DEFAULT = 220;
function readRightPanelWidth(): number {
return clampPanelSize(
@@ -2088,15 +2089,33 @@ export default function StrategyEditorPage({
/>
<footer className="se-terminal" style={{ height: terminalHeight }}>
<div className="se-terminal-label">LOGIC EXPRESSION</div>
<LogicExpressionPreview
buyCondition={buyCondition}
sellCondition={sellCondition}
buyEditorState={buyEditorState}
sellEditorState={sellEditorState}
orphanCount={orphanTotal}
def={DEF}
/>
<div className="se-terminal-scroll">
<section className="se-terminal-section">
<div className="se-terminal-label">LOGIC EXPRESSION</div>
<LogicExpressionPreview
buyCondition={buyCondition}
sellCondition={sellCondition}
buyEditorState={buyEditorState}
sellEditorState={sellEditorState}
orphanCount={orphanTotal}
def={DEF}
/>
</section>
<section className="se-terminal-section se-terminal-section--narrative">
<div className="se-terminal-label se-terminal-label--guide"> </div>
<LogicExpressionNarrative
name={stratName}
description={stratDesc}
buyCondition={buyCondition}
sellCondition={sellCondition}
buyEditorState={buyEditorState}
sellEditorState={sellEditorState}
orphanCount={orphanTotal}
def={DEF}
compact
/>
</section>
</div>
</footer>
</div>
</main>
@@ -4,6 +4,7 @@
import '../styles/backtestDashboard.css';
import '../styles/virtualTradingDashboard.css';
import '../styles/strategyEditorTheme.css';
import '../styles/strategyEditor.css';
import '../styles/strategyEvaluation.css';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { Theme, Timeframe } from '../types';
@@ -20,6 +21,7 @@ import { resolveStrategyPrimaryTimeframe } from '../utils/strategyToChartIndicat
import { fetchEvaluationSnapshotAtBar } from '../utils/strategyEvaluationSnapshot';
import StrategyEvaluationSignalPanel from './strategyEvaluation/StrategyEvaluationSignalPanel';
import StrategyEvaluationChart from './strategyEvaluation/StrategyEvaluationChart';
import StrategyEvaluationConditionPanel from './strategyEvaluation/StrategyEvaluationConditionPanel';
import StrategyEvaluationSettingsTab from './strategyEvaluation/StrategyEvaluationSettingsTab';
import StrategyEditorModal from './strategyEvaluation/StrategyEditorModal';
import SePanelCollapseHandle from './strategyEditor/SePanelCollapseHandle';
@@ -484,6 +486,11 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
evalLoading={evalLoading}
onSignalsChange={handleSignalsChange}
/>
<StrategyEvaluationConditionPanel
strategy={selectedStrategy}
getParams={baseGetParams}
appliedParams={appliedIndicatorParams}
/>
</div>
</main>
@@ -0,0 +1,104 @@
/**
* LOGIC EXPRESSION 아래 — 조건을 알기 쉬운 문장으로 설명
*/
import React, { useMemo } from 'react';
import type { LogicNode } from '../../utils/strategyTypes';
import type { DefType } from '../../utils/strategyEditorShared';
import {
buildStrategyNarrative,
type StrategyDescriptionInput,
} from '../../utils/strategyDescriptionNarrative';
import type { EditorConditionState } from '../../utils/strategyConditionSerde';
import { emptyEditorConditionState } from '../../utils/strategyConditionSerde';
interface Props {
name?: string;
description?: string;
buyCondition: LogicNode | null;
sellCondition: LogicNode | null;
buyEditorState?: EditorConditionState;
sellEditorState?: EditorConditionState;
orphanCount?: number;
def: DefType;
/** 터미널·하단 패널 등 좁은 공간 — 서두·각주 생략, 핵심 bullet 위주 */
compact?: boolean;
className?: string;
}
export default function LogicExpressionNarrative({
name,
description,
buyCondition,
sellCondition,
buyEditorState,
sellEditorState,
orphanCount,
def,
compact = false,
className,
}: Props) {
const input: StrategyDescriptionInput = useMemo(() => ({
name,
description,
buyEditorState: buyEditorState ?? emptyEditorConditionState(),
sellEditorState: sellEditorState ?? emptyEditorConditionState(),
buyCondition,
sellCondition,
orphanCount,
def,
}), [
name,
description,
buyEditorState,
sellEditorState,
buyCondition,
sellCondition,
orphanCount,
def,
]);
const narrative = useMemo(() => buildStrategyNarrative(input), [input]);
const rootClass = ['se-narrative', compact ? 'se-narrative--compact' : '', className]
.filter(Boolean)
.join(' ');
return (
<div className={rootClass}>
{!compact && narrative.intro.map((p, i) => (
<p key={`intro-${i}`} className="se-narrative-intro">{p}</p>
))}
{narrative.sections.map(section => (
<section key={section.title} className="se-narrative-section">
<h4 className="se-narrative-section-title">{section.title}</h4>
{!compact && section.paragraphs.map((p, i) => (
<p key={`${section.title}-p-${i}`} className="se-narrative-para">{p}</p>
))}
{compact && section.paragraphs.length > 0 && (
<p className="se-narrative-para se-narrative-para--lead">
{section.paragraphs[section.paragraphs.length - 1]}
</p>
)}
{section.bullets && section.bullets.length > 0 && (
<ul className="se-narrative-list">
{section.bullets.map((line, i) => (
<li key={`${section.title}-b-${i}`} className="se-narrative-list-item">{line}</li>
))}
</ul>
)}
</section>
))}
{narrative.footnotes.length > 0 && (
<footer className="se-narrative-footnotes">
{narrative.footnotes.map((note, i) => (
<p key={`fn-${i}`} className="se-narrative-footnote">
{compact ? note : `${note}`}
</p>
))}
</footer>
)}
</div>
);
}
@@ -0,0 +1,92 @@
/**
* 전략 평가 — 중앙 하단 LOGIC EXPRESSION + 조건 설명
*/
import React, { useCallback, useMemo } from 'react';
import type { StrategyDto } from '../../utils/backendApi';
import { asLogicNode } from '../../utils/strategyHydrate';
import { decodeConditionForEditor } from '../../utils/strategyConditionSerde';
import { buildDefFromGetParams } from '../../utils/strategyEditorShared';
import LogicExpressionPreview from '../strategyEditor/LogicExpressionPreview';
import LogicExpressionNarrative from '../strategyEditor/LogicExpressionNarrative';
import type { EvalIndicatorParams } from '../../utils/strategyEvaluationParams';
interface Props {
strategy: StrategyDto | null;
getParams: (type: string) => Record<string, number | string | boolean>;
appliedParams: EvalIndicatorParams | null;
}
const StrategyEvaluationConditionPanel: React.FC<Props> = ({
strategy,
getParams,
appliedParams,
}) => {
const resolveGetParams = useCallback(
(type: string, defaults?: Record<string, number | string | boolean>) => {
const base = getParams(type);
const merged = defaults ? { ...defaults, ...base } : base;
const override = appliedParams?.[type];
return override ? { ...merged, ...override } : merged;
},
[getParams, appliedParams],
);
const def = useMemo(() => buildDefFromGetParams(resolveGetParams), [resolveGetParams]);
const buyCondition = useMemo(
() => (strategy ? asLogicNode(strategy.buyCondition) : null),
[strategy?.buyCondition],
);
const sellCondition = useMemo(
() => (strategy ? asLogicNode(strategy.sellCondition) : null),
[strategy?.sellCondition],
);
const buyEditorState = useMemo(
() => decodeConditionForEditor(buyCondition),
[buyCondition],
);
const sellEditorState = useMemo(
() => decodeConditionForEditor(sellCondition),
[sellCondition],
);
if (!strategy || (!buyCondition && !sellCondition)) {
return (
<div className="seval-condition-panel seval-condition-panel--empty">
<p className="seval-condition-panel-empty"> .</p>
</div>
);
}
return (
<div className="seval-condition-panel">
<section className="seval-condition-panel-section">
<div className="seval-condition-panel-label">LOGIC EXPRESSION</div>
<LogicExpressionPreview
buyCondition={buyCondition}
sellCondition={sellCondition}
buyEditorState={buyEditorState}
sellEditorState={sellEditorState}
orphanCount={0}
def={def}
/>
</section>
<section className="seval-condition-panel-section seval-condition-panel-section--narrative">
<div className="seval-condition-panel-label seval-condition-panel-label--guide"> </div>
<LogicExpressionNarrative
name={strategy.name}
description={strategy.description}
buyCondition={buyCondition}
sellCondition={sellCondition}
buyEditorState={buyEditorState}
sellEditorState={sellEditorState}
def={def}
compact
/>
</section>
</div>
);
};
export default StrategyEvaluationConditionPanel;