/** * 정배열/역배열 + Stochastic Slow 기반 매매 전략 * * [정배열/역배열 조건] * - 정배열: 추세 강도 > 0 * - 역배열: 추세 강도 < 0 * * [매매 조건] * 1) 정배열인 경우: * - Stochastic Slow 20 상향 돌파 → 매수 * - 50 상향 돌파 → 매수 * - 80 상향 돌파 → 매수 * - 20 하향 돌파 → 매도 * - 50 하향 돌파 → 매도 * - 80 하향 돌파 → 매도 * * 2) 역배열인 경우: * - 80 상향 돌파 → 매수 * - 80 하향 돌파 → 매도 */ // OHLCV + 지표 데이터 인터페이스 export interface CandleWithIndicators { date: string; timestamp?: string; open: number; high: number; low: number; close: number; volume: number; stochasticK: number | null; // Stochastic Slow %K stochasticD: number | null; // Stochastic Slow %D macdLine?: number | null; signalLine?: number | null; macdHistogram?: number | null; trendStrength?: number; // 추세 강도 (-100 ~ +100) } // 매매 신호 타입 export type SignalType = 'BUY' | 'SELL' | 'HOLD'; // 매매 신호 결과 export interface TradingSignal { date: string; signal: SignalType; reason: string; stochasticK: number; stochasticD: number; trendStrength: number; isBullish: boolean; // 정배열 여부 crossLevel?: number; // 돌파 레벨 (20, 50, 80) crossDirection?: 'UP' | 'DOWN'; // 돌파 방향 } // 돌파 레벨 정의 const OVERSOLD_LEVEL = 20; const NEUTRAL_LEVEL = 50; const OVERBOUGHT_LEVEL = 80; /** * 상향 돌파 여부 확인 * @param prevValue 이전 값 * @param currValue 현재 값 * @param level 돌파 레벨 * @returns 상향 돌파 여부 */ export function isCrossUp(prevValue: number, currValue: number, level: number): boolean { return prevValue < level && currValue >= level; } /** * 하향 돌파 여부 확인 * @param prevValue 이전 값 * @param currValue 현재 값 * @param level 돌파 레벨 * @returns 하향 돌파 여부 */ export function isCrossDown(prevValue: number, currValue: number, level: number): boolean { return prevValue > level && currValue <= level; } /** * 정배열 여부 확인 * @param trendStrength 추세 강도 * @returns 정배열 여부 (추세 강도 > 0) */ export function isBullishTrend(trendStrength: number): boolean { return trendStrength > 0; } /** * 역배열 여부 확인 * @param trendStrength 추세 강도 * @returns 역배열 여부 (추세 강도 < 0) */ export function isBearishTrend(trendStrength: number): boolean { return trendStrength < 0; } /** * 단일 캔들에 대한 매매 신호 생성 * @param current 현재 캔들 데이터 * @param previous 이전 캔들 데이터 * @param trendStrength 추세 강도 * @returns 매매 신호 또는 null */ export function generateSignalForCandle( current: CandleWithIndicators, previous: CandleWithIndicators, trendStrength: number ): TradingSignal | null { // Stochastic 값이 없으면 신호 없음 if (current.stochasticK === null || previous.stochasticK === null) { return null; } const currK = current.stochasticK; const prevK = previous.stochasticK; const isBullish = isBullishTrend(trendStrength); // 정배열인 경우 if (isBullish) { // 20 상향 돌파 → 매수 if (isCrossUp(prevK, currK, OVERSOLD_LEVEL)) { return { date: current.date, signal: 'BUY', reason: '정배열 + Stochastic 20 상향 돌파 (과매도 탈출)', stochasticK: currK, stochasticD: current.stochasticD ?? 0, trendStrength, isBullish: true, crossLevel: OVERSOLD_LEVEL, crossDirection: 'UP', }; } // 50 상향 돌파 → 매수 if (isCrossUp(prevK, currK, NEUTRAL_LEVEL)) { return { date: current.date, signal: 'BUY', reason: '정배열 + Stochastic 50 상향 돌파 (중립선 상향)', stochasticK: currK, stochasticD: current.stochasticD ?? 0, trendStrength, isBullish: true, crossLevel: NEUTRAL_LEVEL, crossDirection: 'UP', }; } // 80 상향 돌파 → 매수 if (isCrossUp(prevK, currK, OVERBOUGHT_LEVEL)) { return { date: current.date, signal: 'BUY', reason: '정배열 + Stochastic 80 상향 돌파 (강한 상승 모멘텀)', stochasticK: currK, stochasticD: current.stochasticD ?? 0, trendStrength, isBullish: true, crossLevel: OVERBOUGHT_LEVEL, crossDirection: 'UP', }; } // 20 하향 돌파 → 매도 if (isCrossDown(prevK, currK, OVERSOLD_LEVEL)) { return { date: current.date, signal: 'SELL', reason: '정배열 + Stochastic 20 하향 돌파 (과매도 진입)', stochasticK: currK, stochasticD: current.stochasticD ?? 0, trendStrength, isBullish: true, crossLevel: OVERSOLD_LEVEL, crossDirection: 'DOWN', }; } // 50 하향 돌파 → 매도 if (isCrossDown(prevK, currK, NEUTRAL_LEVEL)) { return { date: current.date, signal: 'SELL', reason: '정배열 + Stochastic 50 하향 돌파 (중립선 하향)', stochasticK: currK, stochasticD: current.stochasticD ?? 0, trendStrength, isBullish: true, crossLevel: NEUTRAL_LEVEL, crossDirection: 'DOWN', }; } // 80 하향 돌파 → 매도 if (isCrossDown(prevK, currK, OVERBOUGHT_LEVEL)) { return { date: current.date, signal: 'SELL', reason: '정배열 + Stochastic 80 하향 돌파 (과매수 이탈)', stochasticK: currK, stochasticD: current.stochasticD ?? 0, trendStrength, isBullish: true, crossLevel: OVERBOUGHT_LEVEL, crossDirection: 'DOWN', }; } } // 역배열인 경우 else if (isBearishTrend(trendStrength)) { // 80 상향 돌파 → 매수 if (isCrossUp(prevK, currK, OVERBOUGHT_LEVEL)) { return { date: current.date, signal: 'BUY', reason: '역배열 + Stochastic 80 상향 돌파 (반등 시그널)', stochasticK: currK, stochasticD: current.stochasticD ?? 0, trendStrength, isBullish: false, crossLevel: OVERBOUGHT_LEVEL, crossDirection: 'UP', }; } // 80 하향 돌파 → 매도 if (isCrossDown(prevK, currK, OVERBOUGHT_LEVEL)) { return { date: current.date, signal: 'SELL', reason: '역배열 + Stochastic 80 하향 돌파 (하락 지속)', stochasticK: currK, stochasticD: current.stochasticD ?? 0, trendStrength, isBullish: false, crossLevel: OVERBOUGHT_LEVEL, crossDirection: 'DOWN', }; } } // 신호 없음 return null; } /** * 전체 캔들 데이터에 대한 매매 신호 생성 * @param candles 캔들 데이터 배열 (시간순 정렬) * @param trendStrength 추세 강도 (현재 종목의 추세) * @returns 매매 신호 배열 */ export function generateSignals( candles: CandleWithIndicators[], trendStrength: number ): TradingSignal[] { const signals: TradingSignal[] = []; if (candles.length < 2) { return signals; } for (let i = 1; i < candles.length; i++) { const signal = generateSignalForCandle(candles[i], candles[i - 1], trendStrength); if (signal) { signals.push(signal); } } return signals; } /** * 가장 최근 매매 신호 조회 * @param candles 캔들 데이터 배열 (시간순 정렬) * @param trendStrength 추세 강도 * @param lookbackPeriod 조회할 기간 (캔들 수) * @returns 가장 최근 매매 신호 또는 null */ export function getLatestSignal( candles: CandleWithIndicators[], trendStrength: number, lookbackPeriod: number = 10 ): TradingSignal | null { if (candles.length < 2) { return null; } const startIdx = Math.max(1, candles.length - lookbackPeriod); for (let i = candles.length - 1; i >= startIdx; i--) { const signal = generateSignalForCandle(candles[i], candles[i - 1], trendStrength); if (signal) { return signal; } } return null; } /** * 신호 요약 정보 생성 * @param signals 매매 신호 배열 * @returns 요약 정보 */ export function getSignalSummary(signals: TradingSignal[]): { totalSignals: number; buySignals: number; sellSignals: number; lastSignal: TradingSignal | null; bullishSignals: number; bearishSignals: number; } { const buySignals = signals.filter(s => s.signal === 'BUY'); const sellSignals = signals.filter(s => s.signal === 'SELL'); const bullishSignals = signals.filter(s => s.isBullish); const bearishSignals = signals.filter(s => !s.isBullish); return { totalSignals: signals.length, buySignals: buySignals.length, sellSignals: sellSignals.length, lastSignal: signals.length > 0 ? signals[signals.length - 1] : null, bullishSignals: bullishSignals.length, bearishSignals: bearishSignals.length, }; } /** * 전략 설정 인터페이스 */ export interface TrendStochasticStrategyConfig { oversoldLevel: number; // 과매도 레벨 (기본: 20) neutralLevel: number; // 중립 레벨 (기본: 50) overboughtLevel: number; // 과매수 레벨 (기본: 80) enableBullishAllLevels: boolean; // 정배열 시 모든 레벨 활성화 (기본: true) enableBearishOnlyOverbought: boolean; // 역배열 시 80 레벨만 활성화 (기본: true) } /** * 기본 전략 설정 */ export const DEFAULT_STRATEGY_CONFIG: TrendStochasticStrategyConfig = { oversoldLevel: 20, neutralLevel: 50, overboughtLevel: 80, enableBullishAllLevels: true, enableBearishOnlyOverbought: true, }; /** * 설정 가능한 전략 신호 생성 * @param current 현재 캔들 데이터 * @param previous 이전 캔들 데이터 * @param trendStrength 추세 강도 * @param config 전략 설정 * @returns 매매 신호 또는 null */ export function generateSignalWithConfig( current: CandleWithIndicators, previous: CandleWithIndicators, trendStrength: number, config: TrendStochasticStrategyConfig = DEFAULT_STRATEGY_CONFIG ): TradingSignal | null { if (current.stochasticK === null || previous.stochasticK === null) { return null; } const currK = current.stochasticK; const prevK = previous.stochasticK; const isBullish = isBullishTrend(trendStrength); // 정배열인 경우 if (isBullish && config.enableBullishAllLevels) { // 과매도 레벨 상향 돌파 → 매수 if (isCrossUp(prevK, currK, config.oversoldLevel)) { return { date: current.date, signal: 'BUY', reason: `정배열 + Stochastic ${config.oversoldLevel} 상향 돌파`, stochasticK: currK, stochasticD: current.stochasticD ?? 0, trendStrength, isBullish: true, crossLevel: config.oversoldLevel, crossDirection: 'UP', }; } // 중립 레벨 상향 돌파 → 매수 if (isCrossUp(prevK, currK, config.neutralLevel)) { return { date: current.date, signal: 'BUY', reason: `정배열 + Stochastic ${config.neutralLevel} 상향 돌파`, stochasticK: currK, stochasticD: current.stochasticD ?? 0, trendStrength, isBullish: true, crossLevel: config.neutralLevel, crossDirection: 'UP', }; } // 과매수 레벨 상향 돌파 → 매수 if (isCrossUp(prevK, currK, config.overboughtLevel)) { return { date: current.date, signal: 'BUY', reason: `정배열 + Stochastic ${config.overboughtLevel} 상향 돌파`, stochasticK: currK, stochasticD: current.stochasticD ?? 0, trendStrength, isBullish: true, crossLevel: config.overboughtLevel, crossDirection: 'UP', }; } // 과매도 레벨 하향 돌파 → 매도 if (isCrossDown(prevK, currK, config.oversoldLevel)) { return { date: current.date, signal: 'SELL', reason: `정배열 + Stochastic ${config.oversoldLevel} 하향 돌파`, stochasticK: currK, stochasticD: current.stochasticD ?? 0, trendStrength, isBullish: true, crossLevel: config.oversoldLevel, crossDirection: 'DOWN', }; } // 중립 레벨 하향 돌파 → 매도 if (isCrossDown(prevK, currK, config.neutralLevel)) { return { date: current.date, signal: 'SELL', reason: `정배열 + Stochastic ${config.neutralLevel} 하향 돌파`, stochasticK: currK, stochasticD: current.stochasticD ?? 0, trendStrength, isBullish: true, crossLevel: config.neutralLevel, crossDirection: 'DOWN', }; } // 과매수 레벨 하향 돌파 → 매도 if (isCrossDown(prevK, currK, config.overboughtLevel)) { return { date: current.date, signal: 'SELL', reason: `정배열 + Stochastic ${config.overboughtLevel} 하향 돌파`, stochasticK: currK, stochasticD: current.stochasticD ?? 0, trendStrength, isBullish: true, crossLevel: config.overboughtLevel, crossDirection: 'DOWN', }; } } // 역배열인 경우 else if (isBearishTrend(trendStrength) && config.enableBearishOnlyOverbought) { // 과매수 레벨 상향 돌파 → 매수 if (isCrossUp(prevK, currK, config.overboughtLevel)) { return { date: current.date, signal: 'BUY', reason: `역배열 + Stochastic ${config.overboughtLevel} 상향 돌파`, stochasticK: currK, stochasticD: current.stochasticD ?? 0, trendStrength, isBullish: false, crossLevel: config.overboughtLevel, crossDirection: 'UP', }; } // 과매수 레벨 하향 돌파 → 매도 if (isCrossDown(prevK, currK, config.overboughtLevel)) { return { date: current.date, signal: 'SELL', reason: `역배열 + Stochastic ${config.overboughtLevel} 하향 돌파`, stochasticK: currK, stochasticD: current.stochasticD ?? 0, trendStrength, isBullish: false, crossLevel: config.overboughtLevel, crossDirection: 'DOWN', }; } } return null; } // 사용 예시 /* import { generateSignals, getLatestSignal, getSignalSummary, CandleWithIndicators } from './trendStochasticStrategy'; // 캔들 데이터 예시 const candles: CandleWithIndicators[] = [ { date: '2024-01-01', open: 100, high: 105, low: 98, close: 103, volume: 1000, stochasticK: 18, stochasticD: 15 }, { date: '2024-01-02', open: 103, high: 110, low: 101, close: 108, volume: 1200, stochasticK: 22, stochasticD: 19 }, // ... ]; // 추세 강도 (양수: 정배열, 음수: 역배열) const trendStrength = 45.5; // 정배열 // 전체 신호 생성 const signals = generateSignals(candles, trendStrength); console.log('전체 신호:', signals); // 최근 신호 조회 const latestSignal = getLatestSignal(candles, trendStrength); console.log('최근 신호:', latestSignal); // 신호 요약 const summary = getSignalSummary(signals); console.log('요약:', summary); */