155 lines
5.9 KiB
TypeScript
155 lines
5.9 KiB
TypeScript
/**
|
|
* 전략평가 차트 — 선택 봉 기준 N봉 추세선 Drawing 생성
|
|
*/
|
|
import type { Drawing } from '../types';
|
|
import type { OHLCVBar } from '../types';
|
|
import type { BacktestSettingsDto, StrategyDto } from './backendApi';
|
|
import type { ConditionDSL, LogicNode } from './strategyTypes';
|
|
import { trendLineConfigFromSettings, trendLineSegmentAt, trendLineKindForCross, isTrendLinePriceScaleField } from './trendLineGeometry';
|
|
import { calculateIndicator } from './indicatorRegistry';
|
|
import { DSL_TO_REGISTRY } from './strategyToChartIndicators';
|
|
|
|
function walkTrendLineConditions(
|
|
node: LogicNode | null | undefined,
|
|
side: 'buy' | 'sell',
|
|
out: Array<{ side: 'buy' | 'sell'; cond: ConditionDSL }>,
|
|
): void {
|
|
if (!node) return;
|
|
if (node.type === 'TIMEFRAME') {
|
|
node.children?.forEach(c => walkTrendLineConditions(c, side, out));
|
|
return;
|
|
}
|
|
if (node.type === 'NOT') {
|
|
const child = node.children?.[0];
|
|
if (child) walkTrendLineConditions(child, side, out);
|
|
return;
|
|
}
|
|
if (node.type === 'AND' || node.type === 'OR') {
|
|
node.children?.forEach(c => walkTrendLineConditions(c, side, out));
|
|
return;
|
|
}
|
|
if ((!node.type || node.type === 'CONDITION') && node.condition) {
|
|
const ct = node.condition.conditionType;
|
|
if (ct === 'TREND_LINE_CROSS_UP' || ct === 'TREND_LINE_CROSS_DOWN') {
|
|
out.push({ side, cond: node.condition });
|
|
}
|
|
}
|
|
}
|
|
|
|
function registryType(dslType: string): string {
|
|
return DSL_TO_REGISTRY[dslType] ?? dslType;
|
|
}
|
|
|
|
function isPriceScaleField(field?: string): boolean {
|
|
if (!field) return true;
|
|
const f = field.toUpperCase();
|
|
return f.includes('CLOSE') || f.includes('PRICE') || f.includes('HIGH') || f.includes('LOW')
|
|
|| f.includes('OPEN') || f.includes('MA') || f.includes('EMA')
|
|
|| f.includes('UPPER') || f.includes('MIDDLE') || f.includes('LOWER')
|
|
|| f.includes('BAND') || f.includes('DC_');
|
|
}
|
|
|
|
async function resolveSourceValues(
|
|
bars: OHLCVBar[],
|
|
cond: ConditionDSL,
|
|
getParams: (type: string) => Record<string, number | string | boolean>,
|
|
): Promise<number[] | null> {
|
|
const lf = (cond.leftField ?? 'CLOSE_PRICE').toUpperCase();
|
|
if (lf === 'CLOSE_PRICE' || lf === 'CURRENT') {
|
|
return bars.map(b => b.close);
|
|
}
|
|
if (lf === 'HIGH_PRICE') return bars.map(b => b.high);
|
|
if (lf === 'LOW_PRICE') return bars.map(b => b.low);
|
|
if (lf === 'OPEN_PRICE') return bars.map(b => b.open);
|
|
|
|
const regType = registryType(cond.indicatorType);
|
|
try {
|
|
const params = { ...getParams(regType), ...(cond.params ?? {}) };
|
|
const { plots } = await calculateIndicator(regType, bars, params);
|
|
const plotKeys = Object.keys(plots);
|
|
if (plotKeys.length === 0) return null;
|
|
|
|
let plotKey = plotKeys[0]!;
|
|
if (lf.includes('K') && plots.plot0) plotKey = 'plot0';
|
|
else if (lf.includes('SIGNAL') && plots.plot1) plotKey = 'plot1';
|
|
else if (lf.includes('MACD') && plots.plot0) plotKey = 'plot0';
|
|
else if (lf.includes('UPPER') && plots.plot2) plotKey = 'plot2';
|
|
else if (lf.includes('LOWER') && plots.plot0) plotKey = 'plot0';
|
|
else if (lf.includes('MIDDLE') && plots.plot1) plotKey = 'plot1';
|
|
|
|
const plot = plots[plotKey as keyof typeof plots];
|
|
if (!plot || !Array.isArray(plot)) return null;
|
|
const byTime = new Map(plot.map((p: { time: number; value: number }) => [p.time, p.value]));
|
|
return bars.map(b => {
|
|
const v = byTime.get(b.time);
|
|
return v != null && Number.isFinite(v) ? v : NaN;
|
|
});
|
|
} catch {
|
|
return bars.map(b => b.close);
|
|
}
|
|
}
|
|
|
|
/** 선택 봉(barIndex) 기준 추세선만 반환 — 시그널 전체 순회하지 않음 */
|
|
export async function buildSignalTrendLineDrawings(opts: {
|
|
bars: OHLCVBar[];
|
|
/** 캔들 선택봉 인덱스 */
|
|
barIndex: number;
|
|
strategy: StrategyDto | null;
|
|
settings?: BacktestSettingsDto | null;
|
|
getParams: (type: string) => Record<string, number | string | boolean>;
|
|
}): Promise<Drawing[]> {
|
|
const { bars, barIndex, strategy, settings, getParams } = opts;
|
|
if (!strategy || bars.length === 0 || barIndex < 0 || barIndex >= bars.length) return [];
|
|
|
|
const conditions: Array<{ side: 'buy' | 'sell'; cond: ConditionDSL }> = [];
|
|
walkTrendLineConditions(strategy.buyCondition as LogicNode | null, 'buy', conditions);
|
|
walkTrendLineConditions(strategy.sellCondition as LogicNode | null, 'sell', conditions);
|
|
if (conditions.length === 0) return [];
|
|
|
|
const priceConds = conditions.filter(c => isPriceScaleField(c.cond.leftField));
|
|
if (priceConds.length === 0) return [];
|
|
|
|
const config = trendLineConfigFromSettings(settings);
|
|
const barTimes = bars.map(b => b.time);
|
|
const drawings: Drawing[] = [];
|
|
|
|
const sourceCache = new Map<string, number[]>();
|
|
for (const { cond } of priceConds) {
|
|
const key = `${cond.indicatorType}:${cond.leftField}:${JSON.stringify(cond.params ?? {})}`;
|
|
if (!sourceCache.has(key)) {
|
|
const values = await resolveSourceValues(bars, cond, getParams);
|
|
if (values) sourceCache.set(key, values);
|
|
}
|
|
}
|
|
|
|
for (const { side, cond } of priceConds) {
|
|
const cacheKey = `${cond.indicatorType}:${cond.leftField}:${JSON.stringify(cond.params ?? {})}`;
|
|
const sourceValues = sourceCache.get(cacheKey);
|
|
if (!sourceValues) continue;
|
|
|
|
const lookback = Math.max(2, cond.lookbackPeriod ?? config.defaultLookback);
|
|
const kind = trendLineKindForCross(cond.conditionType, side);
|
|
const priceScale = isTrendLinePriceScaleField(cond.leftField);
|
|
const seg = trendLineSegmentAt(
|
|
barTimes, sourceValues, bars, barIndex, lookback, config, kind, priceScale,
|
|
);
|
|
if (!seg) continue;
|
|
|
|
const id = `tl-${barIndex}-${side}-${cond.conditionType}-${lookback}-${config.mode}`;
|
|
drawings.push({
|
|
id,
|
|
type: 'trendline',
|
|
points: [
|
|
{ time: seg.startTimeSec, price: seg.startPrice },
|
|
{ time: seg.endTimeSec, price: seg.endPrice },
|
|
],
|
|
color: side === 'buy' ? '#26a69a' : '#ef5350',
|
|
lineWidth: 2,
|
|
style: 'dashed',
|
|
visible: true,
|
|
});
|
|
}
|
|
|
|
return drawings;
|
|
}
|