@2oolkit/kiwoom-cli 0.1.0 → 0.1.2

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/dist/mcp.js CHANGED
@@ -84,6 +84,14 @@ var ORDER_TYPES = {
84
84
  "81": "\uC7A5\uB9C8\uAC10\uD6C4\uC2DC\uAC04\uC678"
85
85
  };
86
86
  var MARKET_ORDER_TYPES = /* @__PURE__ */ new Set(["3", "13", "23"]);
87
+ var CHART_PER_PAGE_CAP = {
88
+ tick: 900,
89
+ minute: 900,
90
+ day: 600,
91
+ week: 300,
92
+ month: 240,
93
+ year: 30
94
+ };
87
95
 
88
96
  // src/config/store.ts
89
97
  var fs = __toESM(require("fs"));
@@ -374,7 +382,7 @@ var KiwoomClient = class {
374
382
  */
375
383
  async callEndpoint(def, body = {}, opts = {}) {
376
384
  if (opts.paginate && def.listKey) {
377
- const data = await this.requestAll(def.apiId, def.path, body, def.listKey);
385
+ const data = await this.requestAll(def.apiId, def.path, body, def.listKey, opts.maxPages);
378
386
  return { data, contYn: false, nextKey: "" };
379
387
  }
380
388
  return this.request(def.apiId, def.path, body, {
@@ -384,9 +392,11 @@ var KiwoomClient = class {
384
392
  }
385
393
  /**
386
394
  * Fetch all pages of a TR, concatenating the array under `listKey`.
387
- * Caps at `maxPages` to avoid runaway loops.
395
+ * Caps at `maxPages` to avoid runaway loops (default 100 — high enough for
396
+ * large chart pulls; callers pass a tighter bound when they know how many
397
+ * pages a target row count needs).
388
398
  */
389
- async requestAll(apiId, path2, body, listKey, maxPages = 20) {
399
+ async requestAll(apiId, path2, body, listKey, maxPages = 100) {
390
400
  let page = await this.request(apiId, path2, body);
391
401
  const acc = Array.isArray(page.data[listKey]) ? [...page.data[listKey]] : [];
392
402
  let pages = 1;
@@ -508,6 +518,7 @@ var ENDPOINTS = {
508
518
  rankTradeAmount: { apiId: "ka10032", path: PATHS.rkinfo, korean: "\uAC70\uB798\uB300\uAE08\uC0C1\uC704\uC694\uCCAD", listKey: "trde_prica_upper" },
509
519
  rankVolumeSurge: { apiId: "ka10023", path: PATHS.rkinfo, korean: "\uAC70\uB798\uB7C9\uAE09\uC99D\uC694\uCCAD", listKey: "trde_qty_sdnin" },
510
520
  rankPrevVolume: { apiId: "ka10031", path: PATHS.rkinfo, korean: "\uC804\uC77C\uAC70\uB798\uB7C9\uC0C1\uC704\uC694\uCCAD", listKey: "pred_trde_qty_upper" },
521
+ rankForeignInst: { apiId: "ka90009", path: PATHS.rkinfo, korean: "\uC678\uAD6D\uC778\uAE30\uAD00\uB9E4\uB9E4\uC0C1\uC704\uC694\uCCAD", listKey: "frgnr_orgn_trde_upper" },
511
522
  // ── Sector / industry (업종) ───────────────────────────────────────────────
512
523
  sectorPrice: { apiId: "ka20001", path: PATHS.sect, korean: "\uC5C5\uC885\uD604\uC7AC\uAC00\uC694\uCCAD", listKey: "inds_cur_prc_tm" },
513
524
  sectorStocks: { apiId: "ka20002", path: PATHS.sect, korean: "\uC5C5\uC885\uBCC4\uC8FC\uAC00\uC694\uCCAD", listKey: "inds_stkpc" },
@@ -526,6 +537,15 @@ function unpad(value) {
526
537
  const intPart = m[2].replace(/^0+(?=\d)/, "");
527
538
  return `${sign}${intPart}${m[3] ?? ""}`;
528
539
  }
540
+ function withCommas(value) {
541
+ const m = value.match(/^([+-]?)(\d+)(\.\d+)?$/);
542
+ if (!m) return value;
543
+ const grouped = m[2].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
544
+ return `${m[1]}${grouped}${m[3] ?? ""}`;
545
+ }
546
+ function won(value) {
547
+ return withCommas(unpad(value));
548
+ }
529
549
  function formatStamp(value) {
530
550
  if (!value) return "";
531
551
  const s = String(value).trim();
@@ -687,25 +707,30 @@ var PERIOD_EP = {
687
707
  month: ENDPOINTS.monthlyChart,
688
708
  year: ENDPOINTS.yearlyChart
689
709
  };
710
+ var DEFAULT_COUNT = 50;
711
+ var MAX_COUNT = 1e5;
690
712
  function registerChartTools(server2) {
691
713
  tool(
692
714
  server2,
693
715
  "get_chart",
694
716
  {
695
- description: "Get OHLC chart data for a stock. Period charts (day/week/month/year) end at base date; tick/minute use an aggregation scope. Returns latest-first; use count to cap rows.",
717
+ description: "Get OHLC chart data for a stock. Period charts (day/week/month/year) end at base date; tick/minute use an aggregation scope. Returns latest-first; use count to cap rows. Per-request caps: tick/minute 900, day 600, week 300, month 240, year 30. When count exceeds the cap the tool auto-paginates (cont-yn/next-key) and returns up to count rows.",
696
718
  inputSchema: {
697
719
  code: import_zod2.z.string().describe("6-digit stock code"),
698
720
  timeframe: import_zod2.z.enum(["tick", "minute", "day", "week", "month", "year"]).describe("Chart timeframe"),
699
721
  scope: import_zod2.z.string().optional().describe("Tick units (1/3/5/10/30) or minute interval (1/3/5/10/15/30/45/60); default 1"),
700
722
  date: import_zod2.z.string().optional().describe("Base date YYYYMMDD for period charts (default today)"),
701
723
  adjusted: import_zod2.z.boolean().optional().describe("Adjust for splits/rights (default true)"),
702
- count: import_zod2.z.number().min(1).max(900).optional().describe("Max rows to return (default 50)")
724
+ count: import_zod2.z.number().min(1).max(MAX_COUNT).optional().describe(
725
+ "Max rows to return (default 50). Exceeding the per-request cap (tick/min 900, day 600, week 300, month 240, year 30) auto-paginates."
726
+ )
703
727
  }
704
728
  },
705
729
  async ({ code, timeframe, scope, date, adjusted, count }) => withErrorHandling(async () => {
706
730
  const client = clientOrThrow();
707
731
  const stk = normalizeStockCode(code);
708
732
  const upd = adjusted === false ? "0" : "1";
733
+ const want = count ?? DEFAULT_COUNT;
709
734
  let def;
710
735
  let body;
711
736
  if (timeframe === "tick") {
@@ -718,12 +743,15 @@ function registerChartTools(server2) {
718
743
  def = PERIOD_EP[timeframe];
719
744
  body = { stk_cd: stk, base_dt: date ?? todayKst(), upd_stkpc_tp: upd };
720
745
  }
721
- const { data } = await client.callEndpoint(def, body);
746
+ const cap = CHART_PER_PAGE_CAP[timeframe];
747
+ const paginate = want > cap;
748
+ const maxPages = paginate ? Math.max(1, Math.ceil(want / cap)) : 1;
749
+ const { data } = await client.callEndpoint(def, body, { paginate, maxPages });
722
750
  const rows = Array.isArray(data[def.listKey]) ? data[def.listKey] : [];
723
751
  return mcpJson({
724
752
  code: stk,
725
753
  timeframe,
726
- rows: rows.slice(0, count ?? 50)
754
+ rows: rows.slice(0, want)
727
755
  });
728
756
  })
729
757
  );
@@ -968,6 +996,28 @@ ${JSON.stringify(preview, null, 2)}`);
968
996
 
969
997
  // src/mcp/tools/ranking.ts
970
998
  var import_zod5 = require("zod");
999
+
1000
+ // src/utils/ranking.ts
1001
+ var NETTRADE_FIELDS = {
1002
+ foreign: { buy: "for_netprps", sell: "for_netslmt" },
1003
+ institution: { buy: "orgn_netprps", sell: "orgn_netslmt" }
1004
+ };
1005
+ function extractNetTrade(rows, prefix, n) {
1006
+ return rows.slice(0, n).map((r, i) => ({
1007
+ rank: i + 1,
1008
+ code: r[`${prefix}_stk_cd`],
1009
+ name: r[`${prefix}_stk_nm`],
1010
+ \uAE08\uC561: won(unpad(r[`${prefix}_amt`] ?? "")),
1011
+ \uC218\uB7C9: won(unpad(r[`${prefix}_qty`] ?? ""))
1012
+ })).filter((x) => x.code);
1013
+ }
1014
+ function buildNetTradeResult(rows, investors, side, n) {
1015
+ const result = {};
1016
+ for (const inv of investors) result[inv] = extractNetTrade(rows, NETTRADE_FIELDS[inv][side], n);
1017
+ return result;
1018
+ }
1019
+
1020
+ // src/mcp/tools/ranking.ts
971
1021
  function registerRankingTools(server2) {
972
1022
  tool(
973
1023
  server2,
@@ -1040,6 +1090,39 @@ function registerRankingTools(server2) {
1040
1090
  return mcpJson(data);
1041
1091
  })
1042
1092
  );
1093
+ tool(
1094
+ server2,
1095
+ "get_net_buy_ranking",
1096
+ {
1097
+ description: "Get the foreign/institution net-buy (\uC218\uAE09) ranking (ka90009): top stocks by foreign and/or institutional net buying or selling. One call returns parallel rankings for both investor types.",
1098
+ inputSchema: {
1099
+ by: import_zod5.z.enum(["foreign", "institution", "both"]).optional().describe("Investor type: foreign=\uC678\uAD6D\uC778, institution=\uAE30\uAD00, both (default both)"),
1100
+ side: import_zod5.z.enum(["buy", "sell"]).optional().describe("buy=\uC21C\uB9E4\uC218 (default), sell=\uC21C\uB9E4\uB3C4"),
1101
+ market: import_zod5.z.enum(["000", "001", "101"]).optional().describe("000=all, 001=KOSPI, 101=KOSDAQ"),
1102
+ exchange: import_zod5.z.enum(["1", "2", "3"]).optional().describe("1=KRX (default), 2=NXT, 3=unified"),
1103
+ count: import_zod5.z.number().int().min(1).max(50).optional().describe("Top N (default 10)"),
1104
+ rankBy: import_zod5.z.enum(["1", "2"]).optional().describe("Rank by 1=amount \uAE08\uC561 (default), 2=quantity \uC218\uB7C9"),
1105
+ date: import_zod5.z.string().optional().describe("Query date YYYYMMDD (default: latest)")
1106
+ }
1107
+ },
1108
+ async ({ by, side, market, exchange, count, rankBy, date }) => withErrorHandling(async () => {
1109
+ const investors = by === "foreign" ? ["foreign"] : by === "institution" ? ["institution"] : ["foreign", "institution"];
1110
+ const netSide = side === "sell" ? "sell" : "buy";
1111
+ const { data } = await clientOrThrow().callEndpoint(ENDPOINTS.rankForeignInst, {
1112
+ mrkt_tp: market ?? "000",
1113
+ amt_qty_tp: rankBy === "2" ? "2" : "1",
1114
+ qry_dt_tp: date ? "1" : "0",
1115
+ date: date ?? "",
1116
+ stex_tp: exchange ?? "1"
1117
+ });
1118
+ const rows = Array.isArray(data?.[ENDPOINTS.rankForeignInst.listKey]) ? data[ENDPOINTS.rankForeignInst.listKey] : [];
1119
+ return mcpJson({
1120
+ side: netSide,
1121
+ rankBy: rankBy === "2" ? "quantity" : "amount",
1122
+ ...buildNetTradeResult(rows, investors, netSide, count ?? 10)
1123
+ });
1124
+ })
1125
+ );
1043
1126
  tool(
1044
1127
  server2,
1045
1128
  "get_sector",