전략 설명 추가
This commit is contained in:
@@ -107,6 +107,7 @@ import {
|
|||||||
type StrategyFlowLayoutStore,
|
type StrategyFlowLayoutStore,
|
||||||
} from '../utils/strategyEditorLayoutStorage';
|
} from '../utils/strategyEditorLayoutStorage';
|
||||||
import LogicExpressionPreview from './strategyEditor/LogicExpressionPreview';
|
import LogicExpressionPreview from './strategyEditor/LogicExpressionPreview';
|
||||||
|
import LogicExpressionNarrative from './strategyEditor/LogicExpressionNarrative';
|
||||||
import StrategyEditorCanvas, { type FlowLayoutSeed } from './strategyEditor/StrategyEditorCanvas';
|
import StrategyEditorCanvas, { type FlowLayoutSeed } from './strategyEditor/StrategyEditorCanvas';
|
||||||
import StrategyListEditor from './strategyEditor/StrategyListEditor';
|
import StrategyListEditor from './strategyEditor/StrategyListEditor';
|
||||||
import StartCombineOpControl from './strategyEditor/StartCombineOpControl';
|
import StartCombineOpControl from './strategyEditor/StartCombineOpControl';
|
||||||
@@ -178,7 +179,7 @@ const RIGHT_PANEL_MAX = 560;
|
|||||||
const RIGHT_PANEL_DEFAULT = 380;
|
const RIGHT_PANEL_DEFAULT = 380;
|
||||||
const TERMINAL_MIN = 88;
|
const TERMINAL_MIN = 88;
|
||||||
const TERMINAL_MAX = 420;
|
const TERMINAL_MAX = 420;
|
||||||
const TERMINAL_DEFAULT = 140;
|
const TERMINAL_DEFAULT = 220;
|
||||||
|
|
||||||
function readRightPanelWidth(): number {
|
function readRightPanelWidth(): number {
|
||||||
return clampPanelSize(
|
return clampPanelSize(
|
||||||
@@ -2088,15 +2089,33 @@ export default function StrategyEditorPage({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<footer className="se-terminal" style={{ height: terminalHeight }}>
|
<footer className="se-terminal" style={{ height: terminalHeight }}>
|
||||||
<div className="se-terminal-label">LOGIC EXPRESSION</div>
|
<div className="se-terminal-scroll">
|
||||||
<LogicExpressionPreview
|
<section className="se-terminal-section">
|
||||||
buyCondition={buyCondition}
|
<div className="se-terminal-label">LOGIC EXPRESSION</div>
|
||||||
sellCondition={sellCondition}
|
<LogicExpressionPreview
|
||||||
buyEditorState={buyEditorState}
|
buyCondition={buyCondition}
|
||||||
sellEditorState={sellEditorState}
|
sellCondition={sellCondition}
|
||||||
orphanCount={orphanTotal}
|
buyEditorState={buyEditorState}
|
||||||
def={DEF}
|
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>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import '../styles/backtestDashboard.css';
|
import '../styles/backtestDashboard.css';
|
||||||
import '../styles/virtualTradingDashboard.css';
|
import '../styles/virtualTradingDashboard.css';
|
||||||
import '../styles/strategyEditorTheme.css';
|
import '../styles/strategyEditorTheme.css';
|
||||||
|
import '../styles/strategyEditor.css';
|
||||||
import '../styles/strategyEvaluation.css';
|
import '../styles/strategyEvaluation.css';
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import type { Theme, Timeframe } from '../types';
|
import type { Theme, Timeframe } from '../types';
|
||||||
@@ -20,6 +21,7 @@ import { resolveStrategyPrimaryTimeframe } from '../utils/strategyToChartIndicat
|
|||||||
import { fetchEvaluationSnapshotAtBar } from '../utils/strategyEvaluationSnapshot';
|
import { fetchEvaluationSnapshotAtBar } from '../utils/strategyEvaluationSnapshot';
|
||||||
import StrategyEvaluationSignalPanel from './strategyEvaluation/StrategyEvaluationSignalPanel';
|
import StrategyEvaluationSignalPanel from './strategyEvaluation/StrategyEvaluationSignalPanel';
|
||||||
import StrategyEvaluationChart from './strategyEvaluation/StrategyEvaluationChart';
|
import StrategyEvaluationChart from './strategyEvaluation/StrategyEvaluationChart';
|
||||||
|
import StrategyEvaluationConditionPanel from './strategyEvaluation/StrategyEvaluationConditionPanel';
|
||||||
import StrategyEvaluationSettingsTab from './strategyEvaluation/StrategyEvaluationSettingsTab';
|
import StrategyEvaluationSettingsTab from './strategyEvaluation/StrategyEvaluationSettingsTab';
|
||||||
import StrategyEditorModal from './strategyEvaluation/StrategyEditorModal';
|
import StrategyEditorModal from './strategyEvaluation/StrategyEditorModal';
|
||||||
import SePanelCollapseHandle from './strategyEditor/SePanelCollapseHandle';
|
import SePanelCollapseHandle from './strategyEditor/SePanelCollapseHandle';
|
||||||
@@ -484,6 +486,11 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
|||||||
evalLoading={evalLoading}
|
evalLoading={evalLoading}
|
||||||
onSignalsChange={handleSignalsChange}
|
onSignalsChange={handleSignalsChange}
|
||||||
/>
|
/>
|
||||||
|
<StrategyEvaluationConditionPanel
|
||||||
|
strategy={selectedStrategy}
|
||||||
|
getParams={baseGetParams}
|
||||||
|
appliedParams={appliedIndicatorParams}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</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;
|
||||||
@@ -1552,7 +1552,18 @@
|
|||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
background: var(--se-terminal-bg);
|
background: var(--se-terminal-bg);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
min-height: 88px;
|
min-height: 120px;
|
||||||
|
}
|
||||||
|
.se-terminal-scroll {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.se-terminal-section + .se-terminal-section {
|
||||||
|
border-top: 1px solid var(--se-border);
|
||||||
|
}
|
||||||
|
.se-terminal-section--narrative {
|
||||||
|
background: color-mix(in srgb, var(--se-terminal-bg) 88%, var(--se-gold) 12%);
|
||||||
}
|
}
|
||||||
.se-terminal-label {
|
.se-terminal-label {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
@@ -1563,6 +1574,9 @@
|
|||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
border-bottom: 1px solid var(--se-border);
|
border-bottom: 1px solid var(--se-border);
|
||||||
}
|
}
|
||||||
|
.se-terminal-label--guide {
|
||||||
|
color: color-mix(in srgb, var(--se-gold) 85%, var(--se-text) 15%);
|
||||||
|
}
|
||||||
.se-terminal-text {
|
.se-terminal-text {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
@@ -2626,3 +2640,64 @@ body.se-palette-drag-armed .se-palette-section--scroll {
|
|||||||
color: var(--se-text-muted);
|
color: var(--se-text-muted);
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Terminal narrative (조건 설명) ── */
|
||||||
|
.se-narrative {
|
||||||
|
padding: 10px 14px 12px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
line-height: 1.55;
|
||||||
|
color: var(--se-text);
|
||||||
|
}
|
||||||
|
.se-narrative--compact {
|
||||||
|
padding: 8px 14px 10px;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
}
|
||||||
|
.se-narrative-intro {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
color: var(--se-text-muted);
|
||||||
|
}
|
||||||
|
.se-narrative-section + .se-narrative-section {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding-top: 8px;
|
||||||
|
border-top: 1px dashed var(--se-border);
|
||||||
|
}
|
||||||
|
.se-narrative-section-title {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: var(--se-gold);
|
||||||
|
}
|
||||||
|
.se-narrative-para {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
color: var(--se-text-muted);
|
||||||
|
}
|
||||||
|
.se-narrative-para--lead {
|
||||||
|
font-size: 0.76rem;
|
||||||
|
}
|
||||||
|
.se-narrative-list {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
padding: 0 0 0 1.1em;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
.se-narrative-list-item {
|
||||||
|
margin: 3px 0;
|
||||||
|
padding-left: 2px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: keep-all;
|
||||||
|
color: var(--se-text);
|
||||||
|
}
|
||||||
|
.se-narrative-footnotes {
|
||||||
|
margin-top: 8px;
|
||||||
|
padding-top: 6px;
|
||||||
|
border-top: 1px dashed var(--se-border);
|
||||||
|
}
|
||||||
|
.se-narrative-footnote {
|
||||||
|
margin: 3px 0 0;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: var(--se-text-muted);
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
.se-narrative--compact .se-narrative-footnotes {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|||||||
@@ -428,6 +428,61 @@
|
|||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── 중앙 하단 — LOGIC EXPRESSION + 조건 설명 ── */
|
||||||
|
.seval-condition-panel {
|
||||||
|
flex-shrink: 0;
|
||||||
|
max-height: min(38vh, 320px);
|
||||||
|
margin-top: 6px;
|
||||||
|
border: 1px solid var(--se-terminal-border, rgba(255, 255, 255, 0.08));
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--se-terminal-bg, color-mix(in srgb, var(--bg) 92%, #000));
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.seval-condition-panel--empty {
|
||||||
|
max-height: none;
|
||||||
|
padding: 12px 14px;
|
||||||
|
}
|
||||||
|
.seval-condition-panel-empty {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--se-text-muted);
|
||||||
|
}
|
||||||
|
.seval-condition-panel-section {
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.seval-condition-panel-section--narrative {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
border-top: 1px solid var(--se-border);
|
||||||
|
background: color-mix(in srgb, var(--se-terminal-bg, var(--bg)) 88%, var(--se-gold, #c9a227) 12%);
|
||||||
|
}
|
||||||
|
.seval-condition-panel-label {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
font-size: 0.62rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
color: var(--se-terminal-label, var(--se-text-muted));
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-bottom: 1px solid var(--se-border);
|
||||||
|
background: inherit;
|
||||||
|
}
|
||||||
|
.seval-condition-panel-label--guide {
|
||||||
|
color: color-mix(in srgb, var(--se-gold, #c9a227) 85%, var(--se-text) 15%);
|
||||||
|
}
|
||||||
|
.seval-condition-panel .se-terminal-text {
|
||||||
|
max-height: 120px;
|
||||||
|
}
|
||||||
|
.seval-condition-panel .se-narrative--compact {
|
||||||
|
max-height: 160px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── 세로 선택바 ─────────────────────────────────────────────────────────── */
|
/* ── 세로 선택바 ─────────────────────────────────────────────────────────── */
|
||||||
.seval-bar-selector-layer {
|
.seval-bar-selector-layer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
Reference in New Issue
Block a user