516 lines
13 KiB
TypeScript
516 lines
13 KiB
TypeScript
import axios from 'axios';
|
|
import { getApiBaseUrl } from './apiConfig';
|
|
|
|
// API 베이스 URL (통합 설정 사용)
|
|
const API_BASE_URL = getApiBaseUrl();
|
|
|
|
const alertApi = axios.create({
|
|
baseURL: API_BASE_URL,
|
|
timeout: 60000, // 60초로 증가 (외부 DB 연결 시간 고려)
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json'
|
|
},
|
|
withCredentials: false,
|
|
});
|
|
|
|
// ==================== Types ====================
|
|
|
|
export type IndicatorType =
|
|
| 'VOLUME'
|
|
| 'MACD'
|
|
| 'STOCHASTIC'
|
|
| 'STOCHASTIC_K'
|
|
| 'STOCHASTIC_D'
|
|
| 'RSI'
|
|
| 'ADX'
|
|
| 'CCI'
|
|
| 'TRIX'
|
|
| 'DISPARITY'
|
|
| 'NEW_PSYCHOLOGICAL'
|
|
| 'INVEST_PSYCHOLOGICAL'
|
|
| 'WILLIAMS_R'
|
|
| 'BWI'
|
|
| 'OBV'
|
|
| 'VOLUME_OSC'
|
|
| 'VR'
|
|
| 'DMI';
|
|
|
|
export type ConditionType =
|
|
| 'CROSS_UP'
|
|
| 'CROSS_DOWN'
|
|
| 'ABOVE'
|
|
| 'BELOW'
|
|
| 'EQUAL'
|
|
| 'BETWEEN'
|
|
| 'ZERO_CROSS_UP'
|
|
| 'ZERO_CROSS_DOWN'
|
|
| 'SIGNAL_CROSS_UP'
|
|
| 'SIGNAL_CROSS_DOWN'
|
|
| 'HISTOGRAM_POSITIVE'
|
|
| 'HISTOGRAM_NEGATIVE'
|
|
| 'HISTOGRAM_INCREASE'
|
|
| 'HISTOGRAM_DECREASE'
|
|
| 'K_CROSS_D_UP'
|
|
| 'K_CROSS_D_DOWN'
|
|
| 'VOLUME_SPIKE'
|
|
| 'VOLUME_ABOVE_AVG'
|
|
| 'VOLUME_BELOW_AVG'
|
|
| 'INCREASE'
|
|
| 'DECREASE'
|
|
| 'GOLDEN_CROSS'
|
|
| 'DEAD_CROSS';
|
|
|
|
export interface AlertConditionDto {
|
|
id?: number;
|
|
cryptoAlertId?: number;
|
|
indicatorType: IndicatorType;
|
|
conditionType: ConditionType;
|
|
targetValue?: number;
|
|
secondaryValue?: number;
|
|
period?: number;
|
|
comparePeriod?: number;
|
|
enabled?: boolean;
|
|
orderIndex?: number;
|
|
description?: string;
|
|
logicalOperator?: 'AND' | 'OR';
|
|
}
|
|
|
|
export interface CryptoAlertDto {
|
|
id?: number;
|
|
symbol: string;
|
|
koreanName: string;
|
|
englishName?: string;
|
|
enabled?: boolean;
|
|
userId?: number;
|
|
conditions?: AlertConditionDto[]; // 하위 호환성
|
|
createdAt?: string;
|
|
updatedAt?: string;
|
|
}
|
|
|
|
export interface AlertNotificationDto {
|
|
id?: number;
|
|
cryptoAlertId?: number;
|
|
symbol?: string;
|
|
koreanName?: string;
|
|
message: string;
|
|
conditionDescription?: string;
|
|
isRead?: boolean;
|
|
triggeredAt?: string;
|
|
notifiedAt?: string;
|
|
userId?: number;
|
|
/** 조건 충족한 봉 시각(백엔드 LocalDateTime → ISO 또는 배열) */
|
|
candleTime?: string;
|
|
/** 알림 스케줄/조건의 캔들 간격 (예: 3m, 1h, 1d) */
|
|
timeInterval?: string;
|
|
}
|
|
|
|
export interface IndicatorDataDto {
|
|
time?: string;
|
|
open?: number;
|
|
high?: number;
|
|
low?: number;
|
|
price?: number;
|
|
ma10?: number;
|
|
ma20?: number;
|
|
ma60?: number;
|
|
ma120?: number;
|
|
rsi?: number;
|
|
macdLine?: number;
|
|
signalLine?: number;
|
|
histogram?: number;
|
|
stochK?: number;
|
|
stochD?: number;
|
|
cci?: number;
|
|
adx?: number;
|
|
plusDI?: number;
|
|
minusDI?: number;
|
|
williamsR?: number;
|
|
newPsy?: number;
|
|
investPsy?: number;
|
|
bwi?: number;
|
|
obv?: number;
|
|
obvSignal?: number; // OBV Signal Line (이동평균)
|
|
volumeOsc?: number;
|
|
vr?: number;
|
|
trix?: number;
|
|
trixSignal?: number; // TRIX Signal Line
|
|
ichimokuTenkan?: number;
|
|
ichimokuKijun?: number;
|
|
ichimokuSenkouA?: number;
|
|
ichimokuSenkouB?: number;
|
|
ichimokuChikou?: number;
|
|
disparityMid?: number;
|
|
volume?: number;
|
|
}
|
|
|
|
export interface AlertNotificationDetailDto extends AlertNotificationDto {
|
|
englishName?: string;
|
|
indicatorData?: IndicatorDataDto;
|
|
/** 알림 발생 시점 전후 지표 시계열 (조건 관련 차트 렌더링용) */
|
|
indicatorDataSeries?: IndicatorDataDto[];
|
|
satisfiedConditions?: AlertConditionDto[];
|
|
currentPrice?: number;
|
|
changeRate?: number;
|
|
volume?: number;
|
|
/** 백엔드: 알림 발생 봉 기준 OHLCV 스냅샷 */
|
|
snapshotOpen?: number;
|
|
snapshotHigh?: number;
|
|
snapshotLow?: number;
|
|
snapshotClose?: number;
|
|
snapshotVolume?: number;
|
|
}
|
|
|
|
export interface AlertScheduleConfigDto {
|
|
id?: number;
|
|
userId?: number;
|
|
cronExpression?: string;
|
|
timeInterval?: string; // 시간봉 간격 (1m, 3m, 5m, 10m, 15m, 30m, 1h, 4h, 1d, 1w)
|
|
/** 전략 조건 평가 주기(초) 3~30, 스케줄러 실행 간격 */
|
|
strategyCheckIntervalSeconds?: number;
|
|
enabled?: boolean;
|
|
lastRunTime?: string;
|
|
nextRunTime?: string;
|
|
createdAt?: string;
|
|
updatedAt?: string;
|
|
}
|
|
|
|
// ==================== CryptoAlert APIs ====================
|
|
|
|
/**
|
|
* 전체 알림 목록 조회
|
|
*/
|
|
export const getAllAlerts = async (): Promise<CryptoAlertDto[]> => {
|
|
const response = await alertApi.get<CryptoAlertDto[]>('/alerts');
|
|
return response.data;
|
|
};
|
|
|
|
/**
|
|
* 활성화된 알림 목록 조회
|
|
*/
|
|
export const getEnabledAlerts = async (): Promise<CryptoAlertDto[]> => {
|
|
const response = await alertApi.get<CryptoAlertDto[]>('/alerts/enabled');
|
|
return response.data;
|
|
};
|
|
|
|
/**
|
|
* 알림 상세 조회
|
|
*/
|
|
export const getAlert = async (alertId: number): Promise<CryptoAlertDto> => {
|
|
const response = await alertApi.get<CryptoAlertDto>(`/alerts/${alertId}`);
|
|
return response.data;
|
|
};
|
|
|
|
/**
|
|
* 심볼로 알림 조회
|
|
*/
|
|
export const getAlertBySymbol = async (symbol: string): Promise<CryptoAlertDto | null> => {
|
|
try {
|
|
const response = await alertApi.get<CryptoAlertDto>(`/alerts/symbol/${symbol}`);
|
|
return response.data;
|
|
} catch (error: any) {
|
|
if (error.response?.status === 404) {
|
|
return null;
|
|
}
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 알림 생성
|
|
*/
|
|
export const createAlert = async (dto: CryptoAlertDto): Promise<CryptoAlertDto> => {
|
|
const response = await alertApi.post<CryptoAlertDto>('/alerts', dto);
|
|
return response.data;
|
|
};
|
|
|
|
/**
|
|
* 알림 ON/OFF 토글
|
|
*/
|
|
export const toggleAlert = async (alertId: number, enabled: boolean): Promise<CryptoAlertDto> => {
|
|
const response = await alertApi.put<CryptoAlertDto>(
|
|
`/alerts/${alertId}/toggle`,
|
|
null,
|
|
{ params: { enabled } }
|
|
);
|
|
return response.data;
|
|
};
|
|
|
|
/**
|
|
* 알림 삭제
|
|
*/
|
|
export const deleteAlert = async (alertId: number): Promise<void> => {
|
|
await alertApi.delete(`/alerts/${alertId}`);
|
|
};
|
|
|
|
// ==================== AlertCondition APIs ====================
|
|
|
|
/**
|
|
* 전역 알림 조건 목록 조회
|
|
*/
|
|
export const getGlobalConditions = async (): Promise<AlertConditionDto[]> => {
|
|
const response = await alertApi.get<AlertConditionDto[]>('/alerts/conditions/global');
|
|
return response.data;
|
|
};
|
|
|
|
/**
|
|
* 알림 조건 목록 조회 (개별 종목용 - 하위 호환)
|
|
*/
|
|
export const getConditions = async (alertId: number): Promise<AlertConditionDto[]> => {
|
|
const response = await alertApi.get<AlertConditionDto[]>(`/alerts/${alertId}/conditions`);
|
|
return response.data;
|
|
};
|
|
|
|
/**
|
|
* 전역 조건 추가
|
|
*/
|
|
export const addGlobalCondition = async (dto: AlertConditionDto): Promise<AlertConditionDto> => {
|
|
const response = await alertApi.post<AlertConditionDto>('/alerts/conditions/global', dto);
|
|
return response.data;
|
|
};
|
|
|
|
/**
|
|
* 조건 추가 (개별 종목용 - 하위 호환)
|
|
*/
|
|
export const addCondition = async (
|
|
alertId: number,
|
|
dto: AlertConditionDto
|
|
): Promise<AlertConditionDto> => {
|
|
const response = await alertApi.post<AlertConditionDto>(
|
|
`/alerts/${alertId}/conditions`,
|
|
dto
|
|
);
|
|
return response.data;
|
|
};
|
|
|
|
/**
|
|
* 조건 수정
|
|
*/
|
|
export const updateCondition = async (
|
|
conditionId: number,
|
|
dto: AlertConditionDto
|
|
): Promise<AlertConditionDto> => {
|
|
const response = await alertApi.put<AlertConditionDto>(
|
|
`/alerts/conditions/${conditionId}`,
|
|
dto
|
|
);
|
|
return response.data;
|
|
};
|
|
|
|
/**
|
|
* 조건 삭제
|
|
*/
|
|
export const deleteCondition = async (conditionId: number): Promise<void> => {
|
|
await alertApi.delete(`/alerts/conditions/${conditionId}`);
|
|
};
|
|
|
|
/**
|
|
* 조건 활성화/비활성화
|
|
*/
|
|
export const toggleCondition = async (
|
|
conditionId: number,
|
|
enabled: boolean
|
|
): Promise<AlertConditionDto> => {
|
|
const response = await alertApi.put<AlertConditionDto>(
|
|
`/alerts/conditions/${conditionId}/toggle`,
|
|
null,
|
|
{ params: { enabled } }
|
|
);
|
|
return response.data;
|
|
};
|
|
|
|
// ==================== AlertNotification APIs ====================
|
|
|
|
/**
|
|
* 미읽음 알림 조회
|
|
*/
|
|
export const getUnreadNotifications = async (userId?: number): Promise<AlertNotificationDto[]> => {
|
|
const response = await alertApi.get<AlertNotificationDto[]>(
|
|
'/alerts/notifications/unread',
|
|
{ params: { userId } }
|
|
);
|
|
return response.data;
|
|
};
|
|
|
|
/**
|
|
* 알림 목록 조회 (페이징)
|
|
*/
|
|
export const getNotifications = async (
|
|
userId?: number,
|
|
page: number = 0,
|
|
size: number = 20
|
|
): Promise<{ content: AlertNotificationDto[]; totalPages: number; totalElements: number }> => {
|
|
const response = await alertApi.get('/alerts/notifications', {
|
|
params: { userId, page, size },
|
|
});
|
|
return response.data;
|
|
};
|
|
|
|
/**
|
|
* 알림 읽음 처리
|
|
*/
|
|
export const markAsRead = async (notificationId: number): Promise<void> => {
|
|
await alertApi.put(`/alerts/notifications/${notificationId}/read`);
|
|
};
|
|
|
|
/**
|
|
* 모든 미읽은 알림 읽음 처리
|
|
*/
|
|
export const markAllAsRead = async (): Promise<void> => {
|
|
await alertApi.put('/alerts/notifications/read-all');
|
|
};
|
|
|
|
/**
|
|
* 알림 상세 정보 조회 (분석 데이터 포함)
|
|
* @param symbol 통합 알림 등 행별 마켓(예: KRW-BTC). 지정 시 해당 종목 캔들·지표로 스냅샷 (알림 봉 시각은 동일).
|
|
*/
|
|
export const getNotificationDetail = async (
|
|
notificationId: number,
|
|
options?: { symbol?: string | null }
|
|
): Promise<AlertNotificationDetailDto> => {
|
|
const sym = options?.symbol?.trim();
|
|
const response = await alertApi.get<AlertNotificationDetailDto>(
|
|
`/alerts/notifications/${notificationId}/detail`,
|
|
sym ? { params: { symbol: sym } } : undefined
|
|
);
|
|
return response.data;
|
|
};
|
|
|
|
// ==================== AlertScheduleConfig APIs ====================
|
|
|
|
/**
|
|
* 스케줄 설정 조회
|
|
*/
|
|
export const getScheduleConfig = async (userId?: number): Promise<AlertScheduleConfigDto> => {
|
|
const response = await alertApi.get<AlertScheduleConfigDto>(
|
|
'/alerts/schedule',
|
|
{ params: { userId } }
|
|
);
|
|
return response.data;
|
|
};
|
|
|
|
/**
|
|
* 스케줄 설정 저장/업데이트
|
|
*/
|
|
export const saveScheduleConfig = async (
|
|
dto: AlertScheduleConfigDto
|
|
): Promise<AlertScheduleConfigDto> => {
|
|
const response = await alertApi.put<AlertScheduleConfigDto>('/alerts/schedule', dto);
|
|
return response.data;
|
|
};
|
|
|
|
/**
|
|
* 알림 삭제
|
|
*/
|
|
export const deleteNotification = async (notificationId: number): Promise<void> => {
|
|
await alertApi.delete(`/alerts/notifications/${notificationId}`);
|
|
};
|
|
|
|
/**
|
|
* 읽은 알림 모두 삭제
|
|
*/
|
|
export const deleteReadNotifications = async (userId?: number): Promise<void> => {
|
|
await alertApi.delete('/alerts/notifications/read', {
|
|
params: { userId },
|
|
});
|
|
};
|
|
|
|
// ==================== Scheduler Service APIs ====================
|
|
|
|
export interface SchedulerStatusDto {
|
|
running: boolean;
|
|
status?: string;
|
|
error?: string;
|
|
schedulerRunning?: boolean;
|
|
kafkaConnected?: boolean;
|
|
}
|
|
|
|
export interface SchedulerStartResultDto {
|
|
success: boolean;
|
|
message: string;
|
|
manualCommand?: string;
|
|
}
|
|
|
|
/**
|
|
* 스케줄러 서비스 상태 조회
|
|
*/
|
|
export const getSchedulerStatus = async (): Promise<SchedulerStatusDto> => {
|
|
const response = await alertApi.get<SchedulerStatusDto>('/scheduler/status');
|
|
return response.data;
|
|
};
|
|
|
|
/**
|
|
* 스케줄러 서비스 시작 시도
|
|
*/
|
|
export const startScheduler = async (): Promise<SchedulerStartResultDto> => {
|
|
const response = await alertApi.post<SchedulerStartResultDto>('/scheduler/start');
|
|
return response.data;
|
|
};
|
|
|
|
/** Internal `/internal/alerts/evaluate` 요청 본문 */
|
|
export interface AlertEvaluateRequest {
|
|
symbol: string;
|
|
strategyRuleId: number;
|
|
timeInterval: string;
|
|
liveCandles?: Record<string, unknown>[];
|
|
useLiveCandleTail?: boolean;
|
|
}
|
|
|
|
export interface AlertEvaluateResponse {
|
|
symbol?: string;
|
|
strategyRuleId?: number;
|
|
conditionMet: boolean;
|
|
message?: string;
|
|
}
|
|
|
|
/**
|
|
* 알림 전략 조건 평가 (스케줄러와 동일 백엔드 경로)
|
|
* `liveCandles`가 있으면 WS 버퍼 기준, 없으면 서버 Redis/REST 200봉.
|
|
*/
|
|
export const evaluateAlertStrategy = async (body: AlertEvaluateRequest): Promise<AlertEvaluateResponse> => {
|
|
const response = await alertApi.post<AlertEvaluateResponse>('/internal/alerts/evaluate', body);
|
|
return response.data;
|
|
};
|
|
|
|
/**
|
|
* 테스트 알림 생성 (파이프라인 검증용)
|
|
* Internal API - 첫 번째 종목으로 테스트 알림 생성
|
|
*/
|
|
export const createTestNotification = async (): Promise<AlertNotificationDto> => {
|
|
const response = await alertApi.post<AlertNotificationDto>('/internal/alerts/test');
|
|
return response.data;
|
|
};
|
|
|
|
/**
|
|
* 알림 읽음 처리
|
|
*/
|
|
export const markNotificationAsRead = async (notificationId: number): Promise<void> => {
|
|
await alertApi.put(`/alerts/notifications/${notificationId}/read`);
|
|
};
|
|
|
|
export default {
|
|
getAllAlerts,
|
|
getEnabledAlerts,
|
|
getAlert,
|
|
getAlertBySymbol,
|
|
createAlert,
|
|
toggleAlert,
|
|
deleteAlert,
|
|
getGlobalConditions,
|
|
getConditions,
|
|
addGlobalCondition,
|
|
addCondition,
|
|
updateCondition,
|
|
deleteCondition,
|
|
toggleCondition,
|
|
getUnreadNotifications,
|
|
getNotifications,
|
|
markAsRead,
|
|
getScheduleConfig,
|
|
saveScheduleConfig,
|
|
deleteNotification,
|
|
deleteReadNotifications,
|
|
markNotificationAsRead,
|
|
};
|
|
|