132 lines
3.6 KiB
TypeScript
132 lines
3.6 KiB
TypeScript
/** 상승추세 검색그룹 — 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 const WEIGHT_STEP = 5;
|
|
|
|
export function snapToWeightStep(value: number): number {
|
|
return Math.max(0, Math.min(100, Math.round(value / WEIGHT_STEP) * WEIGHT_STEP));
|
|
}
|
|
|
|
/** range 슬라이더 트랙 클릭 위치 → 5점 단위 배점 */
|
|
export function weightFromSliderPointer(clientX: number, rect: DOMRect): number {
|
|
const ratio = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
|
|
return snapToWeightStep(ratio * 100);
|
|
}
|
|
|
|
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 = snapToWeightStep(newValue);
|
|
const oldValue = snapToWeightStep(current[changedKey]);
|
|
if (clamped === oldValue) return current;
|
|
|
|
const result: Record<BullishWeightKey, number> = { ...current };
|
|
for (const k of keys) result[k] = snapToWeightStep(result[k]);
|
|
result[changedKey] = clamped;
|
|
|
|
const delta = clamped - oldValue;
|
|
const otherKeys = keys.filter(k => k !== changedKey);
|
|
|
|
const distribute = (amount: number, mode: 'take' | 'give') => {
|
|
let remaining = amount;
|
|
let idx = 0;
|
|
let guard = 0;
|
|
while (remaining > 0 && guard < 200) {
|
|
const k = otherKeys[idx % otherKeys.length];
|
|
if (mode === 'take') {
|
|
const take = Math.min(WEIGHT_STEP, remaining, result[k]);
|
|
if (take > 0) {
|
|
result[k] -= take;
|
|
remaining -= take;
|
|
}
|
|
} else {
|
|
const give = Math.min(WEIGHT_STEP, remaining, 100 - result[k]);
|
|
if (give > 0) {
|
|
result[k] += give;
|
|
remaining -= give;
|
|
}
|
|
}
|
|
idx += 1;
|
|
guard += 1;
|
|
}
|
|
};
|
|
|
|
if (delta > 0) distribute(delta, 'take');
|
|
else if (delta < 0) distribute(-delta, 'give');
|
|
|
|
const total = keys.reduce((s, k) => s + result[k], 0);
|
|
if (total !== 100) {
|
|
const fixKey = otherKeys[0] ?? changedKey;
|
|
result[fixKey] = snapToWeightStep(Math.max(0, Math.min(100, result[fixKey] + (100 - total))));
|
|
}
|
|
|
|
return result;
|
|
}
|