위젯항목 추가
This commit is contained in:
@@ -59,6 +59,8 @@ interface Props {
|
|||||||
reportDisabled?: boolean;
|
reportDisabled?: boolean;
|
||||||
/** 거래 로그 클릭 시 해당 시점으로 차트 이동 */
|
/** 거래 로그 클릭 시 해당 시점으로 차트 이동 */
|
||||||
focusTimeSec?: number | null;
|
focusTimeSec?: number | null;
|
||||||
|
/** 하단 확대·축소·좌우 이동 툴바 (위젯 등에서는 false) */
|
||||||
|
showHoverToolbar?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const noop = () => {};
|
const noop = () => {};
|
||||||
@@ -114,6 +116,7 @@ const BacktestAnalysisChart: React.FC<Props> = ({
|
|||||||
onOpenReport,
|
onOpenReport,
|
||||||
reportDisabled = false,
|
reportDisabled = false,
|
||||||
focusTimeSec = null,
|
focusTimeSec = null,
|
||||||
|
showHoverToolbar = true,
|
||||||
}) => {
|
}) => {
|
||||||
const { getParams, getVisualConfig } = useIndicatorSettings();
|
const { getParams, getVisualConfig } = useIndicatorSettings();
|
||||||
const { defaults: appDefaults } = useAppSettings();
|
const { defaults: appDefaults } = useAppSettings();
|
||||||
@@ -474,7 +477,7 @@ const BacktestAnalysisChart: React.FC<Props> = ({
|
|||||||
candleAreaPriceLabelsEnabled={priceLabels}
|
candleAreaPriceLabelsEnabled={priceLabels}
|
||||||
indicatorAreaPriceLabelsEnabled={priceLabels}
|
indicatorAreaPriceLabelsEnabled={priceLabels}
|
||||||
seriesPriceLabelsEnabled={priceLabels}
|
seriesPriceLabelsEnabled={priceLabels}
|
||||||
showHoverToolbar
|
showHoverToolbar={showHoverToolbar}
|
||||||
candleOverlayToggles={candleOverlayToggles}
|
candleOverlayToggles={candleOverlayToggles}
|
||||||
onToggleCandleOverlay={handleToggleCandleOverlay}
|
onToggleCandleOverlay={handleToggleCandleOverlay}
|
||||||
showCandleOverlayControls
|
showCandleOverlayControls
|
||||||
|
|||||||
@@ -26,13 +26,17 @@ interface Props {
|
|||||||
market: string;
|
market: string;
|
||||||
strategyId?: number | null;
|
strategyId?: number | null;
|
||||||
theme?: Theme;
|
theme?: Theme;
|
||||||
|
/** 위젯 임베드 시 하단 줌/패닝 툴바 숨김 */
|
||||||
|
showHoverToolbar?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 모의투자 차트 탭 — 전략 백테스트 시그널 + 캔들·보조지표 (백테스팅 분석 차트와 동일)
|
* 모의투자 차트 탭 — 전략 백테스트 시그널 + 캔들·보조지표 (백테스팅 분석 차트와 동일)
|
||||||
* 좌측(과거) 스크롤 시 추가 캔들·보조지표 로드 후 전체 구간 시그널 재계산.
|
* 좌측(과거) 스크롤 시 추가 캔들·보조지표 로드 후 전체 구간 시그널 재계산.
|
||||||
*/
|
*/
|
||||||
const PaperAnalysisChart: React.FC<Props> = ({ market, strategyId, theme = 'dark' }) => {
|
const PaperAnalysisChart: React.FC<Props> = ({
|
||||||
|
market, strategyId, theme = 'dark', showHoverToolbar = true,
|
||||||
|
}) => {
|
||||||
const { getParams } = useIndicatorSettings();
|
const { getParams } = useIndicatorSettings();
|
||||||
const [signals, setSignals] = useState<BacktestSignal[]>([]);
|
const [signals, setSignals] = useState<BacktestSignal[]>([]);
|
||||||
const [strategy, setStrategy] = useState<StrategyDto | undefined>();
|
const [strategy, setStrategy] = useState<StrategyDto | undefined>();
|
||||||
@@ -214,6 +218,7 @@ const PaperAnalysisChart: React.FC<Props> = ({ market, strategyId, theme = 'dark
|
|||||||
onHistoryLoadingChange={setHistoryLoading}
|
onHistoryLoadingChange={setHistoryLoading}
|
||||||
overlayVisibility={overlayVisibility}
|
overlayVisibility={overlayVisibility}
|
||||||
onToggleOverlay={handleToggleOverlay}
|
onToggleOverlay={handleToggleOverlay}
|
||||||
|
showHoverToolbar={showHoverToolbar}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -452,6 +452,10 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fw-window-cell .ptd-analysis-chart .chart-hover-toolbar {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
.fw-window-cell .wd-panel-body {
|
.fw-window-cell .wd-panel-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
@@ -464,6 +468,29 @@
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fw-window-cell .wd-widget-host--analysis {
|
||||||
|
overflow: auto;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fw-window-cell .wd-widget-host--signal-summary {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fw-window-cell .wd-widget-host--signal-match .seval-signal-split {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fw-window-cell .wd-widget-host--backtest-signals {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fw-window-cell .wd-widget-host--backtest-signals .brd-sig-scroll {
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* 레이아웃 선택 팝업 */
|
/* 레이아웃 선택 팝업 */
|
||||||
.fw-layout-picker-grid {
|
.fw-layout-picker-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
@@ -1325,6 +1325,11 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 위젯 — 전략 분석 차트 하단 줌/패닝 툴바 숨김 */
|
||||||
|
.wd-widget-host--chart .ptd-analysis-chart .chart-hover-toolbar {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* 미니 차트 */
|
/* 미니 차트 */
|
||||||
.bps-page--wd .wd-mini-chart-wrap {
|
.bps-page--wd .wd-mini-chart-wrap {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
@@ -1390,3 +1395,56 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 매매 시그널·분석 위젯 */
|
||||||
|
.wd-widget-host--analysis {
|
||||||
|
overflow: auto;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 8px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wd-widget-host--signal-summary {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wd-widget-host--signal-summary .vtd-card {
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wd-widget-host--signal-match .seval-signal-split {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wd-widget-host--strategy-condition .scv-root {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wd-widget-host--backtest-signals {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wd-widget-host--backtest-signals .wd-backtest-sig-table {
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wd-widget-host--backtest-signals .brd-sig-scroll {
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wd-widget-host--indicator-status .vtd-sig-panel-wrap--detail {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bps-page--wd .wd-widget-host--analysis {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wd-widget-empty--loading {
|
||||||
|
color: var(--text3, var(--se-text-muted, #6272a4));
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import BacktestSignalTable from '../../components/backtest/BacktestSignalTable';
|
||||||
|
import { useWidgetDashboard } from '../WidgetDashboardContext';
|
||||||
|
import { useWidgetSelectedStrategy } from '../hooks/useWidgetSelectedStrategy';
|
||||||
|
import { useWidgetBacktestSignals } from '../hooks/useWidgetBacktestSignals';
|
||||||
|
import '../../styles/backtestDashboard.css';
|
||||||
|
|
||||||
|
/** 백테스트 매매 시그널 목록 — BacktestSignalTable */
|
||||||
|
const BacktestSignalsWidgetHost: React.FC<{ config?: Record<string, unknown> }> = ({ config }) => {
|
||||||
|
const { selectedMarket } = useWidgetDashboard();
|
||||||
|
const { strategyId, strategy } = useWidgetSelectedStrategy(config?.strategyId);
|
||||||
|
const { signals, loading, error } = useWidgetBacktestSignals(selectedMarket, strategyId, strategy);
|
||||||
|
|
||||||
|
if (!strategyId) {
|
||||||
|
return <p className="wd-widget-empty">전략을 선택하세요.</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading && signals.length === 0 && !error) {
|
||||||
|
return <p className="wd-widget-empty wd-widget-empty--loading">시그널 계산 중…</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error && signals.length === 0) {
|
||||||
|
return <p className="wd-widget-empty">{error}</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="wd-widget-host wd-widget-host--analysis wd-widget-host--backtest-signals">
|
||||||
|
<BacktestSignalTable signals={signals} expanded className="wd-backtest-sig-table" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BacktestSignalsWidgetHost;
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import VirtualTargetSignalPanel from '../../components/virtual/VirtualTargetSignalPanel';
|
||||||
|
import { useTradeNotificationSignalSnapshot } from '../../hooks/useTradeNotificationSignalSnapshot';
|
||||||
|
import { useWidgetDashboard } from '../WidgetDashboardContext';
|
||||||
|
import { useWidgetSelectedStrategy } from '../hooks/useWidgetSelectedStrategy';
|
||||||
|
import '../../styles/virtualTradingDashboard.css';
|
||||||
|
|
||||||
|
/** 지표별 충족 상태 — VirtualTargetSignalPanel detail */
|
||||||
|
const IndicatorStatusWidgetHost: React.FC<{ config?: Record<string, unknown> }> = ({ config }) => {
|
||||||
|
const { selectedMarket } = useWidgetDashboard();
|
||||||
|
const { strategyId, strategy, strategies, strategyLabel } = useWidgetSelectedStrategy(config?.strategyId);
|
||||||
|
|
||||||
|
const { snapshot, loading } = useTradeNotificationSignalSnapshot(
|
||||||
|
selectedMarket,
|
||||||
|
strategyId,
|
||||||
|
strategy,
|
||||||
|
strategyLabel,
|
||||||
|
strategyId != null,
|
||||||
|
strategies,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!strategyId) {
|
||||||
|
return <p className="wd-widget-empty">전략을 선택하세요.</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="wd-widget-host wd-widget-host--analysis wd-widget-host--indicator-status">
|
||||||
|
<VirtualTargetSignalPanel
|
||||||
|
snapshot={snapshot}
|
||||||
|
viewMode="detail"
|
||||||
|
loading={loading}
|
||||||
|
hasStrategy
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IndicatorStatusWidgetHost;
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import StrategyEvaluationSignalPanel from '../../components/strategyEvaluation/StrategyEvaluationSignalPanel';
|
||||||
|
import { useTradeNotificationSignalSnapshot } from '../../hooks/useTradeNotificationSignalSnapshot';
|
||||||
|
import { useIndicatorSettings } from '../../hooks/useIndicatorSettings';
|
||||||
|
import { useWidgetDashboard } from '../WidgetDashboardContext';
|
||||||
|
import { useWidgetSelectedStrategy } from '../hooks/useWidgetSelectedStrategy';
|
||||||
|
import '../../styles/strategyEvaluation.css';
|
||||||
|
import '../../styles/virtualTradingDashboard.css';
|
||||||
|
|
||||||
|
/** 매수·매도 조건 일치율 — StrategyEvaluationSignalPanel */
|
||||||
|
const SignalMatchWidgetHost: React.FC<{ config?: Record<string, unknown> }> = ({ config }) => {
|
||||||
|
const { selectedMarket } = useWidgetDashboard();
|
||||||
|
const { strategyId, strategy, strategies, strategyLabel } = useWidgetSelectedStrategy(config?.strategyId);
|
||||||
|
const { getParams } = useIndicatorSettings();
|
||||||
|
|
||||||
|
const { snapshot, loading } = useTradeNotificationSignalSnapshot(
|
||||||
|
selectedMarket,
|
||||||
|
strategyId,
|
||||||
|
strategy,
|
||||||
|
strategyLabel,
|
||||||
|
strategyId != null,
|
||||||
|
strategies,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!strategyId) {
|
||||||
|
return <p className="wd-widget-empty">전략을 선택하세요.</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="wd-widget-host wd-widget-host--analysis wd-widget-host--signal-match">
|
||||||
|
<StrategyEvaluationSignalPanel
|
||||||
|
snapshot={snapshot}
|
||||||
|
strategy={strategy}
|
||||||
|
loading={loading}
|
||||||
|
hasStrategy
|
||||||
|
getParams={getParams}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SignalMatchWidgetHost;
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import TradeNotificationSignalSummary from '../../components/tradeNotification/TradeNotificationSignalSummary';
|
||||||
|
import { useWidgetDashboard } from '../WidgetDashboardContext';
|
||||||
|
import { useWidgetSelectedStrategy } from '../hooks/useWidgetSelectedStrategy';
|
||||||
|
import '../../styles/virtualTradingDashboard.css';
|
||||||
|
|
||||||
|
/** 실시간 매매 시그널 요약 — VirtualTargetCard 요약형 */
|
||||||
|
const SignalSummaryWidgetHost: React.FC<{ config?: Record<string, unknown> }> = ({ config }) => {
|
||||||
|
const { theme, selectedMarket, tickers, strategies } = useWidgetDashboard();
|
||||||
|
const { strategyId, strategy, strategyLabel } = useWidgetSelectedStrategy(config?.strategyId);
|
||||||
|
|
||||||
|
if (!strategyId) {
|
||||||
|
return <p className="wd-widget-empty">전략을 선택하세요.</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`wd-widget-host wd-widget-host--analysis wd-widget-host--signal-summary se-page se-page--${theme}`}>
|
||||||
|
<TradeNotificationSignalSummary
|
||||||
|
market={selectedMarket}
|
||||||
|
strategyId={strategyId}
|
||||||
|
strategy={strategy}
|
||||||
|
strategies={strategies}
|
||||||
|
theme={theme}
|
||||||
|
ticker={tickers.get(selectedMarket)}
|
||||||
|
strategyLabel={strategyLabel}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SignalSummaryWidgetHost;
|
||||||
@@ -22,6 +22,7 @@ const StrategyChartWidgetHost: React.FC<{ config?: Record<string, unknown> }> =
|
|||||||
market={selectedMarket}
|
market={selectedMarket}
|
||||||
strategyId={strategyId}
|
strategyId={strategyId}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
|
showHoverToolbar={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import StrategyConditionView from '../../components/tradeNotification/StrategyConditionView';
|
||||||
|
import { useWidgetSelectedStrategy } from '../hooks/useWidgetSelectedStrategy';
|
||||||
|
import '../../styles/appPopup.css';
|
||||||
|
|
||||||
|
/** 전략 매수·매도 조건 트리 — StrategyConditionView */
|
||||||
|
const StrategyConditionWidgetHost: React.FC<{ config?: Record<string, unknown> }> = ({ config }) => {
|
||||||
|
const { strategyId, strategy } = useWidgetSelectedStrategy(config?.strategyId);
|
||||||
|
|
||||||
|
if (!strategyId) {
|
||||||
|
return <p className="wd-widget-empty">전략을 선택하세요.</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!strategy) {
|
||||||
|
return <p className="wd-widget-empty wd-widget-empty--loading">전략 조건 불러오는 중…</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!strategy.buyCondition && !strategy.sellCondition) {
|
||||||
|
return <p className="wd-widget-empty">등록된 매수·매도 조건이 없습니다.</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="wd-widget-host wd-widget-host--analysis wd-widget-host--strategy-condition">
|
||||||
|
<StrategyConditionView strategy={strategy} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StrategyConditionWidgetHost;
|
||||||
@@ -22,6 +22,11 @@ import NotificationListWidgetHost from './hosts/NotificationListWidgetHost';
|
|||||||
import StrategyListWidgetHost from './hosts/StrategyListWidgetHost';
|
import StrategyListWidgetHost from './hosts/StrategyListWidgetHost';
|
||||||
import BacktestListWidgetHost from './hosts/BacktestListWidgetHost';
|
import BacktestListWidgetHost from './hosts/BacktestListWidgetHost';
|
||||||
import PaperKpiWidgetHost from './hosts/PaperKpiWidgetHost';
|
import PaperKpiWidgetHost from './hosts/PaperKpiWidgetHost';
|
||||||
|
import SignalSummaryWidgetHost from './hosts/SignalSummaryWidgetHost';
|
||||||
|
import SignalMatchWidgetHost from './hosts/SignalMatchWidgetHost';
|
||||||
|
import StrategyConditionWidgetHost from './hosts/StrategyConditionWidgetHost';
|
||||||
|
import BacktestSignalsWidgetHost from './hosts/BacktestSignalsWidgetHost';
|
||||||
|
import IndicatorStatusWidgetHost from './hosts/IndicatorStatusWidgetHost';
|
||||||
|
|
||||||
export type WidgetGroupId =
|
export type WidgetGroupId =
|
||||||
| 'chart'
|
| 'chart'
|
||||||
@@ -29,6 +34,7 @@ export type WidgetGroupId =
|
|||||||
| 'trade'
|
| 'trade'
|
||||||
| 'portfolio'
|
| 'portfolio'
|
||||||
| 'strategy'
|
| 'strategy'
|
||||||
|
| 'analysis'
|
||||||
| 'alert';
|
| 'alert';
|
||||||
|
|
||||||
export interface WidgetDefinition {
|
export interface WidgetDefinition {
|
||||||
@@ -47,6 +53,7 @@ export const WIDGET_GROUP_LABELS: Record<WidgetGroupId, string> = {
|
|||||||
trade: '매매·호가',
|
trade: '매매·호가',
|
||||||
portfolio: '계좌·포트폴리오',
|
portfolio: '계좌·포트폴리오',
|
||||||
strategy: '전략·분석',
|
strategy: '전략·분석',
|
||||||
|
analysis: '매매 시그널',
|
||||||
alert: '알림',
|
alert: '알림',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -227,6 +234,46 @@ export const WIDGET_REGISTRY: WidgetDefinition[] = [
|
|||||||
group: 'strategy',
|
group: 'strategy',
|
||||||
Host: BacktestListWidgetHost,
|
Host: BacktestListWidgetHost,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'analysis.signalSummary',
|
||||||
|
label: '매매 시그널 요약',
|
||||||
|
description: '선택 종목·전략 실시간 신호 카드',
|
||||||
|
category: 'side',
|
||||||
|
group: 'analysis',
|
||||||
|
Host: SignalSummaryWidgetHost,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'analysis.signalMatch',
|
||||||
|
label: '조건 일치율',
|
||||||
|
description: '매수·매도 조건별 지표 충족률',
|
||||||
|
category: 'side',
|
||||||
|
group: 'analysis',
|
||||||
|
Host: SignalMatchWidgetHost,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'analysis.strategyCondition',
|
||||||
|
label: '전략 조건',
|
||||||
|
description: '매수·매도 DSL 조건 트리',
|
||||||
|
category: 'side',
|
||||||
|
group: 'analysis',
|
||||||
|
Host: StrategyConditionWidgetHost,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'analysis.backtestSignals',
|
||||||
|
label: '백테스트 시그널',
|
||||||
|
description: '최근 캔들 구간 매매 시그널 목록',
|
||||||
|
category: 'side',
|
||||||
|
group: 'analysis',
|
||||||
|
Host: BacktestSignalsWidgetHost,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'analysis.indicatorDetail',
|
||||||
|
label: '지표별 상태',
|
||||||
|
description: '조건별 현재값·충족 여부 상세',
|
||||||
|
category: 'side',
|
||||||
|
group: 'analysis',
|
||||||
|
Host: IndicatorStatusWidgetHost,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const byType = new Map(WIDGET_REGISTRY.map(w => [w.type, w]));
|
const byType = new Map(WIDGET_REGISTRY.map(w => [w.type, w]));
|
||||||
@@ -244,7 +291,7 @@ export function widgetsByGroup(category: WidgetCategory): { group: WidgetGroupId
|
|||||||
const items = widgetsForCategory(category);
|
const items = widgetsForCategory(category);
|
||||||
const order: WidgetGroupId[] = category === 'center'
|
const order: WidgetGroupId[] = category === 'center'
|
||||||
? ['chart']
|
? ['chart']
|
||||||
: ['market', 'trade', 'portfolio', 'strategy', 'alert'];
|
: ['market', 'trade', 'portfolio', 'strategy', 'analysis', 'alert'];
|
||||||
return order
|
return order
|
||||||
.map(group => ({
|
.map(group => ({
|
||||||
group,
|
group,
|
||||||
|
|||||||
Reference in New Issue
Block a user