krx-cli 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -17
- package/SKILL.md +91 -4
- package/dist/cli.js +1193 -621
- package/dist/cli.js.map +4 -4
- package/dist/mcp.js +1106 -145
- package/dist/mcp.js.map +4 -4
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -12,9 +12,6 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
12
12
|
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
13
13
|
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
14
14
|
});
|
|
15
|
-
var __esm = (fn, res) => function __init() {
|
|
16
|
-
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
17
|
-
};
|
|
18
15
|
var __commonJS = (cb, mod) => function __require2() {
|
|
19
16
|
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
20
17
|
};
|
|
@@ -1202,8 +1199,8 @@ var require_command = __commonJS({
|
|
|
1202
1199
|
"node_modules/.pnpm/commander@14.0.3/node_modules/commander/lib/command.js"(exports) {
|
|
1203
1200
|
var EventEmitter = __require("node:events").EventEmitter;
|
|
1204
1201
|
var childProcess = __require("node:child_process");
|
|
1205
|
-
var
|
|
1206
|
-
var
|
|
1202
|
+
var path6 = __require("node:path");
|
|
1203
|
+
var fs6 = __require("node:fs");
|
|
1207
1204
|
var process3 = __require("node:process");
|
|
1208
1205
|
var { Argument: Argument2, humanReadableArgName } = require_argument();
|
|
1209
1206
|
var { CommanderError: CommanderError2 } = require_error();
|
|
@@ -2197,7 +2194,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2197
2194
|
* @param {string} subcommandName
|
|
2198
2195
|
*/
|
|
2199
2196
|
_checkForMissingExecutable(executableFile, executableDir, subcommandName) {
|
|
2200
|
-
if (
|
|
2197
|
+
if (fs6.existsSync(executableFile)) return;
|
|
2201
2198
|
const executableDirMessage = executableDir ? `searched for local subcommand relative to directory '${executableDir}'` : "no directory for search for local subcommand, use .executableDir() to supply a custom directory";
|
|
2202
2199
|
const executableMissing = `'${executableFile}' does not exist
|
|
2203
2200
|
- if '${subcommandName}' is not meant to be an executable command, remove description parameter from '.command()' and use '.description()' instead
|
|
@@ -2215,11 +2212,11 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2215
2212
|
let launchWithNode = false;
|
|
2216
2213
|
const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
|
|
2217
2214
|
function findFile(baseDir, baseName) {
|
|
2218
|
-
const localBin =
|
|
2219
|
-
if (
|
|
2220
|
-
if (sourceExt.includes(
|
|
2215
|
+
const localBin = path6.resolve(baseDir, baseName);
|
|
2216
|
+
if (fs6.existsSync(localBin)) return localBin;
|
|
2217
|
+
if (sourceExt.includes(path6.extname(baseName))) return void 0;
|
|
2221
2218
|
const foundExt = sourceExt.find(
|
|
2222
|
-
(ext) =>
|
|
2219
|
+
(ext) => fs6.existsSync(`${localBin}${ext}`)
|
|
2223
2220
|
);
|
|
2224
2221
|
if (foundExt) return `${localBin}${foundExt}`;
|
|
2225
2222
|
return void 0;
|
|
@@ -2231,21 +2228,21 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2231
2228
|
if (this._scriptPath) {
|
|
2232
2229
|
let resolvedScriptPath;
|
|
2233
2230
|
try {
|
|
2234
|
-
resolvedScriptPath =
|
|
2231
|
+
resolvedScriptPath = fs6.realpathSync(this._scriptPath);
|
|
2235
2232
|
} catch {
|
|
2236
2233
|
resolvedScriptPath = this._scriptPath;
|
|
2237
2234
|
}
|
|
2238
|
-
executableDir =
|
|
2239
|
-
|
|
2235
|
+
executableDir = path6.resolve(
|
|
2236
|
+
path6.dirname(resolvedScriptPath),
|
|
2240
2237
|
executableDir
|
|
2241
2238
|
);
|
|
2242
2239
|
}
|
|
2243
2240
|
if (executableDir) {
|
|
2244
2241
|
let localFile = findFile(executableDir, executableFile);
|
|
2245
2242
|
if (!localFile && !subcommand._executableFile && this._scriptPath) {
|
|
2246
|
-
const legacyName =
|
|
2243
|
+
const legacyName = path6.basename(
|
|
2247
2244
|
this._scriptPath,
|
|
2248
|
-
|
|
2245
|
+
path6.extname(this._scriptPath)
|
|
2249
2246
|
);
|
|
2250
2247
|
if (legacyName !== this._name) {
|
|
2251
2248
|
localFile = findFile(
|
|
@@ -2256,7 +2253,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2256
2253
|
}
|
|
2257
2254
|
executableFile = localFile || executableFile;
|
|
2258
2255
|
}
|
|
2259
|
-
launchWithNode = sourceExt.includes(
|
|
2256
|
+
launchWithNode = sourceExt.includes(path6.extname(executableFile));
|
|
2260
2257
|
let proc;
|
|
2261
2258
|
if (process3.platform !== "win32") {
|
|
2262
2259
|
if (launchWithNode) {
|
|
@@ -3171,7 +3168,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
3171
3168
|
* @return {Command}
|
|
3172
3169
|
*/
|
|
3173
3170
|
nameFromFilename(filename) {
|
|
3174
|
-
this._name =
|
|
3171
|
+
this._name = path6.basename(filename, path6.extname(filename));
|
|
3175
3172
|
return this;
|
|
3176
3173
|
}
|
|
3177
3174
|
/**
|
|
@@ -3185,9 +3182,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
3185
3182
|
* @param {string} [path]
|
|
3186
3183
|
* @return {(string|null|Command)}
|
|
3187
3184
|
*/
|
|
3188
|
-
executableDir(
|
|
3189
|
-
if (
|
|
3190
|
-
this._executableDir =
|
|
3185
|
+
executableDir(path7) {
|
|
3186
|
+
if (path7 === void 0) return this._executableDir;
|
|
3187
|
+
this._executableDir = path7;
|
|
3191
3188
|
return this;
|
|
3192
3189
|
}
|
|
3193
3190
|
/**
|
|
@@ -3465,72 +3462,6 @@ var require_commander = __commonJS({
|
|
|
3465
3462
|
}
|
|
3466
3463
|
});
|
|
3467
3464
|
|
|
3468
|
-
// src/client/rate-limit.ts
|
|
3469
|
-
var rate_limit_exports = {};
|
|
3470
|
-
__export(rate_limit_exports, {
|
|
3471
|
-
checkRateLimit: () => checkRateLimit,
|
|
3472
|
-
getRateLimitStatus: () => getRateLimitStatus,
|
|
3473
|
-
incrementCallCount: () => incrementCallCount
|
|
3474
|
-
});
|
|
3475
|
-
import * as fs from "node:fs";
|
|
3476
|
-
import * as path from "node:path";
|
|
3477
|
-
import * as os from "node:os";
|
|
3478
|
-
function today() {
|
|
3479
|
-
const now = /* @__PURE__ */ new Date();
|
|
3480
|
-
const yyyy = now.getFullYear().toString();
|
|
3481
|
-
const mm = (now.getMonth() + 1).toString().padStart(2, "0");
|
|
3482
|
-
const dd = now.getDate().toString().padStart(2, "0");
|
|
3483
|
-
return `${yyyy}${mm}${dd}`;
|
|
3484
|
-
}
|
|
3485
|
-
function readRateData() {
|
|
3486
|
-
try {
|
|
3487
|
-
const raw = fs.readFileSync(RATE_FILE, "utf-8");
|
|
3488
|
-
return JSON.parse(raw);
|
|
3489
|
-
} catch {
|
|
3490
|
-
return { date: today(), count: 0 };
|
|
3491
|
-
}
|
|
3492
|
-
}
|
|
3493
|
-
function writeRateData(data) {
|
|
3494
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
3495
|
-
fs.writeFileSync(RATE_FILE, JSON.stringify(data, null, 2), "utf-8");
|
|
3496
|
-
}
|
|
3497
|
-
function incrementCallCount() {
|
|
3498
|
-
const data = readRateData();
|
|
3499
|
-
const currentDate = today();
|
|
3500
|
-
const newData = data.date === currentDate ? { date: currentDate, count: data.count + 1 } : { date: currentDate, count: 1 };
|
|
3501
|
-
writeRateData(newData);
|
|
3502
|
-
return { count: newData.count, limit: DAILY_LIMIT };
|
|
3503
|
-
}
|
|
3504
|
-
function checkRateLimit() {
|
|
3505
|
-
const data = readRateData();
|
|
3506
|
-
const currentDate = today();
|
|
3507
|
-
const count = data.date === currentDate ? data.count : 0;
|
|
3508
|
-
const allowed = count < DAILY_LIMIT;
|
|
3509
|
-
const warning = count >= DAILY_LIMIT * WARNING_THRESHOLD;
|
|
3510
|
-
return { allowed, count, limit: DAILY_LIMIT, warning };
|
|
3511
|
-
}
|
|
3512
|
-
function getRateLimitStatus() {
|
|
3513
|
-
const data = readRateData();
|
|
3514
|
-
const currentDate = today();
|
|
3515
|
-
const count = data.date === currentDate ? data.count : 0;
|
|
3516
|
-
return {
|
|
3517
|
-
date: currentDate,
|
|
3518
|
-
count,
|
|
3519
|
-
limit: DAILY_LIMIT,
|
|
3520
|
-
remaining: DAILY_LIMIT - count
|
|
3521
|
-
};
|
|
3522
|
-
}
|
|
3523
|
-
var CONFIG_DIR, RATE_FILE, DAILY_LIMIT, WARNING_THRESHOLD;
|
|
3524
|
-
var init_rate_limit = __esm({
|
|
3525
|
-
"src/client/rate-limit.ts"() {
|
|
3526
|
-
"use strict";
|
|
3527
|
-
CONFIG_DIR = path.join(os.homedir(), ".krx-cli");
|
|
3528
|
-
RATE_FILE = path.join(CONFIG_DIR, "rate-limit.json");
|
|
3529
|
-
DAILY_LIMIT = 1e4;
|
|
3530
|
-
WARNING_THRESHOLD = 0.8;
|
|
3531
|
-
}
|
|
3532
|
-
});
|
|
3533
|
-
|
|
3534
3465
|
// node_modules/.pnpm/commander@14.0.3/node_modules/commander/esm.mjs
|
|
3535
3466
|
var import_index = __toESM(require_commander(), 1);
|
|
3536
3467
|
var {
|
|
@@ -3549,9 +3480,9 @@ var {
|
|
|
3549
3480
|
} = import_index.default;
|
|
3550
3481
|
|
|
3551
3482
|
// src/client/auth.ts
|
|
3552
|
-
import * as
|
|
3553
|
-
import * as
|
|
3554
|
-
import * as
|
|
3483
|
+
import * as fs3 from "node:fs";
|
|
3484
|
+
import * as path3 from "node:path";
|
|
3485
|
+
import * as os3 from "node:os";
|
|
3555
3486
|
|
|
3556
3487
|
// src/client/response-fields.ts
|
|
3557
3488
|
var INDEX_COMMON_FIELDS = [
|
|
@@ -3942,13 +3873,13 @@ var CATEGORIES = [
|
|
|
3942
3873
|
probeEndpoint: "/svc/apis/esg/esg_index_info"
|
|
3943
3874
|
}
|
|
3944
3875
|
];
|
|
3945
|
-
function ep(
|
|
3876
|
+
function ep(path6, description, descriptionKo, category) {
|
|
3946
3877
|
return {
|
|
3947
|
-
path:
|
|
3878
|
+
path: path6,
|
|
3948
3879
|
description,
|
|
3949
3880
|
descriptionKo,
|
|
3950
3881
|
category,
|
|
3951
|
-
responseFields: RESPONSE_FIELDS[
|
|
3882
|
+
responseFields: RESPONSE_FIELDS[path6] ?? []
|
|
3952
3883
|
};
|
|
3953
3884
|
}
|
|
3954
3885
|
var ENDPOINTS = [
|
|
@@ -4137,11 +4068,251 @@ var ENDPOINTS = [
|
|
|
4137
4068
|
ep("/svc/apis/esg/esg_index_info", "ESG index info", "ESG \uC9C0\uC218 \uC815\uBCF4", "esg")
|
|
4138
4069
|
];
|
|
4139
4070
|
|
|
4071
|
+
// src/cache/store.ts
|
|
4072
|
+
import * as fs from "node:fs";
|
|
4073
|
+
import * as path from "node:path";
|
|
4074
|
+
import * as os from "node:os";
|
|
4075
|
+
import * as crypto from "node:crypto";
|
|
4076
|
+
|
|
4077
|
+
// src/utils/date.ts
|
|
4078
|
+
function formatDateToYYYYMMDD(date5) {
|
|
4079
|
+
const yyyy = date5.getFullYear().toString();
|
|
4080
|
+
const mm = (date5.getMonth() + 1).toString().padStart(2, "0");
|
|
4081
|
+
const dd = date5.getDate().toString().padStart(2, "0");
|
|
4082
|
+
return `${yyyy}${mm}${dd}`;
|
|
4083
|
+
}
|
|
4084
|
+
function isWeekend(date5) {
|
|
4085
|
+
const day = date5.getDay();
|
|
4086
|
+
return day === 0 || day === 6;
|
|
4087
|
+
}
|
|
4088
|
+
var KST_OFFSET_MS = 9 * 60 * 60 * 1e3;
|
|
4089
|
+
function getRecentTradingDate() {
|
|
4090
|
+
const nowKst = new Date(Date.now() + KST_OFFSET_MS);
|
|
4091
|
+
const day = nowKst.getUTCDay();
|
|
4092
|
+
const daysBack = day === 0 ? 2 : day === 6 ? 1 : day === 1 ? 3 : 1;
|
|
4093
|
+
const targetKst = new Date(nowKst.getTime() - daysBack * 24 * 60 * 60 * 1e3);
|
|
4094
|
+
const yyyy = targetKst.getUTCFullYear().toString();
|
|
4095
|
+
const mm = (targetKst.getUTCMonth() + 1).toString().padStart(2, "0");
|
|
4096
|
+
const dd = targetKst.getUTCDate().toString().padStart(2, "0");
|
|
4097
|
+
return `${yyyy}${mm}${dd}`;
|
|
4098
|
+
}
|
|
4099
|
+
function parseYYYYMMDD(str) {
|
|
4100
|
+
const year = parseInt(str.substring(0, 4));
|
|
4101
|
+
const month = parseInt(str.substring(4, 6)) - 1;
|
|
4102
|
+
const day = parseInt(str.substring(6, 8));
|
|
4103
|
+
return new Date(year, month, day);
|
|
4104
|
+
}
|
|
4105
|
+
function getTradingDays(from, to) {
|
|
4106
|
+
const fromDate = parseYYYYMMDD(from);
|
|
4107
|
+
const toDate = parseYYYYMMDD(to);
|
|
4108
|
+
const days = [];
|
|
4109
|
+
let current = new Date(fromDate);
|
|
4110
|
+
while (current <= toDate) {
|
|
4111
|
+
if (!isWeekend(current)) {
|
|
4112
|
+
days.push(formatDateToYYYYMMDD(current));
|
|
4113
|
+
}
|
|
4114
|
+
current = new Date(current.getTime() + 24 * 60 * 60 * 1e3);
|
|
4115
|
+
}
|
|
4116
|
+
return days;
|
|
4117
|
+
}
|
|
4118
|
+
|
|
4119
|
+
// src/cache/store.ts
|
|
4120
|
+
var CACHE_DIR = path.join(os.homedir(), ".krx-cli", "cache");
|
|
4121
|
+
var DATE_FORMAT = /^\d{8}$/;
|
|
4122
|
+
function isValidCacheDate(date5) {
|
|
4123
|
+
return DATE_FORMAT.test(date5);
|
|
4124
|
+
}
|
|
4125
|
+
function getCacheKey(endpoint, params) {
|
|
4126
|
+
const sortedEntries = Object.entries(params).sort(
|
|
4127
|
+
([a], [b]) => a.localeCompare(b)
|
|
4128
|
+
);
|
|
4129
|
+
const raw = `${endpoint}:${JSON.stringify(sortedEntries)}`;
|
|
4130
|
+
return crypto.createHash("sha256").update(raw).digest("hex").slice(0, 16);
|
|
4131
|
+
}
|
|
4132
|
+
function getCachePath(date5, key) {
|
|
4133
|
+
return path.join(CACHE_DIR, date5, `${key}.json`);
|
|
4134
|
+
}
|
|
4135
|
+
function isToday(dateStr) {
|
|
4136
|
+
const today2 = formatDateToYYYYMMDD(/* @__PURE__ */ new Date());
|
|
4137
|
+
return dateStr === today2;
|
|
4138
|
+
}
|
|
4139
|
+
function getCached(endpoint, params) {
|
|
4140
|
+
const date5 = params["basDd"];
|
|
4141
|
+
if (!date5 || !isValidCacheDate(date5) || isToday(date5)) {
|
|
4142
|
+
return null;
|
|
4143
|
+
}
|
|
4144
|
+
const key = getCacheKey(endpoint, params);
|
|
4145
|
+
const cachePath = getCachePath(date5, key);
|
|
4146
|
+
try {
|
|
4147
|
+
const raw = fs.readFileSync(cachePath, "utf-8");
|
|
4148
|
+
return JSON.parse(raw);
|
|
4149
|
+
} catch (err) {
|
|
4150
|
+
if (err instanceof Error && err.message.includes("ENOENT")) {
|
|
4151
|
+
return null;
|
|
4152
|
+
}
|
|
4153
|
+
process.stderr.write(`[krx-cli] cache read failed: ${String(err)}
|
|
4154
|
+
`);
|
|
4155
|
+
return null;
|
|
4156
|
+
}
|
|
4157
|
+
}
|
|
4158
|
+
function setCached(endpoint, params, data) {
|
|
4159
|
+
const date5 = params["basDd"];
|
|
4160
|
+
if (!date5 || !isValidCacheDate(date5) || isToday(date5)) {
|
|
4161
|
+
return;
|
|
4162
|
+
}
|
|
4163
|
+
const key = getCacheKey(endpoint, params);
|
|
4164
|
+
const cachePath = getCachePath(date5, key);
|
|
4165
|
+
const dir = path.dirname(cachePath);
|
|
4166
|
+
try {
|
|
4167
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
4168
|
+
fs.writeFileSync(cachePath, JSON.stringify(data), "utf-8");
|
|
4169
|
+
} catch (err) {
|
|
4170
|
+
process.stderr.write(`[krx-cli] cache write failed: ${String(err)}
|
|
4171
|
+
`);
|
|
4172
|
+
}
|
|
4173
|
+
}
|
|
4174
|
+
function clearCache() {
|
|
4175
|
+
let files = 0;
|
|
4176
|
+
let directories = 0;
|
|
4177
|
+
try {
|
|
4178
|
+
if (!fs.existsSync(CACHE_DIR)) {
|
|
4179
|
+
return { files, directories };
|
|
4180
|
+
}
|
|
4181
|
+
const dateDirs = fs.readdirSync(CACHE_DIR);
|
|
4182
|
+
for (const dateDir of dateDirs) {
|
|
4183
|
+
const dirPath = path.join(CACHE_DIR, dateDir);
|
|
4184
|
+
const stat = fs.statSync(dirPath);
|
|
4185
|
+
if (stat.isDirectory()) {
|
|
4186
|
+
const cacheFiles = fs.readdirSync(dirPath);
|
|
4187
|
+
files += cacheFiles.length;
|
|
4188
|
+
fs.rmSync(dirPath, { recursive: true, force: true });
|
|
4189
|
+
directories += 1;
|
|
4190
|
+
}
|
|
4191
|
+
}
|
|
4192
|
+
} catch (err) {
|
|
4193
|
+
process.stderr.write(`[krx-cli] cache clear failed: ${String(err)}
|
|
4194
|
+
`);
|
|
4195
|
+
}
|
|
4196
|
+
return { files, directories };
|
|
4197
|
+
}
|
|
4198
|
+
function getCacheStatus() {
|
|
4199
|
+
let totalFiles = 0;
|
|
4200
|
+
let totalSize = 0;
|
|
4201
|
+
let dates = 0;
|
|
4202
|
+
try {
|
|
4203
|
+
if (!fs.existsSync(CACHE_DIR)) {
|
|
4204
|
+
return { totalFiles, totalSize, dates };
|
|
4205
|
+
}
|
|
4206
|
+
const dateDirs = fs.readdirSync(CACHE_DIR);
|
|
4207
|
+
for (const dateDir of dateDirs) {
|
|
4208
|
+
const dirPath = path.join(CACHE_DIR, dateDir);
|
|
4209
|
+
const stat = fs.statSync(dirPath);
|
|
4210
|
+
if (stat.isDirectory()) {
|
|
4211
|
+
dates += 1;
|
|
4212
|
+
const cacheFiles = fs.readdirSync(dirPath);
|
|
4213
|
+
for (const file2 of cacheFiles) {
|
|
4214
|
+
const fileStat = fs.statSync(path.join(dirPath, file2));
|
|
4215
|
+
totalFiles += 1;
|
|
4216
|
+
totalSize += fileStat.size;
|
|
4217
|
+
}
|
|
4218
|
+
}
|
|
4219
|
+
}
|
|
4220
|
+
} catch (err) {
|
|
4221
|
+
process.stderr.write(`[krx-cli] cache status failed: ${String(err)}
|
|
4222
|
+
`);
|
|
4223
|
+
}
|
|
4224
|
+
return { totalFiles, totalSize, dates };
|
|
4225
|
+
}
|
|
4226
|
+
|
|
4227
|
+
// src/client/rate-limit.ts
|
|
4228
|
+
import * as fs2 from "node:fs";
|
|
4229
|
+
import * as path2 from "node:path";
|
|
4230
|
+
import * as os2 from "node:os";
|
|
4231
|
+
var CONFIG_DIR = path2.join(os2.homedir(), ".krx-cli");
|
|
4232
|
+
var RATE_FILE = path2.join(CONFIG_DIR, "rate-limit.json");
|
|
4233
|
+
var DAILY_LIMIT = 1e4;
|
|
4234
|
+
var WARNING_THRESHOLD = 0.8;
|
|
4235
|
+
function today() {
|
|
4236
|
+
const now = /* @__PURE__ */ new Date();
|
|
4237
|
+
const yyyy = now.getFullYear().toString();
|
|
4238
|
+
const mm = (now.getMonth() + 1).toString().padStart(2, "0");
|
|
4239
|
+
const dd = now.getDate().toString().padStart(2, "0");
|
|
4240
|
+
return `${yyyy}${mm}${dd}`;
|
|
4241
|
+
}
|
|
4242
|
+
function readRateData() {
|
|
4243
|
+
try {
|
|
4244
|
+
const raw = fs2.readFileSync(RATE_FILE, "utf-8");
|
|
4245
|
+
return JSON.parse(raw);
|
|
4246
|
+
} catch {
|
|
4247
|
+
return { date: today(), count: 0 };
|
|
4248
|
+
}
|
|
4249
|
+
}
|
|
4250
|
+
function writeRateData(data) {
|
|
4251
|
+
fs2.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
4252
|
+
fs2.writeFileSync(RATE_FILE, JSON.stringify(data, null, 2), "utf-8");
|
|
4253
|
+
}
|
|
4254
|
+
function incrementCallCount() {
|
|
4255
|
+
const data = readRateData();
|
|
4256
|
+
const currentDate = today();
|
|
4257
|
+
const newData = data.date === currentDate ? { date: currentDate, count: data.count + 1 } : { date: currentDate, count: 1 };
|
|
4258
|
+
writeRateData(newData);
|
|
4259
|
+
return { count: newData.count, limit: DAILY_LIMIT };
|
|
4260
|
+
}
|
|
4261
|
+
function checkRateLimit() {
|
|
4262
|
+
const data = readRateData();
|
|
4263
|
+
const currentDate = today();
|
|
4264
|
+
const count = data.date === currentDate ? data.count : 0;
|
|
4265
|
+
const allowed = count < DAILY_LIMIT;
|
|
4266
|
+
const warning = count >= DAILY_LIMIT * WARNING_THRESHOLD;
|
|
4267
|
+
return { allowed, count, limit: DAILY_LIMIT, warning };
|
|
4268
|
+
}
|
|
4269
|
+
|
|
4270
|
+
// src/client/retry.ts
|
|
4271
|
+
var DEFAULT_MAX_RETRIES = 3;
|
|
4272
|
+
var DEFAULT_BASE_DELAY = 1e3;
|
|
4273
|
+
function isNetworkError(error48) {
|
|
4274
|
+
if (error48 instanceof TypeError) {
|
|
4275
|
+
return true;
|
|
4276
|
+
}
|
|
4277
|
+
if (error48 instanceof Error) {
|
|
4278
|
+
const msg = error48.message.toLowerCase();
|
|
4279
|
+
return msg.includes("econnrefused") || msg.includes("econnreset") || msg.includes("etimedout") || msg.includes("network");
|
|
4280
|
+
}
|
|
4281
|
+
return false;
|
|
4282
|
+
}
|
|
4283
|
+
function delay(ms) {
|
|
4284
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
4285
|
+
}
|
|
4286
|
+
async function withRetry(fn, options) {
|
|
4287
|
+
const maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
4288
|
+
const baseDelay = options?.baseDelay ?? DEFAULT_BASE_DELAY;
|
|
4289
|
+
let lastError;
|
|
4290
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
4291
|
+
try {
|
|
4292
|
+
return await fn();
|
|
4293
|
+
} catch (err) {
|
|
4294
|
+
lastError = err;
|
|
4295
|
+
if (!isNetworkError(err) || attempt === maxRetries) {
|
|
4296
|
+
throw err;
|
|
4297
|
+
}
|
|
4298
|
+
const waitMs = baseDelay * Math.pow(2, attempt);
|
|
4299
|
+
await delay(waitMs);
|
|
4300
|
+
}
|
|
4301
|
+
}
|
|
4302
|
+
throw lastError;
|
|
4303
|
+
}
|
|
4304
|
+
|
|
4140
4305
|
// src/client/client.ts
|
|
4141
4306
|
var BASE_URL = "https://data-dbg.krx.co.kr";
|
|
4142
4307
|
async function krxFetch(options) {
|
|
4143
|
-
const
|
|
4144
|
-
|
|
4308
|
+
const useCache = options.cache !== false;
|
|
4309
|
+
if (useCache) {
|
|
4310
|
+
const cached2 = getCached(options.endpoint, options.params);
|
|
4311
|
+
if (cached2) {
|
|
4312
|
+
return { success: true, data: cached2 };
|
|
4313
|
+
}
|
|
4314
|
+
}
|
|
4315
|
+
const rateStatus = checkRateLimit();
|
|
4145
4316
|
if (!rateStatus.allowed) {
|
|
4146
4317
|
return {
|
|
4147
4318
|
success: false,
|
|
@@ -4157,15 +4328,18 @@ async function krxFetch(options) {
|
|
|
4157
4328
|
);
|
|
4158
4329
|
}
|
|
4159
4330
|
const url2 = `${BASE_URL}${options.endpoint}`;
|
|
4160
|
-
const response = await
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4331
|
+
const response = await withRetry(
|
|
4332
|
+
() => fetch(url2, {
|
|
4333
|
+
method: "POST",
|
|
4334
|
+
headers: {
|
|
4335
|
+
AUTH_KEY: options.apiKey,
|
|
4336
|
+
"Content-Type": "application/json; charset=utf-8"
|
|
4337
|
+
},
|
|
4338
|
+
body: JSON.stringify(options.params)
|
|
4339
|
+
}),
|
|
4340
|
+
{ maxRetries: options.retries ?? 3 }
|
|
4341
|
+
);
|
|
4342
|
+
incrementCallCount();
|
|
4169
4343
|
if (!response.ok) {
|
|
4170
4344
|
let errorMsg = `HTTP ${response.status}: ${response.statusText}`;
|
|
4171
4345
|
let errorCode;
|
|
@@ -4193,6 +4367,9 @@ async function krxFetch(options) {
|
|
|
4193
4367
|
error: "Unexpected response format: missing OutBlock_1"
|
|
4194
4368
|
};
|
|
4195
4369
|
}
|
|
4370
|
+
if (useCache) {
|
|
4371
|
+
setCached(options.endpoint, options.params, outBlock);
|
|
4372
|
+
}
|
|
4196
4373
|
return {
|
|
4197
4374
|
success: true,
|
|
4198
4375
|
data: outBlock
|
|
@@ -4200,19 +4377,19 @@ async function krxFetch(options) {
|
|
|
4200
4377
|
}
|
|
4201
4378
|
|
|
4202
4379
|
// src/client/auth.ts
|
|
4203
|
-
var CONFIG_DIR2 =
|
|
4204
|
-
var CONFIG_FILE =
|
|
4380
|
+
var CONFIG_DIR2 = path3.join(os3.homedir(), ".krx-cli");
|
|
4381
|
+
var CONFIG_FILE = path3.join(CONFIG_DIR2, "config.json");
|
|
4205
4382
|
function readConfig() {
|
|
4206
4383
|
try {
|
|
4207
|
-
const raw =
|
|
4384
|
+
const raw = fs3.readFileSync(CONFIG_FILE, "utf-8");
|
|
4208
4385
|
return JSON.parse(raw);
|
|
4209
4386
|
} catch {
|
|
4210
4387
|
return {};
|
|
4211
4388
|
}
|
|
4212
4389
|
}
|
|
4213
4390
|
function writeConfig(config2) {
|
|
4214
|
-
|
|
4215
|
-
|
|
4391
|
+
fs3.mkdirSync(CONFIG_DIR2, { recursive: true });
|
|
4392
|
+
fs3.writeFileSync(CONFIG_FILE, JSON.stringify(config2, null, 2), "utf-8");
|
|
4216
4393
|
}
|
|
4217
4394
|
function getApiKey() {
|
|
4218
4395
|
return process.env["KRX_API_KEY"] ?? readConfig().apiKey;
|
|
@@ -4221,16 +4398,6 @@ function saveApiKey(apiKey) {
|
|
|
4221
4398
|
const config2 = readConfig();
|
|
4222
4399
|
writeConfig({ ...config2, apiKey });
|
|
4223
4400
|
}
|
|
4224
|
-
function getRecentTradingDate() {
|
|
4225
|
-
const now = /* @__PURE__ */ new Date();
|
|
4226
|
-
const day = now.getDay();
|
|
4227
|
-
const daysBack = day === 0 ? 2 : day === 6 ? 1 : day === 1 ? 3 : 1;
|
|
4228
|
-
const target = new Date(now.getTime() - daysBack * 24 * 60 * 60 * 1e3);
|
|
4229
|
-
const yyyy = target.getFullYear().toString();
|
|
4230
|
-
const mm = (target.getMonth() + 1).toString().padStart(2, "0");
|
|
4231
|
-
const dd = target.getDate().toString().padStart(2, "0");
|
|
4232
|
-
return `${yyyy}${mm}${dd}`;
|
|
4233
|
-
}
|
|
4234
4401
|
async function checkCategoryApproval(apiKey, categoryId) {
|
|
4235
4402
|
const category = CATEGORIES.find((c) => c.id === categoryId);
|
|
4236
4403
|
if (!category) {
|
|
@@ -4282,6 +4449,8 @@ function formatOutput(data, format, fields) {
|
|
|
4282
4449
|
return filtered.map((row) => JSON.stringify(row)).join("\n");
|
|
4283
4450
|
case "table":
|
|
4284
4451
|
return formatTable(filtered);
|
|
4452
|
+
case "csv":
|
|
4453
|
+
return formatCsv(filtered);
|
|
4285
4454
|
}
|
|
4286
4455
|
}
|
|
4287
4456
|
function filterFields(data, fields) {
|
|
@@ -4311,6 +4480,23 @@ function formatTable(data) {
|
|
|
4311
4480
|
);
|
|
4312
4481
|
return [header, separator, ...rows].join("\n");
|
|
4313
4482
|
}
|
|
4483
|
+
function escapeCsvField(value) {
|
|
4484
|
+
if (value.includes(",") || value.includes('"') || value.includes("\n")) {
|
|
4485
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
4486
|
+
}
|
|
4487
|
+
return value;
|
|
4488
|
+
}
|
|
4489
|
+
function formatCsv(data) {
|
|
4490
|
+
if (data.length === 0) return "";
|
|
4491
|
+
const firstRow = data[0];
|
|
4492
|
+
if (!firstRow) return "";
|
|
4493
|
+
const keys = Object.keys(firstRow);
|
|
4494
|
+
const header = keys.join(",");
|
|
4495
|
+
const rows = data.map(
|
|
4496
|
+
(row) => keys.map((k) => escapeCsvField(String(row[k] ?? ""))).join(",")
|
|
4497
|
+
);
|
|
4498
|
+
return [header, ...rows].join("\n");
|
|
4499
|
+
}
|
|
4314
4500
|
function writeOutput(output) {
|
|
4315
4501
|
process.stdout.write(output + "\n");
|
|
4316
4502
|
}
|
|
@@ -5153,10 +5339,10 @@ function mergeDefs(...defs) {
|
|
|
5153
5339
|
function cloneDef(schema) {
|
|
5154
5340
|
return mergeDefs(schema._zod.def);
|
|
5155
5341
|
}
|
|
5156
|
-
function getElementAtPath(obj,
|
|
5157
|
-
if (!
|
|
5342
|
+
function getElementAtPath(obj, path6) {
|
|
5343
|
+
if (!path6)
|
|
5158
5344
|
return obj;
|
|
5159
|
-
return
|
|
5345
|
+
return path6.reduce((acc, key) => acc?.[key], obj);
|
|
5160
5346
|
}
|
|
5161
5347
|
function promiseAllObject(promisesObj) {
|
|
5162
5348
|
const keys = Object.keys(promisesObj);
|
|
@@ -5539,11 +5725,11 @@ function aborted(x, startIndex = 0) {
|
|
|
5539
5725
|
}
|
|
5540
5726
|
return false;
|
|
5541
5727
|
}
|
|
5542
|
-
function prefixIssues(
|
|
5728
|
+
function prefixIssues(path6, issues) {
|
|
5543
5729
|
return issues.map((iss) => {
|
|
5544
5730
|
var _a2;
|
|
5545
5731
|
(_a2 = iss).path ?? (_a2.path = []);
|
|
5546
|
-
iss.path.unshift(
|
|
5732
|
+
iss.path.unshift(path6);
|
|
5547
5733
|
return iss;
|
|
5548
5734
|
});
|
|
5549
5735
|
}
|
|
@@ -5726,7 +5912,7 @@ function formatError(error48, mapper = (issue2) => issue2.message) {
|
|
|
5726
5912
|
}
|
|
5727
5913
|
function treeifyError(error48, mapper = (issue2) => issue2.message) {
|
|
5728
5914
|
const result = { errors: [] };
|
|
5729
|
-
const processError = (error49,
|
|
5915
|
+
const processError = (error49, path6 = []) => {
|
|
5730
5916
|
var _a2, _b;
|
|
5731
5917
|
for (const issue2 of error49.issues) {
|
|
5732
5918
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
@@ -5736,7 +5922,7 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
|
|
|
5736
5922
|
} else if (issue2.code === "invalid_element") {
|
|
5737
5923
|
processError({ issues: issue2.issues }, issue2.path);
|
|
5738
5924
|
} else {
|
|
5739
|
-
const fullpath = [...
|
|
5925
|
+
const fullpath = [...path6, ...issue2.path];
|
|
5740
5926
|
if (fullpath.length === 0) {
|
|
5741
5927
|
result.errors.push(mapper(issue2));
|
|
5742
5928
|
continue;
|
|
@@ -5768,8 +5954,8 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
|
|
|
5768
5954
|
}
|
|
5769
5955
|
function toDotPath(_path) {
|
|
5770
5956
|
const segs = [];
|
|
5771
|
-
const
|
|
5772
|
-
for (const seg of
|
|
5957
|
+
const path6 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
|
|
5958
|
+
for (const seg of path6) {
|
|
5773
5959
|
if (typeof seg === "number")
|
|
5774
5960
|
segs.push(`[${seg}]`);
|
|
5775
5961
|
else if (typeof seg === "symbol")
|
|
@@ -17746,13 +17932,13 @@ function resolveRef(ref, ctx) {
|
|
|
17746
17932
|
if (!ref.startsWith("#")) {
|
|
17747
17933
|
throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
|
|
17748
17934
|
}
|
|
17749
|
-
const
|
|
17750
|
-
if (
|
|
17935
|
+
const path6 = ref.slice(1).split("/").filter(Boolean);
|
|
17936
|
+
if (path6.length === 0) {
|
|
17751
17937
|
return ctx.rootSchema;
|
|
17752
17938
|
}
|
|
17753
17939
|
const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
|
|
17754
|
-
if (
|
|
17755
|
-
const key =
|
|
17940
|
+
if (path6[0] === defsKey) {
|
|
17941
|
+
const key = path6[1];
|
|
17756
17942
|
if (!key || !ctx.defs[key]) {
|
|
17757
17943
|
throw new Error(`Reference not found: ${ref}`);
|
|
17758
17944
|
}
|
|
@@ -18170,6 +18356,17 @@ var dateSchema = external_exports.string().regex(DATE_PATTERN, "Date must be YYY
|
|
|
18170
18356
|
{ message: "Invalid date. Must be a valid date from 2010 onwards." }
|
|
18171
18357
|
);
|
|
18172
18358
|
var marketSchema = external_exports.enum(["kospi", "kosdaq", "konex", "krx"]);
|
|
18359
|
+
var CONTROL_CHAR_PATTERN = /[\x00-\x1f\x7f]/;
|
|
18360
|
+
var PATH_TRAVERSAL_PATTERN = /\.\.[/\\]/;
|
|
18361
|
+
function validateNoInjection(input) {
|
|
18362
|
+
if (CONTROL_CHAR_PATTERN.test(input)) {
|
|
18363
|
+
return "Input contains control characters";
|
|
18364
|
+
}
|
|
18365
|
+
if (PATH_TRAVERSAL_PATTERN.test(input)) {
|
|
18366
|
+
return "Input contains path traversal";
|
|
18367
|
+
}
|
|
18368
|
+
return null;
|
|
18369
|
+
}
|
|
18173
18370
|
function validateDate(value) {
|
|
18174
18371
|
const result = dateSchema.safeParse(value);
|
|
18175
18372
|
if (!result.success) {
|
|
@@ -18187,6 +18384,92 @@ function validateMarket(value) {
|
|
|
18187
18384
|
return result.data;
|
|
18188
18385
|
}
|
|
18189
18386
|
|
|
18387
|
+
// src/cli/command-helper.ts
|
|
18388
|
+
import * as fs4 from "node:fs";
|
|
18389
|
+
import * as path4 from "node:path";
|
|
18390
|
+
|
|
18391
|
+
// src/client/range-fetch.ts
|
|
18392
|
+
var DEFAULT_CONCURRENCY = 5;
|
|
18393
|
+
async function fetchWithConcurrency(tasks, concurrency) {
|
|
18394
|
+
const slots = new Array(tasks.length);
|
|
18395
|
+
let index = 0;
|
|
18396
|
+
async function runNext() {
|
|
18397
|
+
while (index < tasks.length) {
|
|
18398
|
+
const currentIndex = index;
|
|
18399
|
+
index += 1;
|
|
18400
|
+
const task = tasks[currentIndex];
|
|
18401
|
+
if (task) {
|
|
18402
|
+
const result = await task();
|
|
18403
|
+
slots[currentIndex] = Promise.resolve(result);
|
|
18404
|
+
}
|
|
18405
|
+
}
|
|
18406
|
+
}
|
|
18407
|
+
const workers = Array.from(
|
|
18408
|
+
{ length: Math.min(concurrency, tasks.length) },
|
|
18409
|
+
() => runNext()
|
|
18410
|
+
);
|
|
18411
|
+
await Promise.all(workers);
|
|
18412
|
+
return Promise.all(slots);
|
|
18413
|
+
}
|
|
18414
|
+
async function fetchDateRange(options) {
|
|
18415
|
+
const { endpoint, from, to, apiKey, cache, extraParams } = options;
|
|
18416
|
+
const concurrency = options.concurrency ?? DEFAULT_CONCURRENCY;
|
|
18417
|
+
if (from > to) {
|
|
18418
|
+
return {
|
|
18419
|
+
success: false,
|
|
18420
|
+
data: [],
|
|
18421
|
+
error: `'from' date (${from}) must not be after 'to' date (${to})`,
|
|
18422
|
+
fetchedDays: 0,
|
|
18423
|
+
failedDays: 0
|
|
18424
|
+
};
|
|
18425
|
+
}
|
|
18426
|
+
const tradingDays = getTradingDays(from, to);
|
|
18427
|
+
if (tradingDays.length === 0) {
|
|
18428
|
+
return { success: true, data: [], fetchedDays: 0, failedDays: 0 };
|
|
18429
|
+
}
|
|
18430
|
+
const tasks = tradingDays.map((day) => async () => {
|
|
18431
|
+
const params = {
|
|
18432
|
+
basDd: day,
|
|
18433
|
+
...extraParams
|
|
18434
|
+
};
|
|
18435
|
+
return krxFetch({
|
|
18436
|
+
endpoint,
|
|
18437
|
+
params,
|
|
18438
|
+
apiKey,
|
|
18439
|
+
cache
|
|
18440
|
+
});
|
|
18441
|
+
});
|
|
18442
|
+
const results = await fetchWithConcurrency(tasks, concurrency);
|
|
18443
|
+
const failedDays = results.filter((r) => !r.success).length;
|
|
18444
|
+
const mergedData = results.filter((r) => r.success).flatMap((r) => [...r.data]);
|
|
18445
|
+
if (mergedData.length === 0 && failedDays > 0) {
|
|
18446
|
+
return {
|
|
18447
|
+
success: false,
|
|
18448
|
+
data: [],
|
|
18449
|
+
error: `All ${failedDays} trading day(s) failed to fetch`,
|
|
18450
|
+
fetchedDays: 0,
|
|
18451
|
+
failedDays
|
|
18452
|
+
};
|
|
18453
|
+
}
|
|
18454
|
+
return {
|
|
18455
|
+
success: true,
|
|
18456
|
+
data: mergedData,
|
|
18457
|
+
fetchedDays: tradingDays.length - failedDays,
|
|
18458
|
+
failedDays
|
|
18459
|
+
};
|
|
18460
|
+
}
|
|
18461
|
+
|
|
18462
|
+
// src/cli/exit-codes.ts
|
|
18463
|
+
var EXIT_CODES = {
|
|
18464
|
+
SUCCESS: 0,
|
|
18465
|
+
GENERAL_ERROR: 1,
|
|
18466
|
+
USAGE_ERROR: 2,
|
|
18467
|
+
NO_DATA: 3,
|
|
18468
|
+
AUTH_FAILURE: 4,
|
|
18469
|
+
RATE_LIMIT: 5,
|
|
18470
|
+
SERVICE_NOT_APPROVED: 6
|
|
18471
|
+
};
|
|
18472
|
+
|
|
18190
18473
|
// src/cli/error-handler.ts
|
|
18191
18474
|
function handleKrxError(result) {
|
|
18192
18475
|
const exitCode = result.errorCode === "RATE_LIMIT" ? EXIT_CODES.RATE_LIMIT : result.errorCode === "401" ? EXIT_CODES.SERVICE_NOT_APPROVED : EXIT_CODES.GENERAL_ERROR;
|
|
@@ -18194,209 +18477,368 @@ function handleKrxError(result) {
|
|
|
18194
18477
|
process.exit(exitCode);
|
|
18195
18478
|
}
|
|
18196
18479
|
|
|
18197
|
-
// src/
|
|
18198
|
-
|
|
18199
|
-
|
|
18200
|
-
|
|
18201
|
-
|
|
18202
|
-
|
|
18203
|
-
|
|
18204
|
-
|
|
18205
|
-
function registerIndexCommand(program3) {
|
|
18206
|
-
const index = program3.command("index").description("Query KRX index data");
|
|
18207
|
-
index.command("list").description("List index daily trading data").requiredOption("--date <date>", "trading date (YYYYMMDD)").option(
|
|
18208
|
-
"--market <market>",
|
|
18209
|
-
"market: kospi, kosdaq, krx, bond, derivative",
|
|
18210
|
-
"kospi"
|
|
18211
|
-
).action(async (opts) => {
|
|
18212
|
-
try {
|
|
18213
|
-
const date5 = validateDate(opts.date);
|
|
18214
|
-
const market = opts.market.toLowerCase();
|
|
18215
|
-
const endpoint = MARKET_ENDPOINTS[market];
|
|
18216
|
-
if (!endpoint) {
|
|
18217
|
-
writeError(
|
|
18218
|
-
`Invalid market: ${market}. Must be one of: ${Object.keys(MARKET_ENDPOINTS).join(", ")}`
|
|
18219
|
-
);
|
|
18220
|
-
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
18221
|
-
}
|
|
18222
|
-
const apiKey = getApiKey();
|
|
18223
|
-
if (!apiKey) {
|
|
18224
|
-
writeError(
|
|
18225
|
-
"No API key configured. Use 'krx auth set <key>' or set KRX_API_KEY env var."
|
|
18226
|
-
);
|
|
18227
|
-
process.exit(EXIT_CODES.AUTH_FAILURE);
|
|
18228
|
-
}
|
|
18229
|
-
const parentOpts = program3.opts();
|
|
18230
|
-
if (parentOpts.dryRun) {
|
|
18231
|
-
writeOutput(
|
|
18232
|
-
JSON.stringify(
|
|
18233
|
-
{
|
|
18234
|
-
method: "POST",
|
|
18235
|
-
endpoint,
|
|
18236
|
-
params: { basDd: date5 },
|
|
18237
|
-
headers: { AUTH_KEY: "***" }
|
|
18238
|
-
},
|
|
18239
|
-
null,
|
|
18240
|
-
2
|
|
18241
|
-
)
|
|
18242
|
-
);
|
|
18243
|
-
return;
|
|
18244
|
-
}
|
|
18245
|
-
const result = await krxFetch({
|
|
18246
|
-
endpoint,
|
|
18247
|
-
params: { basDd: date5 },
|
|
18248
|
-
apiKey
|
|
18249
|
-
});
|
|
18250
|
-
if (!result.success) {
|
|
18251
|
-
handleKrxError(result);
|
|
18252
|
-
}
|
|
18253
|
-
if (result.data.length === 0) {
|
|
18254
|
-
writeError(`No data for date ${date5}`);
|
|
18255
|
-
process.exit(EXIT_CODES.NO_DATA);
|
|
18256
|
-
}
|
|
18257
|
-
const format = detectOutputFormat(parentOpts.output);
|
|
18258
|
-
const fields = parentOpts.fields?.split(",");
|
|
18259
|
-
writeOutput(
|
|
18260
|
-
formatOutput(
|
|
18261
|
-
result.data,
|
|
18262
|
-
format,
|
|
18263
|
-
fields
|
|
18264
|
-
)
|
|
18265
|
-
);
|
|
18266
|
-
} catch (err) {
|
|
18267
|
-
writeError(err instanceof Error ? err.message : String(err));
|
|
18268
|
-
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
18269
|
-
}
|
|
18270
|
-
});
|
|
18480
|
+
// src/utils/krx-number.ts
|
|
18481
|
+
function parseKrxNumber(value) {
|
|
18482
|
+
const cleaned = value.replace(/,/g, "");
|
|
18483
|
+
const num = parseFloat(cleaned);
|
|
18484
|
+
return isNaN(num) ? 0 : num;
|
|
18485
|
+
}
|
|
18486
|
+
function isKrxNumericString(value) {
|
|
18487
|
+
return /^-?[\d,]+\.?\d*$/.test(value.trim());
|
|
18271
18488
|
}
|
|
18272
18489
|
|
|
18273
|
-
// src/
|
|
18274
|
-
var
|
|
18275
|
-
|
|
18276
|
-
|
|
18277
|
-
|
|
18278
|
-
|
|
18279
|
-
|
|
18280
|
-
|
|
18281
|
-
|
|
18282
|
-
|
|
18283
|
-
|
|
18284
|
-
|
|
18285
|
-
|
|
18286
|
-
|
|
18490
|
+
// src/utils/filter.ts
|
|
18491
|
+
var OPERATOR_PATTERN = /^(\S+)\s+(>=|<=|==|!=|>|<)\s+(.+)$/;
|
|
18492
|
+
function parseFilterExpression(expression) {
|
|
18493
|
+
const trimmed = expression.trim().replace(/\s+/g, " ");
|
|
18494
|
+
const match = OPERATOR_PATTERN.exec(trimmed);
|
|
18495
|
+
if (!match || !match[1] || !match[2] || !match[3]) {
|
|
18496
|
+
return null;
|
|
18497
|
+
}
|
|
18498
|
+
return {
|
|
18499
|
+
field: match[1],
|
|
18500
|
+
operator: match[2],
|
|
18501
|
+
value: match[3].trim()
|
|
18502
|
+
};
|
|
18503
|
+
}
|
|
18504
|
+
function compareValues(fieldValue, operator, filterValue) {
|
|
18505
|
+
const isNumeric = isKrxNumericString(fieldValue) && isKrxNumericString(filterValue);
|
|
18506
|
+
if (isNumeric) {
|
|
18507
|
+
const a = parseKrxNumber(fieldValue);
|
|
18508
|
+
const b = parseKrxNumber(filterValue);
|
|
18509
|
+
switch (operator) {
|
|
18510
|
+
case ">":
|
|
18511
|
+
return a > b;
|
|
18512
|
+
case "<":
|
|
18513
|
+
return a < b;
|
|
18514
|
+
case ">=":
|
|
18515
|
+
return a >= b;
|
|
18516
|
+
case "<=":
|
|
18517
|
+
return a <= b;
|
|
18518
|
+
case "==":
|
|
18519
|
+
return a === b;
|
|
18520
|
+
case "!=":
|
|
18521
|
+
return a !== b;
|
|
18522
|
+
}
|
|
18523
|
+
}
|
|
18524
|
+
switch (operator) {
|
|
18525
|
+
case "==":
|
|
18526
|
+
return fieldValue === filterValue;
|
|
18527
|
+
case "!=":
|
|
18528
|
+
return fieldValue !== filterValue;
|
|
18529
|
+
case ">":
|
|
18530
|
+
return fieldValue > filterValue;
|
|
18531
|
+
case "<":
|
|
18532
|
+
return fieldValue < filterValue;
|
|
18533
|
+
case ">=":
|
|
18534
|
+
return fieldValue >= filterValue;
|
|
18535
|
+
case "<=":
|
|
18536
|
+
return fieldValue <= filterValue;
|
|
18537
|
+
default: {
|
|
18538
|
+
const _exhaustive = operator;
|
|
18539
|
+
void _exhaustive;
|
|
18540
|
+
return false;
|
|
18541
|
+
}
|
|
18542
|
+
}
|
|
18543
|
+
}
|
|
18544
|
+
function filterData(data, expression) {
|
|
18545
|
+
const parsed = parseFilterExpression(expression);
|
|
18546
|
+
if (!parsed) {
|
|
18547
|
+
return data;
|
|
18548
|
+
}
|
|
18549
|
+
return data.filter((row) => {
|
|
18550
|
+
const fieldValue = row[parsed.field];
|
|
18551
|
+
if (fieldValue === void 0 || fieldValue === null) {
|
|
18552
|
+
return false;
|
|
18553
|
+
}
|
|
18554
|
+
return compareValues(String(fieldValue), parsed.operator, parsed.value);
|
|
18555
|
+
});
|
|
18556
|
+
}
|
|
18557
|
+
|
|
18558
|
+
// src/utils/data-pipeline.ts
|
|
18559
|
+
function sortData(data, field, direction = "desc") {
|
|
18560
|
+
const sorted = [...data].sort((a, b) => {
|
|
18561
|
+
const aVal = String(a[field] ?? "");
|
|
18562
|
+
const bVal = String(b[field] ?? "");
|
|
18563
|
+
if (isKrxNumericString(aVal) && isKrxNumericString(bVal)) {
|
|
18564
|
+
return parseKrxNumber(aVal) - parseKrxNumber(bVal);
|
|
18565
|
+
}
|
|
18566
|
+
return aVal.localeCompare(bVal, "ko");
|
|
18567
|
+
});
|
|
18568
|
+
return direction === "desc" ? sorted.reverse() : sorted;
|
|
18569
|
+
}
|
|
18570
|
+
function limitData(data, limit) {
|
|
18571
|
+
return data.slice(0, limit);
|
|
18572
|
+
}
|
|
18573
|
+
function applyPipeline(data, options) {
|
|
18574
|
+
let result = data;
|
|
18575
|
+
if (options.filter) {
|
|
18576
|
+
result = filterData(result, options.filter);
|
|
18577
|
+
}
|
|
18578
|
+
if (options.sort) {
|
|
18579
|
+
result = sortData(result, options.sort, options.direction ?? "desc");
|
|
18580
|
+
}
|
|
18581
|
+
if (options.limit && options.limit > 0) {
|
|
18582
|
+
result = limitData(result, options.limit);
|
|
18583
|
+
}
|
|
18584
|
+
return result;
|
|
18585
|
+
}
|
|
18586
|
+
|
|
18587
|
+
// src/cli/command-helper.ts
|
|
18588
|
+
async function executeCommand(options) {
|
|
18589
|
+
const { endpoint, params, program: program3, noDataMessage } = options;
|
|
18590
|
+
const apiKey = getApiKey();
|
|
18591
|
+
if (!apiKey) {
|
|
18592
|
+
writeError(
|
|
18593
|
+
"No API key configured. Use 'krx auth set <key>' or set KRX_API_KEY env var."
|
|
18594
|
+
);
|
|
18595
|
+
process.exit(EXIT_CODES.AUTH_FAILURE);
|
|
18596
|
+
}
|
|
18597
|
+
const parentOpts = program3.opts();
|
|
18598
|
+
const finalParams = parentOpts.code ? { ...params, isuCd: parentOpts.code } : params;
|
|
18599
|
+
if (parentOpts.dryRun) {
|
|
18600
|
+
writeOutput(
|
|
18601
|
+
JSON.stringify(
|
|
18602
|
+
{
|
|
18603
|
+
method: "POST",
|
|
18604
|
+
endpoint,
|
|
18605
|
+
params: finalParams,
|
|
18606
|
+
headers: { AUTH_KEY: "***" }
|
|
18607
|
+
},
|
|
18608
|
+
null,
|
|
18609
|
+
2
|
|
18610
|
+
)
|
|
18611
|
+
);
|
|
18612
|
+
return;
|
|
18613
|
+
}
|
|
18614
|
+
const fromDate = parentOpts.from;
|
|
18615
|
+
const toDate = parentOpts.to;
|
|
18616
|
+
if (fromDate && !toDate || !fromDate && toDate) {
|
|
18617
|
+
writeError("Both --from and --to must be provided together");
|
|
18618
|
+
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
18619
|
+
}
|
|
18620
|
+
if (fromDate) {
|
|
18287
18621
|
try {
|
|
18288
|
-
|
|
18289
|
-
|
|
18290
|
-
|
|
18291
|
-
|
|
18292
|
-
writeError(`Invalid market for stock list: ${market}`);
|
|
18293
|
-
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
18294
|
-
}
|
|
18295
|
-
const apiKey = getApiKey();
|
|
18296
|
-
if (!apiKey) {
|
|
18297
|
-
writeError(
|
|
18298
|
-
"No API key configured. Use 'krx auth set <key>' or set KRX_API_KEY env var."
|
|
18299
|
-
);
|
|
18300
|
-
process.exit(EXIT_CODES.AUTH_FAILURE);
|
|
18301
|
-
}
|
|
18302
|
-
const parentOpts = program3.opts();
|
|
18303
|
-
if (parentOpts.dryRun) {
|
|
18304
|
-
writeOutput(
|
|
18305
|
-
JSON.stringify(
|
|
18306
|
-
{
|
|
18307
|
-
method: "POST",
|
|
18308
|
-
endpoint,
|
|
18309
|
-
params: { basDd: date5 },
|
|
18310
|
-
headers: { AUTH_KEY: "***" }
|
|
18311
|
-
},
|
|
18312
|
-
null,
|
|
18313
|
-
2
|
|
18314
|
-
)
|
|
18315
|
-
);
|
|
18316
|
-
return;
|
|
18317
|
-
}
|
|
18318
|
-
const result = await krxFetch({
|
|
18319
|
-
endpoint,
|
|
18320
|
-
params: { basDd: date5 },
|
|
18321
|
-
apiKey
|
|
18322
|
-
});
|
|
18323
|
-
if (!result.success) {
|
|
18324
|
-
handleKrxError(result);
|
|
18325
|
-
}
|
|
18326
|
-
if (result.data.length === 0) {
|
|
18327
|
-
writeError(`No data for date ${date5}`);
|
|
18328
|
-
process.exit(EXIT_CODES.NO_DATA);
|
|
18329
|
-
}
|
|
18330
|
-
const format = detectOutputFormat(parentOpts.output);
|
|
18331
|
-
const fields = parentOpts.fields?.split(",");
|
|
18332
|
-
writeOutput(
|
|
18333
|
-
formatOutput(
|
|
18334
|
-
result.data,
|
|
18335
|
-
format,
|
|
18336
|
-
fields
|
|
18337
|
-
)
|
|
18622
|
+
validateDate(fromDate);
|
|
18623
|
+
} catch (err) {
|
|
18624
|
+
writeError(
|
|
18625
|
+
`Invalid --from date: ${err instanceof Error ? err.message : String(err)}`
|
|
18338
18626
|
);
|
|
18627
|
+
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
18628
|
+
}
|
|
18629
|
+
}
|
|
18630
|
+
if (toDate) {
|
|
18631
|
+
try {
|
|
18632
|
+
validateDate(toDate);
|
|
18339
18633
|
} catch (err) {
|
|
18340
|
-
writeError(
|
|
18634
|
+
writeError(
|
|
18635
|
+
`Invalid --to date: ${err instanceof Error ? err.message : String(err)}`
|
|
18636
|
+
);
|
|
18341
18637
|
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
18342
18638
|
}
|
|
18639
|
+
}
|
|
18640
|
+
const isDateRange = fromDate && toDate;
|
|
18641
|
+
let data;
|
|
18642
|
+
if (isDateRange) {
|
|
18643
|
+
const restParams = Object.fromEntries(
|
|
18644
|
+
Object.entries(finalParams).filter(([k]) => k !== "basDd")
|
|
18645
|
+
);
|
|
18646
|
+
const rangeResult = await fetchDateRange({
|
|
18647
|
+
endpoint,
|
|
18648
|
+
from: fromDate,
|
|
18649
|
+
to: toDate,
|
|
18650
|
+
apiKey,
|
|
18651
|
+
cache: parentOpts.cache,
|
|
18652
|
+
extraParams: restParams
|
|
18653
|
+
});
|
|
18654
|
+
if (!rangeResult.success) {
|
|
18655
|
+
writeError(rangeResult.error ?? "Date range fetch failed");
|
|
18656
|
+
process.exit(EXIT_CODES.GENERAL_ERROR);
|
|
18657
|
+
}
|
|
18658
|
+
if (rangeResult.failedDays > 0) {
|
|
18659
|
+
writeError(`Warning: ${rangeResult.failedDays} day(s) failed to fetch`);
|
|
18660
|
+
}
|
|
18661
|
+
data = rangeResult.data;
|
|
18662
|
+
} else {
|
|
18663
|
+
const result = await krxFetch({
|
|
18664
|
+
endpoint,
|
|
18665
|
+
params: finalParams,
|
|
18666
|
+
apiKey,
|
|
18667
|
+
cache: parentOpts.cache,
|
|
18668
|
+
retries: parentOpts.retries
|
|
18669
|
+
});
|
|
18670
|
+
if (!result.success) {
|
|
18671
|
+
handleKrxError(result);
|
|
18672
|
+
}
|
|
18673
|
+
data = result.data;
|
|
18674
|
+
}
|
|
18675
|
+
if (data.length === 0) {
|
|
18676
|
+
writeError(noDataMessage ?? "No data");
|
|
18677
|
+
process.exit(EXIT_CODES.NO_DATA);
|
|
18678
|
+
}
|
|
18679
|
+
data = applyPipeline(data, {
|
|
18680
|
+
filter: parentOpts.filter,
|
|
18681
|
+
sort: parentOpts.sort,
|
|
18682
|
+
direction: parentOpts.asc ? "asc" : "desc",
|
|
18683
|
+
limit: parentOpts.limit
|
|
18343
18684
|
});
|
|
18344
|
-
|
|
18685
|
+
if (data.length === 0) {
|
|
18686
|
+
writeError(
|
|
18687
|
+
parentOpts.filter ? `No results matched filter: ${parentOpts.filter}` : noDataMessage ?? "No data"
|
|
18688
|
+
);
|
|
18689
|
+
process.exit(EXIT_CODES.NO_DATA);
|
|
18690
|
+
}
|
|
18691
|
+
const format = detectOutputFormat(parentOpts.output);
|
|
18692
|
+
const fields = parentOpts.fields?.split(",");
|
|
18693
|
+
const output = formatOutput(data, format, fields);
|
|
18694
|
+
const savePath = parentOpts.save;
|
|
18695
|
+
if (savePath) {
|
|
18345
18696
|
try {
|
|
18346
|
-
const
|
|
18347
|
-
|
|
18348
|
-
|
|
18349
|
-
|
|
18350
|
-
|
|
18351
|
-
|
|
18352
|
-
|
|
18353
|
-
|
|
18354
|
-
|
|
18355
|
-
|
|
18356
|
-
|
|
18357
|
-
|
|
18358
|
-
|
|
18359
|
-
|
|
18360
|
-
|
|
18361
|
-
|
|
18362
|
-
|
|
18363
|
-
|
|
18364
|
-
|
|
18365
|
-
|
|
18366
|
-
|
|
18367
|
-
|
|
18368
|
-
|
|
18369
|
-
|
|
18370
|
-
|
|
18371
|
-
|
|
18372
|
-
|
|
18373
|
-
|
|
18374
|
-
|
|
18697
|
+
const dir = path4.dirname(savePath);
|
|
18698
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
18699
|
+
fs4.writeFileSync(savePath, output + "\n", "utf-8");
|
|
18700
|
+
writeOutput(JSON.stringify({ saved: savePath, records: data.length }));
|
|
18701
|
+
} catch (err) {
|
|
18702
|
+
writeError(
|
|
18703
|
+
`Failed to save file: ${err instanceof Error ? err.message : String(err)}`
|
|
18704
|
+
);
|
|
18705
|
+
process.exit(EXIT_CODES.GENERAL_ERROR);
|
|
18706
|
+
}
|
|
18707
|
+
} else {
|
|
18708
|
+
writeOutput(output);
|
|
18709
|
+
}
|
|
18710
|
+
}
|
|
18711
|
+
function resolveEndpoint(endpoints, key, label) {
|
|
18712
|
+
const endpoint = endpoints[key.toLowerCase()];
|
|
18713
|
+
if (!endpoint) {
|
|
18714
|
+
writeError(
|
|
18715
|
+
`Invalid ${label}: ${key}. Must be one of: ${Object.keys(endpoints).join(", ")}`
|
|
18716
|
+
);
|
|
18717
|
+
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
18718
|
+
}
|
|
18719
|
+
return endpoint;
|
|
18720
|
+
}
|
|
18721
|
+
|
|
18722
|
+
// src/cli/commands/index-cmd.ts
|
|
18723
|
+
var MARKET_ENDPOINTS = {
|
|
18724
|
+
kospi: "/svc/apis/idx/kospi_dd_trd",
|
|
18725
|
+
kosdaq: "/svc/apis/idx/kosdaq_dd_trd",
|
|
18726
|
+
krx: "/svc/apis/idx/krx_dd_trd",
|
|
18727
|
+
bond: "/svc/apis/idx/bon_dd_trd",
|
|
18728
|
+
derivative: "/svc/apis/idx/drvprod_dd_trd"
|
|
18729
|
+
};
|
|
18730
|
+
function registerIndexCommand(program3) {
|
|
18731
|
+
const index = program3.command("index").description("Query KRX index data");
|
|
18732
|
+
index.command("list").description("List index daily trading data").requiredOption("--date <date>", "trading date (YYYYMMDD)").option(
|
|
18733
|
+
"--market <market>",
|
|
18734
|
+
"market: kospi, kosdaq, krx, bond, derivative",
|
|
18735
|
+
"kospi"
|
|
18736
|
+
).action(async (opts) => {
|
|
18737
|
+
const date5 = validateDate(opts.date);
|
|
18738
|
+
const endpoint = resolveEndpoint(MARKET_ENDPOINTS, opts.market, "market");
|
|
18739
|
+
await executeCommand({
|
|
18740
|
+
endpoint,
|
|
18741
|
+
params: { basDd: date5 },
|
|
18742
|
+
program: program3,
|
|
18743
|
+
noDataMessage: `No data for date ${date5}`
|
|
18744
|
+
});
|
|
18745
|
+
});
|
|
18746
|
+
}
|
|
18747
|
+
|
|
18748
|
+
// src/client/search.ts
|
|
18749
|
+
var BASE_INFO_ENDPOINTS = [
|
|
18750
|
+
{ endpoint: "/svc/apis/sto/stk_isu_base_info", market: "KOSPI" },
|
|
18751
|
+
{ endpoint: "/svc/apis/sto/ksq_isu_base_info", market: "KOSDAQ" }
|
|
18752
|
+
];
|
|
18753
|
+
async function searchStock(apiKey, query) {
|
|
18754
|
+
const basDd = getRecentTradingDate();
|
|
18755
|
+
const lowerQuery = query.toLowerCase();
|
|
18756
|
+
const results = await Promise.all(
|
|
18757
|
+
BASE_INFO_ENDPOINTS.map(async ({ endpoint, market }) => {
|
|
18375
18758
|
const result = await krxFetch({
|
|
18376
18759
|
endpoint,
|
|
18377
|
-
params: {},
|
|
18760
|
+
params: { basDd },
|
|
18378
18761
|
apiKey
|
|
18379
18762
|
});
|
|
18380
18763
|
if (!result.success) {
|
|
18381
|
-
|
|
18382
|
-
}
|
|
18383
|
-
if (result.data.length === 0) {
|
|
18384
|
-
writeError("No data");
|
|
18385
|
-
process.exit(EXIT_CODES.NO_DATA);
|
|
18764
|
+
return [];
|
|
18386
18765
|
}
|
|
18387
|
-
|
|
18388
|
-
|
|
18389
|
-
|
|
18390
|
-
|
|
18391
|
-
|
|
18392
|
-
|
|
18393
|
-
|
|
18394
|
-
|
|
18766
|
+
return result.data.filter((row) => {
|
|
18767
|
+
const name = row["ISU_NM"] ?? "";
|
|
18768
|
+
const shortName = row["ISU_ABBRV"] ?? "";
|
|
18769
|
+
return name.toLowerCase().includes(lowerQuery) || shortName.toLowerCase().includes(lowerQuery);
|
|
18770
|
+
}).map((row) => ({
|
|
18771
|
+
ISU_CD: row["ISU_CD"] ?? "",
|
|
18772
|
+
ISU_SRT_CD: row["ISU_SRT_CD"] ?? "",
|
|
18773
|
+
ISU_NM: row["ISU_NM"] ?? row["ISU_ABBRV"] ?? "",
|
|
18774
|
+
MKT_NM: market
|
|
18775
|
+
}));
|
|
18776
|
+
})
|
|
18777
|
+
);
|
|
18778
|
+
return results.flat();
|
|
18779
|
+
}
|
|
18780
|
+
|
|
18781
|
+
// src/cli/commands/stock.ts
|
|
18782
|
+
var TRADING_ENDPOINTS = {
|
|
18783
|
+
kospi: "/svc/apis/sto/stk_bydd_trd",
|
|
18784
|
+
kosdaq: "/svc/apis/sto/ksq_bydd_trd",
|
|
18785
|
+
konex: "/svc/apis/sto/knx_bydd_trd"
|
|
18786
|
+
};
|
|
18787
|
+
var INFO_ENDPOINTS = {
|
|
18788
|
+
kospi: "/svc/apis/sto/stk_isu_base_info",
|
|
18789
|
+
kosdaq: "/svc/apis/sto/ksq_isu_base_info",
|
|
18790
|
+
konex: "/svc/apis/sto/knx_isu_base_info"
|
|
18791
|
+
};
|
|
18792
|
+
function registerStockCommand(program3) {
|
|
18793
|
+
const stock = program3.command("stock").description("Query KRX stock data");
|
|
18794
|
+
stock.command("list").description("List stock daily trading data").requiredOption("--date <date>", "trading date (YYYYMMDD)").option("--market <market>", "market: kospi, kosdaq, konex", "kospi").action(async (opts) => {
|
|
18795
|
+
const date5 = validateDate(opts.date);
|
|
18796
|
+
validateMarket(opts.market);
|
|
18797
|
+
const endpoint = resolveEndpoint(
|
|
18798
|
+
TRADING_ENDPOINTS,
|
|
18799
|
+
opts.market,
|
|
18800
|
+
"market"
|
|
18801
|
+
);
|
|
18802
|
+
await executeCommand({
|
|
18803
|
+
endpoint,
|
|
18804
|
+
params: { basDd: date5 },
|
|
18805
|
+
program: program3,
|
|
18806
|
+
noDataMessage: `No data for date ${date5}`
|
|
18807
|
+
});
|
|
18808
|
+
});
|
|
18809
|
+
stock.command("info").description("List stock base information").option("--market <market>", "market: kospi, kosdaq, konex", "kospi").action(async (opts) => {
|
|
18810
|
+
validateMarket(opts.market);
|
|
18811
|
+
const endpoint = resolveEndpoint(INFO_ENDPOINTS, opts.market, "market");
|
|
18812
|
+
await executeCommand({
|
|
18813
|
+
endpoint,
|
|
18814
|
+
params: {},
|
|
18815
|
+
program: program3
|
|
18816
|
+
});
|
|
18817
|
+
});
|
|
18818
|
+
stock.command("search <query>").description("Search stocks by name").action(async (query) => {
|
|
18819
|
+
validateNoInjection(query);
|
|
18820
|
+
const apiKey = getApiKey();
|
|
18821
|
+
if (!apiKey) {
|
|
18822
|
+
writeError(
|
|
18823
|
+
"No API key configured. Use 'krx auth set <key>' or set KRX_API_KEY env var."
|
|
18395
18824
|
);
|
|
18396
|
-
|
|
18397
|
-
writeError(err instanceof Error ? err.message : String(err));
|
|
18398
|
-
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
18825
|
+
process.exit(EXIT_CODES.AUTH_FAILURE);
|
|
18399
18826
|
}
|
|
18827
|
+
const results = await searchStock(apiKey, query);
|
|
18828
|
+
if (results.length === 0) {
|
|
18829
|
+
writeError(`No stocks found matching "${query}"`);
|
|
18830
|
+
process.exit(EXIT_CODES.NO_DATA);
|
|
18831
|
+
}
|
|
18832
|
+
const parentOpts = program3.opts();
|
|
18833
|
+
const format = detectOutputFormat(parentOpts.output);
|
|
18834
|
+
const fields = parentOpts.fields?.split(",");
|
|
18835
|
+
writeOutput(
|
|
18836
|
+
formatOutput(
|
|
18837
|
+
results,
|
|
18838
|
+
format,
|
|
18839
|
+
fields
|
|
18840
|
+
)
|
|
18841
|
+
);
|
|
18400
18842
|
});
|
|
18401
18843
|
}
|
|
18402
18844
|
|
|
@@ -18409,64 +18851,14 @@ var TYPE_ENDPOINTS = {
|
|
|
18409
18851
|
function registerEtpCommand(program3) {
|
|
18410
18852
|
const etp = program3.command("etp").description("Query KRX ETP data (ETF/ETN/ELW)");
|
|
18411
18853
|
etp.command("list").description("List ETP daily trading data").requiredOption("--date <date>", "trading date (YYYYMMDD)").option("--type <type>", "type: etf, etn, elw", "etf").action(async (opts) => {
|
|
18412
|
-
|
|
18413
|
-
|
|
18414
|
-
|
|
18415
|
-
|
|
18416
|
-
|
|
18417
|
-
|
|
18418
|
-
|
|
18419
|
-
|
|
18420
|
-
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
18421
|
-
}
|
|
18422
|
-
const apiKey = getApiKey();
|
|
18423
|
-
if (!apiKey) {
|
|
18424
|
-
writeError(
|
|
18425
|
-
"No API key configured. Use 'krx auth set <key>' or set KRX_API_KEY env var."
|
|
18426
|
-
);
|
|
18427
|
-
process.exit(EXIT_CODES.AUTH_FAILURE);
|
|
18428
|
-
}
|
|
18429
|
-
const parentOpts = program3.opts();
|
|
18430
|
-
if (parentOpts.dryRun) {
|
|
18431
|
-
writeOutput(
|
|
18432
|
-
JSON.stringify(
|
|
18433
|
-
{
|
|
18434
|
-
method: "POST",
|
|
18435
|
-
endpoint,
|
|
18436
|
-
params: { basDd: date5 },
|
|
18437
|
-
headers: { AUTH_KEY: "***" }
|
|
18438
|
-
},
|
|
18439
|
-
null,
|
|
18440
|
-
2
|
|
18441
|
-
)
|
|
18442
|
-
);
|
|
18443
|
-
return;
|
|
18444
|
-
}
|
|
18445
|
-
const result = await krxFetch({
|
|
18446
|
-
endpoint,
|
|
18447
|
-
params: { basDd: date5 },
|
|
18448
|
-
apiKey
|
|
18449
|
-
});
|
|
18450
|
-
if (!result.success) {
|
|
18451
|
-
handleKrxError(result);
|
|
18452
|
-
}
|
|
18453
|
-
if (result.data.length === 0) {
|
|
18454
|
-
writeError(`No data for date ${date5}`);
|
|
18455
|
-
process.exit(EXIT_CODES.NO_DATA);
|
|
18456
|
-
}
|
|
18457
|
-
const format = detectOutputFormat(parentOpts.output);
|
|
18458
|
-
const fields = parentOpts.fields?.split(",");
|
|
18459
|
-
writeOutput(
|
|
18460
|
-
formatOutput(
|
|
18461
|
-
result.data,
|
|
18462
|
-
format,
|
|
18463
|
-
fields
|
|
18464
|
-
)
|
|
18465
|
-
);
|
|
18466
|
-
} catch (err) {
|
|
18467
|
-
writeError(err instanceof Error ? err.message : String(err));
|
|
18468
|
-
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
18469
|
-
}
|
|
18854
|
+
const date5 = validateDate(opts.date);
|
|
18855
|
+
const endpoint = resolveEndpoint(TYPE_ENDPOINTS, opts.type, "type");
|
|
18856
|
+
await executeCommand({
|
|
18857
|
+
endpoint,
|
|
18858
|
+
params: { basDd: date5 },
|
|
18859
|
+
program: program3,
|
|
18860
|
+
noDataMessage: `No data for date ${date5}`
|
|
18861
|
+
});
|
|
18470
18862
|
});
|
|
18471
18863
|
}
|
|
18472
18864
|
|
|
@@ -18479,64 +18871,14 @@ var MARKET_ENDPOINTS2 = {
|
|
|
18479
18871
|
function registerBondCommand(program3) {
|
|
18480
18872
|
const bond = program3.command("bond").description("Query KRX bond data");
|
|
18481
18873
|
bond.command("list").description("List bond daily trading data").requiredOption("--date <date>", "trading date (YYYYMMDD)").option("--market <market>", "market: kts, general, small", "general").action(async (opts) => {
|
|
18482
|
-
|
|
18483
|
-
|
|
18484
|
-
|
|
18485
|
-
|
|
18486
|
-
|
|
18487
|
-
|
|
18488
|
-
|
|
18489
|
-
|
|
18490
|
-
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
18491
|
-
}
|
|
18492
|
-
const apiKey = getApiKey();
|
|
18493
|
-
if (!apiKey) {
|
|
18494
|
-
writeError(
|
|
18495
|
-
"No API key configured. Use 'krx auth set <key>' or set KRX_API_KEY env var."
|
|
18496
|
-
);
|
|
18497
|
-
process.exit(EXIT_CODES.AUTH_FAILURE);
|
|
18498
|
-
}
|
|
18499
|
-
const parentOpts = program3.opts();
|
|
18500
|
-
if (parentOpts.dryRun) {
|
|
18501
|
-
writeOutput(
|
|
18502
|
-
JSON.stringify(
|
|
18503
|
-
{
|
|
18504
|
-
method: "POST",
|
|
18505
|
-
endpoint,
|
|
18506
|
-
params: { basDd: date5 },
|
|
18507
|
-
headers: { AUTH_KEY: "***" }
|
|
18508
|
-
},
|
|
18509
|
-
null,
|
|
18510
|
-
2
|
|
18511
|
-
)
|
|
18512
|
-
);
|
|
18513
|
-
return;
|
|
18514
|
-
}
|
|
18515
|
-
const result = await krxFetch({
|
|
18516
|
-
endpoint,
|
|
18517
|
-
params: { basDd: date5 },
|
|
18518
|
-
apiKey
|
|
18519
|
-
});
|
|
18520
|
-
if (!result.success) {
|
|
18521
|
-
handleKrxError(result);
|
|
18522
|
-
}
|
|
18523
|
-
if (result.data.length === 0) {
|
|
18524
|
-
writeError(`No data for date ${date5}`);
|
|
18525
|
-
process.exit(EXIT_CODES.NO_DATA);
|
|
18526
|
-
}
|
|
18527
|
-
const format = detectOutputFormat(parentOpts.output);
|
|
18528
|
-
const fields = parentOpts.fields?.split(",");
|
|
18529
|
-
writeOutput(
|
|
18530
|
-
formatOutput(
|
|
18531
|
-
result.data,
|
|
18532
|
-
format,
|
|
18533
|
-
fields
|
|
18534
|
-
)
|
|
18535
|
-
);
|
|
18536
|
-
} catch (err) {
|
|
18537
|
-
writeError(err instanceof Error ? err.message : String(err));
|
|
18538
|
-
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
18539
|
-
}
|
|
18874
|
+
const date5 = validateDate(opts.date);
|
|
18875
|
+
const endpoint = resolveEndpoint(MARKET_ENDPOINTS2, opts.market, "market");
|
|
18876
|
+
await executeCommand({
|
|
18877
|
+
endpoint,
|
|
18878
|
+
params: { basDd: date5 },
|
|
18879
|
+
program: program3,
|
|
18880
|
+
noDataMessage: `No data for date ${date5}`
|
|
18881
|
+
});
|
|
18540
18882
|
});
|
|
18541
18883
|
}
|
|
18542
18884
|
|
|
@@ -18556,64 +18898,14 @@ function registerDerivativeCommand(program3) {
|
|
|
18556
18898
|
"type: futures, futures-kospi, futures-kosdaq, options, options-kospi, options-kosdaq",
|
|
18557
18899
|
"futures"
|
|
18558
18900
|
).action(async (opts) => {
|
|
18559
|
-
|
|
18560
|
-
|
|
18561
|
-
|
|
18562
|
-
|
|
18563
|
-
|
|
18564
|
-
|
|
18565
|
-
|
|
18566
|
-
|
|
18567
|
-
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
18568
|
-
}
|
|
18569
|
-
const apiKey = getApiKey();
|
|
18570
|
-
if (!apiKey) {
|
|
18571
|
-
writeError(
|
|
18572
|
-
"No API key configured. Use 'krx auth set <key>' or set KRX_API_KEY env var."
|
|
18573
|
-
);
|
|
18574
|
-
process.exit(EXIT_CODES.AUTH_FAILURE);
|
|
18575
|
-
}
|
|
18576
|
-
const parentOpts = program3.opts();
|
|
18577
|
-
if (parentOpts.dryRun) {
|
|
18578
|
-
writeOutput(
|
|
18579
|
-
JSON.stringify(
|
|
18580
|
-
{
|
|
18581
|
-
method: "POST",
|
|
18582
|
-
endpoint,
|
|
18583
|
-
params: { basDd: date5 },
|
|
18584
|
-
headers: { AUTH_KEY: "***" }
|
|
18585
|
-
},
|
|
18586
|
-
null,
|
|
18587
|
-
2
|
|
18588
|
-
)
|
|
18589
|
-
);
|
|
18590
|
-
return;
|
|
18591
|
-
}
|
|
18592
|
-
const result = await krxFetch({
|
|
18593
|
-
endpoint,
|
|
18594
|
-
params: { basDd: date5 },
|
|
18595
|
-
apiKey
|
|
18596
|
-
});
|
|
18597
|
-
if (!result.success) {
|
|
18598
|
-
handleKrxError(result);
|
|
18599
|
-
}
|
|
18600
|
-
if (result.data.length === 0) {
|
|
18601
|
-
writeError(`No data for date ${date5}`);
|
|
18602
|
-
process.exit(EXIT_CODES.NO_DATA);
|
|
18603
|
-
}
|
|
18604
|
-
const format = detectOutputFormat(parentOpts.output);
|
|
18605
|
-
const fields = parentOpts.fields?.split(",");
|
|
18606
|
-
writeOutput(
|
|
18607
|
-
formatOutput(
|
|
18608
|
-
result.data,
|
|
18609
|
-
format,
|
|
18610
|
-
fields
|
|
18611
|
-
)
|
|
18612
|
-
);
|
|
18613
|
-
} catch (err) {
|
|
18614
|
-
writeError(err instanceof Error ? err.message : String(err));
|
|
18615
|
-
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
18616
|
-
}
|
|
18901
|
+
const date5 = validateDate(opts.date);
|
|
18902
|
+
const endpoint = resolveEndpoint(TYPE_ENDPOINTS2, opts.type, "type");
|
|
18903
|
+
await executeCommand({
|
|
18904
|
+
endpoint,
|
|
18905
|
+
params: { basDd: date5 },
|
|
18906
|
+
program: program3,
|
|
18907
|
+
noDataMessage: `No data for date ${date5}`
|
|
18908
|
+
});
|
|
18617
18909
|
});
|
|
18618
18910
|
}
|
|
18619
18911
|
|
|
@@ -18626,64 +18918,14 @@ var TYPE_ENDPOINTS3 = {
|
|
|
18626
18918
|
function registerCommodityCommand(program3) {
|
|
18627
18919
|
const commodity = program3.command("commodity").description("Query KRX commodity data (oil/gold/emission)");
|
|
18628
18920
|
commodity.command("list").description("List commodity daily trading data").requiredOption("--date <date>", "trading date (YYYYMMDD)").option("--type <type>", "type: oil, gold, emission", "gold").action(async (opts) => {
|
|
18629
|
-
|
|
18630
|
-
|
|
18631
|
-
|
|
18632
|
-
|
|
18633
|
-
|
|
18634
|
-
|
|
18635
|
-
|
|
18636
|
-
|
|
18637
|
-
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
18638
|
-
}
|
|
18639
|
-
const apiKey = getApiKey();
|
|
18640
|
-
if (!apiKey) {
|
|
18641
|
-
writeError(
|
|
18642
|
-
"No API key configured. Use 'krx auth set <key>' or set KRX_API_KEY env var."
|
|
18643
|
-
);
|
|
18644
|
-
process.exit(EXIT_CODES.AUTH_FAILURE);
|
|
18645
|
-
}
|
|
18646
|
-
const parentOpts = program3.opts();
|
|
18647
|
-
if (parentOpts.dryRun) {
|
|
18648
|
-
writeOutput(
|
|
18649
|
-
JSON.stringify(
|
|
18650
|
-
{
|
|
18651
|
-
method: "POST",
|
|
18652
|
-
endpoint,
|
|
18653
|
-
params: { basDd: date5 },
|
|
18654
|
-
headers: { AUTH_KEY: "***" }
|
|
18655
|
-
},
|
|
18656
|
-
null,
|
|
18657
|
-
2
|
|
18658
|
-
)
|
|
18659
|
-
);
|
|
18660
|
-
return;
|
|
18661
|
-
}
|
|
18662
|
-
const result = await krxFetch({
|
|
18663
|
-
endpoint,
|
|
18664
|
-
params: { basDd: date5 },
|
|
18665
|
-
apiKey
|
|
18666
|
-
});
|
|
18667
|
-
if (!result.success) {
|
|
18668
|
-
handleKrxError(result);
|
|
18669
|
-
}
|
|
18670
|
-
if (result.data.length === 0) {
|
|
18671
|
-
writeError(`No data for date ${date5}`);
|
|
18672
|
-
process.exit(EXIT_CODES.NO_DATA);
|
|
18673
|
-
}
|
|
18674
|
-
const format = detectOutputFormat(parentOpts.output);
|
|
18675
|
-
const fields = parentOpts.fields?.split(",");
|
|
18676
|
-
writeOutput(
|
|
18677
|
-
formatOutput(
|
|
18678
|
-
result.data,
|
|
18679
|
-
format,
|
|
18680
|
-
fields
|
|
18681
|
-
)
|
|
18682
|
-
);
|
|
18683
|
-
} catch (err) {
|
|
18684
|
-
writeError(err instanceof Error ? err.message : String(err));
|
|
18685
|
-
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
18686
|
-
}
|
|
18921
|
+
const date5 = validateDate(opts.date);
|
|
18922
|
+
const endpoint = resolveEndpoint(TYPE_ENDPOINTS3, opts.type, "type");
|
|
18923
|
+
await executeCommand({
|
|
18924
|
+
endpoint,
|
|
18925
|
+
params: { basDd: date5 },
|
|
18926
|
+
program: program3,
|
|
18927
|
+
noDataMessage: `No data for date ${date5}`
|
|
18928
|
+
});
|
|
18687
18929
|
});
|
|
18688
18930
|
}
|
|
18689
18931
|
|
|
@@ -18696,64 +18938,14 @@ var TYPE_ENDPOINTS4 = {
|
|
|
18696
18938
|
function registerEsgCommand(program3) {
|
|
18697
18939
|
const esg = program3.command("esg").description("Query KRX ESG data");
|
|
18698
18940
|
esg.command("list").description("List ESG data").requiredOption("--date <date>", "trading date (YYYYMMDD)").option("--type <type>", "type: sri-bond, etp, index", "index").action(async (opts) => {
|
|
18699
|
-
|
|
18700
|
-
|
|
18701
|
-
|
|
18702
|
-
|
|
18703
|
-
|
|
18704
|
-
|
|
18705
|
-
|
|
18706
|
-
|
|
18707
|
-
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
18708
|
-
}
|
|
18709
|
-
const apiKey = getApiKey();
|
|
18710
|
-
if (!apiKey) {
|
|
18711
|
-
writeError(
|
|
18712
|
-
"No API key configured. Use 'krx auth set <key>' or set KRX_API_KEY env var."
|
|
18713
|
-
);
|
|
18714
|
-
process.exit(EXIT_CODES.AUTH_FAILURE);
|
|
18715
|
-
}
|
|
18716
|
-
const parentOpts = program3.opts();
|
|
18717
|
-
if (parentOpts.dryRun) {
|
|
18718
|
-
writeOutput(
|
|
18719
|
-
JSON.stringify(
|
|
18720
|
-
{
|
|
18721
|
-
method: "POST",
|
|
18722
|
-
endpoint,
|
|
18723
|
-
params: { basDd: date5 },
|
|
18724
|
-
headers: { AUTH_KEY: "***" }
|
|
18725
|
-
},
|
|
18726
|
-
null,
|
|
18727
|
-
2
|
|
18728
|
-
)
|
|
18729
|
-
);
|
|
18730
|
-
return;
|
|
18731
|
-
}
|
|
18732
|
-
const result = await krxFetch({
|
|
18733
|
-
endpoint,
|
|
18734
|
-
params: { basDd: date5 },
|
|
18735
|
-
apiKey
|
|
18736
|
-
});
|
|
18737
|
-
if (!result.success) {
|
|
18738
|
-
handleKrxError(result);
|
|
18739
|
-
}
|
|
18740
|
-
if (result.data.length === 0) {
|
|
18741
|
-
writeError(`No data for date ${date5}`);
|
|
18742
|
-
process.exit(EXIT_CODES.NO_DATA);
|
|
18743
|
-
}
|
|
18744
|
-
const format = detectOutputFormat(parentOpts.output);
|
|
18745
|
-
const fields = parentOpts.fields?.split(",");
|
|
18746
|
-
writeOutput(
|
|
18747
|
-
formatOutput(
|
|
18748
|
-
result.data,
|
|
18749
|
-
format,
|
|
18750
|
-
fields
|
|
18751
|
-
)
|
|
18752
|
-
);
|
|
18753
|
-
} catch (err) {
|
|
18754
|
-
writeError(err instanceof Error ? err.message : String(err));
|
|
18755
|
-
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
18756
|
-
}
|
|
18941
|
+
const date5 = validateDate(opts.date);
|
|
18942
|
+
const endpoint = resolveEndpoint(TYPE_ENDPOINTS4, opts.type, "type");
|
|
18943
|
+
await executeCommand({
|
|
18944
|
+
endpoint,
|
|
18945
|
+
params: { basDd: date5 },
|
|
18946
|
+
program: program3,
|
|
18947
|
+
noDataMessage: `No data for date ${date5}`
|
|
18948
|
+
});
|
|
18757
18949
|
});
|
|
18758
18950
|
}
|
|
18759
18951
|
|
|
@@ -18805,20 +18997,402 @@ function registerSchemaCommand(program3) {
|
|
|
18805
18997
|
});
|
|
18806
18998
|
}
|
|
18807
18999
|
|
|
19000
|
+
// src/cli/commands/cache.ts
|
|
19001
|
+
function registerCacheCommand(program3) {
|
|
19002
|
+
const cache = program3.command("cache").description("Manage API response cache");
|
|
19003
|
+
cache.command("status").description("Show cache status").action(() => {
|
|
19004
|
+
const status = getCacheStatus();
|
|
19005
|
+
writeOutput(
|
|
19006
|
+
JSON.stringify(
|
|
19007
|
+
{
|
|
19008
|
+
dates: status.dates,
|
|
19009
|
+
files: status.totalFiles,
|
|
19010
|
+
sizeBytes: status.totalSize,
|
|
19011
|
+
sizeMB: Math.round(status.totalSize / 1024 / 1024 * 100) / 100
|
|
19012
|
+
},
|
|
19013
|
+
null,
|
|
19014
|
+
2
|
|
19015
|
+
)
|
|
19016
|
+
);
|
|
19017
|
+
});
|
|
19018
|
+
cache.command("clear").description("Clear all cached data").action(() => {
|
|
19019
|
+
const result = clearCache();
|
|
19020
|
+
writeOutput(
|
|
19021
|
+
JSON.stringify(
|
|
19022
|
+
{
|
|
19023
|
+
cleared: true,
|
|
19024
|
+
files: result.files,
|
|
19025
|
+
directories: result.directories
|
|
19026
|
+
},
|
|
19027
|
+
null,
|
|
19028
|
+
2
|
|
19029
|
+
)
|
|
19030
|
+
);
|
|
19031
|
+
});
|
|
19032
|
+
}
|
|
19033
|
+
|
|
19034
|
+
// src/client/market-summary.ts
|
|
19035
|
+
var KOSPI_INDEX_ENDPOINT = "/svc/apis/idx/kospi_dd_trd";
|
|
19036
|
+
var KOSDAQ_INDEX_ENDPOINT = "/svc/apis/idx/kosdaq_dd_trd";
|
|
19037
|
+
var KOSPI_STOCK_ENDPOINT = "/svc/apis/sto/stk_bydd_trd";
|
|
19038
|
+
var KOSDAQ_STOCK_ENDPOINT = "/svc/apis/sto/ksq_bydd_trd";
|
|
19039
|
+
var TOP_N = 5;
|
|
19040
|
+
function computeStockStats(stocks) {
|
|
19041
|
+
return stocks.reduce(
|
|
19042
|
+
(acc, stock) => {
|
|
19043
|
+
const rate = parseKrxNumber(stock["FLUC_RT"] ?? "0");
|
|
19044
|
+
return {
|
|
19045
|
+
advancing: acc.advancing + (rate > 0 ? 1 : 0),
|
|
19046
|
+
declining: acc.declining + (rate < 0 ? 1 : 0),
|
|
19047
|
+
unchanged: acc.unchanged + (rate === 0 ? 1 : 0),
|
|
19048
|
+
totalVolume: acc.totalVolume + parseKrxNumber(stock["ACC_TRDVOL"] ?? "0"),
|
|
19049
|
+
totalValue: acc.totalValue + parseKrxNumber(stock["ACC_TRDVAL"] ?? "0")
|
|
19050
|
+
};
|
|
19051
|
+
},
|
|
19052
|
+
{
|
|
19053
|
+
advancing: 0,
|
|
19054
|
+
declining: 0,
|
|
19055
|
+
unchanged: 0,
|
|
19056
|
+
totalVolume: 0,
|
|
19057
|
+
totalValue: 0
|
|
19058
|
+
}
|
|
19059
|
+
);
|
|
19060
|
+
}
|
|
19061
|
+
function computeTopMovers(stocks) {
|
|
19062
|
+
const sorted = [...stocks].sort((a, b) => {
|
|
19063
|
+
const rateA = parseKrxNumber(a["FLUC_RT"] ?? "0");
|
|
19064
|
+
const rateB = parseKrxNumber(b["FLUC_RT"] ?? "0");
|
|
19065
|
+
return rateB - rateA;
|
|
19066
|
+
});
|
|
19067
|
+
const topGainers = sorted.slice(0, TOP_N);
|
|
19068
|
+
const topLosers = sorted.slice(-TOP_N).reverse();
|
|
19069
|
+
return { topGainers, topLosers };
|
|
19070
|
+
}
|
|
19071
|
+
function safeFetch(...args) {
|
|
19072
|
+
return krxFetch(...args).catch(
|
|
19073
|
+
(err) => ({
|
|
19074
|
+
success: false,
|
|
19075
|
+
data: [],
|
|
19076
|
+
error: err instanceof Error ? err.message : "Network error"
|
|
19077
|
+
})
|
|
19078
|
+
);
|
|
19079
|
+
}
|
|
19080
|
+
async function fetchMarketSummary(options) {
|
|
19081
|
+
const { apiKey, date: date5, cache } = options;
|
|
19082
|
+
const params = { basDd: date5 };
|
|
19083
|
+
const [kospiIdx, kosdaqIdx, kospiStk, kosdaqStk] = await Promise.all([
|
|
19084
|
+
safeFetch({ endpoint: KOSPI_INDEX_ENDPOINT, params, apiKey, cache }),
|
|
19085
|
+
safeFetch({ endpoint: KOSDAQ_INDEX_ENDPOINT, params, apiKey, cache }),
|
|
19086
|
+
safeFetch({ endpoint: KOSPI_STOCK_ENDPOINT, params, apiKey, cache }),
|
|
19087
|
+
safeFetch({ endpoint: KOSDAQ_STOCK_ENDPOINT, params, apiKey, cache })
|
|
19088
|
+
]);
|
|
19089
|
+
const allFailed = !kospiIdx.success && !kosdaqIdx.success && !kospiStk.success && !kosdaqStk.success;
|
|
19090
|
+
if (allFailed) {
|
|
19091
|
+
return {
|
|
19092
|
+
success: false,
|
|
19093
|
+
error: "All market data fetches failed"
|
|
19094
|
+
};
|
|
19095
|
+
}
|
|
19096
|
+
const kospiIndexData = kospiIdx.success ? kospiIdx.data : [];
|
|
19097
|
+
const kosdaqIndexData = kosdaqIdx.success ? kosdaqIdx.data : [];
|
|
19098
|
+
const kospiStockData = kospiStk.success ? kospiStk.data : [];
|
|
19099
|
+
const kosdaqStockData = kosdaqStk.success ? kosdaqStk.data : [];
|
|
19100
|
+
const allStocks = [...kospiStockData, ...kosdaqStockData];
|
|
19101
|
+
const stockStats = computeStockStats(allStocks);
|
|
19102
|
+
const { topGainers, topLosers } = computeTopMovers(allStocks);
|
|
19103
|
+
return {
|
|
19104
|
+
success: true,
|
|
19105
|
+
data: {
|
|
19106
|
+
date: date5,
|
|
19107
|
+
kospiIndex: kospiIndexData,
|
|
19108
|
+
kosdaqIndex: kosdaqIndexData,
|
|
19109
|
+
stockStats,
|
|
19110
|
+
topGainers,
|
|
19111
|
+
topLosers
|
|
19112
|
+
}
|
|
19113
|
+
};
|
|
19114
|
+
}
|
|
19115
|
+
|
|
19116
|
+
// src/cli/commands/market.ts
|
|
19117
|
+
function registerMarketCommand(program3) {
|
|
19118
|
+
const market = program3.command("market").description("Market overview commands");
|
|
19119
|
+
market.command("summary").description("Market summary: indices, top movers, volume").option("-d, --date <date>", "trading date (YYYYMMDD)").action(async (opts) => {
|
|
19120
|
+
const apiKey = getApiKey();
|
|
19121
|
+
if (!apiKey) {
|
|
19122
|
+
writeError(
|
|
19123
|
+
"No API key configured. Use 'krx auth set <key>' or set KRX_API_KEY env var."
|
|
19124
|
+
);
|
|
19125
|
+
process.exit(EXIT_CODES.AUTH_FAILURE);
|
|
19126
|
+
}
|
|
19127
|
+
const date5 = opts.date ?? getRecentTradingDate();
|
|
19128
|
+
try {
|
|
19129
|
+
validateDate(date5);
|
|
19130
|
+
} catch (err) {
|
|
19131
|
+
writeError(
|
|
19132
|
+
`Invalid date: ${err instanceof Error ? err.message : String(err)}`
|
|
19133
|
+
);
|
|
19134
|
+
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
19135
|
+
}
|
|
19136
|
+
const parentOpts = program3.opts();
|
|
19137
|
+
const result = await fetchMarketSummary({
|
|
19138
|
+
apiKey,
|
|
19139
|
+
date: date5,
|
|
19140
|
+
cache: parentOpts.cache
|
|
19141
|
+
});
|
|
19142
|
+
if (!result.success) {
|
|
19143
|
+
writeError(result.error ?? "Failed to fetch market summary");
|
|
19144
|
+
process.exit(EXIT_CODES.GENERAL_ERROR);
|
|
19145
|
+
}
|
|
19146
|
+
writeOutput(JSON.stringify(result.data, null, 2));
|
|
19147
|
+
});
|
|
19148
|
+
}
|
|
19149
|
+
|
|
19150
|
+
// src/watchlist/store.ts
|
|
19151
|
+
import * as fs5 from "node:fs";
|
|
19152
|
+
import * as path5 from "node:path";
|
|
19153
|
+
import * as os4 from "node:os";
|
|
19154
|
+
function getWatchlistPath() {
|
|
19155
|
+
return path5.join(os4.homedir(), ".krx-cli", "watchlist.json");
|
|
19156
|
+
}
|
|
19157
|
+
function getWatchlist() {
|
|
19158
|
+
const filePath = getWatchlistPath();
|
|
19159
|
+
if (!fs5.existsSync(filePath)) {
|
|
19160
|
+
return [];
|
|
19161
|
+
}
|
|
19162
|
+
try {
|
|
19163
|
+
const raw = fs5.readFileSync(filePath, "utf-8");
|
|
19164
|
+
return JSON.parse(raw);
|
|
19165
|
+
} catch (err) {
|
|
19166
|
+
process.stderr.write(`[krx-cli] watchlist read failed: ${String(err)}
|
|
19167
|
+
`);
|
|
19168
|
+
return [];
|
|
19169
|
+
}
|
|
19170
|
+
}
|
|
19171
|
+
function saveWatchlist(entries) {
|
|
19172
|
+
const filePath = getWatchlistPath();
|
|
19173
|
+
const dir = path5.dirname(filePath);
|
|
19174
|
+
try {
|
|
19175
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
19176
|
+
fs5.writeFileSync(filePath, JSON.stringify(entries, null, 2), "utf-8");
|
|
19177
|
+
return true;
|
|
19178
|
+
} catch (err) {
|
|
19179
|
+
process.stderr.write(`[krx-cli] watchlist write failed: ${String(err)}
|
|
19180
|
+
`);
|
|
19181
|
+
return false;
|
|
19182
|
+
}
|
|
19183
|
+
}
|
|
19184
|
+
function addToWatchlist(entry) {
|
|
19185
|
+
const current = getWatchlist();
|
|
19186
|
+
const isDuplicate = current.some((e) => e.isuCd === entry.isuCd);
|
|
19187
|
+
if (isDuplicate) {
|
|
19188
|
+
return { added: false, reason: "duplicate" };
|
|
19189
|
+
}
|
|
19190
|
+
const saved = saveWatchlist([...current, entry]);
|
|
19191
|
+
return saved ? { added: true } : { added: false, reason: "write_error" };
|
|
19192
|
+
}
|
|
19193
|
+
function removeFromWatchlist(nameOrCode) {
|
|
19194
|
+
const current = getWatchlist();
|
|
19195
|
+
const filtered = current.filter(
|
|
19196
|
+
(e) => e.isuCd !== nameOrCode && e.name !== nameOrCode
|
|
19197
|
+
);
|
|
19198
|
+
if (filtered.length === current.length) {
|
|
19199
|
+
return { removed: false, reason: "not_found" };
|
|
19200
|
+
}
|
|
19201
|
+
const saved = saveWatchlist(filtered);
|
|
19202
|
+
return saved ? { removed: true } : { removed: false, reason: "write_error" };
|
|
19203
|
+
}
|
|
19204
|
+
|
|
19205
|
+
// src/cli/commands/watchlist.ts
|
|
19206
|
+
var STOCK_ENDPOINTS = {
|
|
19207
|
+
KOSPI: "/svc/apis/sto/stk_bydd_trd",
|
|
19208
|
+
KOSDAQ: "/svc/apis/sto/ksq_bydd_trd"
|
|
19209
|
+
};
|
|
19210
|
+
function registerWatchlistCommand(program3) {
|
|
19211
|
+
const watchlist = program3.command("watchlist").description("Manage watchlist: add, remove, list, show");
|
|
19212
|
+
watchlist.command("add <name>").description("Add stock to watchlist by name (searches first)").action(async (name) => {
|
|
19213
|
+
const apiKey = getApiKey();
|
|
19214
|
+
if (!apiKey) {
|
|
19215
|
+
writeError(
|
|
19216
|
+
"No API key configured. Use 'krx auth set <key>' or set KRX_API_KEY env var."
|
|
19217
|
+
);
|
|
19218
|
+
process.exit(EXIT_CODES.AUTH_FAILURE);
|
|
19219
|
+
}
|
|
19220
|
+
const injectionError = validateNoInjection(name);
|
|
19221
|
+
if (injectionError) {
|
|
19222
|
+
writeError(`Invalid input: ${injectionError}`);
|
|
19223
|
+
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
19224
|
+
}
|
|
19225
|
+
let results;
|
|
19226
|
+
try {
|
|
19227
|
+
results = await searchStock(apiKey, name);
|
|
19228
|
+
} catch (err) {
|
|
19229
|
+
writeError(
|
|
19230
|
+
`Search failed: ${err instanceof Error ? err.message : String(err)}`
|
|
19231
|
+
);
|
|
19232
|
+
process.exit(EXIT_CODES.GENERAL_ERROR);
|
|
19233
|
+
}
|
|
19234
|
+
if (results.length === 0) {
|
|
19235
|
+
writeError(`No stock found matching '${name}'`);
|
|
19236
|
+
process.exit(EXIT_CODES.NO_DATA);
|
|
19237
|
+
}
|
|
19238
|
+
if (results.length > 1) {
|
|
19239
|
+
writeOutput(
|
|
19240
|
+
JSON.stringify(
|
|
19241
|
+
{
|
|
19242
|
+
message: `Multiple matches found for '${name}'. Please be more specific.`,
|
|
19243
|
+
matches: results
|
|
19244
|
+
},
|
|
19245
|
+
null,
|
|
19246
|
+
2
|
|
19247
|
+
)
|
|
19248
|
+
);
|
|
19249
|
+
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
19250
|
+
}
|
|
19251
|
+
const stock = results[0];
|
|
19252
|
+
const result = addToWatchlist({
|
|
19253
|
+
isuCd: stock.ISU_CD,
|
|
19254
|
+
isuSrtCd: stock.ISU_SRT_CD,
|
|
19255
|
+
name: stock.ISU_NM,
|
|
19256
|
+
market: stock.MKT_NM
|
|
19257
|
+
});
|
|
19258
|
+
if (!result.added) {
|
|
19259
|
+
const message = result.reason === "write_error" ? "Failed to save watchlist" : `'${stock.ISU_NM}' is already in watchlist`;
|
|
19260
|
+
writeError(message);
|
|
19261
|
+
if (result.reason === "write_error") {
|
|
19262
|
+
process.exit(EXIT_CODES.GENERAL_ERROR);
|
|
19263
|
+
}
|
|
19264
|
+
return;
|
|
19265
|
+
}
|
|
19266
|
+
writeOutput(
|
|
19267
|
+
JSON.stringify({
|
|
19268
|
+
message: `Added '${stock.ISU_NM}' (${stock.ISU_CD}) to watchlist`,
|
|
19269
|
+
entry: {
|
|
19270
|
+
isuCd: stock.ISU_CD,
|
|
19271
|
+
isuSrtCd: stock.ISU_SRT_CD,
|
|
19272
|
+
name: stock.ISU_NM,
|
|
19273
|
+
market: stock.MKT_NM
|
|
19274
|
+
}
|
|
19275
|
+
})
|
|
19276
|
+
);
|
|
19277
|
+
});
|
|
19278
|
+
watchlist.command("remove <name>").description("Remove stock from watchlist by name or code").action((name) => {
|
|
19279
|
+
const injectionError = validateNoInjection(name);
|
|
19280
|
+
if (injectionError) {
|
|
19281
|
+
writeError(`Invalid input: ${injectionError}`);
|
|
19282
|
+
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
19283
|
+
}
|
|
19284
|
+
const result = removeFromWatchlist(name);
|
|
19285
|
+
if (!result.removed) {
|
|
19286
|
+
const message = result.reason === "write_error" ? "Failed to save watchlist" : `'${name}' not found in watchlist`;
|
|
19287
|
+
writeError(message);
|
|
19288
|
+
process.exit(
|
|
19289
|
+
result.reason === "write_error" ? EXIT_CODES.GENERAL_ERROR : EXIT_CODES.NO_DATA
|
|
19290
|
+
);
|
|
19291
|
+
}
|
|
19292
|
+
writeOutput(
|
|
19293
|
+
JSON.stringify({ message: `Removed '${name}' from watchlist` })
|
|
19294
|
+
);
|
|
19295
|
+
});
|
|
19296
|
+
watchlist.command("list").description("List all stocks in watchlist").action(() => {
|
|
19297
|
+
const entries = getWatchlist();
|
|
19298
|
+
if (entries.length === 0) {
|
|
19299
|
+
writeOutput(
|
|
19300
|
+
JSON.stringify({ message: "Watchlist is empty", entries: [] })
|
|
19301
|
+
);
|
|
19302
|
+
return;
|
|
19303
|
+
}
|
|
19304
|
+
writeOutput(JSON.stringify(entries, null, 2));
|
|
19305
|
+
});
|
|
19306
|
+
watchlist.command("show").description("Show current prices for watchlist stocks").option("-d, --date <date>", "trading date (YYYYMMDD)").action(async (opts) => {
|
|
19307
|
+
const apiKey = getApiKey();
|
|
19308
|
+
if (!apiKey) {
|
|
19309
|
+
writeError(
|
|
19310
|
+
"No API key configured. Use 'krx auth set <key>' or set KRX_API_KEY env var."
|
|
19311
|
+
);
|
|
19312
|
+
process.exit(EXIT_CODES.AUTH_FAILURE);
|
|
19313
|
+
}
|
|
19314
|
+
const entries = getWatchlist();
|
|
19315
|
+
if (entries.length === 0) {
|
|
19316
|
+
writeOutput(
|
|
19317
|
+
JSON.stringify({ message: "Watchlist is empty", entries: [] })
|
|
19318
|
+
);
|
|
19319
|
+
return;
|
|
19320
|
+
}
|
|
19321
|
+
const date5 = opts.date ?? getRecentTradingDate();
|
|
19322
|
+
try {
|
|
19323
|
+
validateDate(date5);
|
|
19324
|
+
} catch (err) {
|
|
19325
|
+
writeError(
|
|
19326
|
+
`Invalid date: ${err instanceof Error ? err.message : String(err)}`
|
|
19327
|
+
);
|
|
19328
|
+
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
19329
|
+
}
|
|
19330
|
+
const parentOpts = program3.opts();
|
|
19331
|
+
const isuCds = new Set(entries.map((e) => e.isuCd));
|
|
19332
|
+
const errors = [];
|
|
19333
|
+
const [kospiResult, kosdaqResult] = await Promise.all([
|
|
19334
|
+
krxFetch({
|
|
19335
|
+
endpoint: STOCK_ENDPOINTS.KOSPI,
|
|
19336
|
+
params: { basDd: date5 },
|
|
19337
|
+
apiKey,
|
|
19338
|
+
cache: parentOpts.cache
|
|
19339
|
+
}).catch((err) => {
|
|
19340
|
+
errors.push(
|
|
19341
|
+
`KOSPI: ${err instanceof Error ? err.message : String(err)}`
|
|
19342
|
+
);
|
|
19343
|
+
return {
|
|
19344
|
+
success: false,
|
|
19345
|
+
data: []
|
|
19346
|
+
};
|
|
19347
|
+
}),
|
|
19348
|
+
krxFetch({
|
|
19349
|
+
endpoint: STOCK_ENDPOINTS.KOSDAQ,
|
|
19350
|
+
params: { basDd: date5 },
|
|
19351
|
+
apiKey,
|
|
19352
|
+
cache: parentOpts.cache
|
|
19353
|
+
}).catch((err) => {
|
|
19354
|
+
errors.push(
|
|
19355
|
+
`KOSDAQ: ${err instanceof Error ? err.message : String(err)}`
|
|
19356
|
+
);
|
|
19357
|
+
return {
|
|
19358
|
+
success: false,
|
|
19359
|
+
data: []
|
|
19360
|
+
};
|
|
19361
|
+
})
|
|
19362
|
+
]);
|
|
19363
|
+
if (!kospiResult.success && !kosdaqResult.success) {
|
|
19364
|
+
writeError(`All market data fetches failed: ${errors.join("; ")}`);
|
|
19365
|
+
process.exit(EXIT_CODES.GENERAL_ERROR);
|
|
19366
|
+
}
|
|
19367
|
+
const allStocks = [
|
|
19368
|
+
...kospiResult.success ? kospiResult.data : [],
|
|
19369
|
+
...kosdaqResult.success ? kosdaqResult.data : []
|
|
19370
|
+
];
|
|
19371
|
+
const watchlistData = allStocks.filter(
|
|
19372
|
+
(s) => isuCds.has(s["ISU_CD"] ?? "")
|
|
19373
|
+
);
|
|
19374
|
+
const output = { date: date5, stocks: watchlistData };
|
|
19375
|
+
if (errors.length > 0) {
|
|
19376
|
+
output["warnings"] = errors;
|
|
19377
|
+
}
|
|
19378
|
+
writeOutput(JSON.stringify(output, null, 2));
|
|
19379
|
+
});
|
|
19380
|
+
}
|
|
19381
|
+
|
|
18808
19382
|
// src/cli/commands/version.ts
|
|
18809
|
-
import { readFileSync as
|
|
19383
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
18810
19384
|
import { fileURLToPath } from "node:url";
|
|
18811
|
-
import { dirname, resolve } from "node:path";
|
|
19385
|
+
import { dirname as dirname4, resolve } from "node:path";
|
|
18812
19386
|
function getLocalVersion() {
|
|
18813
19387
|
const __filename = fileURLToPath(import.meta.url);
|
|
18814
|
-
const __dirname =
|
|
19388
|
+
const __dirname = dirname4(__filename);
|
|
18815
19389
|
const candidates = [
|
|
18816
19390
|
resolve(__dirname, "..", "package.json"),
|
|
18817
19391
|
resolve(__dirname, "..", "..", "..", "package.json")
|
|
18818
19392
|
];
|
|
18819
19393
|
for (const candidate of candidates) {
|
|
18820
19394
|
try {
|
|
18821
|
-
const raw =
|
|
19395
|
+
const raw = readFileSync5(candidate, "utf-8");
|
|
18822
19396
|
const pkg = JSON.parse(raw);
|
|
18823
19397
|
return pkg.version;
|
|
18824
19398
|
} catch {
|
|
@@ -18900,17 +19474,12 @@ function registerUpdateCommand(program3) {
|
|
|
18900
19474
|
}
|
|
18901
19475
|
|
|
18902
19476
|
// src/cli/index.ts
|
|
18903
|
-
var EXIT_CODES = {
|
|
18904
|
-
SUCCESS: 0,
|
|
18905
|
-
GENERAL_ERROR: 1,
|
|
18906
|
-
USAGE_ERROR: 2,
|
|
18907
|
-
NO_DATA: 3,
|
|
18908
|
-
AUTH_FAILURE: 4,
|
|
18909
|
-
RATE_LIMIT: 5,
|
|
18910
|
-
SERVICE_NOT_APPROVED: 6
|
|
18911
|
-
};
|
|
18912
19477
|
var program2 = new Command();
|
|
18913
|
-
program2.name("krx").description("Agent-native CLI for KRX (Korea Exchange) Open API").version(getVersion()).option("-o, --output <format>", "output format: json, table, ndjson").option("-f, --fields <fields>", "comma-separated fields to include").option("--dry-run", "show request without calling API").option("-v, --verbose", "verbose output to stderr")
|
|
19478
|
+
program2.name("krx").description("Agent-native CLI for KRX (Korea Exchange) Open API").version(getVersion()).option("-o, --output <format>", "output format: json, table, ndjson, csv").option("-f, --fields <fields>", "comma-separated fields to include").option("--dry-run", "show request without calling API").option("-v, --verbose", "verbose output to stderr").option("--code <isuCd>", "filter by stock code (ISU_CD)").option("--sort <field>", "sort results by field name").option("--asc", "sort ascending (default: descending)").option("--limit <n>", "limit number of results", parseInt).option("--no-cache", "bypass cache and fetch fresh data").option("--from <date>", "start date for range query (YYYYMMDD)").option("--to <date>", "end date for range query (YYYYMMDD)").option("--filter <expression>", 'filter results (e.g. "FLUC_RT > 5")').option("--save <path>", "save output to file instead of stdout").option(
|
|
19479
|
+
"--retries <n>",
|
|
19480
|
+
"max retries on network error (default: 3)",
|
|
19481
|
+
parseInt
|
|
19482
|
+
);
|
|
18914
19483
|
registerAuthCommand(program2);
|
|
18915
19484
|
registerIndexCommand(program2);
|
|
18916
19485
|
registerStockCommand(program2);
|
|
@@ -18920,6 +19489,9 @@ registerDerivativeCommand(program2);
|
|
|
18920
19489
|
registerCommodityCommand(program2);
|
|
18921
19490
|
registerEsgCommand(program2);
|
|
18922
19491
|
registerSchemaCommand(program2);
|
|
19492
|
+
registerCacheCommand(program2);
|
|
19493
|
+
registerMarketCommand(program2);
|
|
19494
|
+
registerWatchlistCommand(program2);
|
|
18923
19495
|
registerVersionCommand(program2);
|
|
18924
19496
|
registerUpdateCommand(program2);
|
|
18925
19497
|
program2.exitOverride();
|