추세검색 수정
This commit is contained in:
@@ -1158,6 +1158,17 @@ export interface TrendSearchRequest {
|
||||
sortBy: string;
|
||||
minMatchRate?: number;
|
||||
|
||||
/** 상승추세 검색그룹 (가중 점수화) */
|
||||
bullishTrendEnabled: boolean;
|
||||
weightMaAlignment: number;
|
||||
weightMaSlope: number;
|
||||
weightAdxTrend: number;
|
||||
weightMacdMomentum: number;
|
||||
weightPricePosition: number;
|
||||
minTrendScore: number;
|
||||
emaSlopeLookback: number;
|
||||
adxTrendMin: number;
|
||||
|
||||
maAlignEnabled: boolean;
|
||||
trendEnabled: boolean;
|
||||
volumePowerEnabled: boolean;
|
||||
@@ -1196,19 +1207,29 @@ export const DEFAULT_TREND_SEARCH_REQUEST: TrendSearchRequest = {
|
||||
sortBy: 'matchRate',
|
||||
minMatchRate: 0,
|
||||
|
||||
maAlignEnabled: true,
|
||||
trendEnabled: true,
|
||||
bullishTrendEnabled: true,
|
||||
weightMaAlignment: 30,
|
||||
weightMaSlope: 15,
|
||||
weightAdxTrend: 25,
|
||||
weightMacdMomentum: 15,
|
||||
weightPricePosition: 15,
|
||||
minTrendScore: 70,
|
||||
emaSlopeLookback: 5,
|
||||
adxTrendMin: 20,
|
||||
|
||||
maAlignEnabled: false,
|
||||
trendEnabled: false,
|
||||
volumePowerEnabled: false,
|
||||
indicatorEnabled: false,
|
||||
|
||||
priceAboveMa: true,
|
||||
maAlignment: true,
|
||||
priceAboveMa: false,
|
||||
maAlignment: false,
|
||||
maConvergence: false,
|
||||
maConvergencePct: 3,
|
||||
|
||||
ma20SlopeUp: true,
|
||||
ma20SlopeUp: false,
|
||||
ma20SlopeBars: 2,
|
||||
newHighBreakout: true,
|
||||
newHighBreakout: false,
|
||||
newHighBreakoutDays: 20,
|
||||
newHighNearPct: 5,
|
||||
ichimokuAboveCloud: false,
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
/** 상승추세 검색그룹 — 5개 평가항목 배점 (합계 100 고정) */
|
||||
|
||||
export type BullishWeightKey =
|
||||
| 'weightMaAlignment'
|
||||
| 'weightMaSlope'
|
||||
| 'weightAdxTrend'
|
||||
| 'weightMacdMomentum'
|
||||
| 'weightPricePosition';
|
||||
|
||||
export interface BullishWeightItem {
|
||||
key: BullishWeightKey;
|
||||
label: string;
|
||||
desc: string;
|
||||
}
|
||||
|
||||
export const BULLISH_WEIGHT_ITEMS: BullishWeightItem[] = [
|
||||
{
|
||||
key: 'weightMaAlignment',
|
||||
label: '이평선 정배열',
|
||||
desc: 'EMA(20) > EMA(60) > EMA(120)',
|
||||
},
|
||||
{
|
||||
key: 'weightMaSlope',
|
||||
label: '이평선 기울기',
|
||||
desc: 'EMA(20) 현재 > 5봉 전',
|
||||
},
|
||||
{
|
||||
key: 'weightAdxTrend',
|
||||
label: '추세강도 (ADX)',
|
||||
desc: '+DI > -DI · ADX ≥ 20',
|
||||
},
|
||||
{
|
||||
key: 'weightMacdMomentum',
|
||||
label: '모멘텀 (MACD)',
|
||||
desc: 'MACD > Signal · MACD > 0',
|
||||
},
|
||||
{
|
||||
key: 'weightPricePosition',
|
||||
label: '가격위치',
|
||||
desc: '종가 > EMA(20)',
|
||||
},
|
||||
];
|
||||
|
||||
export const DEFAULT_BULLISH_WEIGHTS: Record<BullishWeightKey, number> = {
|
||||
weightMaAlignment: 30,
|
||||
weightMaSlope: 15,
|
||||
weightAdxTrend: 25,
|
||||
weightMacdMomentum: 15,
|
||||
weightPricePosition: 15,
|
||||
};
|
||||
|
||||
export function bullishWeightsFromRequest(
|
||||
req: Pick<import('./backendApi').TrendSearchRequest, BullishWeightKey>,
|
||||
): number[] {
|
||||
return BULLISH_WEIGHT_ITEMS.map(item => req[item.key] ?? DEFAULT_BULLISH_WEIGHTS[item.key]);
|
||||
}
|
||||
|
||||
export function sumWeights(weights: number[]): number {
|
||||
return weights.reduce((a, b) => a + b, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 한 항목 배점 변경 시 나머지 항목에 차이를 균등 분배해 합계 100 유지.
|
||||
*/
|
||||
export function adjustBullishWeight(
|
||||
current: Record<BullishWeightKey, number>,
|
||||
changedKey: BullishWeightKey,
|
||||
newValue: number,
|
||||
): Record<BullishWeightKey, number> {
|
||||
const keys = BULLISH_WEIGHT_ITEMS.map(i => i.key);
|
||||
const idx = keys.indexOf(changedKey);
|
||||
if (idx < 0) return current;
|
||||
|
||||
const clamped = Math.max(0, Math.min(100, Math.round(newValue)));
|
||||
const oldValue = current[changedKey];
|
||||
if (clamped === oldValue) return current;
|
||||
|
||||
const result: Record<BullishWeightKey, number> = { ...current };
|
||||
result[changedKey] = clamped;
|
||||
|
||||
const delta = clamped - oldValue;
|
||||
const otherKeys = keys.filter(k => k !== changedKey);
|
||||
|
||||
if (delta > 0) {
|
||||
let remaining = delta;
|
||||
for (let i = 0; i < otherKeys.length; i++) {
|
||||
const k = otherKeys[i];
|
||||
const take = i === otherKeys.length - 1
|
||||
? remaining
|
||||
: Math.floor(delta / otherKeys.length);
|
||||
result[k] = Math.max(0, result[k] - take);
|
||||
remaining -= take;
|
||||
}
|
||||
} else {
|
||||
const add = Math.abs(delta);
|
||||
let remaining = add;
|
||||
for (let i = 0; i < otherKeys.length; i++) {
|
||||
const k = otherKeys[i];
|
||||
const give = i === otherKeys.length - 1
|
||||
? remaining
|
||||
: Math.floor(add / otherKeys.length);
|
||||
result[k] += give;
|
||||
remaining -= give;
|
||||
}
|
||||
}
|
||||
|
||||
const total = keys.reduce((s, k) => s + result[k], 0);
|
||||
if (total !== 100) {
|
||||
const fixKey = otherKeys[0] ?? changedKey;
|
||||
result[fixKey] = Math.max(0, result[fixKey] + (100 - total));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { TrendSearchConditionDto } from './trendSearchApi';
|
||||
import type { TrendSearchConditionDto, TrendSearchResultDto } from './trendSearchApi';
|
||||
import type { ConditionMetric, ConditionStatus } from './virtualSignalMetrics';
|
||||
import type { VirtualConditionRow } from './virtualStrategyConditions';
|
||||
|
||||
@@ -42,3 +42,13 @@ export function tfLabelShort(tf: string): string {
|
||||
if (tf.endsWith('w')) return `${tf.replace('w', '')}주`;
|
||||
return tf;
|
||||
}
|
||||
|
||||
/** 추세강도(점수) 내림차순 정렬 */
|
||||
export function sortTrendSearchByStrength(
|
||||
results: TrendSearchResultDto[],
|
||||
): TrendSearchResultDto[] {
|
||||
return [...results].sort((a, b) => {
|
||||
if (b.matchRate !== a.matchRate) return b.matchRate - a.matchRate;
|
||||
return a.market.localeCompare(b.market);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user