goldenChat base source add
This commit is contained in:
@@ -0,0 +1,499 @@
|
||||
// 전략 평가 Web Worker
|
||||
// 별도 스레드에서 전략 조건 평가를 수행하여 메인 UI 스레드 블로킹 방지
|
||||
|
||||
interface StrategyConditionDto {
|
||||
id?: number;
|
||||
indicatorType: string;
|
||||
conditionType: string;
|
||||
targetValue?: number;
|
||||
targetValue2?: number;
|
||||
description?: string;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
interface ConditionGroupDto {
|
||||
id?: number;
|
||||
name?: string;
|
||||
logicalOperator: 'AND' | 'OR';
|
||||
conditions: StrategyConditionDto[];
|
||||
childGroups?: ConditionGroupDto[];
|
||||
groupType?: 'BUY' | 'SELL';
|
||||
signalType?: string;
|
||||
}
|
||||
|
||||
interface StrategyRuleDto {
|
||||
id: number;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
conditionGroups: ConditionGroupDto[];
|
||||
buyConditionGroups?: ConditionGroupDto[];
|
||||
sellConditionGroups?: ConditionGroupDto[];
|
||||
enableMaBuy?: boolean;
|
||||
enableAuxBuy?: boolean;
|
||||
totalConditionCount?: number;
|
||||
conditionSummary?: string;
|
||||
}
|
||||
|
||||
interface EvaluationMessage {
|
||||
type: 'evaluate';
|
||||
strategies: StrategyRuleDto[];
|
||||
currentData: any;
|
||||
prevData: any | null;
|
||||
currentIndex: number;
|
||||
referenceTime: string;
|
||||
}
|
||||
|
||||
interface ConditionResult {
|
||||
description: string;
|
||||
matched: boolean;
|
||||
}
|
||||
|
||||
interface EvaluationResult {
|
||||
buy: {
|
||||
total: number;
|
||||
matched: number;
|
||||
percentage: number;
|
||||
conditions: ConditionResult[];
|
||||
};
|
||||
sell: {
|
||||
total: number;
|
||||
matched: number;
|
||||
percentage: number;
|
||||
conditions: ConditionResult[];
|
||||
};
|
||||
}
|
||||
|
||||
interface StrategyEvaluationResult {
|
||||
strategyId: number;
|
||||
strategyName: string;
|
||||
enabled: boolean;
|
||||
evaluation: EvaluationResult;
|
||||
}
|
||||
|
||||
interface WorkerResponse {
|
||||
type: 'result';
|
||||
results: StrategyEvaluationResult[];
|
||||
currentIndex: number;
|
||||
referenceTime: string;
|
||||
}
|
||||
|
||||
// 단일 조건 평가 함수
|
||||
function evaluateCondition(
|
||||
condition: StrategyConditionDto,
|
||||
currentData: any,
|
||||
prevData: any | null
|
||||
): boolean {
|
||||
if (!currentData || condition.enabled === false) return false;
|
||||
|
||||
const { indicatorType, conditionType, targetValue, targetValue2 } = condition;
|
||||
|
||||
try {
|
||||
switch (indicatorType) {
|
||||
case 'RSI': {
|
||||
const rsiValue = currentData.rsi;
|
||||
const prevRsi = prevData?.rsi;
|
||||
if (rsiValue == null) return false;
|
||||
|
||||
switch (conditionType) {
|
||||
case 'ABOVE': return rsiValue > (targetValue || 50);
|
||||
case 'BELOW': return rsiValue < (targetValue || 50);
|
||||
case 'CROSS_UP': return prevRsi != null && prevRsi <= (targetValue || 50) && rsiValue > (targetValue || 50);
|
||||
case 'CROSS_DOWN': return prevRsi != null && prevRsi >= (targetValue || 50) && rsiValue < (targetValue || 50);
|
||||
case 'OVERBOUGHT': return rsiValue >= (targetValue || 70);
|
||||
case 'OVERSOLD': return rsiValue <= (targetValue || 30);
|
||||
case 'RANGE': return rsiValue >= (targetValue || 30) && rsiValue <= (targetValue2 || 70);
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
case 'STOCHASTIC':
|
||||
case 'STOCHASTIC_K':
|
||||
case 'STOCHASTIC_D': {
|
||||
const kValue = currentData.stochasticK;
|
||||
const dValue = currentData.stochasticD;
|
||||
const prevK = prevData?.stochasticK;
|
||||
const prevD = prevData?.stochasticD;
|
||||
const value = indicatorType === 'STOCHASTIC_D' ? dValue : kValue;
|
||||
const prevValue = indicatorType === 'STOCHASTIC_D' ? prevD : prevK;
|
||||
|
||||
if (value == null) return false;
|
||||
|
||||
switch (conditionType) {
|
||||
case 'ABOVE': return value > (targetValue || 50);
|
||||
case 'BELOW': return value < (targetValue || 50);
|
||||
case 'CROSS_UP': return prevValue != null && prevValue <= (targetValue || 50) && value > (targetValue || 50);
|
||||
case 'CROSS_DOWN': return prevValue != null && prevValue >= (targetValue || 50) && value < (targetValue || 50);
|
||||
case 'GOLDEN_CROSS': return prevK != null && prevD != null && kValue != null && dValue != null && prevK <= prevD && kValue > dValue;
|
||||
case 'DEAD_CROSS': return prevK != null && prevD != null && kValue != null && dValue != null && prevK >= prevD && kValue < dValue;
|
||||
case 'OVERBOUGHT': return value >= (targetValue || 80);
|
||||
case 'OVERSOLD': return value <= (targetValue || 20);
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
case 'MACD':
|
||||
case 'MACD_LINE':
|
||||
case 'MACD_SIGNAL':
|
||||
case 'MACD_HISTOGRAM': {
|
||||
const macdLine = currentData.macdLine;
|
||||
const signalLine = currentData.signalLine;
|
||||
const histogram = currentData.macdHistogram;
|
||||
const prevMacd = prevData?.macdLine;
|
||||
const prevSignal = prevData?.signalLine;
|
||||
const prevHist = prevData?.macdHistogram;
|
||||
|
||||
let value: number | null = null;
|
||||
let prevValue: number | null = null;
|
||||
|
||||
if (indicatorType === 'MACD_SIGNAL') {
|
||||
value = signalLine;
|
||||
prevValue = prevSignal;
|
||||
} else if (indicatorType === 'MACD_HISTOGRAM') {
|
||||
value = histogram;
|
||||
prevValue = prevHist;
|
||||
} else {
|
||||
value = macdLine;
|
||||
prevValue = prevMacd;
|
||||
}
|
||||
|
||||
if (value == null) return false;
|
||||
|
||||
switch (conditionType) {
|
||||
case 'ABOVE': return value > (targetValue || 0);
|
||||
case 'BELOW': return value < (targetValue || 0);
|
||||
case 'CROSS_UP': return prevValue != null && prevValue <= (targetValue || 0) && value > (targetValue || 0);
|
||||
case 'CROSS_DOWN': return prevValue != null && prevValue >= (targetValue || 0) && value < (targetValue || 0);
|
||||
case 'GOLDEN_CROSS': return prevMacd != null && prevSignal != null && macdLine != null && signalLine != null && prevMacd <= prevSignal && macdLine > signalLine;
|
||||
case 'DEAD_CROSS': return prevMacd != null && prevSignal != null && macdLine != null && signalLine != null && prevMacd >= prevSignal && macdLine < signalLine;
|
||||
case 'HISTOGRAM_POSITIVE': return histogram != null && histogram > 0;
|
||||
case 'HISTOGRAM_NEGATIVE': return histogram != null && histogram < 0;
|
||||
case 'HISTOGRAM_INCREASING': return histogram != null && prevHist != null && histogram > prevHist;
|
||||
case 'HISTOGRAM_DECREASING': return histogram != null && prevHist != null && histogram < prevHist;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
case 'ADX': {
|
||||
const adxValue = currentData.adx;
|
||||
if (adxValue == null) return false;
|
||||
|
||||
switch (conditionType) {
|
||||
case 'ABOVE': return adxValue > (targetValue || 25);
|
||||
case 'BELOW': return adxValue < (targetValue || 25);
|
||||
case 'STRONG_TREND': return adxValue >= (targetValue || 25);
|
||||
case 'WEAK_TREND': return adxValue < (targetValue || 20);
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
case 'BOLLINGER':
|
||||
case 'BOLLINGER_UPPER':
|
||||
case 'BOLLINGER_LOWER':
|
||||
case 'BOLLINGER_MIDDLE': {
|
||||
const close = currentData.close;
|
||||
const upper = currentData.bollingerUpper;
|
||||
const lower = currentData.bollingerLower;
|
||||
const middle = currentData.bollingerMiddle;
|
||||
|
||||
if (close == null) return false;
|
||||
|
||||
switch (conditionType) {
|
||||
case 'ABOVE_UPPER': return upper != null && close > upper;
|
||||
case 'BELOW_LOWER': return lower != null && close < lower;
|
||||
case 'TOUCH_UPPER': return upper != null && Math.abs(close - upper) / upper < 0.001;
|
||||
case 'TOUCH_LOWER': return lower != null && Math.abs(close - lower) / lower < 0.001;
|
||||
case 'INSIDE_BANDS': return upper != null && lower != null && close <= upper && close >= lower;
|
||||
case 'ABOVE_MIDDLE': return middle != null && close > middle;
|
||||
case 'BELOW_MIDDLE': return middle != null && close < middle;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
case 'MA':
|
||||
case 'MA5':
|
||||
case 'MA10':
|
||||
case 'MA20':
|
||||
case 'MA60':
|
||||
case 'MA120': {
|
||||
const close = currentData.close;
|
||||
const prevClose = prevData?.close;
|
||||
|
||||
let maValue: number | null = null;
|
||||
let prevMa: number | null = null;
|
||||
|
||||
switch (indicatorType) {
|
||||
case 'MA5': maValue = currentData.ma5; prevMa = prevData?.ma5; break;
|
||||
case 'MA10': maValue = currentData.ma10; prevMa = prevData?.ma10; break;
|
||||
case 'MA20': maValue = currentData.ma20; prevMa = prevData?.ma20; break;
|
||||
case 'MA60': maValue = currentData.ma60; prevMa = prevData?.ma60; break;
|
||||
case 'MA120': maValue = currentData.ma120; prevMa = prevData?.ma120; break;
|
||||
default: maValue = currentData.ma20; prevMa = prevData?.ma20;
|
||||
}
|
||||
|
||||
if (close == null || maValue == null) return false;
|
||||
|
||||
switch (conditionType) {
|
||||
case 'ABOVE': return close > maValue;
|
||||
case 'BELOW': return close < maValue;
|
||||
case 'CROSS_UP': return prevClose != null && prevMa != null && prevClose <= prevMa && close > maValue;
|
||||
case 'CROSS_DOWN': return prevClose != null && prevMa != null && prevClose >= prevMa && close < maValue;
|
||||
case 'GOLDEN_CROSS': {
|
||||
// 단기MA > 장기MA 교차
|
||||
const shortMa = currentData.ma5 || currentData.ma10;
|
||||
const longMa = currentData.ma20 || currentData.ma60;
|
||||
const prevShort = prevData?.ma5 || prevData?.ma10;
|
||||
const prevLong = prevData?.ma20 || prevData?.ma60;
|
||||
return shortMa != null && longMa != null && prevShort != null && prevLong != null &&
|
||||
prevShort <= prevLong && shortMa > longMa;
|
||||
}
|
||||
case 'DEAD_CROSS': {
|
||||
const shortMa = currentData.ma5 || currentData.ma10;
|
||||
const longMa = currentData.ma20 || currentData.ma60;
|
||||
const prevShort = prevData?.ma5 || prevData?.ma10;
|
||||
const prevLong = prevData?.ma20 || prevData?.ma60;
|
||||
return shortMa != null && longMa != null && prevShort != null && prevLong != null &&
|
||||
prevShort >= prevLong && shortMa < longMa;
|
||||
}
|
||||
case 'UPTREND': return maValue > (prevMa || 0);
|
||||
case 'DOWNTREND': return maValue < (prevMa || Infinity);
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
case 'PRICE': {
|
||||
const close = currentData.close;
|
||||
const prevClose = prevData?.close;
|
||||
|
||||
if (close == null) return false;
|
||||
|
||||
switch (conditionType) {
|
||||
case 'ABOVE': return close > (targetValue || 0);
|
||||
case 'BELOW': return close < (targetValue || 0);
|
||||
case 'CHANGE_PERCENT': {
|
||||
if (prevClose == null) return false;
|
||||
const changePercent = ((close - prevClose) / prevClose) * 100;
|
||||
return changePercent >= (targetValue || 0);
|
||||
}
|
||||
case 'RANGE': return close >= (targetValue || 0) && close <= (targetValue2 || Infinity);
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
case 'VOLUME': {
|
||||
const volume = currentData.volume;
|
||||
const prevVolume = prevData?.volume;
|
||||
|
||||
if (volume == null) return false;
|
||||
|
||||
switch (conditionType) {
|
||||
case 'ABOVE': return volume > (targetValue || 0);
|
||||
case 'ABOVE_AVERAGE': {
|
||||
const avgVolume = currentData.volumeAvg || currentData.volumeMa20;
|
||||
return avgVolume != null && volume > avgVolume * (targetValue || 1);
|
||||
}
|
||||
case 'INCREASING': return prevVolume != null && volume > prevVolume;
|
||||
case 'SPIKE': {
|
||||
const avgVolume = currentData.volumeAvg || currentData.volumeMa20;
|
||||
return avgVolume != null && volume > avgVolume * (targetValue || 2);
|
||||
}
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
case 'CCI': {
|
||||
const cciValue = currentData.cci;
|
||||
const cciSignal = currentData.cciSignal;
|
||||
const prevCci = prevData?.cci;
|
||||
const prevCciSignal = prevData?.cciSignal;
|
||||
|
||||
if (cciValue == null) return false;
|
||||
|
||||
switch (conditionType) {
|
||||
case 'ABOVE': return cciValue > (targetValue || 0);
|
||||
case 'BELOW': return cciValue < (targetValue || 0);
|
||||
case 'CROSS_UP': {
|
||||
// targetValue가 있으면 해당 값 돌파, 없으면 -100/0/100 중 하나라도 돌파
|
||||
if (targetValue != null) {
|
||||
return prevCci != null && prevCci <= targetValue && cciValue > targetValue;
|
||||
} else {
|
||||
// 침체(-100), 중앙(0), 과열(100) 중 하나라도 상향 돌파
|
||||
const crossOversold = prevCci != null && prevCci < -100 && cciValue >= -100;
|
||||
const crossMiddle = prevCci != null && prevCci < 0 && cciValue >= 0;
|
||||
const crossOverbought = prevCci != null && prevCci < 100 && cciValue >= 100;
|
||||
return crossOversold || crossMiddle || crossOverbought;
|
||||
}
|
||||
}
|
||||
case 'CROSS_DOWN': {
|
||||
// targetValue가 있으면 해당 값 돌파, 없으면 -100/0/100 중 하나라도 돌파
|
||||
if (targetValue != null) {
|
||||
return prevCci != null && prevCci >= targetValue && cciValue < targetValue;
|
||||
} else {
|
||||
// 침체(-100), 중앙(0), 과열(100) 중 하나라도 하향 돌파
|
||||
const crossOverbought = prevCci != null && prevCci > 100 && cciValue <= 100;
|
||||
const crossMiddle = prevCci != null && prevCci > 0 && cciValue <= 0;
|
||||
const crossOversold = prevCci != null && prevCci > -100 && cciValue <= -100;
|
||||
return crossOverbought || crossMiddle || crossOversold;
|
||||
}
|
||||
}
|
||||
case 'GOLDEN_CROSS':
|
||||
// CCI가 CCI Signal을 상향 돌파
|
||||
return cciSignal != null && prevCci != null && prevCciSignal != null &&
|
||||
prevCci <= prevCciSignal && cciValue > cciSignal;
|
||||
case 'DEAD_CROSS':
|
||||
// CCI가 CCI Signal을 하향 돌파
|
||||
return cciSignal != null && prevCci != null && prevCciSignal != null &&
|
||||
prevCci >= prevCciSignal && cciValue < cciSignal;
|
||||
case 'OVERBOUGHT': return cciValue >= (targetValue || 100);
|
||||
case 'OVERSOLD': return cciValue <= (targetValue || -100);
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
case 'WILLIAMS_R': {
|
||||
const wrValue = currentData.williamsR;
|
||||
if (wrValue == null) return false;
|
||||
|
||||
switch (conditionType) {
|
||||
case 'OVERBOUGHT': return wrValue >= (targetValue || -20);
|
||||
case 'OVERSOLD': return wrValue <= (targetValue || -80);
|
||||
case 'ABOVE': return wrValue > (targetValue || -50);
|
||||
case 'BELOW': return wrValue < (targetValue || -50);
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Worker: 조건 평가 오류:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 조건 그룹 평가 함수
|
||||
function evaluateConditionGroup(
|
||||
group: ConditionGroupDto,
|
||||
currentData: any,
|
||||
prevData: any | null
|
||||
): { total: number; matched: number; conditions: ConditionResult[] } {
|
||||
const results: ConditionResult[] = [];
|
||||
let total = 0;
|
||||
let matched = 0;
|
||||
|
||||
const groupAny = group as any;
|
||||
const conditions = group.conditions || groupAny.strategyConditions || [];
|
||||
|
||||
for (const condition of conditions) {
|
||||
if (condition.enabled === false) continue;
|
||||
|
||||
const isMatched = evaluateCondition(condition, currentData, prevData);
|
||||
const description = condition.description ||
|
||||
`${condition.indicatorType} ${condition.conditionType}${condition.targetValue ? ` ${condition.targetValue}` : ''}`;
|
||||
results.push({ description, matched: isMatched });
|
||||
total++;
|
||||
if (isMatched) matched++;
|
||||
}
|
||||
|
||||
const childGroups = group.childGroups || groupAny.children || [];
|
||||
for (const childGroup of childGroups) {
|
||||
const childResult = evaluateConditionGroup(childGroup, currentData, prevData);
|
||||
total += childResult.total;
|
||||
matched += childResult.matched;
|
||||
results.push(...childResult.conditions);
|
||||
}
|
||||
|
||||
return { total, matched, conditions: results };
|
||||
}
|
||||
|
||||
// 전략 규칙 평가 함수
|
||||
function evaluateStrategyRule(
|
||||
rule: StrategyRuleDto,
|
||||
currentData: any,
|
||||
prevData: any | null
|
||||
): EvaluationResult {
|
||||
const buyResult = { total: 0, matched: 0, percentage: 0, conditions: [] as ConditionResult[] };
|
||||
const sellResult = { total: 0, matched: 0, percentage: 0, conditions: [] as ConditionResult[] };
|
||||
|
||||
// buyConditionGroups 처리
|
||||
if (rule.buyConditionGroups && rule.buyConditionGroups.length > 0) {
|
||||
for (const group of rule.buyConditionGroups) {
|
||||
const result = evaluateConditionGroup(group, currentData, prevData);
|
||||
buyResult.total += result.total;
|
||||
buyResult.matched += result.matched;
|
||||
buyResult.conditions.push(...result.conditions);
|
||||
}
|
||||
}
|
||||
|
||||
// sellConditionGroups 처리
|
||||
if (rule.sellConditionGroups && rule.sellConditionGroups.length > 0) {
|
||||
for (const group of rule.sellConditionGroups) {
|
||||
const result = evaluateConditionGroup(group, currentData, prevData);
|
||||
sellResult.total += result.total;
|
||||
sellResult.matched += result.matched;
|
||||
sellResult.conditions.push(...result.conditions);
|
||||
}
|
||||
}
|
||||
|
||||
// 기존 conditionGroups 처리
|
||||
if (rule.conditionGroups && rule.conditionGroups.length > 0) {
|
||||
for (const group of rule.conditionGroups) {
|
||||
const result = evaluateConditionGroup(group, currentData, prevData);
|
||||
const groupAny = group as any;
|
||||
const isBuy = group.groupType === 'BUY' || groupAny.signalType === 'BUY';
|
||||
const isSell = group.groupType === 'SELL' || groupAny.signalType === 'SELL';
|
||||
|
||||
if (isSell) {
|
||||
sellResult.total += result.total;
|
||||
sellResult.matched += result.matched;
|
||||
sellResult.conditions.push(...result.conditions);
|
||||
} else {
|
||||
buyResult.total += result.total;
|
||||
buyResult.matched += result.matched;
|
||||
buyResult.conditions.push(...result.conditions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 비율 계산
|
||||
buyResult.percentage = buyResult.total > 0 ? Math.round((buyResult.matched / buyResult.total) * 100) : 0;
|
||||
sellResult.percentage = sellResult.total > 0 ? Math.round((sellResult.matched / sellResult.total) * 100) : 0;
|
||||
|
||||
return { buy: buyResult, sell: sellResult };
|
||||
}
|
||||
|
||||
// Worker 메시지 핸들러
|
||||
self.onmessage = (event: MessageEvent<EvaluationMessage>) => {
|
||||
const { type, strategies, currentData, prevData, currentIndex, referenceTime } = event.data;
|
||||
|
||||
if (type !== 'evaluate') return;
|
||||
|
||||
try {
|
||||
const results: StrategyEvaluationResult[] = strategies.map(strategy => ({
|
||||
strategyId: strategy.id,
|
||||
strategyName: strategy.name,
|
||||
enabled: strategy.enabled,
|
||||
evaluation: evaluateStrategyRule(strategy, currentData, prevData)
|
||||
}));
|
||||
|
||||
const response: WorkerResponse = {
|
||||
type: 'result',
|
||||
results,
|
||||
currentIndex,
|
||||
referenceTime
|
||||
};
|
||||
|
||||
self.postMessage(response);
|
||||
} catch (error) {
|
||||
console.error('Worker: 전략 평가 오류:', error);
|
||||
self.postMessage({
|
||||
type: 'error',
|
||||
error: String(error),
|
||||
currentIndex,
|
||||
referenceTime
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export {};
|
||||
|
||||
Reference in New Issue
Block a user