@2oolkit/kiwoom-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 +201 -0
- package/dist/index.js +2049 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp.js +1097 -0
- package/dist/mcp.js.map +1 -0
- package/package.json +75 -0
- package/skill/SKILL.md +265 -0
- package/skill/references/account.md +74 -0
- package/skill/references/market-data.md +78 -0
- package/skill/references/trading.md +99 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2049 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/index.ts
|
|
27
|
+
var import_commander = require("commander");
|
|
28
|
+
|
|
29
|
+
// src/commands/config.ts
|
|
30
|
+
var readline = __toESM(require("readline"));
|
|
31
|
+
|
|
32
|
+
// src/config/store.ts
|
|
33
|
+
var fs = __toESM(require("fs"));
|
|
34
|
+
var path = __toESM(require("path"));
|
|
35
|
+
var os = __toESM(require("os"));
|
|
36
|
+
|
|
37
|
+
// src/config/constants.ts
|
|
38
|
+
var BASE_URLS = {
|
|
39
|
+
real: "https://api.kiwoom.com",
|
|
40
|
+
mock: "https://mockapi.kiwoom.com"
|
|
41
|
+
};
|
|
42
|
+
var ENV_ALIASES = {
|
|
43
|
+
real: "real",
|
|
44
|
+
prod: "real",
|
|
45
|
+
production: "real",
|
|
46
|
+
live: "real",
|
|
47
|
+
\uC2E4\uC804: "real",
|
|
48
|
+
\uC2E4\uC804\uD22C\uC790: "real",
|
|
49
|
+
mock: "mock",
|
|
50
|
+
test: "mock",
|
|
51
|
+
paper: "mock",
|
|
52
|
+
demo: "mock",
|
|
53
|
+
\uBAA8\uC758: "mock",
|
|
54
|
+
\uBAA8\uC758\uD22C\uC790: "mock"
|
|
55
|
+
};
|
|
56
|
+
var TOKEN_PATH = "/oauth2/token";
|
|
57
|
+
var REVOKE_PATH = "/oauth2/revoke";
|
|
58
|
+
var CONFIG_DIR_NAME = ".kiwoom-cli";
|
|
59
|
+
var CONFIG_FILE_NAME = "config.json";
|
|
60
|
+
var TOKEN_FILE_NAME = "token.json";
|
|
61
|
+
var ENV_VARS = {
|
|
62
|
+
appkey: "KIWOOM_APPKEY",
|
|
63
|
+
secretkey: "KIWOOM_SECRETKEY",
|
|
64
|
+
env: "KIWOOM_ENV"
|
|
65
|
+
};
|
|
66
|
+
var TOKEN_REFRESH_BUFFER_MS = 6e4;
|
|
67
|
+
var REQUEST_TIMEOUT_MS = 3e4;
|
|
68
|
+
var SOFT_EMPTY_RETURN_CODES = /* @__PURE__ */ new Set([20]);
|
|
69
|
+
var ORDER_EXCHANGE_TYPES = ["KRX", "NXT", "SOR"];
|
|
70
|
+
var ACCOUNT_EXCHANGE_TYPES = ["KRX", "NXT", "%"];
|
|
71
|
+
var ORDER_TYPES = {
|
|
72
|
+
"0": "\uBCF4\uD1B5(\uC9C0\uC815\uAC00/limit)",
|
|
73
|
+
"3": "\uC2DC\uC7A5\uAC00(market)",
|
|
74
|
+
"5": "\uC870\uAC74\uBD80\uC9C0\uC815\uAC00",
|
|
75
|
+
"6": "\uCD5C\uC720\uB9AC\uC9C0\uC815\uAC00",
|
|
76
|
+
"7": "\uCD5C\uC6B0\uC120\uC9C0\uC815\uAC00",
|
|
77
|
+
"10": "\uBCF4\uD1B5(IOC)",
|
|
78
|
+
"13": "\uC2DC\uC7A5\uAC00(IOC)",
|
|
79
|
+
"16": "\uCD5C\uC720\uB9AC(IOC)",
|
|
80
|
+
"20": "\uBCF4\uD1B5(FOK)",
|
|
81
|
+
"23": "\uC2DC\uC7A5\uAC00(FOK)",
|
|
82
|
+
"26": "\uCD5C\uC720\uB9AC(FOK)",
|
|
83
|
+
"28": "\uC2A4\uD1B1\uC9C0\uC815\uAC00(stop-limit)",
|
|
84
|
+
"29": "\uC911\uAC04\uAC00",
|
|
85
|
+
"30": "\uC911\uAC04\uAC00(IOC)",
|
|
86
|
+
"31": "\uC911\uAC04\uAC00(FOK)",
|
|
87
|
+
"61": "\uC7A5\uC2DC\uC791\uC804\uC2DC\uAC04\uC678",
|
|
88
|
+
"62": "\uC2DC\uAC04\uC678\uB2E8\uC77C\uAC00",
|
|
89
|
+
"81": "\uC7A5\uB9C8\uAC10\uD6C4\uC2DC\uAC04\uC678"
|
|
90
|
+
};
|
|
91
|
+
var MARKET_ORDER_TYPES = /* @__PURE__ */ new Set(["3", "13", "23"]);
|
|
92
|
+
|
|
93
|
+
// src/config/store.ts
|
|
94
|
+
var DEFAULT_CONFIG = { env: "real" };
|
|
95
|
+
function getConfigDir() {
|
|
96
|
+
return path.join(os.homedir(), CONFIG_DIR_NAME);
|
|
97
|
+
}
|
|
98
|
+
function getConfigPath() {
|
|
99
|
+
return path.join(getConfigDir(), CONFIG_FILE_NAME);
|
|
100
|
+
}
|
|
101
|
+
function getTokenPath() {
|
|
102
|
+
return path.join(getConfigDir(), TOKEN_FILE_NAME);
|
|
103
|
+
}
|
|
104
|
+
function ensureConfigDir() {
|
|
105
|
+
const dir = getConfigDir();
|
|
106
|
+
if (!fs.existsSync(dir)) {
|
|
107
|
+
fs.mkdirSync(dir, { mode: 448, recursive: true });
|
|
108
|
+
} else {
|
|
109
|
+
try {
|
|
110
|
+
fs.chmodSync(dir, 448);
|
|
111
|
+
} catch {
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function writeSecure(filePath, data) {
|
|
116
|
+
fs.writeFileSync(filePath, data, { mode: 384 });
|
|
117
|
+
try {
|
|
118
|
+
fs.chmodSync(filePath, 384);
|
|
119
|
+
} catch {
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function loadConfig() {
|
|
123
|
+
try {
|
|
124
|
+
const raw = fs.readFileSync(getConfigPath(), "utf-8");
|
|
125
|
+
return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
|
|
126
|
+
} catch {
|
|
127
|
+
return { ...DEFAULT_CONFIG };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function saveConfig(partial) {
|
|
131
|
+
ensureConfigDir();
|
|
132
|
+
const current = loadConfig();
|
|
133
|
+
const merged = { ...current, ...partial };
|
|
134
|
+
writeSecure(getConfigPath(), JSON.stringify(merged, null, 2));
|
|
135
|
+
}
|
|
136
|
+
function getEffectiveConfig() {
|
|
137
|
+
const disk = loadConfig();
|
|
138
|
+
const envVal = process.env[ENV_VARS.env];
|
|
139
|
+
const env = envVal && ENV_ALIASES[envVal.toLowerCase()] ? ENV_ALIASES[envVal.toLowerCase()] : disk.env;
|
|
140
|
+
return {
|
|
141
|
+
env,
|
|
142
|
+
appkey: disk.appkey ?? process.env[ENV_VARS.appkey],
|
|
143
|
+
secretkey: disk.secretkey ?? process.env[ENV_VARS.secretkey]
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function maskSecret(value) {
|
|
147
|
+
if (!value) return "(not set)";
|
|
148
|
+
if (value.length <= 10) return "****";
|
|
149
|
+
return value.slice(0, 6) + "..." + value.slice(-4);
|
|
150
|
+
}
|
|
151
|
+
function loadTokenCache() {
|
|
152
|
+
try {
|
|
153
|
+
return JSON.parse(fs.readFileSync(getTokenPath(), "utf-8"));
|
|
154
|
+
} catch {
|
|
155
|
+
return {};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function getCachedToken(env) {
|
|
159
|
+
return loadTokenCache()[env];
|
|
160
|
+
}
|
|
161
|
+
function saveCachedToken(env, token) {
|
|
162
|
+
ensureConfigDir();
|
|
163
|
+
const cache = loadTokenCache();
|
|
164
|
+
cache[env] = token;
|
|
165
|
+
writeSecure(getTokenPath(), JSON.stringify(cache, null, 2));
|
|
166
|
+
}
|
|
167
|
+
function clearCachedToken(env) {
|
|
168
|
+
if (!env) {
|
|
169
|
+
try {
|
|
170
|
+
fs.rmSync(getTokenPath());
|
|
171
|
+
} catch {
|
|
172
|
+
}
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const cache = loadTokenCache();
|
|
176
|
+
delete cache[env];
|
|
177
|
+
try {
|
|
178
|
+
ensureConfigDir();
|
|
179
|
+
writeSecure(getTokenPath(), JSON.stringify(cache, null, 2));
|
|
180
|
+
} catch {
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// src/output/error.ts
|
|
185
|
+
var ActionableError = class extends Error {
|
|
186
|
+
suggestedCommand;
|
|
187
|
+
constructor(message, suggestedCommand) {
|
|
188
|
+
super(message);
|
|
189
|
+
this.name = "ActionableError";
|
|
190
|
+
this.suggestedCommand = suggestedCommand;
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
var KiwoomApiError = class extends Error {
|
|
194
|
+
returnCode;
|
|
195
|
+
returnMsg;
|
|
196
|
+
apiId;
|
|
197
|
+
constructor(returnCode, returnMsg, apiId) {
|
|
198
|
+
super(`[${returnCode}] ${returnMsg}`);
|
|
199
|
+
this.name = "KiwoomApiError";
|
|
200
|
+
this.returnCode = returnCode;
|
|
201
|
+
this.returnMsg = returnMsg;
|
|
202
|
+
this.apiId = apiId;
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
function handleError(err) {
|
|
206
|
+
if (err instanceof ActionableError) {
|
|
207
|
+
console.error(`
|
|
208
|
+
Error: ${err.message}`);
|
|
209
|
+
if (err.suggestedCommand) {
|
|
210
|
+
console.error(`
|
|
211
|
+
Try: ${err.suggestedCommand}`);
|
|
212
|
+
}
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
if (err instanceof KiwoomApiError) {
|
|
216
|
+
console.error(`
|
|
217
|
+
Error: ${err.returnMsg} (code ${err.returnCode})`);
|
|
218
|
+
if (isAuthError(err.returnMsg) || err.returnCode === 3) {
|
|
219
|
+
console.error(`
|
|
220
|
+
Try: kiwoom-cli config init`);
|
|
221
|
+
}
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
if (err instanceof Error) {
|
|
225
|
+
let message = err.message;
|
|
226
|
+
if (message.length > 500) {
|
|
227
|
+
message = message.slice(0, 500) + "...";
|
|
228
|
+
}
|
|
229
|
+
if (isAuthError(message)) {
|
|
230
|
+
console.error(`
|
|
231
|
+
Error: ${message}`);
|
|
232
|
+
console.error(`
|
|
233
|
+
Try: kiwoom-cli config init`);
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
console.error(`
|
|
237
|
+
Error: ${message}`);
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
console.error(`
|
|
241
|
+
Unknown error:`, err);
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
function isAuthError(message) {
|
|
245
|
+
return /unauthorized|forbidden|not authenticated|토큰|인증|appkey|access token|만료/i.test(
|
|
246
|
+
message
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// src/utils/helpers.ts
|
|
251
|
+
function parseIntStrict(value, name) {
|
|
252
|
+
const n = parseInt(value, 10);
|
|
253
|
+
if (Number.isNaN(n)) {
|
|
254
|
+
throw new Error(`Invalid ${name}: "${value}" is not a valid integer`);
|
|
255
|
+
}
|
|
256
|
+
return n;
|
|
257
|
+
}
|
|
258
|
+
function normalizeStockCode(code) {
|
|
259
|
+
const trimmed = code.trim().toUpperCase();
|
|
260
|
+
const stripped = trimmed.replace(/^A(?=\d)/, "");
|
|
261
|
+
const base = stripped.split("_")[0];
|
|
262
|
+
if (!/^\d{6}$/.test(base)) {
|
|
263
|
+
throw new ActionableError(
|
|
264
|
+
`Invalid stock code "${code}". Expected a 6-digit code like 005930 (\uC0BC\uC131\uC804\uC790).`
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
return base;
|
|
268
|
+
}
|
|
269
|
+
function parseKiwoomExpiry(expiresDt) {
|
|
270
|
+
const m = expiresDt.match(/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/);
|
|
271
|
+
if (!m) return NaN;
|
|
272
|
+
const [, y, mo, d, h, mi, s] = m;
|
|
273
|
+
return Date.parse(`${y}-${mo}-${d}T${h}:${mi}:${s}+09:00`);
|
|
274
|
+
}
|
|
275
|
+
function isTokenExpired(expiresDt, bufferMs = 0) {
|
|
276
|
+
const expiry = parseKiwoomExpiry(expiresDt);
|
|
277
|
+
if (Number.isNaN(expiry)) return true;
|
|
278
|
+
return Date.now() + bufferMs >= expiry;
|
|
279
|
+
}
|
|
280
|
+
function todayKst() {
|
|
281
|
+
const now = new Date(Date.now() + 9 * 3600 * 1e3);
|
|
282
|
+
return now.toISOString().slice(0, 10).replace(/-/g, "");
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// src/client/api-client.ts
|
|
286
|
+
function fetchWithTimeout(url, init) {
|
|
287
|
+
const controller = new AbortController();
|
|
288
|
+
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
289
|
+
return fetch(url, { ...init, signal: controller.signal }).finally(
|
|
290
|
+
() => clearTimeout(timeoutId)
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
var KiwoomClient = class {
|
|
294
|
+
env;
|
|
295
|
+
baseUrl;
|
|
296
|
+
appkey;
|
|
297
|
+
secretkey;
|
|
298
|
+
tokenOverride;
|
|
299
|
+
memoToken;
|
|
300
|
+
constructor(opts) {
|
|
301
|
+
this.env = opts.env;
|
|
302
|
+
this.baseUrl = BASE_URLS[opts.env];
|
|
303
|
+
if (!this.baseUrl) {
|
|
304
|
+
throw new Error(`Invalid environment: ${opts.env}. Use: real, mock`);
|
|
305
|
+
}
|
|
306
|
+
this.appkey = opts.appkey;
|
|
307
|
+
this.secretkey = opts.secretkey;
|
|
308
|
+
this.tokenOverride = opts.token;
|
|
309
|
+
}
|
|
310
|
+
getEnv() {
|
|
311
|
+
return this.env;
|
|
312
|
+
}
|
|
313
|
+
/** Ensure a valid token exists (issuing + caching as needed) and return it. */
|
|
314
|
+
async authenticate() {
|
|
315
|
+
return this.getToken();
|
|
316
|
+
}
|
|
317
|
+
// ─── OAuth2 ───────────────────────────────────────────────────────────────
|
|
318
|
+
/** Issue a fresh access token from the app key + secret key. */
|
|
319
|
+
async issueToken() {
|
|
320
|
+
if (!this.appkey || !this.secretkey) {
|
|
321
|
+
throw new ActionableError(
|
|
322
|
+
"App key and secret key are required to issue a token.",
|
|
323
|
+
"kiwoom-cli config init"
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
const res = await fetchWithTimeout(`${this.baseUrl}${TOKEN_PATH}`, {
|
|
327
|
+
method: "POST",
|
|
328
|
+
headers: { "Content-Type": "application/json;charset=UTF-8" },
|
|
329
|
+
body: JSON.stringify({
|
|
330
|
+
grant_type: "client_credentials",
|
|
331
|
+
appkey: this.appkey,
|
|
332
|
+
secretkey: this.secretkey
|
|
333
|
+
})
|
|
334
|
+
});
|
|
335
|
+
const body = await res.json().catch(() => ({}));
|
|
336
|
+
if (!res.ok || body.return_code !== 0 || !body.token) {
|
|
337
|
+
throw new KiwoomApiError(
|
|
338
|
+
body.return_code ?? res.status,
|
|
339
|
+
body.return_msg ?? `Token request failed (HTTP ${res.status})`
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
return { token: body.token, expiresDt: body.expires_dt };
|
|
343
|
+
}
|
|
344
|
+
/** Revoke an access token (defaults to the cached/override token). */
|
|
345
|
+
async revokeToken(token) {
|
|
346
|
+
if (!this.appkey || !this.secretkey) {
|
|
347
|
+
throw new ActionableError(
|
|
348
|
+
"App key and secret key are required to revoke a token.",
|
|
349
|
+
"kiwoom-cli config init"
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
const target = token ?? this.tokenOverride ?? getCachedToken(this.env)?.token;
|
|
353
|
+
if (!target) {
|
|
354
|
+
throw new ActionableError("No token to revoke.");
|
|
355
|
+
}
|
|
356
|
+
const res = await fetchWithTimeout(`${this.baseUrl}${REVOKE_PATH}`, {
|
|
357
|
+
method: "POST",
|
|
358
|
+
headers: { "Content-Type": "application/json;charset=UTF-8" },
|
|
359
|
+
body: JSON.stringify({
|
|
360
|
+
appkey: this.appkey,
|
|
361
|
+
secretkey: this.secretkey,
|
|
362
|
+
token: target
|
|
363
|
+
})
|
|
364
|
+
});
|
|
365
|
+
const body = await res.json().catch(() => ({}));
|
|
366
|
+
if (!res.ok || body.return_code !== 0) {
|
|
367
|
+
throw new KiwoomApiError(
|
|
368
|
+
body.return_code ?? res.status,
|
|
369
|
+
body.return_msg ?? `Token revoke failed (HTTP ${res.status})`
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
clearCachedToken(this.env);
|
|
373
|
+
return body;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Return a valid token, reusing the cache when possible and issuing+caching a
|
|
377
|
+
* new one when missing or near expiry.
|
|
378
|
+
*/
|
|
379
|
+
async getToken() {
|
|
380
|
+
if (this.tokenOverride) return this.tokenOverride;
|
|
381
|
+
if (this.memoToken) return this.memoToken;
|
|
382
|
+
const cached = getCachedToken(this.env);
|
|
383
|
+
const hint = this.appkey?.slice(0, 6);
|
|
384
|
+
if (cached && cached.appkeyHint === hint && !isTokenExpired(cached.expiresDt, TOKEN_REFRESH_BUFFER_MS)) {
|
|
385
|
+
this.memoToken = cached.token;
|
|
386
|
+
return cached.token;
|
|
387
|
+
}
|
|
388
|
+
const { token, expiresDt } = await this.issueToken();
|
|
389
|
+
this.memoToken = token;
|
|
390
|
+
if (hint) {
|
|
391
|
+
saveCachedToken(this.env, { token, expiresDt, appkeyHint: hint });
|
|
392
|
+
}
|
|
393
|
+
return token;
|
|
394
|
+
}
|
|
395
|
+
// ─── Generic TR request ──────────────────────────────────────────────────
|
|
396
|
+
/**
|
|
397
|
+
* Execute a TR. `apiId` is sent in the api-id header; `path` is the category
|
|
398
|
+
* route (e.g. /api/dostk/stkinfo). Returns the parsed body plus pagination.
|
|
399
|
+
*/
|
|
400
|
+
async request(apiId, path2, body = {}, options = {}) {
|
|
401
|
+
const send = async (token) => fetchWithTimeout(`${this.baseUrl}${path2}`, {
|
|
402
|
+
method: "POST",
|
|
403
|
+
headers: {
|
|
404
|
+
"Content-Type": "application/json;charset=UTF-8",
|
|
405
|
+
authorization: `Bearer ${token}`,
|
|
406
|
+
"api-id": options.apiId ?? apiId,
|
|
407
|
+
"cont-yn": options.contYn ?? "N",
|
|
408
|
+
"next-key": options.nextKey ?? ""
|
|
409
|
+
},
|
|
410
|
+
body: JSON.stringify(body)
|
|
411
|
+
});
|
|
412
|
+
let res = await send(await this.getToken());
|
|
413
|
+
if (res.status === 401 && !this.tokenOverride) {
|
|
414
|
+
clearCachedToken(this.env);
|
|
415
|
+
this.memoToken = void 0;
|
|
416
|
+
res = await send(await this.getToken());
|
|
417
|
+
}
|
|
418
|
+
const payload = await res.json().catch(() => ({}));
|
|
419
|
+
if (!res.ok) {
|
|
420
|
+
throw new KiwoomApiError(
|
|
421
|
+
payload.return_code ?? res.status,
|
|
422
|
+
payload.return_msg ?? `Request failed (HTTP ${res.status})`,
|
|
423
|
+
apiId
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
if (typeof payload.return_code === "number" && payload.return_code !== 0 && !SOFT_EMPTY_RETURN_CODES.has(payload.return_code)) {
|
|
427
|
+
throw new KiwoomApiError(payload.return_code, payload.return_msg ?? "Error", apiId);
|
|
428
|
+
}
|
|
429
|
+
return {
|
|
430
|
+
data: payload,
|
|
431
|
+
contYn: res.headers.get("cont-yn") === "Y",
|
|
432
|
+
nextKey: res.headers.get("next-key") ?? ""
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Execute an endpoint from the registry. When `paginate` is set and the
|
|
437
|
+
* endpoint declares a listKey, all pages are fetched and concatenated.
|
|
438
|
+
*/
|
|
439
|
+
async callEndpoint(def, body = {}, opts = {}) {
|
|
440
|
+
if (opts.paginate && def.listKey) {
|
|
441
|
+
const data = await this.requestAll(def.apiId, def.path, body, def.listKey);
|
|
442
|
+
return { data, contYn: false, nextKey: "" };
|
|
443
|
+
}
|
|
444
|
+
return this.request(def.apiId, def.path, body, {
|
|
445
|
+
contYn: opts.contYn,
|
|
446
|
+
nextKey: opts.nextKey
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Fetch all pages of a TR, concatenating the array under `listKey`.
|
|
451
|
+
* Caps at `maxPages` to avoid runaway loops.
|
|
452
|
+
*/
|
|
453
|
+
async requestAll(apiId, path2, body, listKey, maxPages = 20) {
|
|
454
|
+
let page = await this.request(apiId, path2, body);
|
|
455
|
+
const acc = Array.isArray(page.data[listKey]) ? [...page.data[listKey]] : [];
|
|
456
|
+
let pages = 1;
|
|
457
|
+
while (page.contYn && page.nextKey && pages < maxPages) {
|
|
458
|
+
page = await this.request(apiId, path2, body, {
|
|
459
|
+
contYn: "Y",
|
|
460
|
+
nextKey: page.nextKey
|
|
461
|
+
});
|
|
462
|
+
if (Array.isArray(page.data[listKey])) acc.push(...page.data[listKey]);
|
|
463
|
+
pages += 1;
|
|
464
|
+
}
|
|
465
|
+
return { ...page.data, [listKey]: acc };
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
// src/output/formatter.ts
|
|
470
|
+
function getOutputFormat(options) {
|
|
471
|
+
return options.output === "json" ? "json" : "table";
|
|
472
|
+
}
|
|
473
|
+
function output(data, format = "table") {
|
|
474
|
+
if (format === "json") {
|
|
475
|
+
console.log(JSON.stringify(data, null, 2));
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
if (Array.isArray(data)) {
|
|
479
|
+
if (data.length === 0) {
|
|
480
|
+
console.log("No data");
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
if (typeof data[0] === "object" && data[0] !== null) {
|
|
484
|
+
console.table(data);
|
|
485
|
+
} else {
|
|
486
|
+
for (const item of data) console.log(` ${String(item)}`);
|
|
487
|
+
}
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
if (data !== null && typeof data === "object") {
|
|
491
|
+
const entries = Object.entries(data);
|
|
492
|
+
if (entries.length === 0) {
|
|
493
|
+
console.log("No data");
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
const maxKeyLen = Math.max(...entries.map(([k]) => k.length));
|
|
497
|
+
for (const [key, value] of entries) {
|
|
498
|
+
const displayValue = value !== null && typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
499
|
+
console.log(` ${key.padEnd(maxKeyLen + 2)} ${displayValue}`);
|
|
500
|
+
}
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
console.log(String(data));
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// src/commands/config.ts
|
|
507
|
+
function prompt(question, hidden = false) {
|
|
508
|
+
return new Promise((resolve) => {
|
|
509
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
510
|
+
if (hidden) {
|
|
511
|
+
process.stdout.write(question);
|
|
512
|
+
const stdin = process.stdin;
|
|
513
|
+
const wasRaw = stdin.isRaw;
|
|
514
|
+
if (stdin.isTTY) stdin.setRawMode(true);
|
|
515
|
+
let input = "";
|
|
516
|
+
const onData = (char) => {
|
|
517
|
+
const c = char.toString();
|
|
518
|
+
const code = char[0];
|
|
519
|
+
if (c === "\n" || c === "\r") {
|
|
520
|
+
stdin.removeListener("data", onData);
|
|
521
|
+
if (stdin.isTTY) stdin.setRawMode(wasRaw ?? false);
|
|
522
|
+
process.stdout.write("\n");
|
|
523
|
+
rl.close();
|
|
524
|
+
resolve(input);
|
|
525
|
+
} else if (code === 3) {
|
|
526
|
+
process.exit(0);
|
|
527
|
+
} else if (code === 127 || code === 8) {
|
|
528
|
+
if (input.length > 0) {
|
|
529
|
+
input = input.slice(0, -1);
|
|
530
|
+
process.stdout.write("\b \b");
|
|
531
|
+
}
|
|
532
|
+
} else {
|
|
533
|
+
input += c;
|
|
534
|
+
process.stdout.write("*");
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
stdin.on("data", onData);
|
|
538
|
+
} else {
|
|
539
|
+
rl.question(question, (answer) => {
|
|
540
|
+
rl.close();
|
|
541
|
+
resolve(answer.trim());
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
async function verifyAndCacheToken(env, appkey, secretkey) {
|
|
547
|
+
try {
|
|
548
|
+
const client = new KiwoomClient({ env, appkey, secretkey });
|
|
549
|
+
await client.authenticate();
|
|
550
|
+
console.log("Access token issued and cached.");
|
|
551
|
+
} catch (err) {
|
|
552
|
+
console.error(
|
|
553
|
+
`
|
|
554
|
+
Keys saved, but token issuance failed: ${err instanceof Error ? err.message : String(err)}`
|
|
555
|
+
);
|
|
556
|
+
console.error("Double-check the app key / secret key and environment.");
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
function registerConfigCommands(program2) {
|
|
560
|
+
const configCmd = program2.command("config").description("Manage CLI configuration (keys, environment)");
|
|
561
|
+
configCmd.command("init").description("Interactive setup wizard").action(async () => {
|
|
562
|
+
try {
|
|
563
|
+
console.log("Kiwoom CLI Setup\n");
|
|
564
|
+
const envInput = await prompt("Environment (real/mock) [real]: ");
|
|
565
|
+
const env = ENV_ALIASES[envInput.toLowerCase()] ?? "real";
|
|
566
|
+
const appkey = await prompt("App key: ");
|
|
567
|
+
if (!appkey) {
|
|
568
|
+
console.error("App key is required.");
|
|
569
|
+
process.exit(1);
|
|
570
|
+
}
|
|
571
|
+
const secretkey = await prompt("Secret key: ", true);
|
|
572
|
+
if (!secretkey) {
|
|
573
|
+
console.error("Secret key is required.");
|
|
574
|
+
process.exit(1);
|
|
575
|
+
}
|
|
576
|
+
saveConfig({ env, appkey, secretkey });
|
|
577
|
+
console.log("\nConfiguration saved to ~/.kiwoom-cli/config.json");
|
|
578
|
+
if (env === "real") {
|
|
579
|
+
console.log('NOTE: "real" places live orders with real money. Use "mock" to practice.');
|
|
580
|
+
}
|
|
581
|
+
await verifyAndCacheToken(env, appkey, secretkey);
|
|
582
|
+
} catch (err) {
|
|
583
|
+
handleError(err);
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
configCmd.command("set").description("Set configuration values").option("--env <environment>", "Environment (real/mock)").option("--appkey <key>", "Kiwoom app key").option("--secretkey <key>", "Kiwoom secret key").action(async (options) => {
|
|
587
|
+
try {
|
|
588
|
+
const updates = {};
|
|
589
|
+
if (options.env) {
|
|
590
|
+
const resolved = ENV_ALIASES[options.env.toLowerCase()];
|
|
591
|
+
if (!resolved) {
|
|
592
|
+
console.error(`Unknown environment: ${options.env}. Use: real, mock`);
|
|
593
|
+
process.exit(1);
|
|
594
|
+
}
|
|
595
|
+
updates.env = resolved;
|
|
596
|
+
}
|
|
597
|
+
if (options.appkey) updates.appkey = options.appkey;
|
|
598
|
+
if (options.secretkey) updates.secretkey = options.secretkey;
|
|
599
|
+
if (Object.keys(updates).length === 0) {
|
|
600
|
+
console.log("No values to set. Use --env, --appkey, or --secretkey");
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
saveConfig(updates);
|
|
604
|
+
console.log("Configuration updated.");
|
|
605
|
+
const cfg = getEffectiveConfig();
|
|
606
|
+
if ((updates.appkey || updates.secretkey) && cfg.appkey && cfg.secretkey) {
|
|
607
|
+
await verifyAndCacheToken(cfg.env, cfg.appkey, cfg.secretkey);
|
|
608
|
+
}
|
|
609
|
+
} catch (err) {
|
|
610
|
+
handleError(err);
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
configCmd.command("get <key>").description("Get a configuration value (env, appkey, secretkey)").action((key) => {
|
|
614
|
+
try {
|
|
615
|
+
const config = getEffectiveConfig();
|
|
616
|
+
const value = config[key];
|
|
617
|
+
if (value === void 0) {
|
|
618
|
+
console.log(`Key "${key}" not found. Available: env, appkey, secretkey`);
|
|
619
|
+
} else if (key === "secretkey" || key === "appkey") {
|
|
620
|
+
console.log(maskSecret(value));
|
|
621
|
+
} else {
|
|
622
|
+
console.log(String(value));
|
|
623
|
+
}
|
|
624
|
+
} catch (err) {
|
|
625
|
+
handleError(err);
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
configCmd.command("list").description("Show all configuration").option("-o, --output <format>", "Output format (table/json)", "table").action((options) => {
|
|
629
|
+
try {
|
|
630
|
+
const config = getEffectiveConfig();
|
|
631
|
+
output(
|
|
632
|
+
{
|
|
633
|
+
env: config.env,
|
|
634
|
+
appkey: maskSecret(config.appkey),
|
|
635
|
+
secretkey: maskSecret(config.secretkey)
|
|
636
|
+
},
|
|
637
|
+
getOutputFormat(options)
|
|
638
|
+
);
|
|
639
|
+
} catch (err) {
|
|
640
|
+
handleError(err);
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// src/commands/_helpers.ts
|
|
646
|
+
function createClient() {
|
|
647
|
+
const config = getEffectiveConfig();
|
|
648
|
+
if (!config.appkey || !config.secretkey) {
|
|
649
|
+
throw new ActionableError(
|
|
650
|
+
"App key / secret key are not configured.",
|
|
651
|
+
"kiwoom-cli config init"
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
return new KiwoomClient({
|
|
655
|
+
env: config.env,
|
|
656
|
+
appkey: config.appkey,
|
|
657
|
+
secretkey: config.secretkey
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// src/utils/format.ts
|
|
662
|
+
var NUMERIC_RE = /^([+-]*)0*(\d+)(\.\d+)?$/;
|
|
663
|
+
function unpad(value) {
|
|
664
|
+
if (value === null || value === void 0) return "";
|
|
665
|
+
const str = String(value).trim();
|
|
666
|
+
const m = str.match(NUMERIC_RE);
|
|
667
|
+
if (!m) return str;
|
|
668
|
+
const sign = m[1].includes("-") ? "-" : m[1].includes("+") ? "+" : "";
|
|
669
|
+
const intPart = m[2].replace(/^0+(?=\d)/, "");
|
|
670
|
+
return `${sign}${intPart}${m[3] ?? ""}`;
|
|
671
|
+
}
|
|
672
|
+
function toNumber(value) {
|
|
673
|
+
if (value === null || value === void 0 || value === "") return NaN;
|
|
674
|
+
const cleaned = unpad(value).replace(/^\+/, "");
|
|
675
|
+
if (cleaned === "" || cleaned === "-" || cleaned === "+") return NaN;
|
|
676
|
+
const n = Number(cleaned);
|
|
677
|
+
return Number.isNaN(n) ? NaN : n;
|
|
678
|
+
}
|
|
679
|
+
function withCommas(value) {
|
|
680
|
+
const m = value.match(/^([+-]?)(\d+)(\.\d+)?$/);
|
|
681
|
+
if (!m) return value;
|
|
682
|
+
const grouped = m[2].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
683
|
+
return `${m[1]}${grouped}${m[3] ?? ""}`;
|
|
684
|
+
}
|
|
685
|
+
function won(value) {
|
|
686
|
+
return withCommas(unpad(value));
|
|
687
|
+
}
|
|
688
|
+
function price(value) {
|
|
689
|
+
return withCommas(unpad(value).replace(/^[+-]/, ""));
|
|
690
|
+
}
|
|
691
|
+
function formatStamp(value) {
|
|
692
|
+
if (!value) return "";
|
|
693
|
+
const s = String(value).trim();
|
|
694
|
+
if (/^\d{14}$/.test(s)) {
|
|
695
|
+
return `${s.slice(0, 4)}-${s.slice(4, 6)}-${s.slice(6, 8)} ${s.slice(8, 10)}:${s.slice(10, 12)}:${s.slice(12, 14)}`;
|
|
696
|
+
}
|
|
697
|
+
if (/^\d{8}$/.test(s)) {
|
|
698
|
+
return `${s.slice(0, 4)}-${s.slice(4, 6)}-${s.slice(6, 8)}`;
|
|
699
|
+
}
|
|
700
|
+
if (/^\d{6}$/.test(s)) {
|
|
701
|
+
return `${s.slice(0, 2)}:${s.slice(2, 4)}:${s.slice(4, 6)}`;
|
|
702
|
+
}
|
|
703
|
+
return s;
|
|
704
|
+
}
|
|
705
|
+
function formatFields(row, formatters) {
|
|
706
|
+
const out = { ...row };
|
|
707
|
+
for (const [key, fn] of Object.entries(formatters)) {
|
|
708
|
+
if (fn && out[key] !== void 0 && out[key] !== null) {
|
|
709
|
+
out[key] = fn(String(out[key]));
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
return out;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// src/commands/auth.ts
|
|
716
|
+
function registerAuthCommands(program2) {
|
|
717
|
+
const authCmd = program2.command("auth").description("Access-token management (OAuth2)");
|
|
718
|
+
authCmd.command("token").description("Issue (or reuse) an access token and cache it").option("--force", "Force a fresh token even if a valid one is cached").option("-o, --output <format>", "Output format (table/json)", "table").action(async (options) => {
|
|
719
|
+
try {
|
|
720
|
+
const cfg = getEffectiveConfig();
|
|
721
|
+
const client = createClient();
|
|
722
|
+
if (options.force) clearCachedToken(cfg.env);
|
|
723
|
+
await client.authenticate();
|
|
724
|
+
const cached = getCachedToken(cfg.env);
|
|
725
|
+
output(
|
|
726
|
+
{
|
|
727
|
+
env: cfg.env,
|
|
728
|
+
token: maskSecret(cached?.token),
|
|
729
|
+
expiresAt: formatStamp(cached?.expiresDt),
|
|
730
|
+
valid: cached ? !isTokenExpired(cached.expiresDt) : false
|
|
731
|
+
},
|
|
732
|
+
getOutputFormat(options)
|
|
733
|
+
);
|
|
734
|
+
} catch (err) {
|
|
735
|
+
handleError(err);
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
authCmd.command("status").description("Show the cached token status").option("-o, --output <format>", "Output format (table/json)", "table").action((options) => {
|
|
739
|
+
try {
|
|
740
|
+
const cfg = getEffectiveConfig();
|
|
741
|
+
const cached = getCachedToken(cfg.env);
|
|
742
|
+
if (!cached) {
|
|
743
|
+
output({ env: cfg.env, cached: false }, getOutputFormat(options));
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
output(
|
|
747
|
+
{
|
|
748
|
+
env: cfg.env,
|
|
749
|
+
cached: true,
|
|
750
|
+
token: maskSecret(cached.token),
|
|
751
|
+
expiresAt: formatStamp(cached.expiresDt),
|
|
752
|
+
valid: !isTokenExpired(cached.expiresDt)
|
|
753
|
+
},
|
|
754
|
+
getOutputFormat(options)
|
|
755
|
+
);
|
|
756
|
+
} catch (err) {
|
|
757
|
+
handleError(err);
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
authCmd.command("revoke").description("Revoke the cached access token").action(async () => {
|
|
761
|
+
try {
|
|
762
|
+
const cfg = getEffectiveConfig();
|
|
763
|
+
const cached = getCachedToken(cfg.env);
|
|
764
|
+
if (!cached) {
|
|
765
|
+
throw new ActionableError("No cached token to revoke.", "kiwoom-cli auth token");
|
|
766
|
+
}
|
|
767
|
+
const client = createClient();
|
|
768
|
+
await client.revokeToken(cached.token);
|
|
769
|
+
console.log("Token revoked and cache cleared.");
|
|
770
|
+
} catch (err) {
|
|
771
|
+
handleError(err);
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// src/client/endpoints.ts
|
|
777
|
+
var PATHS = {
|
|
778
|
+
stkinfo: "/api/dostk/stkinfo",
|
|
779
|
+
mrkcond: "/api/dostk/mrkcond",
|
|
780
|
+
chart: "/api/dostk/chart",
|
|
781
|
+
acnt: "/api/dostk/acnt",
|
|
782
|
+
ordr: "/api/dostk/ordr",
|
|
783
|
+
crdordr: "/api/dostk/crdordr",
|
|
784
|
+
rkinfo: "/api/dostk/rkinfo",
|
|
785
|
+
sect: "/api/dostk/sect"
|
|
786
|
+
};
|
|
787
|
+
var ENDPOINTS = {
|
|
788
|
+
// ── Stock info (종목정보) ──────────────────────────────────────────────────
|
|
789
|
+
stockInfo: { apiId: "ka10001", path: PATHS.stkinfo, korean: "\uC8FC\uC2DD\uAE30\uBCF8\uC815\uBCF4\uC694\uCCAD" },
|
|
790
|
+
tradingMembers: { apiId: "ka10002", path: PATHS.stkinfo, korean: "\uC8FC\uC2DD\uAC70\uB798\uC6D0\uC694\uCCAD" },
|
|
791
|
+
stockTrades: { apiId: "ka10003", path: PATHS.stkinfo, korean: "\uCCB4\uACB0\uC815\uBCF4\uC694\uCCAD", listKey: "cntr_infr" },
|
|
792
|
+
watchlist: { apiId: "ka10095", path: PATHS.stkinfo, korean: "\uAD00\uC2EC\uC885\uBAA9\uC815\uBCF4\uC694\uCCAD", listKey: "atn_stk_infr" },
|
|
793
|
+
stockList: { apiId: "ka10099", path: PATHS.stkinfo, korean: "\uC885\uBAA9\uC815\uBCF4 \uB9AC\uC2A4\uD2B8", listKey: "list" },
|
|
794
|
+
stockInfoSingle: { apiId: "ka10100", path: PATHS.stkinfo, korean: "\uC885\uBAA9\uC815\uBCF4 \uC870\uD68C" },
|
|
795
|
+
sectorCodeList: { apiId: "ka10101", path: PATHS.stkinfo, korean: "\uC5C5\uC885\uCF54\uB4DC \uB9AC\uC2A4\uD2B8", listKey: "list" },
|
|
796
|
+
creditTrend: { apiId: "ka10013", path: PATHS.stkinfo, korean: "\uC2E0\uC6A9\uB9E4\uB9E4\uB3D9\uD5A5\uC694\uCCAD", listKey: "crd_trde_trend" },
|
|
797
|
+
// ── Market quote (시세) ────────────────────────────────────────────────────
|
|
798
|
+
orderbook: { apiId: "ka10004", path: PATHS.mrkcond, korean: "\uC8FC\uC2DD\uD638\uAC00\uC694\uCCAD" },
|
|
799
|
+
quoteSnapshot: { apiId: "ka10006", path: PATHS.mrkcond, korean: "\uC8FC\uC2DD\uC2DC\uBD84\uC694\uCCAD" },
|
|
800
|
+
priceTableInfo: { apiId: "ka10007", path: PATHS.mrkcond, korean: "\uC2DC\uC138\uD45C\uC131\uC815\uBCF4\uC694\uCCAD" },
|
|
801
|
+
dailyPrice: { apiId: "ka10086", path: PATHS.mrkcond, korean: "\uC77C\uBCC4\uC8FC\uAC00\uC694\uCCAD", listKey: "daly_stkpc" },
|
|
802
|
+
instTradedStocks: { apiId: "ka10044", path: PATHS.mrkcond, korean: "\uC77C\uBCC4\uAE30\uAD00\uB9E4\uB9E4\uC885\uBAA9\uC694\uCCAD", listKey: "daly_orgn_trde_stk" },
|
|
803
|
+
instForeignTrend: { apiId: "ka10045", path: PATHS.mrkcond, korean: "\uC885\uBAA9\uBCC4\uAE30\uAD00\uB9E4\uB9E4\uCD94\uC774\uC694\uCCAD", listKey: "stk_orgn_trde_trnsn" },
|
|
804
|
+
strengthByTime: { apiId: "ka10046", path: PATHS.mrkcond, korean: "\uCCB4\uACB0\uAC15\uB3C4\uC2DC\uAC04\uBCC4\uC694\uCCAD", listKey: "cntr_str_tm" },
|
|
805
|
+
strengthByDay: { apiId: "ka10047", path: PATHS.mrkcond, korean: "\uCCB4\uACB0\uAC15\uB3C4\uC77C\uBCC4\uC694\uCCAD", listKey: "cntr_str_daly" },
|
|
806
|
+
afterHoursOrderbook: { apiId: "ka10087", path: PATHS.mrkcond, korean: "\uC2DC\uAC04\uC678\uB2E8\uC77C\uAC00\uC694\uCCAD" },
|
|
807
|
+
// ── Chart (차트) ───────────────────────────────────────────────────────────
|
|
808
|
+
tickChart: { apiId: "ka10079", path: PATHS.chart, korean: "\uC8FC\uC2DD\uD2F1\uCC28\uD2B8\uC870\uD68C", listKey: "stk_tic_chart_qry" },
|
|
809
|
+
minuteChart: { apiId: "ka10080", path: PATHS.chart, korean: "\uC8FC\uC2DD\uBD84\uBD09\uCC28\uD2B8\uC870\uD68C", listKey: "stk_min_pole_chart_qry" },
|
|
810
|
+
dailyChart: { apiId: "ka10081", path: PATHS.chart, korean: "\uC8FC\uC2DD\uC77C\uBD09\uCC28\uD2B8\uC870\uD68C", listKey: "stk_dt_pole_chart_qry" },
|
|
811
|
+
// NOTE: weekly list key is the doubled "stk_stk_..." — verified live, not a typo.
|
|
812
|
+
weeklyChart: { apiId: "ka10082", path: PATHS.chart, korean: "\uC8FC\uC2DD\uC8FC\uBD09\uCC28\uD2B8\uC870\uD68C", listKey: "stk_stk_pole_chart_qry" },
|
|
813
|
+
monthlyChart: { apiId: "ka10083", path: PATHS.chart, korean: "\uC8FC\uC2DD\uC6D4\uBD09\uCC28\uD2B8\uC870\uD68C", listKey: "stk_mth_pole_chart_qry" },
|
|
814
|
+
yearlyChart: { apiId: "ka10094", path: PATHS.chart, korean: "\uC8FC\uC2DD\uB144\uBD09\uCC28\uD2B8\uC870\uD68C", listKey: "stk_yr_pole_chart_qry" },
|
|
815
|
+
// ── Account (계좌) ─────────────────────────────────────────────────────────
|
|
816
|
+
balance: { apiId: "kt00018", path: PATHS.acnt, korean: "\uACC4\uC88C\uD3C9\uAC00\uC794\uACE0\uB0B4\uC5ED\uC694\uCCAD", listKey: "acnt_evlt_remn_indv_tot" },
|
|
817
|
+
deposit: { apiId: "kt00001", path: PATHS.acnt, korean: "\uC608\uC218\uAE08\uC0C1\uC138\uD604\uD669\uC694\uCCAD", listKey: "stk_entr_prst" },
|
|
818
|
+
evalStatus: { apiId: "kt00004", path: PATHS.acnt, korean: "\uACC4\uC88C\uD3C9\uAC00\uD604\uD669\uC694\uCCAD", listKey: "stk_acnt_evlt_prst" },
|
|
819
|
+
settledBalance: { apiId: "kt00005", path: PATHS.acnt, korean: "\uCCB4\uACB0\uC794\uACE0\uC694\uCCAD", listKey: "stk_cntr_remn" },
|
|
820
|
+
orderDetail: { apiId: "kt00007", path: PATHS.acnt, korean: "\uACC4\uC88C\uBCC4\uC8FC\uBB38\uCCB4\uACB0\uB0B4\uC5ED\uC0C1\uC138\uC694\uCCAD", listKey: "acnt_ord_cntr_prps_dtl" },
|
|
821
|
+
orderStatus: { apiId: "kt00009", path: PATHS.acnt, korean: "\uACC4\uC88C\uBCC4\uC8FC\uBB38\uCCB4\uACB0\uD604\uD669\uC694\uCCAD", listKey: "acnt_ord_cntr_prst" },
|
|
822
|
+
openOrders: { apiId: "ka10075", path: PATHS.acnt, korean: "\uBBF8\uCCB4\uACB0\uC694\uCCAD", listKey: "oso" },
|
|
823
|
+
executions: { apiId: "ka10076", path: PATHS.acnt, korean: "\uCCB4\uACB0\uC694\uCCAD", listKey: "cntr" },
|
|
824
|
+
realizedPlByDate: { apiId: "ka10072", path: PATHS.acnt, korean: "\uC77C\uC790\uBCC4\uC885\uBAA9\uBCC4\uC2E4\uD604\uC190\uC775\uC694\uCCAD_\uC77C\uC790", listKey: "dt_stk_div_rlzt_pl" },
|
|
825
|
+
realizedPlByPeriod: { apiId: "ka10073", path: PATHS.acnt, korean: "\uC77C\uC790\uBCC4\uC885\uBAA9\uBCC4\uC2E4\uD604\uC190\uC775\uC694\uCCAD_\uAE30\uAC04", listKey: "dt_stk_rlzt_pl" },
|
|
826
|
+
tradeJournal: { apiId: "ka10170", path: PATHS.acnt, korean: "\uB2F9\uC77C\uB9E4\uB9E4\uC77C\uC9C0\uC694\uCCAD", listKey: "tdy_trde_diary" },
|
|
827
|
+
dailyReturn: { apiId: "kt00016", path: PATHS.acnt, korean: "\uC77C\uBCC4\uACC4\uC88C\uC218\uC775\uB960\uC0C1\uC138\uD604\uD669\uC694\uCCAD" },
|
|
828
|
+
// ── Order (주문) — WRITE, real money ──────────────────────────────────────
|
|
829
|
+
buy: { apiId: "kt10000", path: PATHS.ordr, korean: "\uC8FC\uC2DD \uB9E4\uC218\uC8FC\uBB38", isWrite: true },
|
|
830
|
+
sell: { apiId: "kt10001", path: PATHS.ordr, korean: "\uC8FC\uC2DD \uB9E4\uB3C4\uC8FC\uBB38", isWrite: true },
|
|
831
|
+
modify: { apiId: "kt10002", path: PATHS.ordr, korean: "\uC8FC\uC2DD \uC815\uC815\uC8FC\uBB38", isWrite: true },
|
|
832
|
+
cancel: { apiId: "kt10003", path: PATHS.ordr, korean: "\uC8FC\uC2DD \uCDE8\uC18C\uC8FC\uBB38", isWrite: true },
|
|
833
|
+
creditBuy: { apiId: "kt10006", path: PATHS.crdordr, korean: "\uC2E0\uC6A9 \uB9E4\uC218\uC8FC\uBB38", isWrite: true },
|
|
834
|
+
creditSell: { apiId: "kt10007", path: PATHS.crdordr, korean: "\uC2E0\uC6A9 \uB9E4\uB3C4\uC8FC\uBB38", isWrite: true },
|
|
835
|
+
creditModify: { apiId: "kt10008", path: PATHS.crdordr, korean: "\uC2E0\uC6A9 \uC815\uC815\uC8FC\uBB38", isWrite: true },
|
|
836
|
+
creditCancel: { apiId: "kt10009", path: PATHS.crdordr, korean: "\uC2E0\uC6A9 \uCDE8\uC18C\uC8FC\uBB38", isWrite: true },
|
|
837
|
+
// ── Ranking (순위정보) ─────────────────────────────────────────────────────
|
|
838
|
+
rankFluctuation: { apiId: "ka10027", path: PATHS.rkinfo, korean: "\uC804\uC77C\uB300\uBE44\uB4F1\uB77D\uB960\uC21C\uC704\uC694\uCCAD", listKey: "pred_pre_flu_rt_upper" },
|
|
839
|
+
rankVolume: { apiId: "ka10030", path: PATHS.rkinfo, korean: "\uB2F9\uC77C\uAC70\uB798\uB7C9\uC0C1\uC704\uC694\uCCAD", listKey: "tdy_trde_qty_upper" },
|
|
840
|
+
rankTradeAmount: { apiId: "ka10032", path: PATHS.rkinfo, korean: "\uAC70\uB798\uB300\uAE08\uC0C1\uC704\uC694\uCCAD", listKey: "trde_prica_upper" },
|
|
841
|
+
rankVolumeSurge: { apiId: "ka10023", path: PATHS.rkinfo, korean: "\uAC70\uB798\uB7C9\uAE09\uC99D\uC694\uCCAD", listKey: "trde_qty_sdnin" },
|
|
842
|
+
rankPrevVolume: { apiId: "ka10031", path: PATHS.rkinfo, korean: "\uC804\uC77C\uAC70\uB798\uB7C9\uC0C1\uC704\uC694\uCCAD", listKey: "pred_trde_qty_upper" },
|
|
843
|
+
// ── Sector / industry (업종) ───────────────────────────────────────────────
|
|
844
|
+
sectorPrice: { apiId: "ka20001", path: PATHS.sect, korean: "\uC5C5\uC885\uD604\uC7AC\uAC00\uC694\uCCAD", listKey: "inds_cur_prc_tm" },
|
|
845
|
+
sectorStocks: { apiId: "ka20002", path: PATHS.sect, korean: "\uC5C5\uC885\uBCC4\uC8FC\uAC00\uC694\uCCAD", listKey: "inds_stkpc" },
|
|
846
|
+
sectorAllIndex: { apiId: "ka20003", path: PATHS.sect, korean: "\uC804\uC5C5\uC885\uC9C0\uC218\uC694\uCCAD", listKey: "all_inds_idex" },
|
|
847
|
+
sectorDaily: { apiId: "ka20009", path: PATHS.sect, korean: "\uC5C5\uC885\uD604\uC7AC\uAC00 \uC77C\uBCC4\uC694\uCCAD", listKey: "inds_cur_prc_daly_rept" }
|
|
848
|
+
};
|
|
849
|
+
|
|
850
|
+
// src/utils/orderbook.ts
|
|
851
|
+
function parseOrderbook(d) {
|
|
852
|
+
const asks = [];
|
|
853
|
+
for (let n = 10; n >= 2; n--) {
|
|
854
|
+
asks.push({
|
|
855
|
+
level: n,
|
|
856
|
+
price: unpad(d[`sel_${n}th_pre_bid`]),
|
|
857
|
+
qty: unpad(d[`sel_${n}th_pre_req`])
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
asks.push({ level: 1, price: unpad(d.sel_fpr_bid), qty: unpad(d.sel_fpr_req) });
|
|
861
|
+
const bids = [];
|
|
862
|
+
bids.push({ level: 1, price: unpad(d.buy_fpr_bid), qty: unpad(d.buy_fpr_req) });
|
|
863
|
+
for (let n = 2; n <= 10; n++) {
|
|
864
|
+
bids.push({
|
|
865
|
+
level: n,
|
|
866
|
+
price: unpad(d[`buy_${n}th_pre_bid`]),
|
|
867
|
+
qty: unpad(d[`buy_${n}th_pre_req`])
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
return {
|
|
871
|
+
baseTime: formatStamp(d.bid_req_base_tm),
|
|
872
|
+
asks,
|
|
873
|
+
bids,
|
|
874
|
+
totalAskQty: unpad(d.tot_sel_req),
|
|
875
|
+
totalBidQty: unpad(d.tot_buy_req)
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
function renderOrderbook(book, title) {
|
|
879
|
+
const lines = [];
|
|
880
|
+
lines.push(`
|
|
881
|
+
Order Book: ${title} (${book.baseTime})
|
|
882
|
+
`);
|
|
883
|
+
lines.push(" \u2500\u2500 Asks \u2500\u2500");
|
|
884
|
+
for (const a of book.asks) {
|
|
885
|
+
lines.push(` ${a.price.padStart(12)} ${a.qty.padStart(12)}`);
|
|
886
|
+
}
|
|
887
|
+
lines.push(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
888
|
+
for (const b of book.bids) {
|
|
889
|
+
lines.push(` ${b.price.padStart(12)} ${b.qty.padStart(12)}`);
|
|
890
|
+
}
|
|
891
|
+
lines.push(" \u2500\u2500 Bids \u2500\u2500");
|
|
892
|
+
lines.push(`
|
|
893
|
+
Total ask qty: ${book.totalAskQty} Total bid qty: ${book.totalBidQty}
|
|
894
|
+
`);
|
|
895
|
+
return lines.join("\n");
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// src/commands/market.ts
|
|
899
|
+
function registerMarketCommands(program2) {
|
|
900
|
+
const market = program2.command("market").description("Market data \u2014 prices, quotes, order book, trades");
|
|
901
|
+
market.command("price <code>").description("Current price snapshot (ka10007)").option("-o, --output <format>", "Output format (table/json)", "table").action(async (code, options) => {
|
|
902
|
+
try {
|
|
903
|
+
const client = createClient();
|
|
904
|
+
const stk = normalizeStockCode(code);
|
|
905
|
+
const { data } = await client.callEndpoint(ENDPOINTS.priceTableInfo, { stk_cd: stk });
|
|
906
|
+
const fmt = getOutputFormat(options);
|
|
907
|
+
if (fmt === "json") {
|
|
908
|
+
output(data, "json");
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
const chg = Math.abs(toNumber(data.cur_prc)) - Math.abs(toNumber(data.pred_close_pric));
|
|
912
|
+
output(
|
|
913
|
+
{
|
|
914
|
+
name: data.stk_nm,
|
|
915
|
+
code: data.stk_cd,
|
|
916
|
+
price: price(data.cur_prc),
|
|
917
|
+
change: Number.isNaN(chg) ? "" : `${chg > 0 ? "+" : ""}${chg}`,
|
|
918
|
+
changeRate: `${unpad(data.flu_rt)}%`,
|
|
919
|
+
open: price(data.open_pric),
|
|
920
|
+
high: price(data.high_pric),
|
|
921
|
+
low: price(data.low_pric),
|
|
922
|
+
volume: won(data.trde_qty),
|
|
923
|
+
bestAsk: price(data.sel_1bid),
|
|
924
|
+
bestBid: price(data.buy_1bid),
|
|
925
|
+
prevClose: price(data.pred_close_pric)
|
|
926
|
+
},
|
|
927
|
+
"table"
|
|
928
|
+
);
|
|
929
|
+
} catch (err) {
|
|
930
|
+
handleError(err);
|
|
931
|
+
}
|
|
932
|
+
});
|
|
933
|
+
market.command("orderbook <code>").alias("book").description("10-level bid/ask order book (ka10004)").option("-o, --output <format>", "Output format (table/json)", "table").action(async (code, options) => {
|
|
934
|
+
try {
|
|
935
|
+
const client = createClient();
|
|
936
|
+
const stk = normalizeStockCode(code);
|
|
937
|
+
const { data } = await client.callEndpoint(ENDPOINTS.orderbook, { stk_cd: stk });
|
|
938
|
+
if (getOutputFormat(options) === "json") {
|
|
939
|
+
output(data, "json");
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
console.log(renderOrderbook(parseOrderbook(data), stk));
|
|
943
|
+
} catch (err) {
|
|
944
|
+
handleError(err);
|
|
945
|
+
}
|
|
946
|
+
});
|
|
947
|
+
market.command("after-hours <code>").description("After-hours single-price quotes (ka10087)").option("-o, --output <format>", "Output format (table/json)", "table").action(async (code, options) => {
|
|
948
|
+
try {
|
|
949
|
+
const client = createClient();
|
|
950
|
+
const stk = normalizeStockCode(code);
|
|
951
|
+
const { data } = await client.callEndpoint(ENDPOINTS.afterHoursOrderbook, { stk_cd: stk });
|
|
952
|
+
const fmt = getOutputFormat(options);
|
|
953
|
+
if (fmt === "json") {
|
|
954
|
+
output(data, "json");
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
output(
|
|
958
|
+
{
|
|
959
|
+
price: unpad(data.ovt_sigpric_cur_prc),
|
|
960
|
+
change: unpad(data.ovt_sigpric_pred_pre),
|
|
961
|
+
changeRate: `${unpad(data.ovt_sigpric_flu_rt)}%`,
|
|
962
|
+
volume: won(data.ovt_sigpric_acc_trde_qty),
|
|
963
|
+
askTotal: won(data.ovt_sigpric_sel_bid_tot_req),
|
|
964
|
+
bidTotal: won(data.ovt_sigpric_buy_bid_tot_req),
|
|
965
|
+
baseTime: formatStamp(data.bid_req_base_tm)
|
|
966
|
+
},
|
|
967
|
+
"table"
|
|
968
|
+
);
|
|
969
|
+
} catch (err) {
|
|
970
|
+
handleError(err);
|
|
971
|
+
}
|
|
972
|
+
});
|
|
973
|
+
market.command("daily <code>").description("Daily price history (ka10086)").option("-d, --date <yyyymmdd>", "Base date (most recent day)", todayKst()).option("-t, --type <0|1>", "Display type: 0=quantity, 1=amount", "0").option("-o, --output <format>", "Output format (table/json)", "table").action(async (code, options) => {
|
|
974
|
+
try {
|
|
975
|
+
const client = createClient();
|
|
976
|
+
const stk = normalizeStockCode(code);
|
|
977
|
+
const { data } = await client.callEndpoint(ENDPOINTS.dailyPrice, {
|
|
978
|
+
stk_cd: stk,
|
|
979
|
+
qry_dt: options.date,
|
|
980
|
+
indc_tp: options.type
|
|
981
|
+
});
|
|
982
|
+
emitList(
|
|
983
|
+
data,
|
|
984
|
+
ENDPOINTS.dailyPrice.listKey,
|
|
985
|
+
getOutputFormat(options),
|
|
986
|
+
(row) => formatFields(row, {
|
|
987
|
+
date: formatStamp,
|
|
988
|
+
open_pric: unpad,
|
|
989
|
+
high_pric: unpad,
|
|
990
|
+
low_pric: unpad,
|
|
991
|
+
close_pric: unpad,
|
|
992
|
+
flu_rt: unpad,
|
|
993
|
+
trde_qty: won
|
|
994
|
+
})
|
|
995
|
+
);
|
|
996
|
+
} catch (err) {
|
|
997
|
+
handleError(err);
|
|
998
|
+
}
|
|
999
|
+
});
|
|
1000
|
+
market.command("trades <code>").description("Recent tick-by-tick executions (ka10003)").option("-o, --output <format>", "Output format (table/json)", "table").action(async (code, options) => {
|
|
1001
|
+
try {
|
|
1002
|
+
const client = createClient();
|
|
1003
|
+
const stk = normalizeStockCode(code);
|
|
1004
|
+
const { data } = await client.callEndpoint(ENDPOINTS.stockTrades, { stk_cd: stk });
|
|
1005
|
+
emitList(
|
|
1006
|
+
data,
|
|
1007
|
+
ENDPOINTS.stockTrades.listKey,
|
|
1008
|
+
getOutputFormat(options),
|
|
1009
|
+
(row) => formatFields(row, {
|
|
1010
|
+
tm: formatStamp,
|
|
1011
|
+
cur_prc: unpad,
|
|
1012
|
+
pred_pre: unpad,
|
|
1013
|
+
pre_rt: unpad,
|
|
1014
|
+
cntr_trde_qty: unpad,
|
|
1015
|
+
acc_trde_qty: won
|
|
1016
|
+
})
|
|
1017
|
+
);
|
|
1018
|
+
} catch (err) {
|
|
1019
|
+
handleError(err);
|
|
1020
|
+
}
|
|
1021
|
+
});
|
|
1022
|
+
market.command("strength <code>").description("Trade strength (\uCCB4\uACB0\uAC15\uB3C4) time series (ka10046 / ka10047)").option("--daily", "Daily series instead of intraday").option("-o, --output <format>", "Output format (table/json)", "table").action(async (code, options) => {
|
|
1023
|
+
try {
|
|
1024
|
+
const client = createClient();
|
|
1025
|
+
const stk = normalizeStockCode(code);
|
|
1026
|
+
const ep = options.daily ? ENDPOINTS.strengthByDay : ENDPOINTS.strengthByTime;
|
|
1027
|
+
const { data } = await client.callEndpoint(ep, { stk_cd: stk });
|
|
1028
|
+
emitList(
|
|
1029
|
+
data,
|
|
1030
|
+
ep.listKey,
|
|
1031
|
+
getOutputFormat(options),
|
|
1032
|
+
(row) => formatFields(row, {
|
|
1033
|
+
cntr_tm: formatStamp,
|
|
1034
|
+
dt: formatStamp,
|
|
1035
|
+
cur_prc: unpad,
|
|
1036
|
+
flu_rt: unpad,
|
|
1037
|
+
cntr_str: unpad
|
|
1038
|
+
})
|
|
1039
|
+
);
|
|
1040
|
+
} catch (err) {
|
|
1041
|
+
handleError(err);
|
|
1042
|
+
}
|
|
1043
|
+
});
|
|
1044
|
+
market.command("inst-foreign <code>").description("Per-stock institution/foreigner trading trend (ka10045)").option("-s, --start <yyyymmdd>", "Start date").option("-e, --end <yyyymmdd>", "End date", todayKst()).option("-o, --output <format>", "Output format (table/json)", "table").action(async (code, options) => {
|
|
1045
|
+
try {
|
|
1046
|
+
const client = createClient();
|
|
1047
|
+
const stk = normalizeStockCode(code);
|
|
1048
|
+
const { data } = await client.callEndpoint(ENDPOINTS.instForeignTrend, {
|
|
1049
|
+
stk_cd: stk,
|
|
1050
|
+
strt_dt: options.start ?? options.end,
|
|
1051
|
+
end_dt: options.end,
|
|
1052
|
+
orgn_prsm_unp_tp: "0",
|
|
1053
|
+
for_prsm_unp_tp: "0"
|
|
1054
|
+
});
|
|
1055
|
+
emitList(
|
|
1056
|
+
data,
|
|
1057
|
+
ENDPOINTS.instForeignTrend.listKey,
|
|
1058
|
+
getOutputFormat(options),
|
|
1059
|
+
(row) => formatFields(row, {
|
|
1060
|
+
dt: formatStamp,
|
|
1061
|
+
close_pric: unpad,
|
|
1062
|
+
flu_rt: unpad,
|
|
1063
|
+
orgn_daly_nettrde_qty: unpad,
|
|
1064
|
+
for_daly_nettrde_qty: unpad
|
|
1065
|
+
})
|
|
1066
|
+
);
|
|
1067
|
+
} catch (err) {
|
|
1068
|
+
handleError(err);
|
|
1069
|
+
}
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
function emitList(data, listKey, fmt, rowFormatter) {
|
|
1073
|
+
if (fmt === "json") {
|
|
1074
|
+
output(data, "json");
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
const rows = Array.isArray(data[listKey]) ? data[listKey] : [];
|
|
1078
|
+
if (rows.length === 0) {
|
|
1079
|
+
console.log("No data");
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
output(rowFormatter ? rows.map(rowFormatter) : rows, "table");
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// src/commands/stock.ts
|
|
1086
|
+
function registerStockCommands(program2) {
|
|
1087
|
+
const stock = program2.command("stock").description("Stock information \u2014 fundamentals, search, members");
|
|
1088
|
+
stock.command("info <code>").description("Stock fundamentals snapshot (ka10001)").option("-o, --output <format>", "Output format (table/json)", "table").action(async (code, options) => {
|
|
1089
|
+
try {
|
|
1090
|
+
const client = createClient();
|
|
1091
|
+
const stk = normalizeStockCode(code);
|
|
1092
|
+
const { data } = await client.callEndpoint(ENDPOINTS.stockInfo, { stk_cd: stk });
|
|
1093
|
+
const fmt = getOutputFormat(options);
|
|
1094
|
+
if (fmt === "json") {
|
|
1095
|
+
output(data, "json");
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
output(
|
|
1099
|
+
{
|
|
1100
|
+
name: data.stk_nm,
|
|
1101
|
+
code: data.stk_cd,
|
|
1102
|
+
price: price(data.cur_prc),
|
|
1103
|
+
change: unpad(data.pred_pre),
|
|
1104
|
+
changeRate: `${unpad(data.flu_rt)}%`,
|
|
1105
|
+
open: price(data.open_pric),
|
|
1106
|
+
high: price(data.high_pric),
|
|
1107
|
+
low: price(data.low_pric),
|
|
1108
|
+
upperLimit: price(data.upl_pric),
|
|
1109
|
+
lowerLimit: price(data.lst_pric),
|
|
1110
|
+
volume: won(data.trde_qty),
|
|
1111
|
+
per: data.per,
|
|
1112
|
+
eps: data.eps,
|
|
1113
|
+
roe: data.roe,
|
|
1114
|
+
pbr: data.pbr,
|
|
1115
|
+
bps: data.bps,
|
|
1116
|
+
marketCap: won(data.mac),
|
|
1117
|
+
foreignRate: `${unpad(data.for_exh_rt)}%`,
|
|
1118
|
+
high52w: price(data["250hgst"]),
|
|
1119
|
+
low52w: price(data["250lwst"])
|
|
1120
|
+
},
|
|
1121
|
+
"table"
|
|
1122
|
+
);
|
|
1123
|
+
} catch (err) {
|
|
1124
|
+
handleError(err);
|
|
1125
|
+
}
|
|
1126
|
+
});
|
|
1127
|
+
stock.command("search <keyword>").description("Search the stock list by name or code (ka10099)").option("-m, --market <0|10>", "Market: 0=KOSPI, 10=KOSDAQ", "0").option("-n, --limit <n>", "Maximum rows to return", "30").option("-o, --output <format>", "Output format (table/json)", "table").action(async (keyword, options) => {
|
|
1128
|
+
try {
|
|
1129
|
+
const client = createClient();
|
|
1130
|
+
const { data } = await client.callEndpoint(
|
|
1131
|
+
ENDPOINTS.stockList,
|
|
1132
|
+
{ mrkt_tp: options.market },
|
|
1133
|
+
{ paginate: true }
|
|
1134
|
+
);
|
|
1135
|
+
const fmt = getOutputFormat(options);
|
|
1136
|
+
const limit = parseIntStrict(options.limit, "limit");
|
|
1137
|
+
const needle = keyword.toLowerCase();
|
|
1138
|
+
const list = Array.isArray(data[ENDPOINTS.stockList.listKey]) ? data[ENDPOINTS.stockList.listKey] : [];
|
|
1139
|
+
const filtered = list.filter(
|
|
1140
|
+
(item) => String(item.name ?? "").toLowerCase().includes(needle) || String(item.code ?? "").startsWith(keyword)
|
|
1141
|
+
).slice(0, limit);
|
|
1142
|
+
if (filtered.length === 0) {
|
|
1143
|
+
console.log("No matches");
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
if (fmt === "json") {
|
|
1147
|
+
output(filtered, "json");
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
const rows = filtered.map((item) => ({
|
|
1151
|
+
code: item.code,
|
|
1152
|
+
name: item.name,
|
|
1153
|
+
market: item.marketName,
|
|
1154
|
+
sector: item.upName,
|
|
1155
|
+
lastPrice: unpad(item.lastPrice)
|
|
1156
|
+
}));
|
|
1157
|
+
output(rows, "table");
|
|
1158
|
+
} catch (err) {
|
|
1159
|
+
handleError(err);
|
|
1160
|
+
}
|
|
1161
|
+
});
|
|
1162
|
+
stock.command("resolve <code>").description("Resolve a single stock's listing metadata (ka10100)").option("-o, --output <format>", "Output format (table/json)", "table").action(async (code, options) => {
|
|
1163
|
+
try {
|
|
1164
|
+
const client = createClient();
|
|
1165
|
+
const stk = normalizeStockCode(code);
|
|
1166
|
+
const { data } = await client.callEndpoint(ENDPOINTS.stockInfoSingle, { stk_cd: stk });
|
|
1167
|
+
const fmt = getOutputFormat(options);
|
|
1168
|
+
if (fmt === "json") {
|
|
1169
|
+
output(data, "json");
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
output(
|
|
1173
|
+
{
|
|
1174
|
+
code: data.code,
|
|
1175
|
+
name: data.name,
|
|
1176
|
+
market: data.marketName,
|
|
1177
|
+
sector: data.upName,
|
|
1178
|
+
sizeTier: data.upSizeName,
|
|
1179
|
+
listedShares: won(data.listCount),
|
|
1180
|
+
lastPrice: unpad(data.lastPrice),
|
|
1181
|
+
state: data.state,
|
|
1182
|
+
nxtEnable: data.nxtEnable
|
|
1183
|
+
},
|
|
1184
|
+
"table"
|
|
1185
|
+
);
|
|
1186
|
+
} catch (err) {
|
|
1187
|
+
handleError(err);
|
|
1188
|
+
}
|
|
1189
|
+
});
|
|
1190
|
+
stock.command("members <code>").description("Top 5 buy/sell trading members (ka10002)").option("-o, --output <format>", "Output format (table/json)", "table").action(async (code, options) => {
|
|
1191
|
+
try {
|
|
1192
|
+
const client = createClient();
|
|
1193
|
+
const stk = normalizeStockCode(code);
|
|
1194
|
+
const { data } = await client.callEndpoint(ENDPOINTS.tradingMembers, { stk_cd: stk });
|
|
1195
|
+
const fmt = getOutputFormat(options);
|
|
1196
|
+
if (fmt === "json") {
|
|
1197
|
+
output(data, "json");
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
const rows = [];
|
|
1201
|
+
for (let i = 1; i <= 5; i++) {
|
|
1202
|
+
rows.push({
|
|
1203
|
+
rank: i,
|
|
1204
|
+
sellMember: data["sel_trde_ori_nm_" + i],
|
|
1205
|
+
sellQty: unpad(data["sel_trde_qty_" + i]),
|
|
1206
|
+
buyMember: data["buy_trde_ori_nm_" + i],
|
|
1207
|
+
buyQty: unpad(data["buy_trde_qty_" + i])
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
output(rows, "table");
|
|
1211
|
+
} catch (err) {
|
|
1212
|
+
handleError(err);
|
|
1213
|
+
}
|
|
1214
|
+
});
|
|
1215
|
+
stock.command("credit-trend <code>").description("Credit trading trend (\uC2E0\uC6A9\uB9E4\uB9E4\uB3D9\uD5A5) (ka10013)").option("-d, --date <yyyymmdd>", "Base date", todayKst()).option("-t, --type <1|2>", "Query type: 1=\uC735\uC790, 2=\uB300\uC8FC", "1").option("-o, --output <format>", "Output format (table/json)", "table").action(async (code, options) => {
|
|
1216
|
+
try {
|
|
1217
|
+
const client = createClient();
|
|
1218
|
+
const stk = normalizeStockCode(code);
|
|
1219
|
+
const { data } = await client.callEndpoint(ENDPOINTS.creditTrend, {
|
|
1220
|
+
stk_cd: stk,
|
|
1221
|
+
dt: options.date ?? todayKst(),
|
|
1222
|
+
qry_tp: options.type ?? "1"
|
|
1223
|
+
});
|
|
1224
|
+
emitList(
|
|
1225
|
+
data,
|
|
1226
|
+
ENDPOINTS.creditTrend.listKey,
|
|
1227
|
+
getOutputFormat(options),
|
|
1228
|
+
(row) => formatFields(row, {
|
|
1229
|
+
dt: formatStamp,
|
|
1230
|
+
cur_prc: unpad,
|
|
1231
|
+
trde_qty: won,
|
|
1232
|
+
new: won,
|
|
1233
|
+
rpya: won,
|
|
1234
|
+
remn: won
|
|
1235
|
+
})
|
|
1236
|
+
);
|
|
1237
|
+
} catch (err) {
|
|
1238
|
+
handleError(err);
|
|
1239
|
+
}
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
// src/commands/chart.ts
|
|
1244
|
+
var CHART_ROW_FORMATTERS = {
|
|
1245
|
+
cntr_tm: formatStamp,
|
|
1246
|
+
dt: formatStamp,
|
|
1247
|
+
open_pric: unpad,
|
|
1248
|
+
high_pric: unpad,
|
|
1249
|
+
low_pric: unpad,
|
|
1250
|
+
cur_prc: unpad,
|
|
1251
|
+
trde_qty: won,
|
|
1252
|
+
acc_trde_qty: won,
|
|
1253
|
+
trde_prica: won,
|
|
1254
|
+
pred_pre: unpad,
|
|
1255
|
+
trde_tern_rt: unpad
|
|
1256
|
+
};
|
|
1257
|
+
function emitChart(data, ep, fmt, count) {
|
|
1258
|
+
if (fmt === "json") {
|
|
1259
|
+
output(data, "json");
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
const all = Array.isArray(data[ep.listKey]) ? data[ep.listKey] : [];
|
|
1263
|
+
if (all.length === 0) {
|
|
1264
|
+
console.log("No data");
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
output(
|
|
1268
|
+
all.slice(0, count).map((row) => formatFields(row, CHART_ROW_FORMATTERS)),
|
|
1269
|
+
"table"
|
|
1270
|
+
);
|
|
1271
|
+
}
|
|
1272
|
+
function registerChartCommands(program2) {
|
|
1273
|
+
const chart = program2.command("chart").description("OHLC charts \u2014 tick / minute / daily / weekly / monthly / yearly");
|
|
1274
|
+
async function runIntraday(ep, code, ticScope, options) {
|
|
1275
|
+
const client = createClient();
|
|
1276
|
+
const stk = normalizeStockCode(code);
|
|
1277
|
+
const { data } = await client.callEndpoint(ep, {
|
|
1278
|
+
stk_cd: stk,
|
|
1279
|
+
tic_scope: ticScope,
|
|
1280
|
+
upd_stkpc_tp: options.raw ? "0" : "1"
|
|
1281
|
+
});
|
|
1282
|
+
emitChart(data, ep, getOutputFormat(options), parseIntStrict(options.count, "count"));
|
|
1283
|
+
}
|
|
1284
|
+
async function runPeriod(ep, code, baseDt, options) {
|
|
1285
|
+
const client = createClient();
|
|
1286
|
+
const stk = normalizeStockCode(code);
|
|
1287
|
+
const { data } = await client.callEndpoint(ep, {
|
|
1288
|
+
stk_cd: stk,
|
|
1289
|
+
base_dt: baseDt,
|
|
1290
|
+
upd_stkpc_tp: options.raw ? "0" : "1"
|
|
1291
|
+
});
|
|
1292
|
+
emitChart(data, ep, getOutputFormat(options), parseIntStrict(options.count, "count"));
|
|
1293
|
+
}
|
|
1294
|
+
chart.command("tick <code>").description("Tick chart (ka10079)").option("-s, --scope <n>", "Ticks per candle (1/3/5/10/30)", "1").option("-n, --count <n>", "Number of candles to display", "50").option("--raw", "Unadjusted (\uC218\uC815\uC8FC\uAC00 \uBBF8\uBC18\uC601) prices").option("-o, --output <format>", "Output format (table/json)", "table").action(async (code, options) => {
|
|
1295
|
+
try {
|
|
1296
|
+
await runIntraday(ENDPOINTS.tickChart, code, options.scope, options);
|
|
1297
|
+
} catch (err) {
|
|
1298
|
+
handleError(err);
|
|
1299
|
+
}
|
|
1300
|
+
});
|
|
1301
|
+
chart.command("min <code>").alias("minute").description("Minute chart (ka10080)").option("-i, --interval <n>", "Minutes per candle (1/3/5/10/15/30/45/60)", "1").option("-n, --count <n>", "Number of candles to display", "50").option("--raw", "Unadjusted (\uC218\uC815\uC8FC\uAC00 \uBBF8\uBC18\uC601) prices").option("-o, --output <format>", "Output format (table/json)", "table").action(async (code, options) => {
|
|
1302
|
+
try {
|
|
1303
|
+
await runIntraday(ENDPOINTS.minuteChart, code, options.interval, options);
|
|
1304
|
+
} catch (err) {
|
|
1305
|
+
handleError(err);
|
|
1306
|
+
}
|
|
1307
|
+
});
|
|
1308
|
+
chart.command("day <code>").alias("daily").description("Daily chart (ka10081)").option("-d, --date <yyyymmdd>", "Base date (most recent candle)", todayKst()).option("-n, --count <n>", "Number of candles to display", "50").option("--raw", "Unadjusted (\uC218\uC815\uC8FC\uAC00 \uBBF8\uBC18\uC601) prices").option("-o, --output <format>", "Output format (table/json)", "table").action(async (code, options) => {
|
|
1309
|
+
try {
|
|
1310
|
+
await runPeriod(ENDPOINTS.dailyChart, code, options.date, options);
|
|
1311
|
+
} catch (err) {
|
|
1312
|
+
handleError(err);
|
|
1313
|
+
}
|
|
1314
|
+
});
|
|
1315
|
+
chart.command("week <code>").description("Weekly chart (ka10082)").option("-d, --date <yyyymmdd>", "Base date (most recent candle)", todayKst()).option("-n, --count <n>", "Number of candles to display", "50").option("--raw", "Unadjusted (\uC218\uC815\uC8FC\uAC00 \uBBF8\uBC18\uC601) prices").option("-o, --output <format>", "Output format (table/json)", "table").action(async (code, options) => {
|
|
1316
|
+
try {
|
|
1317
|
+
await runPeriod(ENDPOINTS.weeklyChart, code, options.date, options);
|
|
1318
|
+
} catch (err) {
|
|
1319
|
+
handleError(err);
|
|
1320
|
+
}
|
|
1321
|
+
});
|
|
1322
|
+
chart.command("month <code>").description("Monthly chart (ka10083)").option("-d, --date <yyyymmdd>", "Base date (most recent candle)", todayKst()).option("-n, --count <n>", "Number of candles to display", "50").option("--raw", "Unadjusted (\uC218\uC815\uC8FC\uAC00 \uBBF8\uBC18\uC601) prices").option("-o, --output <format>", "Output format (table/json)", "table").action(async (code, options) => {
|
|
1323
|
+
try {
|
|
1324
|
+
await runPeriod(ENDPOINTS.monthlyChart, code, options.date, options);
|
|
1325
|
+
} catch (err) {
|
|
1326
|
+
handleError(err);
|
|
1327
|
+
}
|
|
1328
|
+
});
|
|
1329
|
+
chart.command("year <code>").description("Yearly chart (ka10094)").option("-d, --date <yyyymmdd>", "Base date (most recent candle)", todayKst()).option("-n, --count <n>", "Number of candles to display", "50").option("--raw", "Unadjusted (\uC218\uC815\uC8FC\uAC00 \uBBF8\uBC18\uC601) prices").option("-o, --output <format>", "Output format (table/json)", "table").action(async (code, options) => {
|
|
1330
|
+
try {
|
|
1331
|
+
await runPeriod(ENDPOINTS.yearlyChart, code, options.date, options);
|
|
1332
|
+
} catch (err) {
|
|
1333
|
+
handleError(err);
|
|
1334
|
+
}
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
// src/commands/account.ts
|
|
1339
|
+
var pct = (v) => `${unpad(v)}%`;
|
|
1340
|
+
function registerAccountCommands(program2) {
|
|
1341
|
+
const account = program2.command("account").alias("acct").description("Account \u2014 balance, deposit, orders, executions, P/L");
|
|
1342
|
+
const exchangeHint = `Exchange filter (${ACCOUNT_EXCHANGE_TYPES.join("/")})`;
|
|
1343
|
+
account.command("balance").description("Account evaluation balance with holdings (kt00018)").option("-x, --exchange <KRX|NXT|%>", exchangeHint, "KRX").option("-o, --output <format>", "Output format (table/json)", "table").action(async (options) => {
|
|
1344
|
+
try {
|
|
1345
|
+
const client = createClient();
|
|
1346
|
+
const { data } = await client.callEndpoint(ENDPOINTS.balance, {
|
|
1347
|
+
qry_tp: "1",
|
|
1348
|
+
dmst_stex_tp: options.exchange || "KRX"
|
|
1349
|
+
});
|
|
1350
|
+
const fmt = getOutputFormat(options);
|
|
1351
|
+
if (fmt === "json") {
|
|
1352
|
+
output(data, "json");
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
output(
|
|
1356
|
+
{
|
|
1357
|
+
totalPurchase: won(data.tot_pur_amt),
|
|
1358
|
+
totalEval: won(data.tot_evlt_amt),
|
|
1359
|
+
evalPnl: won(data.tot_evlt_pl),
|
|
1360
|
+
profitRate: `${unpad(data.tot_prft_rt)}%`,
|
|
1361
|
+
estDepositAsset: won(data.prsm_dpst_aset_amt)
|
|
1362
|
+
},
|
|
1363
|
+
"table"
|
|
1364
|
+
);
|
|
1365
|
+
emitList(data, ENDPOINTS.balance.listKey, "table", (row) => ({
|
|
1366
|
+
code: row.stk_cd,
|
|
1367
|
+
name: row.stk_nm,
|
|
1368
|
+
qty: won(row.rmnd_qty),
|
|
1369
|
+
avgPrice: won(row.pur_pric),
|
|
1370
|
+
curPrice: won(row.cur_prc),
|
|
1371
|
+
evalAmt: won(row.evlt_amt),
|
|
1372
|
+
pnl: won(row.evltv_prft),
|
|
1373
|
+
pnlRate: pct(row.prft_rt),
|
|
1374
|
+
weight: pct(row.poss_rt)
|
|
1375
|
+
}));
|
|
1376
|
+
} catch (err) {
|
|
1377
|
+
handleError(err);
|
|
1378
|
+
}
|
|
1379
|
+
});
|
|
1380
|
+
account.command("deposit").description("Deposit detail \u2014 cash, orderable, withdrawable (kt00001)").option("-o, --output <format>", "Output format (table/json)", "table").action(async (options) => {
|
|
1381
|
+
try {
|
|
1382
|
+
const client = createClient();
|
|
1383
|
+
const { data } = await client.callEndpoint(ENDPOINTS.deposit, { qry_tp: "3" });
|
|
1384
|
+
const fmt = getOutputFormat(options);
|
|
1385
|
+
if (fmt === "json") {
|
|
1386
|
+
output(data, "json");
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
output(
|
|
1390
|
+
{
|
|
1391
|
+
cash: won(data.entr),
|
|
1392
|
+
orderable: won(data.ord_alow_amt),
|
|
1393
|
+
withdrawable: won(data.pymn_alow_amt),
|
|
1394
|
+
d2Deposit: won(data.d2_entra),
|
|
1395
|
+
substitute: won(data.repl_amt)
|
|
1396
|
+
},
|
|
1397
|
+
"table"
|
|
1398
|
+
);
|
|
1399
|
+
} catch (err) {
|
|
1400
|
+
handleError(err);
|
|
1401
|
+
}
|
|
1402
|
+
});
|
|
1403
|
+
account.command("eval").description("Account evaluation status with holdings (kt00004)").option("-x, --exchange <KRX|NXT|%>", exchangeHint, "KRX").option("-o, --output <format>", "Output format (table/json)", "table").action(async (options) => {
|
|
1404
|
+
try {
|
|
1405
|
+
const client = createClient();
|
|
1406
|
+
const { data } = await client.callEndpoint(ENDPOINTS.evalStatus, {
|
|
1407
|
+
qry_tp: "0",
|
|
1408
|
+
dmst_stex_tp: options.exchange || "KRX"
|
|
1409
|
+
});
|
|
1410
|
+
const fmt = getOutputFormat(options);
|
|
1411
|
+
if (fmt === "json") {
|
|
1412
|
+
output(data, "json");
|
|
1413
|
+
return;
|
|
1414
|
+
}
|
|
1415
|
+
output(
|
|
1416
|
+
{
|
|
1417
|
+
accountName: data.acnt_nm,
|
|
1418
|
+
branch: data.brch_nm,
|
|
1419
|
+
deposit: won(data.entr),
|
|
1420
|
+
totalEstimate: won(data.tot_est_amt),
|
|
1421
|
+
assetEval: won(data.aset_evlt_amt),
|
|
1422
|
+
totalPurchase: won(data.tot_pur_amt)
|
|
1423
|
+
},
|
|
1424
|
+
"table"
|
|
1425
|
+
);
|
|
1426
|
+
emitList(data, ENDPOINTS.evalStatus.listKey, "table", (row) => ({
|
|
1427
|
+
code: row.stk_cd,
|
|
1428
|
+
name: row.stk_nm,
|
|
1429
|
+
qty: won(row.rmnd_qty),
|
|
1430
|
+
avgPrice: won(row.avg_prc),
|
|
1431
|
+
curPrice: won(row.cur_prc),
|
|
1432
|
+
evalAmt: won(row.evlt_amt),
|
|
1433
|
+
pnl: won(row.pl_amt),
|
|
1434
|
+
pnlRate: pct(row.pl_rt)
|
|
1435
|
+
}));
|
|
1436
|
+
} catch (err) {
|
|
1437
|
+
handleError(err);
|
|
1438
|
+
}
|
|
1439
|
+
});
|
|
1440
|
+
account.command("settled").description("Settled (executed) balance (kt00005)").option("-x, --exchange <KRX|NXT|%>", exchangeHint, "KRX").option("-o, --output <format>", "Output format (table/json)", "table").action(async (options) => {
|
|
1441
|
+
try {
|
|
1442
|
+
const client = createClient();
|
|
1443
|
+
const { data } = await client.callEndpoint(ENDPOINTS.settledBalance, {
|
|
1444
|
+
dmst_stex_tp: options.exchange || "KRX"
|
|
1445
|
+
});
|
|
1446
|
+
emitList(data, "stk_cntr_remn", getOutputFormat(options), (row) => ({
|
|
1447
|
+
code: row.stk_cd,
|
|
1448
|
+
name: row.stk_nm,
|
|
1449
|
+
qty: won(row.cur_qty),
|
|
1450
|
+
avgPrice: won(row.buy_uv),
|
|
1451
|
+
curPrice: won(row.cur_prc),
|
|
1452
|
+
evalAmt: won(row.evlt_amt),
|
|
1453
|
+
pnl: won(row.evltv_prft),
|
|
1454
|
+
pnlRate: pct(row.pl_rt)
|
|
1455
|
+
}));
|
|
1456
|
+
} catch (err) {
|
|
1457
|
+
handleError(err);
|
|
1458
|
+
}
|
|
1459
|
+
});
|
|
1460
|
+
account.command("open-orders").description("Unfilled (open) orders (ka10075)").option("-c, --code <code>", "Filter by stock code").option("-o, --output <format>", "Output format (table/json)", "table").action(async (options) => {
|
|
1461
|
+
try {
|
|
1462
|
+
const client = createClient();
|
|
1463
|
+
const body = options.code ? {
|
|
1464
|
+
all_stk_tp: "0",
|
|
1465
|
+
trde_tp: "0",
|
|
1466
|
+
stk_cd: normalizeStockCode(options.code),
|
|
1467
|
+
stex_tp: "0"
|
|
1468
|
+
} : { all_stk_tp: "1", trde_tp: "0", stk_cd: "", stex_tp: "0" };
|
|
1469
|
+
const { data } = await client.callEndpoint(ENDPOINTS.openOrders, body);
|
|
1470
|
+
emitList(
|
|
1471
|
+
data,
|
|
1472
|
+
"oso",
|
|
1473
|
+
getOutputFormat(options),
|
|
1474
|
+
(row) => formatFields(row, {
|
|
1475
|
+
stk_nm: (v) => v,
|
|
1476
|
+
ord_no: (v) => v,
|
|
1477
|
+
ord_qty: won,
|
|
1478
|
+
ord_pric: won,
|
|
1479
|
+
oso_qty: won,
|
|
1480
|
+
cur_prc: unpad,
|
|
1481
|
+
tm: formatStamp
|
|
1482
|
+
})
|
|
1483
|
+
);
|
|
1484
|
+
} catch (err) {
|
|
1485
|
+
handleError(err);
|
|
1486
|
+
}
|
|
1487
|
+
});
|
|
1488
|
+
account.command("executions").description("Filled executions (ka10076)").option("-c, --code <code>", "Filter by stock code").option("-o, --output <format>", "Output format (table/json)", "table").action(async (options) => {
|
|
1489
|
+
try {
|
|
1490
|
+
const client = createClient();
|
|
1491
|
+
const { data } = await client.callEndpoint(ENDPOINTS.executions, {
|
|
1492
|
+
stk_cd: options.code ? normalizeStockCode(options.code) : "",
|
|
1493
|
+
qry_tp: options.code ? "1" : "0",
|
|
1494
|
+
sell_tp: "0",
|
|
1495
|
+
ord_no: "",
|
|
1496
|
+
stex_tp: "0"
|
|
1497
|
+
});
|
|
1498
|
+
emitList(
|
|
1499
|
+
data,
|
|
1500
|
+
"cntr",
|
|
1501
|
+
getOutputFormat(options),
|
|
1502
|
+
(row) => formatFields(row, {
|
|
1503
|
+
stk_nm: (v) => v,
|
|
1504
|
+
ord_no: (v) => v,
|
|
1505
|
+
cntr_qty: won,
|
|
1506
|
+
cntr_uv: won,
|
|
1507
|
+
ord_uv: won,
|
|
1508
|
+
cnfm_tm: formatStamp
|
|
1509
|
+
})
|
|
1510
|
+
);
|
|
1511
|
+
} catch (err) {
|
|
1512
|
+
handleError(err);
|
|
1513
|
+
}
|
|
1514
|
+
});
|
|
1515
|
+
account.command("order-detail").description("Detailed order/execution history (kt00007)").option("-d, --date <yyyymmdd>", "Order date", "").option("-c, --code <code>", "Filter by stock code").option("-o, --output <format>", "Output format (table/json)", "table").action(async (options) => {
|
|
1516
|
+
try {
|
|
1517
|
+
const client = createClient();
|
|
1518
|
+
const { data } = await client.callEndpoint(ENDPOINTS.orderDetail, {
|
|
1519
|
+
ord_dt: options.date || "",
|
|
1520
|
+
qry_tp: "1",
|
|
1521
|
+
stk_bond_tp: "0",
|
|
1522
|
+
sell_tp: "0",
|
|
1523
|
+
stk_cd: options.code ? normalizeStockCode(options.code) : "",
|
|
1524
|
+
fr_ord_no: "",
|
|
1525
|
+
dmst_stex_tp: "%"
|
|
1526
|
+
});
|
|
1527
|
+
emitList(data, "acnt_ord_cntr_prps_dtl", getOutputFormat(options));
|
|
1528
|
+
} catch (err) {
|
|
1529
|
+
handleError(err);
|
|
1530
|
+
}
|
|
1531
|
+
});
|
|
1532
|
+
account.command("pnl <code>").description("Realized profit/loss by date or period (ka10072 / ka10073)").option("-s, --start <yyyymmdd>", "Start date").option("-e, --end <yyyymmdd>", "End date (enables period query, max 3 months)").option("-o, --output <format>", "Output format (table/json)", "table").action(async (code, options) => {
|
|
1533
|
+
try {
|
|
1534
|
+
const client = createClient();
|
|
1535
|
+
const stk = normalizeStockCode(code);
|
|
1536
|
+
const ep = options.end ? ENDPOINTS.realizedPlByPeriod : ENDPOINTS.realizedPlByDate;
|
|
1537
|
+
const body = options.end ? { stk_cd: stk, strt_dt: options.start || options.end, end_dt: options.end } : { stk_cd: stk, strt_dt: options.start || todayKst() };
|
|
1538
|
+
const { data } = await client.callEndpoint(ep, body);
|
|
1539
|
+
emitList(
|
|
1540
|
+
data,
|
|
1541
|
+
ep.listKey,
|
|
1542
|
+
getOutputFormat(options),
|
|
1543
|
+
(row) => formatFields(row, {
|
|
1544
|
+
dt: formatStamp,
|
|
1545
|
+
stk_nm: (v) => v,
|
|
1546
|
+
cntr_qty: won,
|
|
1547
|
+
buy_uv: won,
|
|
1548
|
+
cntr_pric: won,
|
|
1549
|
+
tdy_sel_pl: won,
|
|
1550
|
+
pl_rt: unpad
|
|
1551
|
+
})
|
|
1552
|
+
);
|
|
1553
|
+
} catch (err) {
|
|
1554
|
+
handleError(err);
|
|
1555
|
+
}
|
|
1556
|
+
});
|
|
1557
|
+
account.command("journal").description("Today's trading journal with totals (ka10170)").option("-d, --date <yyyymmdd>", "Base date", "").option("-o, --output <format>", "Output format (table/json)", "table").action(async (options) => {
|
|
1558
|
+
try {
|
|
1559
|
+
const client = createClient();
|
|
1560
|
+
const { data } = await client.callEndpoint(ENDPOINTS.tradeJournal, {
|
|
1561
|
+
base_dt: options.date || "",
|
|
1562
|
+
ottks_tp: "1",
|
|
1563
|
+
ch_crd_tp: "0"
|
|
1564
|
+
});
|
|
1565
|
+
const fmt = getOutputFormat(options);
|
|
1566
|
+
if (fmt === "json") {
|
|
1567
|
+
output(data, "json");
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
output(
|
|
1571
|
+
{
|
|
1572
|
+
totalSell: won(data.tot_sell_amt),
|
|
1573
|
+
totalBuy: won(data.tot_buy_amt),
|
|
1574
|
+
commissionTax: won(data.tot_cmsn_tax),
|
|
1575
|
+
pnl: won(data.tot_pl_amt),
|
|
1576
|
+
profitRate: `${unpad(data.tot_prft_rt)}%`
|
|
1577
|
+
},
|
|
1578
|
+
"table"
|
|
1579
|
+
);
|
|
1580
|
+
emitList(data, "tdy_trde_diary", "table");
|
|
1581
|
+
} catch (err) {
|
|
1582
|
+
handleError(err);
|
|
1583
|
+
}
|
|
1584
|
+
});
|
|
1585
|
+
account.command("returns").description("Daily account return detail over a period (kt00016)").option("-s, --start <yyyymmdd>", "Start date (defaults to first of this month)").option("-e, --end <yyyymmdd>", "End date", todayKst()).option("-o, --output <format>", "Output format (table/json)", "table").action(async (options) => {
|
|
1586
|
+
try {
|
|
1587
|
+
const client = createClient();
|
|
1588
|
+
const { data } = await client.callEndpoint(ENDPOINTS.dailyReturn, {
|
|
1589
|
+
fr_dt: options.start || `${todayKst().slice(0, 6)}01`,
|
|
1590
|
+
to_dt: options.end || todayKst()
|
|
1591
|
+
});
|
|
1592
|
+
const fmt = getOutputFormat(options);
|
|
1593
|
+
if (fmt === "json") {
|
|
1594
|
+
output(data, "json");
|
|
1595
|
+
return;
|
|
1596
|
+
}
|
|
1597
|
+
output(
|
|
1598
|
+
{
|
|
1599
|
+
investBase: won(data.invt_bsamt),
|
|
1600
|
+
evalProfit: won(data.evltv_prft),
|
|
1601
|
+
profitRate: `${unpad(data.prft_rt)}%`,
|
|
1602
|
+
turnoverRate: `${unpad(data.tern_rt)}%`,
|
|
1603
|
+
totalFrom: won(data.tot_amt_fr),
|
|
1604
|
+
totalTo: won(data.tot_amt_to)
|
|
1605
|
+
},
|
|
1606
|
+
"table"
|
|
1607
|
+
);
|
|
1608
|
+
} catch (err) {
|
|
1609
|
+
handleError(err);
|
|
1610
|
+
}
|
|
1611
|
+
});
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
// src/commands/order.ts
|
|
1615
|
+
function resolveExchange(value) {
|
|
1616
|
+
const ex = (value ?? "KRX").toUpperCase();
|
|
1617
|
+
if (!ORDER_EXCHANGE_TYPES.includes(ex)) {
|
|
1618
|
+
throw new ActionableError(
|
|
1619
|
+
`Invalid exchange "${value}". Use one of: ${ORDER_EXCHANGE_TYPES.join(", ")}`
|
|
1620
|
+
);
|
|
1621
|
+
}
|
|
1622
|
+
return ex;
|
|
1623
|
+
}
|
|
1624
|
+
function resolveOrderType(typeOpt, priceOpt) {
|
|
1625
|
+
let trde_tp = typeOpt ?? (priceOpt ? "0" : "3");
|
|
1626
|
+
if (!ORDER_TYPES[trde_tp]) {
|
|
1627
|
+
throw new ActionableError(
|
|
1628
|
+
`Invalid order type "${trde_tp}". Known types: ${Object.entries(ORDER_TYPES).map(([k, v]) => `${k}=${v}`).join(", ")}`
|
|
1629
|
+
);
|
|
1630
|
+
}
|
|
1631
|
+
const isMarket = MARKET_ORDER_TYPES.has(trde_tp);
|
|
1632
|
+
if (isMarket) {
|
|
1633
|
+
if (priceOpt) {
|
|
1634
|
+
throw new ActionableError("Market orders must not specify a price (--price).");
|
|
1635
|
+
}
|
|
1636
|
+
return { trde_tp, ord_uv: "" };
|
|
1637
|
+
}
|
|
1638
|
+
if (!priceOpt) {
|
|
1639
|
+
throw new ActionableError(
|
|
1640
|
+
`Order type ${trde_tp} (${ORDER_TYPES[trde_tp]}) requires --price.`
|
|
1641
|
+
);
|
|
1642
|
+
}
|
|
1643
|
+
return { trde_tp, ord_uv: priceOpt };
|
|
1644
|
+
}
|
|
1645
|
+
async function confirm(summary, skip) {
|
|
1646
|
+
console.log(summary);
|
|
1647
|
+
if (skip) return true;
|
|
1648
|
+
const answer = await prompt("Proceed? (y/N): ");
|
|
1649
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
1650
|
+
}
|
|
1651
|
+
async function submit(def, body, outputFmt) {
|
|
1652
|
+
const client = createClient();
|
|
1653
|
+
const { data } = await client.callEndpoint(def, body);
|
|
1654
|
+
output(
|
|
1655
|
+
{
|
|
1656
|
+
orderNo: data.ord_no,
|
|
1657
|
+
exchange: data.dmst_stex_tp,
|
|
1658
|
+
message: data.return_msg
|
|
1659
|
+
},
|
|
1660
|
+
getOutputFormat(outputFmt)
|
|
1661
|
+
);
|
|
1662
|
+
}
|
|
1663
|
+
function registerOrderCommands(program2) {
|
|
1664
|
+
const order = program2.command("order").description('Place / modify / cancel orders (REAL money on the "real" environment)');
|
|
1665
|
+
order.command("buy <code> <qty>").description("Place a buy order").option("-p, --price <won>", "Limit price; omit for a market order").option("-t, --type <code>", "Order type code (trde_tp); default 0=limit / 3=market").option("--cond-price <won>", "Trigger price for conditional/stop types (e.g. type 28)").option("-x, --exchange <KRX|NXT|SOR>", "Exchange routing", "KRX").option("--credit", "Use a credit (margin) order").option("-y, --yes", "Skip the confirmation prompt").option("-o, --output <format>", "Output format (table/json)", "table").action((code, qty, options) => placeOrder("buy", code, qty, options).catch(handleError));
|
|
1666
|
+
order.command("sell <code> <qty>").description("Place a sell order").option("-p, --price <won>", "Limit price; omit for a market order").option("-t, --type <code>", "Order type code (trde_tp); default 0=limit / 3=market").option("--cond-price <won>", "Trigger price for conditional/stop types (e.g. type 28)").option("-x, --exchange <KRX|NXT|SOR>", "Exchange routing", "KRX").option("--credit", "Use a credit (margin) order").option("--credit-deal <code>", "Credit deal type (\uC2E0\uC6A9\uAC70\uB798\uAD6C\uBD84): 33=\uC735\uC790, 99=\uC735\uC790\uD569", "33").option("--loan-date <yyyymmdd>", "Credit loan date (required for some credit sells)").option("-y, --yes", "Skip the confirmation prompt").option("-o, --output <format>", "Output format (table/json)", "table").action((code, qty, options) => placeOrder("sell", code, qty, options).catch(handleError));
|
|
1667
|
+
order.command("modify <orderNo> <code> <qty> <price>").description("Modify a resting order (new qty + price)").option("--cond-price <won>", "New trigger price for conditional/stop orders").option("-x, --exchange <KRX|NXT|SOR>", "Exchange routing (must match original)", "KRX").option("--credit", "Modify a credit order").option("-y, --yes", "Skip the confirmation prompt").option("-o, --output <format>", "Output format (table/json)", "table").action(async (orderNo, code, qty, price2, options) => {
|
|
1668
|
+
try {
|
|
1669
|
+
const stk = normalizeStockCode(code);
|
|
1670
|
+
parseIntStrict(qty, "qty");
|
|
1671
|
+
parseIntStrict(price2, "price");
|
|
1672
|
+
const exchange = resolveExchange(options.exchange);
|
|
1673
|
+
const def = options.credit ? ENDPOINTS.creditModify : ENDPOINTS.modify;
|
|
1674
|
+
const body = {
|
|
1675
|
+
dmst_stex_tp: exchange,
|
|
1676
|
+
orig_ord_no: orderNo,
|
|
1677
|
+
stk_cd: stk,
|
|
1678
|
+
mdfy_qty: qty,
|
|
1679
|
+
mdfy_uv: price2,
|
|
1680
|
+
mdfy_cond_uv: options.condPrice ?? ""
|
|
1681
|
+
};
|
|
1682
|
+
const ok = await confirm(
|
|
1683
|
+
`
|
|
1684
|
+
MODIFY ${options.credit ? "(credit) " : ""}order ${orderNo} ${stk} \u2192 qty ${qty} @ ${price2} [${exchange}] on ${getEffectiveConfig().env.toUpperCase()}`,
|
|
1685
|
+
options.yes
|
|
1686
|
+
);
|
|
1687
|
+
if (!ok) {
|
|
1688
|
+
console.log("Cancelled.");
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1691
|
+
await submit(def, body, options);
|
|
1692
|
+
} catch (err) {
|
|
1693
|
+
handleError(err);
|
|
1694
|
+
}
|
|
1695
|
+
});
|
|
1696
|
+
order.command("cancel <orderNo> <code>").description("Cancel a resting order").option("-q, --qty <qty>", "Quantity to cancel; 0 = all remaining", "0").option("-x, --exchange <KRX|NXT|SOR>", "Exchange routing (must match original)", "KRX").option("--credit", "Cancel a credit order").option("-y, --yes", "Skip the confirmation prompt").option("-o, --output <format>", "Output format (table/json)", "table").action(async (orderNo, code, options) => {
|
|
1697
|
+
try {
|
|
1698
|
+
const stk = normalizeStockCode(code);
|
|
1699
|
+
parseIntStrict(options.qty, "qty");
|
|
1700
|
+
const exchange = resolveExchange(options.exchange);
|
|
1701
|
+
const def = options.credit ? ENDPOINTS.creditCancel : ENDPOINTS.cancel;
|
|
1702
|
+
const body = {
|
|
1703
|
+
dmst_stex_tp: exchange,
|
|
1704
|
+
orig_ord_no: orderNo,
|
|
1705
|
+
stk_cd: stk,
|
|
1706
|
+
cncl_qty: options.qty
|
|
1707
|
+
};
|
|
1708
|
+
const qtyLabel = options.qty === "0" ? "ALL remaining" : options.qty;
|
|
1709
|
+
const ok = await confirm(
|
|
1710
|
+
`
|
|
1711
|
+
CANCEL ${options.credit ? "(credit) " : ""}order ${orderNo} ${stk} \u2192 ${qtyLabel} [${exchange}] on ${getEffectiveConfig().env.toUpperCase()}`,
|
|
1712
|
+
options.yes
|
|
1713
|
+
);
|
|
1714
|
+
if (!ok) {
|
|
1715
|
+
console.log("Cancelled.");
|
|
1716
|
+
return;
|
|
1717
|
+
}
|
|
1718
|
+
await submit(def, body, options);
|
|
1719
|
+
} catch (err) {
|
|
1720
|
+
handleError(err);
|
|
1721
|
+
}
|
|
1722
|
+
});
|
|
1723
|
+
}
|
|
1724
|
+
async function placeOrder(side, code, qty, options) {
|
|
1725
|
+
const stk = normalizeStockCode(code);
|
|
1726
|
+
parseIntStrict(qty, "qty");
|
|
1727
|
+
const exchange = resolveExchange(options.exchange);
|
|
1728
|
+
const { trde_tp, ord_uv } = resolveOrderType(options.type, options.price);
|
|
1729
|
+
const credit = Boolean(options.credit);
|
|
1730
|
+
const def = credit ? side === "buy" ? ENDPOINTS.creditBuy : ENDPOINTS.creditSell : side === "buy" ? ENDPOINTS.buy : ENDPOINTS.sell;
|
|
1731
|
+
const cond_uv = options.condPrice ?? "";
|
|
1732
|
+
if (trde_tp === "28" && !cond_uv) {
|
|
1733
|
+
throw new ActionableError("Stop-limit orders (type 28) require --cond-price <trigger>.");
|
|
1734
|
+
}
|
|
1735
|
+
const body = {
|
|
1736
|
+
dmst_stex_tp: exchange,
|
|
1737
|
+
stk_cd: stk,
|
|
1738
|
+
ord_qty: qty,
|
|
1739
|
+
ord_uv,
|
|
1740
|
+
trde_tp,
|
|
1741
|
+
cond_uv
|
|
1742
|
+
};
|
|
1743
|
+
if (credit && side === "sell") {
|
|
1744
|
+
body.crd_deal_tp = options.creditDeal ?? "33";
|
|
1745
|
+
if (options.loanDate) body.crd_loan_dt = options.loanDate;
|
|
1746
|
+
}
|
|
1747
|
+
const priceLabel = MARKET_ORDER_TYPES.has(trde_tp) ? "MARKET" : `@ ${ord_uv}`;
|
|
1748
|
+
const env = getEffectiveConfig().env;
|
|
1749
|
+
const summary = `
|
|
1750
|
+
${side.toUpperCase()} ${credit ? "(credit) " : ""}${stk} qty ${qty} ${priceLabel} type ${trde_tp}(${ORDER_TYPES[trde_tp]}) [${exchange}] on ${env.toUpperCase()}` + (env === "real" ? " \u26A0 REAL MONEY" : "");
|
|
1751
|
+
const ok = await confirm(summary, options.yes);
|
|
1752
|
+
if (!ok) {
|
|
1753
|
+
console.log("Cancelled.");
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1756
|
+
await submit(def, body, options);
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
// src/commands/ranking.ts
|
|
1760
|
+
function formatRankRow(row) {
|
|
1761
|
+
return formatFields(row, {
|
|
1762
|
+
stk_cd: (v) => v,
|
|
1763
|
+
stk_nm: (v) => v,
|
|
1764
|
+
cur_prc: unpad,
|
|
1765
|
+
flu_rt: unpad,
|
|
1766
|
+
pred_pre: unpad,
|
|
1767
|
+
trde_qty: won,
|
|
1768
|
+
now_trde_qty: won,
|
|
1769
|
+
trde_prica: won,
|
|
1770
|
+
trde_amt: won,
|
|
1771
|
+
sdnin_rt: unpad
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1774
|
+
function registerRankingCommands(program2) {
|
|
1775
|
+
const ranking = program2.command("ranking").alias("rank").description("Ranking lists \u2014 gainers, volume, value");
|
|
1776
|
+
ranking.command("fluctuation").alias("gainers").description("Price change rate ranking (ka10027)").option("-m, --market <000|001|101>", "Market: 000=all, 001=KOSPI, 101=KOSDAQ", "000").option("-x, --exchange <1|2|3>", "Exchange: 1=KRX, 2=NXT, 3=unified", "3").option("-s, --sort <1-5>", "Sort: 1=\uC0C1\uC2B9\uB960, 2=\uC0C1\uC2B9\uD3ED, 3=\uD558\uB77D\uB960, 4=\uD558\uB77D\uD3ED, 5=\uBCF4\uD569", "1").option("-o, --output <format>", "Output format (table/json)", "table").action(async (options) => {
|
|
1777
|
+
try {
|
|
1778
|
+
const client = createClient();
|
|
1779
|
+
const { data } = await client.callEndpoint(ENDPOINTS.rankFluctuation, {
|
|
1780
|
+
mrkt_tp: options.market,
|
|
1781
|
+
sort_tp: options.sort || "1",
|
|
1782
|
+
trde_qty_cnd: "0000",
|
|
1783
|
+
stk_cnd: "0",
|
|
1784
|
+
crd_cnd: "0",
|
|
1785
|
+
updown_incls: "1",
|
|
1786
|
+
pric_cnd: "0",
|
|
1787
|
+
trde_prica_cnd: "0",
|
|
1788
|
+
stex_tp: options.exchange
|
|
1789
|
+
});
|
|
1790
|
+
emitList(data, ENDPOINTS.rankFluctuation.listKey, getOutputFormat(options), formatRankRow);
|
|
1791
|
+
} catch (err) {
|
|
1792
|
+
handleError(err);
|
|
1793
|
+
}
|
|
1794
|
+
});
|
|
1795
|
+
ranking.command("volume").description("Today volume ranking (ka10030)").option("-m, --market <000|001|101>", "Market: 000=all, 001=KOSPI, 101=KOSDAQ", "000").option("-x, --exchange <1|2|3>", "Exchange: 1=KRX, 2=NXT, 3=unified", "3").option("-s, --sort <1-3>", "Sort: 1=\uAC70\uB798\uB7C9, 2=\uAC70\uB798\uD68C\uC804\uC728, 3=\uAC70\uB798\uB300\uAE08", "1").option("-o, --output <format>", "Output format (table/json)", "table").action(async (options) => {
|
|
1796
|
+
try {
|
|
1797
|
+
const client = createClient();
|
|
1798
|
+
const { data } = await client.callEndpoint(ENDPOINTS.rankVolume, {
|
|
1799
|
+
mrkt_tp: options.market,
|
|
1800
|
+
sort_tp: options.sort || "1",
|
|
1801
|
+
mang_stk_incls: "0",
|
|
1802
|
+
crd_tp: "0",
|
|
1803
|
+
trde_qty_tp: "0",
|
|
1804
|
+
pric_tp: "0",
|
|
1805
|
+
trde_prica_tp: "0",
|
|
1806
|
+
mrkt_open_tp: "0",
|
|
1807
|
+
stex_tp: options.exchange
|
|
1808
|
+
});
|
|
1809
|
+
emitList(data, ENDPOINTS.rankVolume.listKey, getOutputFormat(options), formatRankRow);
|
|
1810
|
+
} catch (err) {
|
|
1811
|
+
handleError(err);
|
|
1812
|
+
}
|
|
1813
|
+
});
|
|
1814
|
+
ranking.command("amount").alias("value").description("Trade value (\uAC70\uB798\uB300\uAE08) ranking (ka10032)").option("-m, --market <000|001|101>", "Market: 000=all, 001=KOSPI, 101=KOSDAQ", "000").option("-x, --exchange <1|2|3>", "Exchange: 1=KRX, 2=NXT, 3=unified", "3").option("-o, --output <format>", "Output format (table/json)", "table").action(async (options) => {
|
|
1815
|
+
try {
|
|
1816
|
+
const client = createClient();
|
|
1817
|
+
const { data } = await client.callEndpoint(ENDPOINTS.rankTradeAmount, {
|
|
1818
|
+
mrkt_tp: options.market,
|
|
1819
|
+
mang_stk_incls: "1",
|
|
1820
|
+
stex_tp: options.exchange
|
|
1821
|
+
});
|
|
1822
|
+
emitList(data, ENDPOINTS.rankTradeAmount.listKey, getOutputFormat(options), formatRankRow);
|
|
1823
|
+
} catch (err) {
|
|
1824
|
+
handleError(err);
|
|
1825
|
+
}
|
|
1826
|
+
});
|
|
1827
|
+
ranking.command("surge").description("Volume surge (\uAC70\uB798\uB7C9\uAE09\uC99D) ranking (ka10023)").option("-m, --market <000|001|101>", "Market: 000=all, 001=KOSPI, 101=KOSDAQ", "000").option("-x, --exchange <1|2|3>", "Exchange: 1=KRX, 2=NXT, 3=unified", "3").option("-s, --sort <1-4>", "Sort type", "1").option("-o, --output <format>", "Output format (table/json)", "table").action(async (options) => {
|
|
1828
|
+
try {
|
|
1829
|
+
const client = createClient();
|
|
1830
|
+
const { data } = await client.callEndpoint(ENDPOINTS.rankVolumeSurge, {
|
|
1831
|
+
mrkt_tp: options.market,
|
|
1832
|
+
sort_tp: options.sort || "1",
|
|
1833
|
+
tm_tp: "2",
|
|
1834
|
+
trde_qty_tp: "5",
|
|
1835
|
+
tm: "",
|
|
1836
|
+
stk_cnd: "0",
|
|
1837
|
+
pric_tp: "0",
|
|
1838
|
+
stex_tp: options.exchange
|
|
1839
|
+
});
|
|
1840
|
+
emitList(data, ENDPOINTS.rankVolumeSurge.listKey, getOutputFormat(options), formatRankRow);
|
|
1841
|
+
} catch (err) {
|
|
1842
|
+
handleError(err);
|
|
1843
|
+
}
|
|
1844
|
+
});
|
|
1845
|
+
ranking.command("prev-volume").description("Previous-day volume ranking (ka10031)").option("-m, --market <000|001|101>", "Market: 000=all, 001=KOSPI, 101=KOSDAQ", "000").option("-x, --exchange <1|2|3>", "Exchange: 1=KRX, 2=NXT, 3=unified", "3").option("--query <1|2>", "Query type", "1").option("--from <n>", "Rank start", "0").option("--to <n>", "Rank end", "100").option("-o, --output <format>", "Output format (table/json)", "table").action(async (options) => {
|
|
1846
|
+
try {
|
|
1847
|
+
const client = createClient();
|
|
1848
|
+
const { data } = await client.callEndpoint(ENDPOINTS.rankPrevVolume, {
|
|
1849
|
+
mrkt_tp: options.market,
|
|
1850
|
+
qry_tp: options.query || "1",
|
|
1851
|
+
rank_strt: options.from || "0",
|
|
1852
|
+
rank_end: options.to || "100",
|
|
1853
|
+
stex_tp: options.exchange
|
|
1854
|
+
});
|
|
1855
|
+
emitList(data, ENDPOINTS.rankPrevVolume.listKey, getOutputFormat(options), formatRankRow);
|
|
1856
|
+
} catch (err) {
|
|
1857
|
+
handleError(err);
|
|
1858
|
+
}
|
|
1859
|
+
});
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
// src/commands/sector.ts
|
|
1863
|
+
function registerSectorCommands(program2) {
|
|
1864
|
+
const sector = program2.command("sector").alias("industry").description("Sector / industry indices");
|
|
1865
|
+
const withSharedOptions = (cmd) => cmd.option("-m, --market <0|1|2>", "Market: 0=KOSPI, 1=KOSDAQ, 2=KOSPI200", "0").option("-c, --code <inds_cd>", "Industry code (001=\uC885\uD569KOSPI, 101=\uC885\uD569KOSDAQ)", "001").option("-o, --output <format>", "Output format (table/json)", "table");
|
|
1866
|
+
withSharedOptions(sector.command("price").description("Sector index snapshot (ka20001)")).action(
|
|
1867
|
+
async (options) => {
|
|
1868
|
+
try {
|
|
1869
|
+
const client = createClient();
|
|
1870
|
+
const { data } = await client.callEndpoint(ENDPOINTS.sectorPrice, {
|
|
1871
|
+
mrkt_tp: options.market || "0",
|
|
1872
|
+
inds_cd: options.code || "001"
|
|
1873
|
+
});
|
|
1874
|
+
const fmt = getOutputFormat(options);
|
|
1875
|
+
if (fmt === "json") {
|
|
1876
|
+
output(data, "json");
|
|
1877
|
+
return;
|
|
1878
|
+
}
|
|
1879
|
+
output(
|
|
1880
|
+
{
|
|
1881
|
+
price: unpad(data.cur_prc),
|
|
1882
|
+
change: unpad(data.pred_pre),
|
|
1883
|
+
changeRate: `${unpad(data.flu_rt)}%`,
|
|
1884
|
+
open: unpad(data.open_pric),
|
|
1885
|
+
high: unpad(data.high_pric),
|
|
1886
|
+
low: unpad(data.low_pric),
|
|
1887
|
+
volume: won(data.trde_qty),
|
|
1888
|
+
rising: data.rising,
|
|
1889
|
+
falling: data.fall
|
|
1890
|
+
},
|
|
1891
|
+
"table"
|
|
1892
|
+
);
|
|
1893
|
+
emitList(
|
|
1894
|
+
data,
|
|
1895
|
+
ENDPOINTS.sectorPrice.listKey,
|
|
1896
|
+
"table",
|
|
1897
|
+
(row) => formatFields(row, {
|
|
1898
|
+
tm_n: formatStamp,
|
|
1899
|
+
cur_prc_n: unpad,
|
|
1900
|
+
trde_qty_n: won
|
|
1901
|
+
})
|
|
1902
|
+
);
|
|
1903
|
+
} catch (err) {
|
|
1904
|
+
handleError(err);
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
);
|
|
1908
|
+
withSharedOptions(
|
|
1909
|
+
sector.command("stocks").description("Stock prices within a sector (ka20002)")
|
|
1910
|
+
).action(async (options) => {
|
|
1911
|
+
try {
|
|
1912
|
+
const client = createClient();
|
|
1913
|
+
const { data } = await client.callEndpoint(ENDPOINTS.sectorStocks, {
|
|
1914
|
+
mrkt_tp: options.market || "0",
|
|
1915
|
+
inds_cd: options.code || "001",
|
|
1916
|
+
stex_tp: "1"
|
|
1917
|
+
});
|
|
1918
|
+
emitList(
|
|
1919
|
+
data,
|
|
1920
|
+
ENDPOINTS.sectorStocks.listKey,
|
|
1921
|
+
getOutputFormat(options),
|
|
1922
|
+
(row) => formatFields(row, {
|
|
1923
|
+
stk_cd: (v) => v,
|
|
1924
|
+
stk_nm: (v) => v,
|
|
1925
|
+
cur_prc: unpad,
|
|
1926
|
+
flu_rt: unpad,
|
|
1927
|
+
now_trde_qty: won,
|
|
1928
|
+
open_pric: unpad,
|
|
1929
|
+
high_pric: unpad,
|
|
1930
|
+
low_pric: unpad
|
|
1931
|
+
})
|
|
1932
|
+
);
|
|
1933
|
+
} catch (err) {
|
|
1934
|
+
handleError(err);
|
|
1935
|
+
}
|
|
1936
|
+
});
|
|
1937
|
+
withSharedOptions(
|
|
1938
|
+
sector.command("all").description("All sector indices (ka20003)")
|
|
1939
|
+
).action(async (options) => {
|
|
1940
|
+
try {
|
|
1941
|
+
const client = createClient();
|
|
1942
|
+
const { data } = await client.callEndpoint(ENDPOINTS.sectorAllIndex, {
|
|
1943
|
+
inds_cd: options.code || "001"
|
|
1944
|
+
});
|
|
1945
|
+
emitList(
|
|
1946
|
+
data,
|
|
1947
|
+
ENDPOINTS.sectorAllIndex.listKey,
|
|
1948
|
+
getOutputFormat(options),
|
|
1949
|
+
(row) => formatFields(row, {
|
|
1950
|
+
stk_cd: (v) => v,
|
|
1951
|
+
stk_nm: (v) => v,
|
|
1952
|
+
cur_prc: unpad,
|
|
1953
|
+
flu_rt: unpad,
|
|
1954
|
+
trde_qty: won,
|
|
1955
|
+
trde_prica: won
|
|
1956
|
+
})
|
|
1957
|
+
);
|
|
1958
|
+
} catch (err) {
|
|
1959
|
+
handleError(err);
|
|
1960
|
+
}
|
|
1961
|
+
});
|
|
1962
|
+
withSharedOptions(
|
|
1963
|
+
sector.command("daily").description("Sector index daily history (ka20009)")
|
|
1964
|
+
).action(async (options) => {
|
|
1965
|
+
try {
|
|
1966
|
+
const client = createClient();
|
|
1967
|
+
const { data } = await client.callEndpoint(ENDPOINTS.sectorDaily, {
|
|
1968
|
+
mrkt_tp: options.market || "0",
|
|
1969
|
+
inds_cd: options.code || "001"
|
|
1970
|
+
});
|
|
1971
|
+
const fmt = getOutputFormat(options);
|
|
1972
|
+
if (fmt === "json") {
|
|
1973
|
+
output(data, "json");
|
|
1974
|
+
return;
|
|
1975
|
+
}
|
|
1976
|
+
output(
|
|
1977
|
+
{
|
|
1978
|
+
price: unpad(data.cur_prc),
|
|
1979
|
+
changeRate: `${unpad(data.flu_rt)}%`,
|
|
1980
|
+
open: unpad(data.open_pric),
|
|
1981
|
+
high: unpad(data.high_pric),
|
|
1982
|
+
low: unpad(data.low_pric)
|
|
1983
|
+
},
|
|
1984
|
+
"table"
|
|
1985
|
+
);
|
|
1986
|
+
emitList(
|
|
1987
|
+
data,
|
|
1988
|
+
ENDPOINTS.sectorDaily.listKey,
|
|
1989
|
+
"table",
|
|
1990
|
+
(row) => formatFields(row, {
|
|
1991
|
+
dt_n: formatStamp,
|
|
1992
|
+
cur_prc_n: unpad,
|
|
1993
|
+
flu_rt_n: unpad,
|
|
1994
|
+
acc_trde_qty_n: won
|
|
1995
|
+
})
|
|
1996
|
+
);
|
|
1997
|
+
} catch (err) {
|
|
1998
|
+
handleError(err);
|
|
1999
|
+
}
|
|
2000
|
+
});
|
|
2001
|
+
withSharedOptions(
|
|
2002
|
+
sector.command("codes").description("Valid industry codes per market (ka10101)")
|
|
2003
|
+
).action(async (options) => {
|
|
2004
|
+
try {
|
|
2005
|
+
const client = createClient();
|
|
2006
|
+
const { data } = await client.callEndpoint(
|
|
2007
|
+
ENDPOINTS.sectorCodeList,
|
|
2008
|
+
{ mrkt_tp: options.market || "0" },
|
|
2009
|
+
{ paginate: true }
|
|
2010
|
+
);
|
|
2011
|
+
emitList(
|
|
2012
|
+
data,
|
|
2013
|
+
ENDPOINTS.sectorCodeList.listKey,
|
|
2014
|
+
getOutputFormat(options),
|
|
2015
|
+
(row) => formatFields(row, {
|
|
2016
|
+
marketCode: (v) => v,
|
|
2017
|
+
code: (v) => v,
|
|
2018
|
+
name: (v) => v,
|
|
2019
|
+
group: (v) => v
|
|
2020
|
+
})
|
|
2021
|
+
);
|
|
2022
|
+
} catch (err) {
|
|
2023
|
+
handleError(err);
|
|
2024
|
+
}
|
|
2025
|
+
});
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
// src/index.ts
|
|
2029
|
+
var program = new import_commander.Command();
|
|
2030
|
+
program.name("kiwoom-cli").description("CLI for Kiwoom Securities (\uD0A4\uC6C0\uC99D\uAD8C) \u2014 quotes, charts, account, and orders").version("0.1.0");
|
|
2031
|
+
program.exitOverride((err) => {
|
|
2032
|
+
if (err.code === "commander.helpDisplayed" || err.code === "commander.version") {
|
|
2033
|
+
process.exit(0);
|
|
2034
|
+
}
|
|
2035
|
+
process.exit(1);
|
|
2036
|
+
});
|
|
2037
|
+
registerConfigCommands(program);
|
|
2038
|
+
registerAuthCommands(program);
|
|
2039
|
+
registerStockCommands(program);
|
|
2040
|
+
registerMarketCommands(program);
|
|
2041
|
+
registerChartCommands(program);
|
|
2042
|
+
registerAccountCommands(program);
|
|
2043
|
+
registerOrderCommands(program);
|
|
2044
|
+
registerRankingCommands(program);
|
|
2045
|
+
registerSectorCommands(program);
|
|
2046
|
+
program.parseAsync(process.argv).catch(() => {
|
|
2047
|
+
process.exit(1);
|
|
2048
|
+
});
|
|
2049
|
+
//# sourceMappingURL=index.js.map
|