536 lines
15 KiB
TypeScript
536 lines
15 KiB
TypeScript
/**
|
|
* 정배열/역배열 + 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);
|
|
*/
|
|
|