krx-cli 0.1.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/LICENSE +21 -0
- package/README.md +161 -0
- package/SKILL.md +173 -0
- package/dist/cli.js +1269 -0
- package/dist/cli.js.map +1 -0
- package/package.json +58 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1269 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/client/rate-limit.ts
|
|
13
|
+
var rate_limit_exports = {};
|
|
14
|
+
__export(rate_limit_exports, {
|
|
15
|
+
checkRateLimit: () => checkRateLimit,
|
|
16
|
+
getRateLimitStatus: () => getRateLimitStatus,
|
|
17
|
+
incrementCallCount: () => incrementCallCount
|
|
18
|
+
});
|
|
19
|
+
import * as fs from "fs";
|
|
20
|
+
import * as path from "path";
|
|
21
|
+
import * as os from "os";
|
|
22
|
+
function today() {
|
|
23
|
+
const now = /* @__PURE__ */ new Date();
|
|
24
|
+
const yyyy = now.getFullYear().toString();
|
|
25
|
+
const mm = (now.getMonth() + 1).toString().padStart(2, "0");
|
|
26
|
+
const dd = now.getDate().toString().padStart(2, "0");
|
|
27
|
+
return `${yyyy}${mm}${dd}`;
|
|
28
|
+
}
|
|
29
|
+
function readRateData() {
|
|
30
|
+
try {
|
|
31
|
+
const raw = fs.readFileSync(RATE_FILE, "utf-8");
|
|
32
|
+
return JSON.parse(raw);
|
|
33
|
+
} catch {
|
|
34
|
+
return { date: today(), count: 0 };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function writeRateData(data) {
|
|
38
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
39
|
+
fs.writeFileSync(RATE_FILE, JSON.stringify(data, null, 2), "utf-8");
|
|
40
|
+
}
|
|
41
|
+
function incrementCallCount() {
|
|
42
|
+
const data = readRateData();
|
|
43
|
+
const currentDate = today();
|
|
44
|
+
const newData = data.date === currentDate ? { date: currentDate, count: data.count + 1 } : { date: currentDate, count: 1 };
|
|
45
|
+
writeRateData(newData);
|
|
46
|
+
return { count: newData.count, limit: DAILY_LIMIT };
|
|
47
|
+
}
|
|
48
|
+
function checkRateLimit() {
|
|
49
|
+
const data = readRateData();
|
|
50
|
+
const currentDate = today();
|
|
51
|
+
const count = data.date === currentDate ? data.count : 0;
|
|
52
|
+
const allowed = count < DAILY_LIMIT;
|
|
53
|
+
const warning = count >= DAILY_LIMIT * WARNING_THRESHOLD;
|
|
54
|
+
return { allowed, count, limit: DAILY_LIMIT, warning };
|
|
55
|
+
}
|
|
56
|
+
function getRateLimitStatus() {
|
|
57
|
+
const data = readRateData();
|
|
58
|
+
const currentDate = today();
|
|
59
|
+
const count = data.date === currentDate ? data.count : 0;
|
|
60
|
+
return {
|
|
61
|
+
date: currentDate,
|
|
62
|
+
count,
|
|
63
|
+
limit: DAILY_LIMIT,
|
|
64
|
+
remaining: DAILY_LIMIT - count
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
var CONFIG_DIR, RATE_FILE, DAILY_LIMIT, WARNING_THRESHOLD;
|
|
68
|
+
var init_rate_limit = __esm({
|
|
69
|
+
"src/client/rate-limit.ts"() {
|
|
70
|
+
"use strict";
|
|
71
|
+
CONFIG_DIR = path.join(os.homedir(), ".krx-cli");
|
|
72
|
+
RATE_FILE = path.join(CONFIG_DIR, "rate-limit.json");
|
|
73
|
+
DAILY_LIMIT = 1e4;
|
|
74
|
+
WARNING_THRESHOLD = 0.8;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// src/cli/index.ts
|
|
79
|
+
import { Command } from "commander";
|
|
80
|
+
|
|
81
|
+
// src/client/auth.ts
|
|
82
|
+
import * as fs2 from "fs";
|
|
83
|
+
import * as path2 from "path";
|
|
84
|
+
import * as os2 from "os";
|
|
85
|
+
|
|
86
|
+
// src/client/endpoints.ts
|
|
87
|
+
var CATEGORIES = [
|
|
88
|
+
{
|
|
89
|
+
id: "index",
|
|
90
|
+
code: "idx",
|
|
91
|
+
name: "Index",
|
|
92
|
+
nameKo: "\uC9C0\uC218",
|
|
93
|
+
probeEndpoint: "/svc/apis/idx/kospi_dd_trd"
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: "stock",
|
|
97
|
+
code: "sto",
|
|
98
|
+
name: "Stock",
|
|
99
|
+
nameKo: "\uC8FC\uC2DD",
|
|
100
|
+
probeEndpoint: "/svc/apis/sto/stk_bydd_trd"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
id: "etp",
|
|
104
|
+
code: "etp",
|
|
105
|
+
name: "ETP",
|
|
106
|
+
nameKo: "\uC99D\uAD8C\uC0C1\uD488",
|
|
107
|
+
probeEndpoint: "/svc/apis/etp/etf_bydd_trd"
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: "bond",
|
|
111
|
+
code: "bon",
|
|
112
|
+
name: "Bond",
|
|
113
|
+
nameKo: "\uCC44\uAD8C",
|
|
114
|
+
probeEndpoint: "/svc/apis/bon/bnd_bydd_trd"
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: "derivative",
|
|
118
|
+
code: "drv",
|
|
119
|
+
name: "Derivative",
|
|
120
|
+
nameKo: "\uD30C\uC0DD\uC0C1\uD488",
|
|
121
|
+
probeEndpoint: "/svc/apis/drv/fut_bydd_trd"
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
id: "commodity",
|
|
125
|
+
code: "gen",
|
|
126
|
+
name: "Commodity",
|
|
127
|
+
nameKo: "\uC77C\uBC18\uC0C1\uD488",
|
|
128
|
+
probeEndpoint: "/svc/apis/gen/gold_bydd_trd"
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: "esg",
|
|
132
|
+
code: "esg",
|
|
133
|
+
name: "ESG",
|
|
134
|
+
nameKo: "ESG",
|
|
135
|
+
probeEndpoint: "/svc/apis/esg/esg_index_info"
|
|
136
|
+
}
|
|
137
|
+
];
|
|
138
|
+
var ENDPOINTS = [
|
|
139
|
+
// Index (idx)
|
|
140
|
+
{
|
|
141
|
+
path: "/svc/apis/idx/krx_dd_trd",
|
|
142
|
+
description: "KRX series index daily trading",
|
|
143
|
+
descriptionKo: "KRX \uC2DC\uB9AC\uC988 \uC9C0\uC218 \uC77C\uBCC4\uC2DC\uC138",
|
|
144
|
+
category: "index"
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
path: "/svc/apis/idx/kospi_dd_trd",
|
|
148
|
+
description: "KOSPI series index daily trading",
|
|
149
|
+
descriptionKo: "KOSPI \uC2DC\uB9AC\uC988 \uC9C0\uC218 \uC77C\uBCC4\uC2DC\uC138",
|
|
150
|
+
category: "index"
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
path: "/svc/apis/idx/kosdaq_dd_trd",
|
|
154
|
+
description: "KOSDAQ series index daily trading",
|
|
155
|
+
descriptionKo: "KOSDAQ \uC2DC\uB9AC\uC988 \uC9C0\uC218 \uC77C\uBCC4\uC2DC\uC138",
|
|
156
|
+
category: "index"
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
path: "/svc/apis/idx/bon_dd_trd",
|
|
160
|
+
description: "Bond index daily trading",
|
|
161
|
+
descriptionKo: "\uCC44\uAD8C\uC9C0\uC218 \uC77C\uBCC4\uC2DC\uC138",
|
|
162
|
+
category: "index"
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
path: "/svc/apis/idx/drvprod_dd_trd",
|
|
166
|
+
description: "Derivatives index daily trading",
|
|
167
|
+
descriptionKo: "\uD30C\uC0DD\uC0C1\uD488\uC9C0\uC218 \uC77C\uBCC4\uC2DC\uC138",
|
|
168
|
+
category: "index"
|
|
169
|
+
},
|
|
170
|
+
// Stock (sto)
|
|
171
|
+
{
|
|
172
|
+
path: "/svc/apis/sto/stk_bydd_trd",
|
|
173
|
+
description: "KOSPI stock daily trading",
|
|
174
|
+
descriptionKo: "\uC720\uAC00\uC99D\uAD8C(KOSPI) \uC8FC\uC2DD \uC77C\uBCC4\uB9E4\uB9E4\uC815\uBCF4",
|
|
175
|
+
category: "stock"
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
path: "/svc/apis/sto/ksq_bydd_trd",
|
|
179
|
+
description: "KOSDAQ stock daily trading",
|
|
180
|
+
descriptionKo: "\uCF54\uC2A4\uB2E5(KOSDAQ) \uC8FC\uC2DD \uC77C\uBCC4\uB9E4\uB9E4\uC815\uBCF4",
|
|
181
|
+
category: "stock"
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
path: "/svc/apis/sto/knx_bydd_trd",
|
|
185
|
+
description: "KONEX stock daily trading",
|
|
186
|
+
descriptionKo: "\uCF54\uB125\uC2A4(KONEX) \uC8FC\uC2DD \uC77C\uBCC4\uB9E4\uB9E4\uC815\uBCF4",
|
|
187
|
+
category: "stock"
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
path: "/svc/apis/sto/sw_bydd_trd",
|
|
191
|
+
description: "Subscription warrant daily trading",
|
|
192
|
+
descriptionKo: "\uC2E0\uC8FC\uC778\uC218\uAD8C\uC99D\uAD8C \uC77C\uBCC4\uB9E4\uB9E4\uC815\uBCF4",
|
|
193
|
+
category: "stock"
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
path: "/svc/apis/sto/sr_bydd_trd",
|
|
197
|
+
description: "Subscription right daily trading",
|
|
198
|
+
descriptionKo: "\uC2E0\uC8FC\uC778\uC218\uAD8C\uC99D\uC11C \uC77C\uBCC4\uB9E4\uB9E4\uC815\uBCF4",
|
|
199
|
+
category: "stock"
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
path: "/svc/apis/sto/stk_isu_base_info",
|
|
203
|
+
description: "KOSPI stock base info",
|
|
204
|
+
descriptionKo: "\uC720\uAC00\uC99D\uAD8C \uC885\uBAA9 \uAE30\uBCF8\uC815\uBCF4",
|
|
205
|
+
category: "stock"
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
path: "/svc/apis/sto/ksq_isu_base_info",
|
|
209
|
+
description: "KOSDAQ stock base info",
|
|
210
|
+
descriptionKo: "\uCF54\uC2A4\uB2E5 \uC885\uBAA9 \uAE30\uBCF8\uC815\uBCF4",
|
|
211
|
+
category: "stock"
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
path: "/svc/apis/sto/knx_isu_base_info",
|
|
215
|
+
description: "KONEX stock base info",
|
|
216
|
+
descriptionKo: "\uCF54\uB125\uC2A4 \uC885\uBAA9 \uAE30\uBCF8\uC815\uBCF4",
|
|
217
|
+
category: "stock"
|
|
218
|
+
},
|
|
219
|
+
// ETP
|
|
220
|
+
{
|
|
221
|
+
path: "/svc/apis/etp/etf_bydd_trd",
|
|
222
|
+
description: "ETF daily trading",
|
|
223
|
+
descriptionKo: "ETF \uC77C\uBCC4\uB9E4\uB9E4\uC815\uBCF4",
|
|
224
|
+
category: "etp"
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
path: "/svc/apis/etp/etn_bydd_trd",
|
|
228
|
+
description: "ETN daily trading",
|
|
229
|
+
descriptionKo: "ETN \uC77C\uBCC4\uB9E4\uB9E4\uC815\uBCF4",
|
|
230
|
+
category: "etp"
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
path: "/svc/apis/etp/elw_bydd_trd",
|
|
234
|
+
description: "ELW daily trading",
|
|
235
|
+
descriptionKo: "ELW \uC77C\uBCC4\uB9E4\uB9E4\uC815\uBCF4",
|
|
236
|
+
category: "etp"
|
|
237
|
+
},
|
|
238
|
+
// Bond (bon)
|
|
239
|
+
{
|
|
240
|
+
path: "/svc/apis/bon/kts_bydd_trd",
|
|
241
|
+
description: "KTS government bond daily trading",
|
|
242
|
+
descriptionKo: "\uAD6D\uCC44\uC804\uBB38\uC720\uD1B5\uC2DC\uC7A5 \uC77C\uBCC4\uB9E4\uB9E4\uC815\uBCF4",
|
|
243
|
+
category: "bond"
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
path: "/svc/apis/bon/bnd_bydd_trd",
|
|
247
|
+
description: "General bond daily trading",
|
|
248
|
+
descriptionKo: "\uC77C\uBC18\uCC44\uAD8C\uC2DC\uC7A5 \uC77C\uBCC4\uB9E4\uB9E4\uC815\uBCF4",
|
|
249
|
+
category: "bond"
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
path: "/svc/apis/bon/smb_bydd_trd",
|
|
253
|
+
description: "Small bond daily trading",
|
|
254
|
+
descriptionKo: "\uC18C\uC561\uCC44\uAD8C\uC2DC\uC7A5 \uC77C\uBCC4\uB9E4\uB9E4\uC815\uBCF4",
|
|
255
|
+
category: "bond"
|
|
256
|
+
},
|
|
257
|
+
// Derivatives (drv)
|
|
258
|
+
{
|
|
259
|
+
path: "/svc/apis/drv/fut_bydd_trd",
|
|
260
|
+
description: "Futures daily trading",
|
|
261
|
+
descriptionKo: "\uC120\uBB3C \uC77C\uBCC4\uB9E4\uB9E4\uC815\uBCF4",
|
|
262
|
+
category: "derivative"
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
path: "/svc/apis/drv/eqsfu_stk_bydd_trd",
|
|
266
|
+
description: "KOSPI stock futures daily trading",
|
|
267
|
+
descriptionKo: "\uC720\uAC00\uC99D\uAD8C \uC8FC\uC2DD\uC120\uBB3C \uC77C\uBCC4\uB9E4\uB9E4\uC815\uBCF4",
|
|
268
|
+
category: "derivative"
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
path: "/svc/apis/drv/eqkfu_ksq_bydd_trd",
|
|
272
|
+
description: "KOSDAQ stock futures daily trading",
|
|
273
|
+
descriptionKo: "\uCF54\uC2A4\uB2E5 \uC8FC\uC2DD\uC120\uBB3C \uC77C\uBCC4\uB9E4\uB9E4\uC815\uBCF4",
|
|
274
|
+
category: "derivative"
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
path: "/svc/apis/drv/opt_bydd_trd",
|
|
278
|
+
description: "Options daily trading",
|
|
279
|
+
descriptionKo: "\uC635\uC158 \uC77C\uBCC4\uB9E4\uB9E4\uC815\uBCF4",
|
|
280
|
+
category: "derivative"
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
path: "/svc/apis/drv/eqsop_bydd_trd",
|
|
284
|
+
description: "KOSPI stock options daily trading",
|
|
285
|
+
descriptionKo: "\uC720\uAC00\uC99D\uAD8C \uC8FC\uC2DD\uC635\uC158 \uC77C\uBCC4\uB9E4\uB9E4\uC815\uBCF4",
|
|
286
|
+
category: "derivative"
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
path: "/svc/apis/drv/eqkop_bydd_trd",
|
|
290
|
+
description: "KOSDAQ stock options daily trading",
|
|
291
|
+
descriptionKo: "\uCF54\uC2A4\uB2E5 \uC8FC\uC2DD\uC635\uC158 \uC77C\uBCC4\uB9E4\uB9E4\uC815\uBCF4",
|
|
292
|
+
category: "derivative"
|
|
293
|
+
},
|
|
294
|
+
// Commodities (gen)
|
|
295
|
+
{
|
|
296
|
+
path: "/svc/apis/gen/oil_bydd_trd",
|
|
297
|
+
description: "Oil market daily trading",
|
|
298
|
+
descriptionKo: "\uC11D\uC720\uC2DC\uC7A5 \uC77C\uBCC4\uB9E4\uB9E4\uC815\uBCF4",
|
|
299
|
+
category: "commodity"
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
path: "/svc/apis/gen/gold_bydd_trd",
|
|
303
|
+
description: "Gold market daily trading",
|
|
304
|
+
descriptionKo: "\uAE08\uC2DC\uC7A5 \uC77C\uBCC4\uB9E4\uB9E4\uC815\uBCF4",
|
|
305
|
+
category: "commodity"
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
path: "/svc/apis/gen/ets_bydd_trd",
|
|
309
|
+
description: "Emission trading daily",
|
|
310
|
+
descriptionKo: "\uBC30\uCD9C\uAD8C\uC2DC\uC7A5 \uC77C\uBCC4\uB9E4\uB9E4\uC815\uBCF4",
|
|
311
|
+
category: "commodity"
|
|
312
|
+
},
|
|
313
|
+
// ESG
|
|
314
|
+
{
|
|
315
|
+
path: "/svc/apis/esg/sri_bond_info",
|
|
316
|
+
description: "SRI bond info",
|
|
317
|
+
descriptionKo: "\uC0AC\uD68C\uCC45\uC784\uD22C\uC790\uCC44\uAD8C \uC885\uBAA9\uC815\uBCF4",
|
|
318
|
+
category: "esg"
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
path: "/svc/apis/esg/esg_etp_info",
|
|
322
|
+
description: "ESG ETP info",
|
|
323
|
+
descriptionKo: "ESG \uC99D\uAD8C\uC0C1\uD488 \uC815\uBCF4",
|
|
324
|
+
category: "esg"
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
path: "/svc/apis/esg/esg_index_info",
|
|
328
|
+
description: "ESG index info",
|
|
329
|
+
descriptionKo: "ESG \uC9C0\uC218 \uC815\uBCF4",
|
|
330
|
+
category: "esg"
|
|
331
|
+
}
|
|
332
|
+
];
|
|
333
|
+
|
|
334
|
+
// src/client/client.ts
|
|
335
|
+
var BASE_URL = "https://data-dbg.krx.co.kr";
|
|
336
|
+
async function krxFetch(options) {
|
|
337
|
+
const { checkRateLimit: checkRateLimit2, incrementCallCount: incrementCallCount2 } = await Promise.resolve().then(() => (init_rate_limit(), rate_limit_exports));
|
|
338
|
+
const rateStatus = checkRateLimit2();
|
|
339
|
+
if (!rateStatus.allowed) {
|
|
340
|
+
return {
|
|
341
|
+
success: false,
|
|
342
|
+
data: [],
|
|
343
|
+
error: `Daily rate limit exceeded (${rateStatus.count}/${rateStatus.limit})`,
|
|
344
|
+
errorCode: "RATE_LIMIT"
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
if (rateStatus.warning) {
|
|
348
|
+
process.stderr.write(
|
|
349
|
+
`Warning: ${rateStatus.count}/${rateStatus.limit} API calls used today
|
|
350
|
+
`
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
const url = `${BASE_URL}${options.endpoint}`;
|
|
354
|
+
const response = await fetch(url, {
|
|
355
|
+
method: "POST",
|
|
356
|
+
headers: {
|
|
357
|
+
AUTH_KEY: options.apiKey,
|
|
358
|
+
"Content-Type": "application/json; charset=utf-8"
|
|
359
|
+
},
|
|
360
|
+
body: JSON.stringify(options.params)
|
|
361
|
+
});
|
|
362
|
+
incrementCallCount2();
|
|
363
|
+
if (!response.ok) {
|
|
364
|
+
let errorMsg = `HTTP ${response.status}: ${response.statusText}`;
|
|
365
|
+
let errorCode;
|
|
366
|
+
try {
|
|
367
|
+
const errorBody = await response.json();
|
|
368
|
+
if (errorBody.respMsg) {
|
|
369
|
+
errorMsg = errorBody.respMsg;
|
|
370
|
+
}
|
|
371
|
+
errorCode = errorBody.respCode;
|
|
372
|
+
} catch {
|
|
373
|
+
}
|
|
374
|
+
return {
|
|
375
|
+
success: false,
|
|
376
|
+
data: [],
|
|
377
|
+
error: errorMsg,
|
|
378
|
+
errorCode
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
const body = await response.json();
|
|
382
|
+
const outBlock = body["OutBlock_1"];
|
|
383
|
+
if (!Array.isArray(outBlock)) {
|
|
384
|
+
return {
|
|
385
|
+
success: false,
|
|
386
|
+
data: [],
|
|
387
|
+
error: "Unexpected response format: missing OutBlock_1"
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
return {
|
|
391
|
+
success: true,
|
|
392
|
+
data: outBlock
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// src/client/auth.ts
|
|
397
|
+
var CONFIG_DIR2 = path2.join(os2.homedir(), ".krx-cli");
|
|
398
|
+
var CONFIG_FILE = path2.join(CONFIG_DIR2, "config.json");
|
|
399
|
+
function readConfig() {
|
|
400
|
+
try {
|
|
401
|
+
const raw = fs2.readFileSync(CONFIG_FILE, "utf-8");
|
|
402
|
+
return JSON.parse(raw);
|
|
403
|
+
} catch {
|
|
404
|
+
return {};
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
function writeConfig(config) {
|
|
408
|
+
fs2.mkdirSync(CONFIG_DIR2, { recursive: true });
|
|
409
|
+
fs2.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
|
|
410
|
+
}
|
|
411
|
+
function getApiKey() {
|
|
412
|
+
return process.env["KRX_API_KEY"] ?? readConfig().apiKey;
|
|
413
|
+
}
|
|
414
|
+
function saveApiKey(apiKey) {
|
|
415
|
+
const config = readConfig();
|
|
416
|
+
writeConfig({ ...config, apiKey });
|
|
417
|
+
}
|
|
418
|
+
function getRecentTradingDate() {
|
|
419
|
+
const now = /* @__PURE__ */ new Date();
|
|
420
|
+
const day = now.getDay();
|
|
421
|
+
const daysBack = day === 0 ? 2 : day === 6 ? 1 : day === 1 ? 3 : 1;
|
|
422
|
+
const target = new Date(now.getTime() - daysBack * 24 * 60 * 60 * 1e3);
|
|
423
|
+
const yyyy = target.getFullYear().toString();
|
|
424
|
+
const mm = (target.getMonth() + 1).toString().padStart(2, "0");
|
|
425
|
+
const dd = target.getDate().toString().padStart(2, "0");
|
|
426
|
+
return `${yyyy}${mm}${dd}`;
|
|
427
|
+
}
|
|
428
|
+
async function checkCategoryApproval(apiKey, categoryId) {
|
|
429
|
+
const category = CATEGORIES.find((c) => c.id === categoryId);
|
|
430
|
+
if (!category) {
|
|
431
|
+
return { approved: false, checkedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
432
|
+
}
|
|
433
|
+
const basDd = getRecentTradingDate();
|
|
434
|
+
try {
|
|
435
|
+
const result = await krxFetch({
|
|
436
|
+
endpoint: category.probeEndpoint,
|
|
437
|
+
params: { basDd },
|
|
438
|
+
apiKey
|
|
439
|
+
});
|
|
440
|
+
const status = {
|
|
441
|
+
approved: result.success,
|
|
442
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
443
|
+
...result.error ? { error: result.error } : {}
|
|
444
|
+
};
|
|
445
|
+
const config = readConfig();
|
|
446
|
+
const serviceStatus = { ...config.serviceStatus, [categoryId]: status };
|
|
447
|
+
writeConfig({ ...config, serviceStatus });
|
|
448
|
+
return status;
|
|
449
|
+
} catch {
|
|
450
|
+
return { approved: false, checkedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
async function checkAllCategories(apiKey) {
|
|
454
|
+
const results = await Promise.all(
|
|
455
|
+
CATEGORIES.map(async (cat) => {
|
|
456
|
+
const status = await checkCategoryApproval(apiKey, cat.id);
|
|
457
|
+
return [cat.id, status];
|
|
458
|
+
})
|
|
459
|
+
);
|
|
460
|
+
return Object.fromEntries(results);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// src/output/formatter.ts
|
|
464
|
+
function detectOutputFormat(explicit) {
|
|
465
|
+
if (explicit) {
|
|
466
|
+
return explicit;
|
|
467
|
+
}
|
|
468
|
+
return process.stdout.isTTY ? "table" : "json";
|
|
469
|
+
}
|
|
470
|
+
function formatOutput(data, format, fields) {
|
|
471
|
+
const filtered = fields ? filterFields(data, fields) : data;
|
|
472
|
+
switch (format) {
|
|
473
|
+
case "json":
|
|
474
|
+
return JSON.stringify(filtered, null, 2);
|
|
475
|
+
case "ndjson":
|
|
476
|
+
return filtered.map((row) => JSON.stringify(row)).join("\n");
|
|
477
|
+
case "table":
|
|
478
|
+
return formatTable(filtered);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
function filterFields(data, fields) {
|
|
482
|
+
return data.map((row) => {
|
|
483
|
+
const filtered = {};
|
|
484
|
+
for (const field of fields) {
|
|
485
|
+
if (field in row) {
|
|
486
|
+
filtered[field] = row[field];
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return filtered;
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
function formatTable(data) {
|
|
493
|
+
if (data.length === 0) return "(no data)";
|
|
494
|
+
const firstRow = data[0];
|
|
495
|
+
if (!firstRow) return "(no data)";
|
|
496
|
+
const keys = Object.keys(firstRow);
|
|
497
|
+
const widths = keys.map((key) => {
|
|
498
|
+
const values = data.map((row) => String(row[key] ?? ""));
|
|
499
|
+
return Math.max(key.length, ...values.map((v) => v.length));
|
|
500
|
+
});
|
|
501
|
+
const header = keys.map((k, i) => k.padEnd(widths[i] ?? 0)).join(" ");
|
|
502
|
+
const separator = widths.map((w) => "-".repeat(w)).join(" ");
|
|
503
|
+
const rows = data.map(
|
|
504
|
+
(row) => keys.map((k, i) => String(row[k] ?? "").padEnd(widths[i] ?? 0)).join(" ")
|
|
505
|
+
);
|
|
506
|
+
return [header, separator, ...rows].join("\n");
|
|
507
|
+
}
|
|
508
|
+
function writeOutput(output) {
|
|
509
|
+
process.stdout.write(output + "\n");
|
|
510
|
+
}
|
|
511
|
+
function writeError(message) {
|
|
512
|
+
process.stderr.write(`Error: ${message}
|
|
513
|
+
`);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// src/cli/commands/auth.ts
|
|
517
|
+
function registerAuthCommand(program2) {
|
|
518
|
+
const auth = program2.command("auth").description("Manage API key and service approvals");
|
|
519
|
+
auth.command("set <api-key>").description("Save KRX API key").action((apiKey) => {
|
|
520
|
+
saveApiKey(apiKey);
|
|
521
|
+
writeOutput(JSON.stringify({ success: true, message: "API key saved" }));
|
|
522
|
+
});
|
|
523
|
+
auth.command("status").description("Check API key and service approval status").action(async () => {
|
|
524
|
+
const apiKey = getApiKey();
|
|
525
|
+
if (!apiKey) {
|
|
526
|
+
writeError(
|
|
527
|
+
"No API key configured. Use 'krx auth set <key>' or set KRX_API_KEY env var."
|
|
528
|
+
);
|
|
529
|
+
process.exit(EXIT_CODES.AUTH_FAILURE);
|
|
530
|
+
}
|
|
531
|
+
writeError("Checking service approvals...");
|
|
532
|
+
const statuses = await checkAllCategories(apiKey);
|
|
533
|
+
const format = detectOutputFormat(
|
|
534
|
+
program2.parent?.opts().output ?? program2.opts().output
|
|
535
|
+
);
|
|
536
|
+
const result = {
|
|
537
|
+
api_key_set: true,
|
|
538
|
+
services: statuses
|
|
539
|
+
};
|
|
540
|
+
if (format === "json" || format === "ndjson") {
|
|
541
|
+
writeOutput(JSON.stringify(result, null, 2));
|
|
542
|
+
} else {
|
|
543
|
+
const rows = CATEGORIES.map((cat) => {
|
|
544
|
+
const status = statuses[cat.id];
|
|
545
|
+
return {
|
|
546
|
+
category: cat.id,
|
|
547
|
+
name: cat.nameKo,
|
|
548
|
+
approved: status?.approved ? "YES" : "NO",
|
|
549
|
+
checked_at: status?.checkedAt ?? "-"
|
|
550
|
+
};
|
|
551
|
+
});
|
|
552
|
+
writeOutput(
|
|
553
|
+
formatOutput(
|
|
554
|
+
rows,
|
|
555
|
+
"table"
|
|
556
|
+
)
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
auth.command("check <category>").description("Check approval for a specific category").action(async (category) => {
|
|
561
|
+
const apiKey = getApiKey();
|
|
562
|
+
if (!apiKey) {
|
|
563
|
+
writeError(
|
|
564
|
+
"No API key configured. Use 'krx auth set <key>' or set KRX_API_KEY env var."
|
|
565
|
+
);
|
|
566
|
+
process.exit(EXIT_CODES.AUTH_FAILURE);
|
|
567
|
+
}
|
|
568
|
+
const validCategories = CATEGORIES.map((c) => c.id);
|
|
569
|
+
if (!validCategories.includes(category)) {
|
|
570
|
+
writeError(
|
|
571
|
+
`Invalid category: ${category}. Must be one of: ${validCategories.join(", ")}`
|
|
572
|
+
);
|
|
573
|
+
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
574
|
+
}
|
|
575
|
+
const status = await checkCategoryApproval(
|
|
576
|
+
apiKey,
|
|
577
|
+
category
|
|
578
|
+
);
|
|
579
|
+
writeOutput(JSON.stringify({ category, ...status }, null, 2));
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// src/validator/index.ts
|
|
584
|
+
import { z } from "zod/v4";
|
|
585
|
+
var DATE_PATTERN = /^\d{8}$/;
|
|
586
|
+
var dateSchema = z.string().regex(DATE_PATTERN, "Date must be YYYYMMDD format (8 digits)").refine(
|
|
587
|
+
(val) => {
|
|
588
|
+
const year = parseInt(val.slice(0, 4), 10);
|
|
589
|
+
const month = parseInt(val.slice(4, 6), 10);
|
|
590
|
+
const day = parseInt(val.slice(6, 8), 10);
|
|
591
|
+
if (year < 2010 || year > 2100) return false;
|
|
592
|
+
if (month < 1 || month > 12) return false;
|
|
593
|
+
if (day < 1 || day > 31) return false;
|
|
594
|
+
const date = new Date(year, month - 1, day);
|
|
595
|
+
return date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day;
|
|
596
|
+
},
|
|
597
|
+
{ message: "Invalid date. Must be a valid date from 2010 onwards." }
|
|
598
|
+
);
|
|
599
|
+
var marketSchema = z.enum(["kospi", "kosdaq", "konex", "krx"]);
|
|
600
|
+
function validateDate(value) {
|
|
601
|
+
const result = dateSchema.safeParse(value);
|
|
602
|
+
if (!result.success) {
|
|
603
|
+
throw new Error(result.error.issues[0]?.message ?? "Invalid date");
|
|
604
|
+
}
|
|
605
|
+
return result.data;
|
|
606
|
+
}
|
|
607
|
+
function validateMarket(value) {
|
|
608
|
+
const result = marketSchema.safeParse(value.toLowerCase());
|
|
609
|
+
if (!result.success) {
|
|
610
|
+
throw new Error(
|
|
611
|
+
`Invalid market: ${value}. Must be one of: kospi, kosdaq, konex, krx`
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
return result.data;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// src/cli/error-handler.ts
|
|
618
|
+
function handleKrxError(result) {
|
|
619
|
+
const exitCode = result.errorCode === "RATE_LIMIT" ? EXIT_CODES.RATE_LIMIT : result.errorCode === "401" ? EXIT_CODES.SERVICE_NOT_APPROVED : EXIT_CODES.GENERAL_ERROR;
|
|
620
|
+
writeError(result.error ?? "Unknown error");
|
|
621
|
+
process.exit(exitCode);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// src/cli/commands/index-cmd.ts
|
|
625
|
+
var MARKET_ENDPOINTS = {
|
|
626
|
+
kospi: "/svc/apis/idx/kospi_dd_trd",
|
|
627
|
+
kosdaq: "/svc/apis/idx/kosdaq_dd_trd",
|
|
628
|
+
krx: "/svc/apis/idx/krx_dd_trd",
|
|
629
|
+
bond: "/svc/apis/idx/bon_dd_trd",
|
|
630
|
+
derivative: "/svc/apis/idx/drvprod_dd_trd"
|
|
631
|
+
};
|
|
632
|
+
function registerIndexCommand(program2) {
|
|
633
|
+
const index = program2.command("index").description("Query KRX index data");
|
|
634
|
+
index.command("list").description("List index daily trading data").requiredOption("--date <date>", "trading date (YYYYMMDD)").option(
|
|
635
|
+
"--market <market>",
|
|
636
|
+
"market: kospi, kosdaq, krx, bond, derivative",
|
|
637
|
+
"kospi"
|
|
638
|
+
).action(async (opts) => {
|
|
639
|
+
try {
|
|
640
|
+
const date = validateDate(opts.date);
|
|
641
|
+
const market = opts.market.toLowerCase();
|
|
642
|
+
const endpoint = MARKET_ENDPOINTS[market];
|
|
643
|
+
if (!endpoint) {
|
|
644
|
+
writeError(
|
|
645
|
+
`Invalid market: ${market}. Must be one of: ${Object.keys(MARKET_ENDPOINTS).join(", ")}`
|
|
646
|
+
);
|
|
647
|
+
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
648
|
+
}
|
|
649
|
+
const apiKey = getApiKey();
|
|
650
|
+
if (!apiKey) {
|
|
651
|
+
writeError(
|
|
652
|
+
"No API key configured. Use 'krx auth set <key>' or set KRX_API_KEY env var."
|
|
653
|
+
);
|
|
654
|
+
process.exit(EXIT_CODES.AUTH_FAILURE);
|
|
655
|
+
}
|
|
656
|
+
const parentOpts = program2.opts();
|
|
657
|
+
if (parentOpts.dryRun) {
|
|
658
|
+
writeOutput(
|
|
659
|
+
JSON.stringify(
|
|
660
|
+
{
|
|
661
|
+
method: "POST",
|
|
662
|
+
endpoint,
|
|
663
|
+
params: { basDd: date },
|
|
664
|
+
headers: { AUTH_KEY: "***" }
|
|
665
|
+
},
|
|
666
|
+
null,
|
|
667
|
+
2
|
|
668
|
+
)
|
|
669
|
+
);
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
const result = await krxFetch({
|
|
673
|
+
endpoint,
|
|
674
|
+
params: { basDd: date },
|
|
675
|
+
apiKey
|
|
676
|
+
});
|
|
677
|
+
if (!result.success) {
|
|
678
|
+
handleKrxError(result);
|
|
679
|
+
}
|
|
680
|
+
if (result.data.length === 0) {
|
|
681
|
+
writeError(`No data for date ${date}`);
|
|
682
|
+
process.exit(EXIT_CODES.NO_DATA);
|
|
683
|
+
}
|
|
684
|
+
const format = detectOutputFormat(parentOpts.output);
|
|
685
|
+
const fields = parentOpts.fields?.split(",");
|
|
686
|
+
writeOutput(
|
|
687
|
+
formatOutput(
|
|
688
|
+
result.data,
|
|
689
|
+
format,
|
|
690
|
+
fields
|
|
691
|
+
)
|
|
692
|
+
);
|
|
693
|
+
} catch (err) {
|
|
694
|
+
writeError(err instanceof Error ? err.message : String(err));
|
|
695
|
+
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// src/cli/commands/stock.ts
|
|
701
|
+
var TRADING_ENDPOINTS = {
|
|
702
|
+
kospi: "/svc/apis/sto/stk_bydd_trd",
|
|
703
|
+
kosdaq: "/svc/apis/sto/ksq_bydd_trd",
|
|
704
|
+
konex: "/svc/apis/sto/knx_bydd_trd"
|
|
705
|
+
};
|
|
706
|
+
var INFO_ENDPOINTS = {
|
|
707
|
+
kospi: "/svc/apis/sto/stk_isu_base_info",
|
|
708
|
+
kosdaq: "/svc/apis/sto/ksq_isu_base_info",
|
|
709
|
+
konex: "/svc/apis/sto/knx_isu_base_info"
|
|
710
|
+
};
|
|
711
|
+
function registerStockCommand(program2) {
|
|
712
|
+
const stock = program2.command("stock").description("Query KRX stock data");
|
|
713
|
+
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) => {
|
|
714
|
+
try {
|
|
715
|
+
const date = validateDate(opts.date);
|
|
716
|
+
const market = validateMarket(opts.market);
|
|
717
|
+
const endpoint = TRADING_ENDPOINTS[market];
|
|
718
|
+
if (!endpoint) {
|
|
719
|
+
writeError(`Invalid market for stock list: ${market}`);
|
|
720
|
+
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
721
|
+
}
|
|
722
|
+
const apiKey = getApiKey();
|
|
723
|
+
if (!apiKey) {
|
|
724
|
+
writeError(
|
|
725
|
+
"No API key configured. Use 'krx auth set <key>' or set KRX_API_KEY env var."
|
|
726
|
+
);
|
|
727
|
+
process.exit(EXIT_CODES.AUTH_FAILURE);
|
|
728
|
+
}
|
|
729
|
+
const parentOpts = program2.opts();
|
|
730
|
+
if (parentOpts.dryRun) {
|
|
731
|
+
writeOutput(
|
|
732
|
+
JSON.stringify(
|
|
733
|
+
{
|
|
734
|
+
method: "POST",
|
|
735
|
+
endpoint,
|
|
736
|
+
params: { basDd: date },
|
|
737
|
+
headers: { AUTH_KEY: "***" }
|
|
738
|
+
},
|
|
739
|
+
null,
|
|
740
|
+
2
|
|
741
|
+
)
|
|
742
|
+
);
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
const result = await krxFetch({
|
|
746
|
+
endpoint,
|
|
747
|
+
params: { basDd: date },
|
|
748
|
+
apiKey
|
|
749
|
+
});
|
|
750
|
+
if (!result.success) {
|
|
751
|
+
handleKrxError(result);
|
|
752
|
+
}
|
|
753
|
+
if (result.data.length === 0) {
|
|
754
|
+
writeError(`No data for date ${date}`);
|
|
755
|
+
process.exit(EXIT_CODES.NO_DATA);
|
|
756
|
+
}
|
|
757
|
+
const format = detectOutputFormat(parentOpts.output);
|
|
758
|
+
const fields = parentOpts.fields?.split(",");
|
|
759
|
+
writeOutput(
|
|
760
|
+
formatOutput(
|
|
761
|
+
result.data,
|
|
762
|
+
format,
|
|
763
|
+
fields
|
|
764
|
+
)
|
|
765
|
+
);
|
|
766
|
+
} catch (err) {
|
|
767
|
+
writeError(err instanceof Error ? err.message : String(err));
|
|
768
|
+
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
stock.command("info").description("List stock base information").option("--market <market>", "market: kospi, kosdaq, konex", "kospi").action(async (opts) => {
|
|
772
|
+
try {
|
|
773
|
+
const market = validateMarket(opts.market);
|
|
774
|
+
const endpoint = INFO_ENDPOINTS[market];
|
|
775
|
+
if (!endpoint) {
|
|
776
|
+
writeError(`Invalid market for stock info: ${market}`);
|
|
777
|
+
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
778
|
+
}
|
|
779
|
+
const apiKey = getApiKey();
|
|
780
|
+
if (!apiKey) {
|
|
781
|
+
writeError(
|
|
782
|
+
"No API key configured. Use 'krx auth set <key>' or set KRX_API_KEY env var."
|
|
783
|
+
);
|
|
784
|
+
process.exit(EXIT_CODES.AUTH_FAILURE);
|
|
785
|
+
}
|
|
786
|
+
const parentOpts = program2.opts();
|
|
787
|
+
if (parentOpts.dryRun) {
|
|
788
|
+
writeOutput(
|
|
789
|
+
JSON.stringify(
|
|
790
|
+
{
|
|
791
|
+
method: "POST",
|
|
792
|
+
endpoint,
|
|
793
|
+
params: {},
|
|
794
|
+
headers: { AUTH_KEY: "***" }
|
|
795
|
+
},
|
|
796
|
+
null,
|
|
797
|
+
2
|
|
798
|
+
)
|
|
799
|
+
);
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
const result = await krxFetch({
|
|
803
|
+
endpoint,
|
|
804
|
+
params: {},
|
|
805
|
+
apiKey
|
|
806
|
+
});
|
|
807
|
+
if (!result.success) {
|
|
808
|
+
handleKrxError(result);
|
|
809
|
+
}
|
|
810
|
+
if (result.data.length === 0) {
|
|
811
|
+
writeError("No data");
|
|
812
|
+
process.exit(EXIT_CODES.NO_DATA);
|
|
813
|
+
}
|
|
814
|
+
const format = detectOutputFormat(parentOpts.output);
|
|
815
|
+
const fields = parentOpts.fields?.split(",");
|
|
816
|
+
writeOutput(
|
|
817
|
+
formatOutput(
|
|
818
|
+
result.data,
|
|
819
|
+
format,
|
|
820
|
+
fields
|
|
821
|
+
)
|
|
822
|
+
);
|
|
823
|
+
} catch (err) {
|
|
824
|
+
writeError(err instanceof Error ? err.message : String(err));
|
|
825
|
+
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// src/cli/commands/etp.ts
|
|
831
|
+
var TYPE_ENDPOINTS = {
|
|
832
|
+
etf: "/svc/apis/etp/etf_bydd_trd",
|
|
833
|
+
etn: "/svc/apis/etp/etn_bydd_trd",
|
|
834
|
+
elw: "/svc/apis/etp/elw_bydd_trd"
|
|
835
|
+
};
|
|
836
|
+
function registerEtpCommand(program2) {
|
|
837
|
+
const etp = program2.command("etp").description("Query KRX ETP data (ETF/ETN/ELW)");
|
|
838
|
+
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) => {
|
|
839
|
+
try {
|
|
840
|
+
const date = validateDate(opts.date);
|
|
841
|
+
const type = opts.type.toLowerCase();
|
|
842
|
+
const endpoint = TYPE_ENDPOINTS[type];
|
|
843
|
+
if (!endpoint) {
|
|
844
|
+
writeError(
|
|
845
|
+
`Invalid type: ${type}. Must be one of: ${Object.keys(TYPE_ENDPOINTS).join(", ")}`
|
|
846
|
+
);
|
|
847
|
+
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
848
|
+
}
|
|
849
|
+
const apiKey = getApiKey();
|
|
850
|
+
if (!apiKey) {
|
|
851
|
+
writeError(
|
|
852
|
+
"No API key configured. Use 'krx auth set <key>' or set KRX_API_KEY env var."
|
|
853
|
+
);
|
|
854
|
+
process.exit(EXIT_CODES.AUTH_FAILURE);
|
|
855
|
+
}
|
|
856
|
+
const parentOpts = program2.opts();
|
|
857
|
+
if (parentOpts.dryRun) {
|
|
858
|
+
writeOutput(
|
|
859
|
+
JSON.stringify(
|
|
860
|
+
{
|
|
861
|
+
method: "POST",
|
|
862
|
+
endpoint,
|
|
863
|
+
params: { basDd: date },
|
|
864
|
+
headers: { AUTH_KEY: "***" }
|
|
865
|
+
},
|
|
866
|
+
null,
|
|
867
|
+
2
|
|
868
|
+
)
|
|
869
|
+
);
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
const result = await krxFetch({
|
|
873
|
+
endpoint,
|
|
874
|
+
params: { basDd: date },
|
|
875
|
+
apiKey
|
|
876
|
+
});
|
|
877
|
+
if (!result.success) {
|
|
878
|
+
handleKrxError(result);
|
|
879
|
+
}
|
|
880
|
+
if (result.data.length === 0) {
|
|
881
|
+
writeError(`No data for date ${date}`);
|
|
882
|
+
process.exit(EXIT_CODES.NO_DATA);
|
|
883
|
+
}
|
|
884
|
+
const format = detectOutputFormat(parentOpts.output);
|
|
885
|
+
const fields = parentOpts.fields?.split(",");
|
|
886
|
+
writeOutput(
|
|
887
|
+
formatOutput(
|
|
888
|
+
result.data,
|
|
889
|
+
format,
|
|
890
|
+
fields
|
|
891
|
+
)
|
|
892
|
+
);
|
|
893
|
+
} catch (err) {
|
|
894
|
+
writeError(err instanceof Error ? err.message : String(err));
|
|
895
|
+
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
896
|
+
}
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// src/cli/commands/bond.ts
|
|
901
|
+
var MARKET_ENDPOINTS2 = {
|
|
902
|
+
kts: "/svc/apis/bon/kts_bydd_trd",
|
|
903
|
+
general: "/svc/apis/bon/bnd_bydd_trd",
|
|
904
|
+
small: "/svc/apis/bon/smb_bydd_trd"
|
|
905
|
+
};
|
|
906
|
+
function registerBondCommand(program2) {
|
|
907
|
+
const bond = program2.command("bond").description("Query KRX bond data");
|
|
908
|
+
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) => {
|
|
909
|
+
try {
|
|
910
|
+
const date = validateDate(opts.date);
|
|
911
|
+
const market = opts.market.toLowerCase();
|
|
912
|
+
const endpoint = MARKET_ENDPOINTS2[market];
|
|
913
|
+
if (!endpoint) {
|
|
914
|
+
writeError(
|
|
915
|
+
`Invalid market: ${market}. Must be one of: ${Object.keys(MARKET_ENDPOINTS2).join(", ")}`
|
|
916
|
+
);
|
|
917
|
+
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
918
|
+
}
|
|
919
|
+
const apiKey = getApiKey();
|
|
920
|
+
if (!apiKey) {
|
|
921
|
+
writeError(
|
|
922
|
+
"No API key configured. Use 'krx auth set <key>' or set KRX_API_KEY env var."
|
|
923
|
+
);
|
|
924
|
+
process.exit(EXIT_CODES.AUTH_FAILURE);
|
|
925
|
+
}
|
|
926
|
+
const parentOpts = program2.opts();
|
|
927
|
+
if (parentOpts.dryRun) {
|
|
928
|
+
writeOutput(
|
|
929
|
+
JSON.stringify(
|
|
930
|
+
{
|
|
931
|
+
method: "POST",
|
|
932
|
+
endpoint,
|
|
933
|
+
params: { basDd: date },
|
|
934
|
+
headers: { AUTH_KEY: "***" }
|
|
935
|
+
},
|
|
936
|
+
null,
|
|
937
|
+
2
|
|
938
|
+
)
|
|
939
|
+
);
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
const result = await krxFetch({
|
|
943
|
+
endpoint,
|
|
944
|
+
params: { basDd: date },
|
|
945
|
+
apiKey
|
|
946
|
+
});
|
|
947
|
+
if (!result.success) {
|
|
948
|
+
handleKrxError(result);
|
|
949
|
+
}
|
|
950
|
+
if (result.data.length === 0) {
|
|
951
|
+
writeError(`No data for date ${date}`);
|
|
952
|
+
process.exit(EXIT_CODES.NO_DATA);
|
|
953
|
+
}
|
|
954
|
+
const format = detectOutputFormat(parentOpts.output);
|
|
955
|
+
const fields = parentOpts.fields?.split(",");
|
|
956
|
+
writeOutput(
|
|
957
|
+
formatOutput(
|
|
958
|
+
result.data,
|
|
959
|
+
format,
|
|
960
|
+
fields
|
|
961
|
+
)
|
|
962
|
+
);
|
|
963
|
+
} catch (err) {
|
|
964
|
+
writeError(err instanceof Error ? err.message : String(err));
|
|
965
|
+
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
966
|
+
}
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// src/cli/commands/derivative.ts
|
|
971
|
+
var TYPE_ENDPOINTS2 = {
|
|
972
|
+
futures: "/svc/apis/drv/fut_bydd_trd",
|
|
973
|
+
"futures-kospi": "/svc/apis/drv/eqsfu_stk_bydd_trd",
|
|
974
|
+
"futures-kosdaq": "/svc/apis/drv/eqkfu_ksq_bydd_trd",
|
|
975
|
+
options: "/svc/apis/drv/opt_bydd_trd",
|
|
976
|
+
"options-kospi": "/svc/apis/drv/eqsop_bydd_trd",
|
|
977
|
+
"options-kosdaq": "/svc/apis/drv/eqkop_bydd_trd"
|
|
978
|
+
};
|
|
979
|
+
function registerDerivativeCommand(program2) {
|
|
980
|
+
const derivative = program2.command("derivative").description("Query KRX derivative data");
|
|
981
|
+
derivative.command("list").description("List derivative daily trading data").requiredOption("--date <date>", "trading date (YYYYMMDD)").option(
|
|
982
|
+
"--type <type>",
|
|
983
|
+
"type: futures, futures-kospi, futures-kosdaq, options, options-kospi, options-kosdaq",
|
|
984
|
+
"futures"
|
|
985
|
+
).action(async (opts) => {
|
|
986
|
+
try {
|
|
987
|
+
const date = validateDate(opts.date);
|
|
988
|
+
const type = opts.type.toLowerCase();
|
|
989
|
+
const endpoint = TYPE_ENDPOINTS2[type];
|
|
990
|
+
if (!endpoint) {
|
|
991
|
+
writeError(
|
|
992
|
+
`Invalid type: ${type}. Must be one of: ${Object.keys(TYPE_ENDPOINTS2).join(", ")}`
|
|
993
|
+
);
|
|
994
|
+
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
995
|
+
}
|
|
996
|
+
const apiKey = getApiKey();
|
|
997
|
+
if (!apiKey) {
|
|
998
|
+
writeError(
|
|
999
|
+
"No API key configured. Use 'krx auth set <key>' or set KRX_API_KEY env var."
|
|
1000
|
+
);
|
|
1001
|
+
process.exit(EXIT_CODES.AUTH_FAILURE);
|
|
1002
|
+
}
|
|
1003
|
+
const parentOpts = program2.opts();
|
|
1004
|
+
if (parentOpts.dryRun) {
|
|
1005
|
+
writeOutput(
|
|
1006
|
+
JSON.stringify(
|
|
1007
|
+
{
|
|
1008
|
+
method: "POST",
|
|
1009
|
+
endpoint,
|
|
1010
|
+
params: { basDd: date },
|
|
1011
|
+
headers: { AUTH_KEY: "***" }
|
|
1012
|
+
},
|
|
1013
|
+
null,
|
|
1014
|
+
2
|
|
1015
|
+
)
|
|
1016
|
+
);
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
const result = await krxFetch({
|
|
1020
|
+
endpoint,
|
|
1021
|
+
params: { basDd: date },
|
|
1022
|
+
apiKey
|
|
1023
|
+
});
|
|
1024
|
+
if (!result.success) {
|
|
1025
|
+
handleKrxError(result);
|
|
1026
|
+
}
|
|
1027
|
+
if (result.data.length === 0) {
|
|
1028
|
+
writeError(`No data for date ${date}`);
|
|
1029
|
+
process.exit(EXIT_CODES.NO_DATA);
|
|
1030
|
+
}
|
|
1031
|
+
const format = detectOutputFormat(parentOpts.output);
|
|
1032
|
+
const fields = parentOpts.fields?.split(",");
|
|
1033
|
+
writeOutput(
|
|
1034
|
+
formatOutput(
|
|
1035
|
+
result.data,
|
|
1036
|
+
format,
|
|
1037
|
+
fields
|
|
1038
|
+
)
|
|
1039
|
+
);
|
|
1040
|
+
} catch (err) {
|
|
1041
|
+
writeError(err instanceof Error ? err.message : String(err));
|
|
1042
|
+
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
1043
|
+
}
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// src/cli/commands/commodity.ts
|
|
1048
|
+
var TYPE_ENDPOINTS3 = {
|
|
1049
|
+
oil: "/svc/apis/gen/oil_bydd_trd",
|
|
1050
|
+
gold: "/svc/apis/gen/gold_bydd_trd",
|
|
1051
|
+
emission: "/svc/apis/gen/ets_bydd_trd"
|
|
1052
|
+
};
|
|
1053
|
+
function registerCommodityCommand(program2) {
|
|
1054
|
+
const commodity = program2.command("commodity").description("Query KRX commodity data (oil/gold/emission)");
|
|
1055
|
+
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) => {
|
|
1056
|
+
try {
|
|
1057
|
+
const date = validateDate(opts.date);
|
|
1058
|
+
const type = opts.type.toLowerCase();
|
|
1059
|
+
const endpoint = TYPE_ENDPOINTS3[type];
|
|
1060
|
+
if (!endpoint) {
|
|
1061
|
+
writeError(
|
|
1062
|
+
`Invalid type: ${type}. Must be one of: ${Object.keys(TYPE_ENDPOINTS3).join(", ")}`
|
|
1063
|
+
);
|
|
1064
|
+
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
1065
|
+
}
|
|
1066
|
+
const apiKey = getApiKey();
|
|
1067
|
+
if (!apiKey) {
|
|
1068
|
+
writeError(
|
|
1069
|
+
"No API key configured. Use 'krx auth set <key>' or set KRX_API_KEY env var."
|
|
1070
|
+
);
|
|
1071
|
+
process.exit(EXIT_CODES.AUTH_FAILURE);
|
|
1072
|
+
}
|
|
1073
|
+
const parentOpts = program2.opts();
|
|
1074
|
+
if (parentOpts.dryRun) {
|
|
1075
|
+
writeOutput(
|
|
1076
|
+
JSON.stringify(
|
|
1077
|
+
{
|
|
1078
|
+
method: "POST",
|
|
1079
|
+
endpoint,
|
|
1080
|
+
params: { basDd: date },
|
|
1081
|
+
headers: { AUTH_KEY: "***" }
|
|
1082
|
+
},
|
|
1083
|
+
null,
|
|
1084
|
+
2
|
|
1085
|
+
)
|
|
1086
|
+
);
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
const result = await krxFetch({
|
|
1090
|
+
endpoint,
|
|
1091
|
+
params: { basDd: date },
|
|
1092
|
+
apiKey
|
|
1093
|
+
});
|
|
1094
|
+
if (!result.success) {
|
|
1095
|
+
handleKrxError(result);
|
|
1096
|
+
}
|
|
1097
|
+
if (result.data.length === 0) {
|
|
1098
|
+
writeError(`No data for date ${date}`);
|
|
1099
|
+
process.exit(EXIT_CODES.NO_DATA);
|
|
1100
|
+
}
|
|
1101
|
+
const format = detectOutputFormat(parentOpts.output);
|
|
1102
|
+
const fields = parentOpts.fields?.split(",");
|
|
1103
|
+
writeOutput(
|
|
1104
|
+
formatOutput(
|
|
1105
|
+
result.data,
|
|
1106
|
+
format,
|
|
1107
|
+
fields
|
|
1108
|
+
)
|
|
1109
|
+
);
|
|
1110
|
+
} catch (err) {
|
|
1111
|
+
writeError(err instanceof Error ? err.message : String(err));
|
|
1112
|
+
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
1113
|
+
}
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// src/cli/commands/esg.ts
|
|
1118
|
+
var TYPE_ENDPOINTS4 = {
|
|
1119
|
+
"sri-bond": "/svc/apis/esg/sri_bond_info",
|
|
1120
|
+
etp: "/svc/apis/esg/esg_etp_info",
|
|
1121
|
+
index: "/svc/apis/esg/esg_index_info"
|
|
1122
|
+
};
|
|
1123
|
+
function registerEsgCommand(program2) {
|
|
1124
|
+
const esg = program2.command("esg").description("Query KRX ESG data");
|
|
1125
|
+
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) => {
|
|
1126
|
+
try {
|
|
1127
|
+
const date = validateDate(opts.date);
|
|
1128
|
+
const type = opts.type.toLowerCase();
|
|
1129
|
+
const endpoint = TYPE_ENDPOINTS4[type];
|
|
1130
|
+
if (!endpoint) {
|
|
1131
|
+
writeError(
|
|
1132
|
+
`Invalid type: ${type}. Must be one of: ${Object.keys(TYPE_ENDPOINTS4).join(", ")}`
|
|
1133
|
+
);
|
|
1134
|
+
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
1135
|
+
}
|
|
1136
|
+
const apiKey = getApiKey();
|
|
1137
|
+
if (!apiKey) {
|
|
1138
|
+
writeError(
|
|
1139
|
+
"No API key configured. Use 'krx auth set <key>' or set KRX_API_KEY env var."
|
|
1140
|
+
);
|
|
1141
|
+
process.exit(EXIT_CODES.AUTH_FAILURE);
|
|
1142
|
+
}
|
|
1143
|
+
const parentOpts = program2.opts();
|
|
1144
|
+
if (parentOpts.dryRun) {
|
|
1145
|
+
writeOutput(
|
|
1146
|
+
JSON.stringify(
|
|
1147
|
+
{
|
|
1148
|
+
method: "POST",
|
|
1149
|
+
endpoint,
|
|
1150
|
+
params: { basDd: date },
|
|
1151
|
+
headers: { AUTH_KEY: "***" }
|
|
1152
|
+
},
|
|
1153
|
+
null,
|
|
1154
|
+
2
|
|
1155
|
+
)
|
|
1156
|
+
);
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
const result = await krxFetch({
|
|
1160
|
+
endpoint,
|
|
1161
|
+
params: { basDd: date },
|
|
1162
|
+
apiKey
|
|
1163
|
+
});
|
|
1164
|
+
if (!result.success) {
|
|
1165
|
+
handleKrxError(result);
|
|
1166
|
+
}
|
|
1167
|
+
if (result.data.length === 0) {
|
|
1168
|
+
writeError(`No data for date ${date}`);
|
|
1169
|
+
process.exit(EXIT_CODES.NO_DATA);
|
|
1170
|
+
}
|
|
1171
|
+
const format = detectOutputFormat(parentOpts.output);
|
|
1172
|
+
const fields = parentOpts.fields?.split(",");
|
|
1173
|
+
writeOutput(
|
|
1174
|
+
formatOutput(
|
|
1175
|
+
result.data,
|
|
1176
|
+
format,
|
|
1177
|
+
fields
|
|
1178
|
+
)
|
|
1179
|
+
);
|
|
1180
|
+
} catch (err) {
|
|
1181
|
+
writeError(err instanceof Error ? err.message : String(err));
|
|
1182
|
+
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
1183
|
+
}
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// src/cli/commands/schema.ts
|
|
1188
|
+
var COMMON_PARAMS = [
|
|
1189
|
+
{
|
|
1190
|
+
name: "basDd",
|
|
1191
|
+
type: "string",
|
|
1192
|
+
required: true,
|
|
1193
|
+
description: "Trading date in YYYYMMDD format"
|
|
1194
|
+
}
|
|
1195
|
+
];
|
|
1196
|
+
function buildCommandName(endpoint) {
|
|
1197
|
+
const parts = endpoint.replace("/svc/apis/", "").split("/");
|
|
1198
|
+
const categoryCode = parts[0];
|
|
1199
|
+
const apiName = parts[1];
|
|
1200
|
+
const category = CATEGORIES.find((c) => c.code === categoryCode);
|
|
1201
|
+
return `${category?.id ?? categoryCode}.${apiName}`;
|
|
1202
|
+
}
|
|
1203
|
+
function getAllSchemas() {
|
|
1204
|
+
return ENDPOINTS.map((ep) => ({
|
|
1205
|
+
command: buildCommandName(ep.path),
|
|
1206
|
+
endpoint: ep.path,
|
|
1207
|
+
description: ep.description,
|
|
1208
|
+
descriptionKo: ep.descriptionKo,
|
|
1209
|
+
category: ep.category,
|
|
1210
|
+
params: [...COMMON_PARAMS]
|
|
1211
|
+
}));
|
|
1212
|
+
}
|
|
1213
|
+
function registerSchemaCommand(program2) {
|
|
1214
|
+
program2.command("schema [command]").description("Show API schema for agent introspection").option("--all", "show all schemas").action((command, opts) => {
|
|
1215
|
+
if (opts.all || !command) {
|
|
1216
|
+
const schemas2 = getAllSchemas();
|
|
1217
|
+
writeOutput(JSON.stringify(schemas2, null, 2));
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
const schemas = getAllSchemas();
|
|
1221
|
+
const match = schemas.find(
|
|
1222
|
+
(s) => s.command === command || s.command.toLowerCase() === command.toLowerCase()
|
|
1223
|
+
);
|
|
1224
|
+
if (!match) {
|
|
1225
|
+
writeError(
|
|
1226
|
+
`Unknown command: ${command}. Use 'krx schema --all' to list all.`
|
|
1227
|
+
);
|
|
1228
|
+
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
1229
|
+
}
|
|
1230
|
+
writeOutput(JSON.stringify(match, null, 2));
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// src/cli/index.ts
|
|
1235
|
+
var EXIT_CODES = {
|
|
1236
|
+
SUCCESS: 0,
|
|
1237
|
+
GENERAL_ERROR: 1,
|
|
1238
|
+
USAGE_ERROR: 2,
|
|
1239
|
+
NO_DATA: 3,
|
|
1240
|
+
AUTH_FAILURE: 4,
|
|
1241
|
+
RATE_LIMIT: 5,
|
|
1242
|
+
SERVICE_NOT_APPROVED: 6
|
|
1243
|
+
};
|
|
1244
|
+
var program = new Command();
|
|
1245
|
+
program.name("krx").description("Agent-native CLI for KRX (Korea Exchange) Open API").version("0.1.0").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");
|
|
1246
|
+
registerAuthCommand(program);
|
|
1247
|
+
registerIndexCommand(program);
|
|
1248
|
+
registerStockCommand(program);
|
|
1249
|
+
registerEtpCommand(program);
|
|
1250
|
+
registerBondCommand(program);
|
|
1251
|
+
registerDerivativeCommand(program);
|
|
1252
|
+
registerCommodityCommand(program);
|
|
1253
|
+
registerEsgCommand(program);
|
|
1254
|
+
registerSchemaCommand(program);
|
|
1255
|
+
program.exitOverride();
|
|
1256
|
+
try {
|
|
1257
|
+
await program.parseAsync(process.argv);
|
|
1258
|
+
} catch (err) {
|
|
1259
|
+
if (err instanceof Error && "exitCode" in err) {
|
|
1260
|
+
const exitCode = err.exitCode;
|
|
1261
|
+
process.exit(exitCode);
|
|
1262
|
+
}
|
|
1263
|
+
writeError(err instanceof Error ? err.message : String(err));
|
|
1264
|
+
process.exit(EXIT_CODES.GENERAL_ERROR);
|
|
1265
|
+
}
|
|
1266
|
+
export {
|
|
1267
|
+
EXIT_CODES
|
|
1268
|
+
};
|
|
1269
|
+
//# sourceMappingURL=cli.js.map
|