전략 설명 추가
This commit is contained in:
@@ -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,6 +2089,8 @@ export default function StrategyEditorPage({
|
||||
/>
|
||||
|
||||
<footer className="se-terminal" style={{ height: terminalHeight }}>
|
||||
<div className="se-terminal-scroll">
|
||||
<section className="se-terminal-section">
|
||||
<div className="se-terminal-label">LOGIC EXPRESSION</div>
|
||||
<LogicExpressionPreview
|
||||
buyCondition={buyCondition}
|
||||
@@ -2097,6 +2100,22 @@ export default function StrategyEditorPage({
|
||||
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;
|
||||
@@ -1552,7 +1552,18 @@
|
||||
border-radius: 0;
|
||||
background: var(--se-terminal-bg);
|
||||
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 {
|
||||
flex-shrink: 0;
|
||||
@@ -1563,6 +1574,9 @@
|
||||
padding: 6px 12px;
|
||||
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 {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
@@ -2626,3 +2640,64 @@ body.se-palette-drag-armed .se-palette-section--scroll {
|
||||
color: var(--se-text-muted);
|
||||
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;
|
||||
}
|
||||
|
||||
/* ── 중앙 하단 — 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 {
|
||||
position: absolute;
|
||||
|
||||
Reference in New Issue
Block a user