612 lines
18 KiB
TypeScript
612 lines
18 KiB
TypeScript
import type {
|
|
DashboardChartTimeframe,
|
|
DashboardStockItem,
|
|
StockCandlePoint,
|
|
} from "@/features/trade/types/trade.types";
|
|
import type { KisCredentialInput } from "@/lib/kis/config";
|
|
import { kisGet } from "@/lib/kis/client";
|
|
import {
|
|
mapDomesticKisSessionToMarketPhase,
|
|
resolveDomesticKisSession,
|
|
shouldUseOvertimeOrderBookApi,
|
|
} from "@/lib/kis/domestic-market-session";
|
|
import {
|
|
firstDefinedNumber,
|
|
firstDefinedString,
|
|
firstPositive,
|
|
mergeCandlesByTimestamp,
|
|
minutesForTimeframe,
|
|
normalizeSignedValue,
|
|
nowHmsInKst,
|
|
nowYmdInKst,
|
|
parseDayCandleRow,
|
|
parseMinuteCandleRow,
|
|
parseOutput2Rows,
|
|
readRowString,
|
|
resolveCurrentPriceSource,
|
|
resolveMarket,
|
|
shiftYmd,
|
|
subOneMinute,
|
|
toCandles,
|
|
toNumber,
|
|
toOptionalNumber,
|
|
} from "@/lib/kis/domestic-helpers";
|
|
|
|
/**
|
|
* @file lib/kis/domestic.ts
|
|
* @description KIS 국내주식(현재가/일봉) 조회와 대시보드 모델 변환
|
|
*/
|
|
|
|
interface KisDomesticQuoteOutput {
|
|
hts_kor_isnm?: string;
|
|
rprs_mrkt_kor_name?: string;
|
|
bstp_kor_isnm?: string;
|
|
stck_prpr?: string;
|
|
prdy_vrss?: string;
|
|
prdy_vrss_sign?: string;
|
|
prdy_ctrt?: string;
|
|
stck_oprc?: string;
|
|
stck_hgpr?: string;
|
|
stck_lwpr?: string;
|
|
stck_sdpr?: string;
|
|
stck_prdy_clpr?: string;
|
|
acml_vol?: string;
|
|
}
|
|
|
|
interface KisDomesticCcnlOutput {
|
|
stck_prpr?: string;
|
|
prdy_vrss?: string;
|
|
prdy_vrss_sign?: string;
|
|
prdy_ctrt?: string;
|
|
cntg_vol?: string;
|
|
}
|
|
|
|
interface KisDomesticOvertimePriceOutput {
|
|
ovtm_untp_prpr?: string;
|
|
ovtm_untp_prdy_vrss?: string;
|
|
ovtm_untp_prdy_vrss_sign?: string;
|
|
ovtm_untp_prdy_ctrt?: string;
|
|
ovtm_untp_vol?: string;
|
|
ovtm_untp_oprc?: string;
|
|
ovtm_untp_hgpr?: string;
|
|
ovtm_untp_lwpr?: string;
|
|
}
|
|
|
|
interface KisDomesticDailyPriceOutput {
|
|
stck_bsop_date?: string;
|
|
stck_oprc?: string;
|
|
stck_hgpr?: string;
|
|
stck_lwpr?: string;
|
|
stck_clpr?: string;
|
|
acml_vol?: string;
|
|
}
|
|
export interface KisDomesticOrderBookOutput {
|
|
stck_prpr?: string;
|
|
total_askp_rsqn?: string;
|
|
total_bidp_rsqn?: string;
|
|
askp1?: string;
|
|
askp2?: string;
|
|
askp3?: string;
|
|
askp4?: string;
|
|
askp5?: string;
|
|
askp6?: string;
|
|
askp7?: string;
|
|
askp8?: string;
|
|
askp9?: string;
|
|
askp10?: string;
|
|
bidp1?: string;
|
|
bidp2?: string;
|
|
bidp3?: string;
|
|
bidp4?: string;
|
|
bidp5?: string;
|
|
bidp6?: string;
|
|
bidp7?: string;
|
|
bidp8?: string;
|
|
bidp9?: string;
|
|
bidp10?: string;
|
|
askp_rsqn1?: string;
|
|
askp_rsqn2?: string;
|
|
askp_rsqn3?: string;
|
|
askp_rsqn4?: string;
|
|
askp_rsqn5?: string;
|
|
askp_rsqn6?: string;
|
|
askp_rsqn7?: string;
|
|
askp_rsqn8?: string;
|
|
askp_rsqn9?: string;
|
|
askp_rsqn10?: string;
|
|
bidp_rsqn1?: string;
|
|
bidp_rsqn2?: string;
|
|
bidp_rsqn3?: string;
|
|
bidp_rsqn4?: string;
|
|
bidp_rsqn5?: string;
|
|
bidp_rsqn6?: string;
|
|
bidp_rsqn7?: string;
|
|
bidp_rsqn8?: string;
|
|
bidp_rsqn9?: string;
|
|
bidp_rsqn10?: string;
|
|
}
|
|
|
|
interface DashboardStockFallbackMeta {
|
|
name?: string;
|
|
market?: "KOSPI" | "KOSDAQ";
|
|
}
|
|
|
|
export type DomesticMarketPhase = "regular" | "afterHours";
|
|
export type DomesticPriceSource =
|
|
| "inquire-price"
|
|
| "inquire-ccnl"
|
|
| "inquire-overtime-price";
|
|
|
|
interface DomesticOverviewResult {
|
|
stock: DashboardStockItem;
|
|
priceSource: DomesticPriceSource;
|
|
marketPhase: DomesticMarketPhase;
|
|
}
|
|
|
|
interface DomesticSessionAwareOptions {
|
|
sessionOverride?: string | null;
|
|
}
|
|
|
|
/**
|
|
* 국내주식 현재가 조회
|
|
* @param symbol 6자리 종목코드
|
|
* @param credentials 사용자 입력 키
|
|
* @returns KIS 현재가 output
|
|
*/
|
|
export async function getDomesticQuote(
|
|
symbol: string,
|
|
credentials?: KisCredentialInput,
|
|
) {
|
|
const response = await kisGet<KisDomesticQuoteOutput>(
|
|
"/uapi/domestic-stock/v1/quotations/inquire-price",
|
|
"FHKST01010100",
|
|
{
|
|
FID_COND_MRKT_DIV_CODE: resolvePriceMarketDivCode(),
|
|
FID_INPUT_ISCD: symbol,
|
|
},
|
|
credentials,
|
|
);
|
|
|
|
return response.output ?? {};
|
|
}
|
|
|
|
/**
|
|
* 국내주식 일자별 시세 조회
|
|
* @param symbol 6자리 종목코드
|
|
* @param credentials 사용자 입력 키
|
|
* @returns KIS 일봉 output 배열
|
|
*/
|
|
export async function getDomesticDailyPrice(
|
|
symbol: string,
|
|
credentials?: KisCredentialInput,
|
|
) {
|
|
const response = await kisGet<KisDomesticDailyPriceOutput[]>(
|
|
"/uapi/domestic-stock/v1/quotations/inquire-daily-price",
|
|
"FHKST01010400",
|
|
{
|
|
FID_COND_MRKT_DIV_CODE: "J",
|
|
FID_INPUT_ISCD: symbol,
|
|
FID_PERIOD_DIV_CODE: "D",
|
|
FID_ORG_ADJ_PRC: "1",
|
|
},
|
|
credentials,
|
|
);
|
|
|
|
return Array.isArray(response.output) ? response.output : [];
|
|
}
|
|
|
|
/**
|
|
* 국내주식 현재가 체결 조회
|
|
* @param symbol 6자리 종목코드
|
|
* @param credentials 사용자 입력 키
|
|
* @returns KIS 체결 output
|
|
* @see app/api/kis/domestic/overview/route.ts GET - 대시보드 상세 응답에서 장중 가격 우선 소스
|
|
*/
|
|
export async function getDomesticConclusion(
|
|
symbol: string,
|
|
credentials?: KisCredentialInput,
|
|
) {
|
|
const response = await kisGet<
|
|
KisDomesticCcnlOutput | KisDomesticCcnlOutput[]
|
|
>(
|
|
"/uapi/domestic-stock/v1/quotations/inquire-ccnl",
|
|
"FHKST01010300",
|
|
{
|
|
FID_COND_MRKT_DIV_CODE: resolvePriceMarketDivCode(),
|
|
FID_INPUT_ISCD: symbol,
|
|
},
|
|
credentials,
|
|
);
|
|
|
|
const output = response.output;
|
|
if (Array.isArray(output)) return output[0] ?? {};
|
|
return output ?? {};
|
|
}
|
|
|
|
/**
|
|
* 국내주식 시간외 현재가 조회
|
|
* @param symbol 6자리 종목코드
|
|
* @param credentials 사용자 입력 키
|
|
* @returns KIS 시간외 현재가 output
|
|
* @see app/api/kis/domestic/overview/route.ts GET - 대시보드 상세 응답에서 장외 가격 우선 소스
|
|
*/
|
|
export async function getDomesticOvertimePrice(
|
|
symbol: string,
|
|
credentials?: KisCredentialInput,
|
|
) {
|
|
const response = await kisGet<KisDomesticOvertimePriceOutput>(
|
|
"/uapi/domestic-stock/v1/quotations/inquire-overtime-price",
|
|
"FHPST02300000",
|
|
{
|
|
FID_COND_MRKT_DIV_CODE: "J",
|
|
FID_INPUT_ISCD: symbol,
|
|
},
|
|
credentials,
|
|
);
|
|
|
|
return response.output ?? {};
|
|
}
|
|
|
|
/**
|
|
* 국내주식 호가(10단계) 조회
|
|
* @param symbol 6자리 종목코드
|
|
* @param credentials 사용자 입력 키
|
|
* @returns KIS 호가 output
|
|
*/
|
|
export async function getDomesticOrderBook(
|
|
symbol: string,
|
|
credentials?: KisCredentialInput,
|
|
options?: DomesticSessionAwareOptions,
|
|
) {
|
|
const session = resolveDomesticKisSession(options?.sessionOverride);
|
|
const useOvertimeApi = shouldUseOvertimeOrderBookApi(session);
|
|
const apiPath = useOvertimeApi
|
|
? "/uapi/domestic-stock/v1/quotations/inquire-overtime-asking-price"
|
|
: "/uapi/domestic-stock/v1/quotations/inquire-asking-price-exp-ccn";
|
|
const trId = useOvertimeApi ? "FHPST02300400" : "FHKST01010200";
|
|
|
|
const response = await kisGet<KisDomesticOrderBookOutput>(
|
|
apiPath,
|
|
trId,
|
|
{
|
|
FID_COND_MRKT_DIV_CODE: resolvePriceMarketDivCode(),
|
|
FID_INPUT_ISCD: symbol,
|
|
},
|
|
credentials,
|
|
);
|
|
|
|
if (response.output && typeof response.output === "object") {
|
|
return response.output;
|
|
}
|
|
|
|
if (response.output1 && typeof response.output1 === "object") {
|
|
return response.output1 as KisDomesticOrderBookOutput;
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
/**
|
|
* 현재가 + 일봉을 대시보드 모델로 변환
|
|
* @param symbol 6자리 종목코드
|
|
* @param fallbackMeta 보정 메타(종목명/시장)
|
|
* @param credentials 사용자 입력 키
|
|
* @returns DashboardStockItem
|
|
*/
|
|
export async function getDomesticOverview(
|
|
symbol: string,
|
|
fallbackMeta?: DashboardStockFallbackMeta,
|
|
credentials?: KisCredentialInput,
|
|
options?: DomesticSessionAwareOptions,
|
|
): Promise<DomesticOverviewResult> {
|
|
const marketPhase = getDomesticMarketPhaseInKst(
|
|
new Date(),
|
|
options?.sessionOverride,
|
|
);
|
|
const emptyQuote: KisDomesticQuoteOutput = {};
|
|
const emptyDaily: KisDomesticDailyPriceOutput[] = [];
|
|
const emptyCcnl: KisDomesticCcnlOutput = {};
|
|
const emptyOvertime: KisDomesticOvertimePriceOutput = {};
|
|
|
|
const [quote, daily, ccnl, overtime] = await Promise.all([
|
|
getDomesticQuote(symbol, credentials).catch(() => emptyQuote),
|
|
getDomesticDailyPrice(symbol, credentials).catch(() => emptyDaily),
|
|
getDomesticConclusion(symbol, credentials).catch(() => emptyCcnl),
|
|
marketPhase === "afterHours"
|
|
? getDomesticOvertimePrice(symbol, credentials).catch(() => emptyOvertime)
|
|
: Promise.resolve(emptyOvertime),
|
|
]);
|
|
|
|
const currentPrice =
|
|
firstDefinedNumber(
|
|
toOptionalNumber(ccnl.stck_prpr),
|
|
toOptionalNumber(overtime.ovtm_untp_prpr),
|
|
toOptionalNumber(quote.stck_prpr),
|
|
) ?? 0;
|
|
|
|
const currentPriceSource = resolveCurrentPriceSource(
|
|
marketPhase,
|
|
overtime,
|
|
ccnl,
|
|
quote,
|
|
);
|
|
|
|
const rawChange =
|
|
firstDefinedNumber(
|
|
toOptionalNumber(ccnl.prdy_vrss),
|
|
toOptionalNumber(overtime.ovtm_untp_prdy_vrss),
|
|
toOptionalNumber(quote.prdy_vrss),
|
|
) ?? 0;
|
|
|
|
const signCode = firstDefinedString(
|
|
ccnl.prdy_vrss_sign,
|
|
overtime.ovtm_untp_prdy_vrss_sign,
|
|
quote.prdy_vrss_sign,
|
|
);
|
|
|
|
const change = normalizeSignedValue(rawChange, signCode);
|
|
|
|
const rawChangeRate =
|
|
firstDefinedNumber(
|
|
toOptionalNumber(ccnl.prdy_ctrt),
|
|
toOptionalNumber(overtime.ovtm_untp_prdy_ctrt),
|
|
toOptionalNumber(quote.prdy_ctrt),
|
|
) ?? 0;
|
|
|
|
const changeRate = normalizeSignedValue(rawChangeRate, signCode);
|
|
|
|
const prevClose = firstPositive(
|
|
toNumber(quote.stck_sdpr),
|
|
toNumber(quote.stck_prdy_clpr),
|
|
Math.max(currentPrice - change, 0),
|
|
);
|
|
|
|
const candles = toCandles(daily, currentPrice);
|
|
|
|
return {
|
|
stock: {
|
|
symbol,
|
|
name: quote.hts_kor_isnm?.trim() || fallbackMeta?.name || symbol,
|
|
market: resolveMarket(
|
|
quote.rprs_mrkt_kor_name,
|
|
quote.bstp_kor_isnm,
|
|
fallbackMeta?.market,
|
|
),
|
|
currentPrice,
|
|
change,
|
|
changeRate,
|
|
open: firstPositive(
|
|
toNumber(overtime.ovtm_untp_oprc),
|
|
toNumber(quote.stck_oprc),
|
|
currentPrice,
|
|
),
|
|
high: firstPositive(
|
|
toNumber(overtime.ovtm_untp_hgpr),
|
|
toNumber(quote.stck_hgpr),
|
|
currentPrice,
|
|
),
|
|
low: firstPositive(
|
|
toNumber(overtime.ovtm_untp_lwpr),
|
|
toNumber(quote.stck_lwpr),
|
|
currentPrice,
|
|
),
|
|
prevClose,
|
|
volume: firstPositive(
|
|
toNumber(overtime.ovtm_untp_vol),
|
|
toNumber(quote.acml_vol),
|
|
toNumber(ccnl.cntg_vol),
|
|
),
|
|
candles,
|
|
},
|
|
priceSource: currentPriceSource,
|
|
marketPhase,
|
|
};
|
|
}
|
|
|
|
function getDomesticMarketPhaseInKst(
|
|
now = new Date(),
|
|
sessionOverride?: string | null,
|
|
): DomesticMarketPhase {
|
|
return mapDomesticKisSessionToMarketPhase(
|
|
resolveDomesticKisSession(sessionOverride, now),
|
|
);
|
|
}
|
|
|
|
function resolvePriceMarketDivCode() {
|
|
return "J";
|
|
}
|
|
|
|
export interface DomesticChartResult {
|
|
candles: StockCandlePoint[];
|
|
nextCursor: string | null;
|
|
hasMore: boolean;
|
|
}
|
|
|
|
/**
|
|
* 국내주식 주식일별분봉조회 (과거 분봉)
|
|
* @param symbol 종목코드
|
|
* @param date 조회할 날짜 (YYYYMMDD)
|
|
* @param time 조회할 기준 시간 (HHMMSS) - 이 시간부터 과거로 조회
|
|
* @param credentials
|
|
*/
|
|
export async function getDomesticDailyTimeChart(
|
|
symbol: string,
|
|
date: string,
|
|
time: string,
|
|
credentials?: KisCredentialInput,
|
|
) {
|
|
const response = await kisGet<unknown>(
|
|
"/uapi/domestic-stock/v1/quotations/inquire-time-dailychartprice",
|
|
"FHKST03010230",
|
|
{
|
|
FID_COND_MRKT_DIV_CODE: "J",
|
|
FID_INPUT_ISCD: symbol,
|
|
FID_INPUT_DATE_1: date,
|
|
FID_INPUT_HOUR_1: time,
|
|
FID_PW_DATA_INCU_YN: "N",
|
|
FID_FAKE_TICK_INCU_YN: "",
|
|
},
|
|
credentials,
|
|
);
|
|
|
|
return parseOutput2Rows(response);
|
|
}
|
|
|
|
// ─── 차트 데이터 조회 메인 ─────────────────────────────────
|
|
|
|
/**
|
|
* 종목 차트 데이터 조회 (일봉/주봉/분봉)
|
|
* - 일봉/주봉: inquire-daily-itemchartprice (FHKST03010100), 과거 페이징 지원
|
|
* - 분봉 (오늘): inquire-time-itemchartprice (FHKST03010200)
|
|
* - 분봉 (과거): inquire-time-dailychartprice (FHKST03010230)
|
|
*/
|
|
export async function getDomesticChart(
|
|
symbol: string,
|
|
timeframe: DashboardChartTimeframe,
|
|
credentials?: KisCredentialInput,
|
|
cursor?: string,
|
|
): Promise<DomesticChartResult> {
|
|
// ── 일봉 / 주봉 ──
|
|
if (timeframe === "1d" || timeframe === "1w") {
|
|
const endDate = cursor && /^\d{8}$/.test(cursor) ? cursor : nowYmdInKst();
|
|
const startDate = shiftYmd(endDate, timeframe === "1w" ? -700 : -365);
|
|
|
|
const response = await kisGet<unknown>(
|
|
"/uapi/domestic-stock/v1/quotations/inquire-daily-itemchartprice",
|
|
"FHKST03010100",
|
|
{
|
|
FID_COND_MRKT_DIV_CODE: "J",
|
|
FID_INPUT_ISCD: symbol,
|
|
FID_INPUT_DATE_1: startDate,
|
|
FID_INPUT_DATE_2: endDate,
|
|
FID_PERIOD_DIV_CODE: timeframe === "1w" ? "W" : "D",
|
|
FID_ORG_ADJ_PRC: "1",
|
|
},
|
|
credentials,
|
|
);
|
|
|
|
const parsed = parseOutput2Rows(response)
|
|
.map(parseDayCandleRow)
|
|
.filter((c): c is StockCandlePoint => Boolean(c))
|
|
.sort((a, b) => (a.timestamp ?? 0) - (b.timestamp ?? 0));
|
|
|
|
const oldest = parsed[0];
|
|
const nextCursor =
|
|
parsed.length >= 95 && oldest?.timestamp
|
|
? shiftYmd(
|
|
new Date(oldest.timestamp * 1000)
|
|
.toISOString()
|
|
.slice(0, 10)
|
|
.replace(/-/g, ""),
|
|
-1,
|
|
)
|
|
: null;
|
|
|
|
return { candles: parsed, hasMore: Boolean(nextCursor), nextCursor };
|
|
}
|
|
|
|
// ── 분봉 (1m / 30m / 1h) ──
|
|
const minuteBucket = minutesForTimeframe(timeframe);
|
|
let rawRows: Array<Record<string, unknown>> = [];
|
|
let nextCursor: string | null = null;
|
|
|
|
// Case A: 과거 데이터 조회 (커서 존재)
|
|
if (cursor && cursor.length >= 8) {
|
|
const targetDate = cursor.slice(0, 8);
|
|
const targetTime = cursor.slice(8) || "153000";
|
|
rawRows = await getDomesticDailyTimeChart(
|
|
symbol,
|
|
targetDate,
|
|
targetTime,
|
|
credentials,
|
|
);
|
|
|
|
// 다음 커서 계산
|
|
// 데이터가 있으면 가장 오래된 시간 - 1분? 혹은 해당 날짜의 09:00 도달 시 전일로 이동
|
|
// API가 시간 역순으로 데이터를 준다고 가정 (output[0]이 가장 최신, output[last]가 가장 과거)
|
|
// 실제 KIS API는 보통 최신순 정렬
|
|
if (rawRows.length > 0) {
|
|
// 가장 과거 데이터의 시간 확인
|
|
const oldestRow = rawRows[rawRows.length - 1]; // 마지막이 가장 과거라 가정
|
|
const oldestTime = readRowString(oldestRow, "stck_cntg_hour");
|
|
|
|
// 09:00:00보다 크면 계속 같은 날짜 페이징 (단, KIS가 120건씩 주므로)
|
|
// 만약 09시 근처라면 전일로 이동
|
|
// 간단히: 가져온 데이터 중 090000이 포함되어 있거나, 더 가져올 게 없어 보이면 전일로
|
|
// 여기서는 단순히 전일 153000으로 넘어가는 로직을 사용하거나,
|
|
// 현재 날짜에서 시간을 줄여서 재요청해야 함.
|
|
// KIS API가 '다음 커서'를 주지 않으므로, 마지막 데이터 시간을 기준으로 다음 요청
|
|
|
|
if (oldestTime && Number(oldestTime) > 90000) {
|
|
// 같은 날짜, 시간만 조정 (1분 전)
|
|
// HHMMSS -> number -> subtract -> string
|
|
// 편의상 120개 꽉 찼으면 마지막 시간 사용, 아니면 전일로
|
|
if (rawRows.length >= 120) {
|
|
nextCursor = targetDate + oldestTime; // 다음 요청 시 이 시간 '이전'을 달라고 해야 함 (Inclusive 여부 확인 필요)
|
|
// 만약 Inclusive라면 -1분 해야 함. 안전하게 -1분 처리
|
|
// 시간 연산이 복잡하므로, 단순히 전일로 넘어가는 게 나을 수도 있으나,
|
|
// 하루치 분봉이 380개라 120개로는 부족함.
|
|
// 따라서 시간 연산 필요.
|
|
nextCursor = targetDate + subOneMinute(oldestTime);
|
|
} else {
|
|
// 120개 미만이면 장 시작까지 다 가져왔다고 가정 -> 전일로
|
|
nextCursor = shiftYmd(targetDate, -1) + "153000";
|
|
}
|
|
} else {
|
|
// 09:00 도달 -> 전일로
|
|
nextCursor = shiftYmd(targetDate, -1) + "153000";
|
|
}
|
|
} else {
|
|
// 데이터 없음 (휴장일 등) -> 전일로 계속 시도 (최대 5일? 무한 루프 방지 필요하나 UI에서 제어)
|
|
nextCursor = shiftYmd(targetDate, -1) + "153000";
|
|
// 너무 과거(1년)면 중단? 일단 생략
|
|
}
|
|
|
|
} else {
|
|
// Case B: 초기 진입 (오늘 실시간/장중 데이터)
|
|
const response = await kisGet<unknown>(
|
|
"/uapi/domestic-stock/v1/quotations/inquire-time-itemchartprice",
|
|
"FHKST03010200",
|
|
{
|
|
FID_COND_MRKT_DIV_CODE: "J",
|
|
FID_INPUT_ISCD: symbol,
|
|
FID_INPUT_HOUR_1: nowHmsInKst(),
|
|
FID_PW_DATA_INCU_YN: "Y",
|
|
FID_ETC_CLS_CODE: "",
|
|
},
|
|
credentials,
|
|
);
|
|
rawRows = parseOutput2Rows(response);
|
|
|
|
// 초기 조회는 "오늘 가장 오래된 분봉" 기준으로 같은 날 이전 분봉을 우선 이어 붙입니다.
|
|
const oldestRow = rawRows[rawRows.length - 1];
|
|
const oldestTimeRaw = oldestRow
|
|
? readRowString(oldestRow, "stck_cntg_hour", "STCK_CNTG_HOUR")
|
|
: "";
|
|
const oldestDateRaw = oldestRow
|
|
? readRowString(oldestRow, "stck_bsop_date", "STCK_BSOP_DATE")
|
|
: "";
|
|
const oldestTime = /^\d{6}$/.test(oldestTimeRaw)
|
|
? oldestTimeRaw
|
|
: /^\d{4}$/.test(oldestTimeRaw)
|
|
? `${oldestTimeRaw}00`
|
|
: "";
|
|
const oldestDate = /^\d{8}$/.test(oldestDateRaw)
|
|
? oldestDateRaw
|
|
: nowYmdInKst();
|
|
|
|
nextCursor =
|
|
oldestTime && Number(oldestTime) > 90000
|
|
? oldestDate + subOneMinute(oldestTime)
|
|
: shiftYmd(oldestDate, -1) + "153000";
|
|
}
|
|
|
|
const candles = mergeCandlesByTimestamp(
|
|
rawRows
|
|
.map((row) => parseMinuteCandleRow(row, minuteBucket))
|
|
.filter((c): c is StockCandlePoint => Boolean(c)),
|
|
);
|
|
|
|
return { candles, hasMore: Boolean(nextCursor), nextCursor };
|
|
}
|