전략평가 화면 수정
This commit is contained in:
@@ -4653,6 +4653,67 @@ export class ChartManager {
|
||||
return t === null ? null : (t as number);
|
||||
}
|
||||
|
||||
/**
|
||||
* 전략 평가 — 클라이언트 좌표 → rawBars 내 가장 가까운 봉 index.
|
||||
* 캔들·보조지표 pane 클릭 허용, 가격축·시간축·플롯 밖 제외.
|
||||
*/
|
||||
resolveNearestBarIndexAtClientPoint(clientX: number, clientY: number): number {
|
||||
const rect = this.container.getBoundingClientRect();
|
||||
const chartX = clientX - rect.left;
|
||||
const chartY = clientY - rect.top;
|
||||
|
||||
if (
|
||||
chartX < 0 || chartY < 0
|
||||
|| chartX > this.container.clientWidth
|
||||
|| chartY > this.container.clientHeight
|
||||
) {
|
||||
return -1;
|
||||
}
|
||||
if (this.isOnPriceAxis(chartX, chartY) || this.isOnTimeAxis(chartY)) return -1;
|
||||
|
||||
const layouts = this.getPaneLayouts();
|
||||
const inPane = layouts.some(
|
||||
l => chartY >= l.topY && chartY < l.topY + l.height && l.height >= 4,
|
||||
);
|
||||
if (!inPane || this.rawBars.length === 0) return -1;
|
||||
|
||||
const bar = this._barAtChartX(chartX);
|
||||
if (!bar) return -1;
|
||||
|
||||
const idx = this.rawBars.findIndex(b => (b.time as number) === (bar.time as number));
|
||||
return idx >= 0 ? idx : -1;
|
||||
}
|
||||
|
||||
/** LWC 클릭 — 전략선택봉 이동 (캔들 pane 클릭) */
|
||||
subscribeBarSelectClick(onSelect: (barIndex: number) => void): () => void {
|
||||
const handler = (param: MouseEventParams<Time>) => {
|
||||
if (param.time == null || this.rawBars.length === 0) return;
|
||||
const time = param.time as number;
|
||||
let idx = this.rawBars.findIndex(b => (b.time as number) === time);
|
||||
if (idx < 0) {
|
||||
let bestIdx = 0;
|
||||
let bestDist = Number.POSITIVE_INFINITY;
|
||||
for (let i = 0; i < this.rawBars.length; i++) {
|
||||
const dist = Math.abs((this.rawBars[i].time as number) - time);
|
||||
if (dist < bestDist) {
|
||||
bestDist = dist;
|
||||
bestIdx = i;
|
||||
}
|
||||
}
|
||||
idx = bestIdx;
|
||||
}
|
||||
if (idx >= 0) onSelect(idx);
|
||||
};
|
||||
this.chart.subscribeClick(handler);
|
||||
return () => {
|
||||
try {
|
||||
this.chart.unsubscribeClick(handler);
|
||||
} catch {
|
||||
/* ok */
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
yToPrice(y: number): number | null {
|
||||
const p = this.mainSeries?.coordinateToPrice(y);
|
||||
return p === null || p === undefined ? null : p;
|
||||
|
||||
@@ -13,50 +13,47 @@ function liveFallbackKey(r: LiveConditionRowDto): string {
|
||||
return `${r.side}:${r.timeframe}:${r.indicatorType}:${r.conditionType}:${r.targetValue ?? ''}:${plotKey}`;
|
||||
}
|
||||
|
||||
function liveRowToVirtual(r: LiveConditionRowDto): VirtualConditionRow & { currentValue: number | null } {
|
||||
const plotKey = r.plotKey ?? r.indicatorType;
|
||||
return normalizeConditionRow({
|
||||
id: r.id,
|
||||
indicatorType: r.indicatorType,
|
||||
displayName: formatIndicatorDisplayLabel(r.indicatorType),
|
||||
conditionType: r.conditionType,
|
||||
conditionLabel: r.conditionLabel,
|
||||
targetValue: r.targetValue,
|
||||
timeframe: r.timeframe,
|
||||
side: r.side,
|
||||
plotKey,
|
||||
satisfied: r.satisfied,
|
||||
thresholdLabel: r.thresholdLabel,
|
||||
currentValue: r.currentValue,
|
||||
});
|
||||
}
|
||||
|
||||
export function mergeRows(
|
||||
base: VirtualConditionRow[],
|
||||
live: LiveConditionRowDto[],
|
||||
): Array<VirtualConditionRow & { currentValue: number | null }> {
|
||||
const liveById = new Map<string, LiveConditionRowDto>();
|
||||
const liveByKey = new Map<string, LiveConditionRowDto>();
|
||||
for (const r of live) {
|
||||
liveById.set(r.id, r);
|
||||
liveByKey.set(liveFallbackKey(r), r);
|
||||
if (live.length === 0) {
|
||||
return base.map(row => normalizeConditionRow({ ...row, currentValue: null, satisfied: null }));
|
||||
}
|
||||
|
||||
if (base.length === 0) {
|
||||
return live.map(liveRowToVirtual);
|
||||
}
|
||||
const baseById = new Map(base.map(r => [r.id, r]));
|
||||
const baseByKey = new Map(base.map(r => [rowFallbackKey(r), r]));
|
||||
const matchedBaseIds = new Set<string>();
|
||||
|
||||
return base.map(row => {
|
||||
const liveRow = liveById.get(row.id) ?? liveByKey.get(rowFallbackKey(row));
|
||||
if (!liveRow) {
|
||||
return normalizeConditionRow({ ...row, currentValue: null, satisfied: null });
|
||||
}
|
||||
const merged = live.map(lr => {
|
||||
const baseRow = baseById.get(lr.id) ?? baseByKey.get(liveFallbackKey(lr));
|
||||
if (baseRow) matchedBaseIds.add(baseRow.id);
|
||||
const plotKey = lr.plotKey ?? lr.indicatorType;
|
||||
return normalizeConditionRow({
|
||||
...row,
|
||||
satisfied: liveRow.satisfied,
|
||||
thresholdLabel: liveRow.thresholdLabel,
|
||||
currentValue: liveRow.currentValue,
|
||||
targetValue: liveRow.targetValue ?? row.targetValue,
|
||||
id: lr.id,
|
||||
indicatorType: lr.indicatorType,
|
||||
displayName: baseRow?.displayName ?? formatIndicatorDisplayLabel(lr.indicatorType),
|
||||
conditionType: lr.conditionType,
|
||||
conditionLabel: lr.conditionLabel ?? baseRow?.conditionLabel ?? lr.conditionType,
|
||||
targetValue: lr.targetValue ?? baseRow?.targetValue ?? null,
|
||||
timeframe: lr.timeframe,
|
||||
side: lr.side as 'buy' | 'sell',
|
||||
plotKey,
|
||||
satisfied: lr.satisfied,
|
||||
thresholdLabel: lr.thresholdLabel ?? baseRow?.thresholdLabel,
|
||||
currentValue: lr.currentValue,
|
||||
});
|
||||
});
|
||||
|
||||
for (const row of base) {
|
||||
if (matchedBaseIds.has(row.id)) continue;
|
||||
const hasLive = live.some(
|
||||
lr => lr.id === row.id || liveFallbackKey(lr) === rowFallbackKey(row),
|
||||
);
|
||||
if (!hasLive) {
|
||||
merged.push(normalizeConditionRow({ ...row, currentValue: null, satisfied: null }));
|
||||
}
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
@@ -51,20 +51,25 @@ function walk(
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.type === 'AND' || node.type === 'OR') {
|
||||
node.children?.forEach(c => walk(c, timeframe, side, out, seen));
|
||||
return;
|
||||
}
|
||||
|
||||
// StrategyDslToTa4jAdapter 와 동일: type 미설정(구버전 DSL) 시 CONDITION 으로 처리
|
||||
if ((!node.type || node.type === 'CONDITION') && node.condition) {
|
||||
const c = node.condition;
|
||||
const rowTf = normalizeStartCandleType(c.leftCandleType ?? c.rightCandleType ?? timeframe);
|
||||
const key = `${side}:${rowTf}:${c.indicatorType}:${c.conditionType}:${c.targetValue ?? ''}:${c.leftField ?? ''}`;
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
const rowId = node.id ? `${node.id}-${side}` : `${side}:${rowTf}:${c.indicatorType}:${out.length}`;
|
||||
if (seen.has(rowId)) return;
|
||||
seen.add(rowId);
|
||||
|
||||
const condLabel = CONDITION_LABEL[c.conditionType] ?? c.conditionType;
|
||||
const target = coerceFiniteNumber(c.targetValue ?? c.compareValue ?? null);
|
||||
const plotKey = c.leftField && c.leftField !== 'none' ? c.leftField : c.indicatorType;
|
||||
|
||||
out.push({
|
||||
id: `${node.id}-${side}`,
|
||||
id: rowId,
|
||||
indicatorType: c.indicatorType,
|
||||
displayName: formatIndicatorDisplayLabel(c.indicatorType),
|
||||
conditionType: c.conditionType,
|
||||
@@ -80,6 +85,17 @@ function walk(
|
||||
node.children?.forEach(c => walk(c, timeframe, side, out, seen));
|
||||
}
|
||||
|
||||
/** 목록 표시용 — 동일 지표 복수 조건 구분 */
|
||||
export function formatVirtualConditionListLabel(
|
||||
row: Pick<VirtualConditionRow, 'displayName' | 'thresholdLabel' | 'conditionLabel' | 'timeframe'>,
|
||||
): string {
|
||||
const parts = [row.displayName];
|
||||
if (row.thresholdLabel) parts.push(row.thresholdLabel);
|
||||
else if (row.conditionLabel) parts.push(row.conditionLabel);
|
||||
if (row.timeframe && row.timeframe !== '1m') parts.push(row.timeframe);
|
||||
return parts.join(' · ');
|
||||
}
|
||||
|
||||
export function extractVirtualConditions(strategy: StrategyDto | null | undefined): VirtualConditionRow[] {
|
||||
if (!strategy) return [];
|
||||
const out: VirtualConditionRow[] = [];
|
||||
|
||||
Reference in New Issue
Block a user