kiwoom-mcp-server 0.9.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +5 -2
- package/README.md +5 -2
- package/dist/kiwoom/api.js +87 -4
- package/dist/kiwoom/types.js +71 -0
- package/dist/server.js +6 -1
- package/dist/tools/etf-info.js +16 -8
- package/dist/tools/etf-returns.js +60 -0
- package/dist/tools/market-index.js +6 -4
- package/dist/tools/sector.js +135 -0
- package/dist/tools/stock-chart.js +24 -15
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -25,13 +25,16 @@ allowance.
|
|
|
25
25
|
|---|---|---|
|
|
26
26
|
| `search_stock` | Name → code search (KOSPI/KOSDAQ, incl. ETF/ETN) | ka10099 |
|
|
27
27
|
| `get_stock_price` | Current price / change rate / volume / basic metrics | ka10001 |
|
|
28
|
-
| `get_stock_chart` | Daily/weekly/monthly/minute candles (adjusted price) |
|
|
28
|
+
| `get_stock_chart` | Daily/weekly/monthly/yearly/minute/tick candles (adjusted price) | ka10079~83, ka10094 |
|
|
29
29
|
| `get_orderbook` | 10-level ask/bid quotes and sizes | ka10004 |
|
|
30
30
|
| `get_market_index` | KOSPI/KOSDAQ composite and sector indices | ka20003 |
|
|
31
|
+
| `get_sector_price` | Sector index detail (breadth / 52-week range / intraday trend) | ka20001 |
|
|
32
|
+
| `get_sector_stocks` | Member stocks of a sector with quotes | ka20002 |
|
|
31
33
|
| `get_ranking` | Top gainers / losers / volume / trading value | ka10027/30/32 |
|
|
32
34
|
| `get_market_movers` | New highs / new lows / upper & lower limit / surges / plunges | ka10016/17/19 |
|
|
33
35
|
| `get_investor_trend` | Retail / foreign / institutional net-buy trend (period sum + daily) | ka10059, ka10061 |
|
|
34
|
-
| `get_etf_info` | ETF tracking index / tax type / quote | ka40002, ka10001 |
|
|
36
|
+
| `get_etf_info` | ETF tracking index / tax type / quote / NAV & disparity | ka40002, ka10001, ka40009 |
|
|
37
|
+
| `get_etf_returns` | ETF period returns (1w/1m/6m/1y) vs a benchmark index | ka40001 |
|
|
35
38
|
| `get_short_selling` | Per-stock daily short-selling trend (short volume / weight / avg price) | ka10014 |
|
|
36
39
|
| `get_foreign_holding` | Per-stock foreign holding trend (holdings / holding weight / limit-usage rate) | ka10008 |
|
|
37
40
|
|
package/README.md
CHANGED
|
@@ -23,13 +23,16 @@ Claude Desktop / Claude Code에서 자연어로 국내 주식 시세·차트·
|
|
|
23
23
|
|---|---|---|
|
|
24
24
|
| `search_stock` | 종목명→코드 검색 (코스피/코스닥, ETF/ETN 포함) | ka10099 |
|
|
25
25
|
| `get_stock_price` | 현재가/등락률/거래량/기본지표 | ka10001 |
|
|
26
|
-
| `get_stock_chart` |
|
|
26
|
+
| `get_stock_chart` | 일/주/월/년/분/틱봉 캔들 차트 (수정주가 반영) | ka10079~83, ka10094 |
|
|
27
27
|
| `get_orderbook` | 10단계 매도/매수 호가·잔량 | ka10004 |
|
|
28
28
|
| `get_market_index` | 코스피/코스닥 종합·업종 지수 | ka20003 |
|
|
29
|
+
| `get_sector_price` | 업종 지수 현재가 상세 (등락 구성·52주 고저·시간대별 추이) | ka20001 |
|
|
30
|
+
| `get_sector_stocks` | 업종 구성 종목 시세 (현재가·등락률·거래량·고저가) | ka20002 |
|
|
29
31
|
| `get_ranking` | 상승률/하락률/거래량/거래대금 상위 | ka10027/30/32 |
|
|
30
32
|
| `get_market_movers` | 신고가/신저가/상한가/하한가/급등/급락 특이 종목 | ka10016/17/19 |
|
|
31
33
|
| `get_investor_trend` | 개인/외국인/기관 순매수 동향 (기간 합계 + 일별) | ka10059, ka10061 |
|
|
32
|
-
| `get_etf_info` | ETF
|
|
34
|
+
| `get_etf_info` | ETF 추적지수·과세유형·시세·NAV/괴리율 | ka40002, ka10001, ka40009 |
|
|
35
|
+
| `get_etf_returns` | ETF 기간별(1주/1개월/6개월/1년) 수익률 vs 대상지수 | ka40001 |
|
|
33
36
|
| `get_short_selling` | 종목별 일자별 공매도 추이 (공매도량·비중·평균가) | ka10014 |
|
|
34
37
|
| `get_foreign_holding` | 종목별 외국인 보유 추이 (보유주식수·보유비중·한도소진률) | ka10008 |
|
|
35
38
|
|
package/dist/kiwoom/api.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { sleep } from "../utils/sleep.js";
|
|
3
3
|
import { KiwoomApiError } from "./errors.js";
|
|
4
|
-
import { accountEvaluationResponseSchema, allIndexResponseSchema, dailyChartItemSchema, depositResponseSchema, etfInfoResponseSchema, foreignHoldingResponseSchema, investorDailyItemSchema, investorTotalItemSchema, limitStockItemSchema, minuteChartItemSchema, newHighLowItemSchema, orderbookResponseSchema, pendingOrdersResponseSchema, priceChangeRankItemSchema, priceJumpItemSchema, realizedPnlResponseSchema, shortSellingResponseSchema, stockInfoResponseSchema, stockListResponseSchema, themeGroupsResponseSchema, tradingJournalResponseSchema, themeStocksResponseSchema, transactionsResponseSchema, valueRankItemSchema, volumeRankItemSchema, watchlistGroupDetailResponseSchema, watchlistGroupsResponseSchema, } from "./types.js";
|
|
4
|
+
import { accountEvaluationResponseSchema, allIndexResponseSchema, dailyChartItemSchema, depositResponseSchema, etfInfoResponseSchema, etfNavItemSchema, etfReturnItemSchema, foreignHoldingResponseSchema, investorDailyItemSchema, investorTotalItemSchema, limitStockItemSchema, minuteChartItemSchema, newHighLowItemSchema, orderbookResponseSchema, pendingOrdersResponseSchema, priceChangeRankItemSchema, priceJumpItemSchema, realizedPnlResponseSchema, sectorPriceResponseSchema, sectorStocksResponseSchema, shortSellingResponseSchema, stockInfoResponseSchema, stockListResponseSchema, themeGroupsResponseSchema, tradingJournalResponseSchema, themeStocksResponseSchema, transactionsResponseSchema, valueRankItemSchema, volumeRankItemSchema, watchlistGroupDetailResponseSchema, watchlistGroupsResponseSchema, } from "./types.js";
|
|
5
5
|
const STOCK_INFO_PATH = "/api/dostk/stkinfo";
|
|
6
6
|
const ACCOUNT_PATH = "/api/dostk/acnt";
|
|
7
7
|
const CHART_PATH = "/api/dostk/chart";
|
|
@@ -136,11 +136,13 @@ const CHART_TRS = {
|
|
|
136
136
|
day: { apiId: "ka10081", arrayKey: "stk_dt_pole_chart_qry" },
|
|
137
137
|
week: { apiId: "ka10082", arrayKey: "stk_stk_pole_chart_qry" },
|
|
138
138
|
month: { apiId: "ka10083", arrayKey: "stk_mth_pole_chart_qry" },
|
|
139
|
+
year: { apiId: "ka10094", arrayKey: "stk_yr_pole_chart_qry" },
|
|
139
140
|
};
|
|
140
141
|
/**
|
|
141
|
-
* ka10081/82/83
|
|
142
|
-
*
|
|
143
|
-
*
|
|
142
|
+
* ka10081/82/83/94 일·주·월·년봉 — same item shape, array key differs per TR
|
|
143
|
+
* (년봉 rows carry no pred_pre — schema defaults cover it). Newest first;
|
|
144
|
+
* first page (년봉 ~30, others 240~600 rows) only, which is plenty for LLM
|
|
145
|
+
* consumption and avoids pagination rate-limit cost. upd_stkpc_tp "1" = 수정주가 반영.
|
|
144
146
|
*/
|
|
145
147
|
export async function fetchDailyChart(client, stockCode, period, baseDate) {
|
|
146
148
|
const { apiId, arrayKey } = CHART_TRS[period];
|
|
@@ -160,6 +162,19 @@ export async function fetchMinuteChart(client, stockCode, ticScope) {
|
|
|
160
162
|
});
|
|
161
163
|
return parseArray(res.json, "stk_min_pole_chart_qry", minuteChartItemSchema);
|
|
162
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* ka10079 틱차트 — tic_scope = 캔들당 틱 수 ("1"|"3"|"5"|"10"|"30"). Row shape는
|
|
167
|
+
* ka10080 분봉과 동일(cntr_tm 포함; top-level last_tic_cnt는 미소비). Newest first,
|
|
168
|
+
* first page (900 rows) only.
|
|
169
|
+
*/
|
|
170
|
+
export async function fetchTickChart(client, stockCode, ticScope) {
|
|
171
|
+
const res = await client.call({
|
|
172
|
+
path: CHART_PATH,
|
|
173
|
+
apiId: "ka10079",
|
|
174
|
+
body: { stk_cd: stockCode, tic_scope: ticScope, upd_stkpc_tp: "1" },
|
|
175
|
+
});
|
|
176
|
+
return parseArray(res.json, "stk_tic_chart_qry", minuteChartItemSchema);
|
|
177
|
+
}
|
|
163
178
|
/** ka10004 주식호가 — 10-level orderbook; levels 2-10 live in the loose passthrough. */
|
|
164
179
|
export async function fetchOrderbook(client, stockCode) {
|
|
165
180
|
const res = await client.call({
|
|
@@ -178,6 +193,35 @@ export async function fetchAllIndices(client, indsCd) {
|
|
|
178
193
|
});
|
|
179
194
|
return allIndexResponseSchema.parse(res.json).all_inds_idex;
|
|
180
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* ka20001/ka20002의 mrkt_tp는 inds_cd 선행 자리에서 유도한다
|
|
198
|
+
* (0xx→"0" 코스피, 1xx→"1" 코스닥, 2xx→"2" 코스피200; mock-probed 2026-07-09).
|
|
199
|
+
* inds_cd 자체는 ka20003의 업종코드 공간을 그대로 쓴다.
|
|
200
|
+
*/
|
|
201
|
+
function sectorMarketType(indsCd) {
|
|
202
|
+
return indsCd.startsWith("1") ? "1" : indsCd.startsWith("2") ? "2" : "0";
|
|
203
|
+
}
|
|
204
|
+
/** ka20001 업종현재가 — 업종 지수 스냅샷 + 시간대별 추이. */
|
|
205
|
+
export async function fetchSectorPrice(client, indsCd) {
|
|
206
|
+
const res = await client.call({
|
|
207
|
+
path: SECTOR_PATH,
|
|
208
|
+
apiId: "ka20001",
|
|
209
|
+
body: { mrkt_tp: sectorMarketType(indsCd), inds_cd: indsCd },
|
|
210
|
+
});
|
|
211
|
+
return sectorPriceResponseSchema.parse(res.json);
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* ka20002 업종별주가 — 업종 구성 종목의 시세. 100행/페이지(종목코드순)이며
|
|
215
|
+
* 첫 페이지만 가져온다; 남은 페이지가 있으면 truncated로 알린다.
|
|
216
|
+
*/
|
|
217
|
+
export async function fetchSectorStocks(client, indsCd) {
|
|
218
|
+
const res = await client.call({
|
|
219
|
+
path: SECTOR_PATH,
|
|
220
|
+
apiId: "ka20002",
|
|
221
|
+
body: { mrkt_tp: sectorMarketType(indsCd), inds_cd: indsCd, stex_tp: "1" },
|
|
222
|
+
});
|
|
223
|
+
return { items: sectorStocksResponseSchema.parse(res.json).inds_stkpc, truncated: res.hasNext };
|
|
224
|
+
}
|
|
181
225
|
/** amt_qty_tp: "1"=금액(백만원), "2"=수량(주) — 순매수(trde_tp "0") 기준. */
|
|
182
226
|
const INVESTOR_UNIT_CODES = { amount: "1", quantity: "2" };
|
|
183
227
|
/** ka10061 종목별투자자기관종합 — investor net-buy totals over a date range. */
|
|
@@ -277,6 +321,45 @@ export async function fetchEtfInfo(client, stockCode) {
|
|
|
277
321
|
});
|
|
278
322
|
return etfInfoResponseSchema.parse(res.json);
|
|
279
323
|
}
|
|
324
|
+
/** ka40001의 dt(기간) 코드 — 조회 순서대로 4행 테이블을 만든다. */
|
|
325
|
+
export const ETF_RETURN_PERIODS = [
|
|
326
|
+
{ dt: "0", label: "1주" },
|
|
327
|
+
{ dt: "1", label: "1개월" },
|
|
328
|
+
{ dt: "2", label: "6개월" },
|
|
329
|
+
{ dt: "3", label: "1년" },
|
|
330
|
+
];
|
|
331
|
+
/**
|
|
332
|
+
* ka40001 ETF수익율 — 기간(dt)별 ETF 수익률 + 대상지수 수익률. mock-probed
|
|
333
|
+
* 2026-07-09: etfobjt_idex_cd는 필수(빈값 → 1511 오류)지만 ETF와 대조 검증되지
|
|
334
|
+
* 않는 "벤치마크 지수 선택자"다 — cntr_prft_rt가 그 지수의 기간 수익률로 채워진다.
|
|
335
|
+
* 4개 dt를 순차 호출(같은 TR — 레이트리밋 1.1s 간격)해 기간 순서대로 돌려준다.
|
|
336
|
+
*/
|
|
337
|
+
export async function fetchEtfReturns(client, stockCode, benchmarkIndexCode) {
|
|
338
|
+
const rows = [];
|
|
339
|
+
for (const [i, period] of ETF_RETURN_PERIODS.entries()) {
|
|
340
|
+
if (i > 0)
|
|
341
|
+
await sleep(PAGE_INTERVAL_MS);
|
|
342
|
+
const res = await client.call({
|
|
343
|
+
path: ETF_PATH,
|
|
344
|
+
apiId: "ka40001",
|
|
345
|
+
body: { stk_cd: stockCode, etfobjt_idex_cd: benchmarkIndexCode, dt: period.dt },
|
|
346
|
+
});
|
|
347
|
+
rows.push(parseArray(res.json, "etfprft_rt_lst", etfReturnItemSchema)[0] ?? null);
|
|
348
|
+
}
|
|
349
|
+
return rows;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* ka40009 ETF NAV 추이 — 최신순 배열(etfnavarray)이지만 행에 시각 필드가 없다.
|
|
353
|
+
* mock에서는 NAV 관련 필드가 전부 빈 문자열 — 호출부는 빈값을 허용해야 한다.
|
|
354
|
+
*/
|
|
355
|
+
export async function fetchEtfNav(client, stockCode) {
|
|
356
|
+
const res = await client.call({
|
|
357
|
+
path: ETF_PATH,
|
|
358
|
+
apiId: "ka40009",
|
|
359
|
+
body: { stk_cd: stockCode },
|
|
360
|
+
});
|
|
361
|
+
return parseArray(res.json, "etfnavarray", etfNavItemSchema);
|
|
362
|
+
}
|
|
280
363
|
/**
|
|
281
364
|
* ka01300 관심종목 그룹 리스트 조회 — HTS에 저장된 관심종목 그룹 목록 (읽기 전용).
|
|
282
365
|
* 응답 배열 키는 nofi. 편집(추가/삭제) TR은 키움 REST에 존재하지 않는다.
|
package/dist/kiwoom/types.js
CHANGED
|
@@ -195,6 +195,57 @@ export const allIndexResponseSchema = z.looseObject({
|
|
|
195
195
|
...envelope,
|
|
196
196
|
all_inds_idex: z.array(indexItemSchema).default([]),
|
|
197
197
|
});
|
|
198
|
+
// ── ka20001: 업종현재가 (subset) ──
|
|
199
|
+
/** 시간대별 행 (`*_n` suffix). tm_n "999999"/"888888"은 장마감 집계 센티널 — 표시 시 필터. */
|
|
200
|
+
export const sectorTimeRowSchema = z.looseObject({
|
|
201
|
+
tm_n: str(),
|
|
202
|
+
cur_prc_n: str(),
|
|
203
|
+
pred_pre_n: str(),
|
|
204
|
+
flu_rt_n: str(),
|
|
205
|
+
acc_trde_qty_n: str(), // 누적 거래량 (천주)
|
|
206
|
+
});
|
|
207
|
+
export const sectorPriceResponseSchema = z.looseObject({
|
|
208
|
+
...envelope,
|
|
209
|
+
cur_prc: str(), // 지수 (소수점, 부호 접두)
|
|
210
|
+
pred_pre: str(),
|
|
211
|
+
flu_rt: str(),
|
|
212
|
+
trde_qty: str(), // 천주 (ka20003과 동일 단위)
|
|
213
|
+
trde_prica: str(), // 백만원
|
|
214
|
+
trde_frmatn_stk_num: str(), // 거래형성 종목수
|
|
215
|
+
trde_frmatn_rt: str(), // 거래형성 비율(%), 부호 접두
|
|
216
|
+
open_pric: str(),
|
|
217
|
+
high_pric: str(),
|
|
218
|
+
low_pric: str(),
|
|
219
|
+
upl: str(), // 상한 종목수
|
|
220
|
+
rising: str(),
|
|
221
|
+
stdns: str(), // 보합
|
|
222
|
+
fall: str(),
|
|
223
|
+
lst: str(), // 하한 종목수
|
|
224
|
+
"52wk_hgst_pric": str(),
|
|
225
|
+
"52wk_hgst_pric_dt": str(), // yyyyMMdd
|
|
226
|
+
"52wk_hgst_pric_pre_rt": str(), // 현재가의 52주 최고 대비 등락률
|
|
227
|
+
"52wk_lwst_pric": str(),
|
|
228
|
+
"52wk_lwst_pric_dt": str(),
|
|
229
|
+
"52wk_lwst_pric_pre_rt": str(),
|
|
230
|
+
inds_cur_prc_tm: z.array(sectorTimeRowSchema).default([]),
|
|
231
|
+
});
|
|
232
|
+
// ── ka20002: 업종별주가 (subset) ──
|
|
233
|
+
export const sectorStockItemSchema = z.looseObject({
|
|
234
|
+
stk_cd: str(),
|
|
235
|
+
stk_nm: str(),
|
|
236
|
+
cur_prc: str(),
|
|
237
|
+
pred_pre_sig: str(),
|
|
238
|
+
pred_pre: str(),
|
|
239
|
+
flu_rt: str(),
|
|
240
|
+
now_trde_qty: str(),
|
|
241
|
+
open_pric: str(),
|
|
242
|
+
high_pric: str(),
|
|
243
|
+
low_pric: str(),
|
|
244
|
+
});
|
|
245
|
+
export const sectorStocksResponseSchema = z.looseObject({
|
|
246
|
+
...envelope,
|
|
247
|
+
inds_stkpc: z.array(sectorStockItemSchema).default([]),
|
|
248
|
+
});
|
|
198
249
|
// ── ka10059/ka10061: 종목별 투자자·기관 (13-field investor breakdown) ──
|
|
199
250
|
// amt_qty_tp "1"=금액(백만원), "2"=수량(주) — cross-checked live against volume.
|
|
200
251
|
const investorFields = {
|
|
@@ -259,6 +310,26 @@ export const etfInfoResponseSchema = z.looseObject({
|
|
|
259
310
|
etftxon_type: str(), // ETF 과세유형 ("비과세" 등)
|
|
260
311
|
etntxon_type: str(),
|
|
261
312
|
});
|
|
313
|
+
// ── ka40001: ETF수익율 (array key etfprft_rt_lst; mock-probed 2026-07-09) ──
|
|
314
|
+
// NOTE: cntr_prft_rt is the period return of the index selected by the
|
|
315
|
+
// etfobjt_idex_cd REQUEST field (not validated against the ETF) — probed:
|
|
316
|
+
// same code → same value regardless of stk_cd; bogus code → "0.00".
|
|
317
|
+
export const etfReturnItemSchema = z.looseObject({
|
|
318
|
+
etfprft_rt: str(), // ETF 기간 수익률(%)
|
|
319
|
+
cntr_prft_rt: str(), // 대상지수(etfobjt_idex_cd) 기간 수익률(%)
|
|
320
|
+
for_netprps_qty: str(), // 외인 순매수량
|
|
321
|
+
orgn_netprps_qty: str(), // 기관 순매수량 (mock: blank)
|
|
322
|
+
});
|
|
323
|
+
// ── ka40009: ETF NAV 추이 (array key etfnavarray, newest first, no time field) ──
|
|
324
|
+
// Rows carry NO timestamp; NAV/괴리율/추적오차 fields arrive blank on mock —
|
|
325
|
+
// only stkcnt/base_pric are populated there. Callers must tolerate blanks.
|
|
326
|
+
export const etfNavItemSchema = z.looseObject({
|
|
327
|
+
nav: str(),
|
|
328
|
+
navpred_pre: str(), // NAV 전일대비
|
|
329
|
+
navflu_rt: str(), // NAV 등락률(%)
|
|
330
|
+
trace_eor_rt: str(), // 추적오차율(%)
|
|
331
|
+
dispty_rt: str(), // 괴리율(%)
|
|
332
|
+
});
|
|
262
333
|
// ── ka01300/ka01301: 관심종목 그룹 (HTS 저장 그룹, live-verified 2026-07-06) ──
|
|
263
334
|
// NOTE: array keys are nofi(그룹)/nofj(종목); item fields are terse (gcod/name,
|
|
264
335
|
// cod2/bgb) and differ from the usual snake_case TRs. The response also carries
|
package/dist/server.js
CHANGED
|
@@ -3,6 +3,7 @@ import { isIsaEnabled } from "./config.js";
|
|
|
3
3
|
import { registerAccountBalanceTool } from "./tools/account-balance.js";
|
|
4
4
|
import { registerAccountHoldingsTool } from "./tools/account-holdings.js";
|
|
5
5
|
import { registerEtfInfoTool } from "./tools/etf-info.js";
|
|
6
|
+
import { registerEtfReturnsTool } from "./tools/etf-returns.js";
|
|
6
7
|
import { registerForeignHoldingTool } from "./tools/foreign-holding.js";
|
|
7
8
|
import { registerInvestorTrendTool } from "./tools/investor-trend.js";
|
|
8
9
|
import { registerIsaTaxStatusTool } from "./tools/isa-tax-status.js";
|
|
@@ -12,6 +13,7 @@ import { registerOrderbookTool } from "./tools/orderbook.js";
|
|
|
12
13
|
import { registerPendingOrdersTool } from "./tools/pending-orders.js";
|
|
13
14
|
import { registerPingTool } from "./tools/ping.js";
|
|
14
15
|
import { registerRankingTool } from "./tools/ranking.js";
|
|
16
|
+
import { registerSectorPriceTool, registerSectorStocksTool } from "./tools/sector.js";
|
|
15
17
|
import { registerShortSellingTool } from "./tools/short-selling.js";
|
|
16
18
|
import { registerStockChartTool } from "./tools/stock-chart.js";
|
|
17
19
|
import { registerStockPriceTool } from "./tools/stock-price.js";
|
|
@@ -21,7 +23,7 @@ import { registerTradingJournalTool } from "./tools/trading-journal.js";
|
|
|
21
23
|
import { registerTransactionsTool } from "./tools/transactions.js";
|
|
22
24
|
import { registerWatchlistGroupsTool, registerWatchlistTool } from "./tools/watchlist.js";
|
|
23
25
|
export const SERVER_NAME = "kiwoom-mcp-server";
|
|
24
|
-
export const SERVER_VERSION = "0.
|
|
26
|
+
export const SERVER_VERSION = "0.12.0";
|
|
25
27
|
export function createServer() {
|
|
26
28
|
const server = new McpServer({
|
|
27
29
|
name: SERVER_NAME,
|
|
@@ -34,10 +36,13 @@ export function createServer() {
|
|
|
34
36
|
registerStockChartTool(server);
|
|
35
37
|
registerOrderbookTool(server);
|
|
36
38
|
registerMarketIndexTool(server);
|
|
39
|
+
registerSectorPriceTool(server);
|
|
40
|
+
registerSectorStocksTool(server);
|
|
37
41
|
registerRankingTool(server);
|
|
38
42
|
registerMarketMoversTool(server);
|
|
39
43
|
registerInvestorTrendTool(server);
|
|
40
44
|
registerEtfInfoTool(server);
|
|
45
|
+
registerEtfReturnsTool(server);
|
|
41
46
|
registerShortSellingTool(server);
|
|
42
47
|
registerForeignHoldingTool(server);
|
|
43
48
|
// Watchlist (HTS 저장 관심종목 — read-only; ka01300/ka01301)
|
package/dist/tools/etf-info.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { getKiwoomContext } from "../context.js";
|
|
3
|
-
import { fetchEtfInfo, fetchStockInfo } from "../kiwoom/api.js";
|
|
4
|
-
import { formatKRW, formatPercent, formatQuantity, parseKiwoomNumber, parseKiwoomPrice } from "../utils/num.js";
|
|
3
|
+
import { fetchEtfInfo, fetchEtfNav, fetchStockInfo } from "../kiwoom/api.js";
|
|
4
|
+
import { formatKRW, formatPercent, formatQuantity, formatRatioPercent, formatSigned, parseKiwoomNumber, parseKiwoomPrice, } from "../utils/num.js";
|
|
5
5
|
import { runTool, textResult } from "./helpers.js";
|
|
6
|
-
export function formatEtfInfo(etf, quote, stockCode, modeLabel) {
|
|
6
|
+
export function formatEtfInfo(etf, quote, nav, stockCode, modeLabel) {
|
|
7
7
|
if (!etf.stk_nm) {
|
|
8
8
|
return `[${modeLabel}] ${stockCode}의 ETF 정보가 없습니다. ETF 종목코드인지 확인해 주세요.`;
|
|
9
9
|
}
|
|
@@ -16,14 +16,21 @@ export function formatEtfInfo(etf, quote, stockCode, modeLabel) {
|
|
|
16
16
|
if (quote) {
|
|
17
17
|
lines.push(`- 현재가: ${formatKRW(parseKiwoomPrice(quote.cur_prc))} (${formatPercent(parseKiwoomNumber(quote.flu_rt))})`, `- 거래량: ${formatQuantity(parseKiwoomNumber(quote.trde_qty))}`);
|
|
18
18
|
}
|
|
19
|
+
// ka40009 NAV — 필드가 빈값으로 오는 환경(mock 확인)이 있어 값이 있을 때만 표시.
|
|
20
|
+
const navValue = parseKiwoomPrice(nav?.nav);
|
|
21
|
+
if (nav && navValue !== null) {
|
|
22
|
+
lines.push(`- NAV: ${formatKRW(navValue)} (전일대비 ${formatSigned(parseKiwoomNumber(nav.navpred_pre))}, ` +
|
|
23
|
+
`${formatPercent(parseKiwoomNumber(nav.navflu_rt))})`, `- 괴리율: ${formatPercent(parseKiwoomNumber(nav.dispty_rt))} · ` +
|
|
24
|
+
`추적오차율: ${formatRatioPercent(parseKiwoomNumber(nav.trace_eor_rt))}`);
|
|
25
|
+
}
|
|
19
26
|
lines.push("", "※ 과세유형은 키움 표기 기준입니다. 매매차익 과세 방식(국내주식형 비과세 vs 배당소득 과세)은 " +
|
|
20
|
-
"계좌 유형에 따라 달라질 수 있습니다.");
|
|
27
|
+
"계좌 유형에 따라 달라질 수 있습니다.", "※ 기간별(1주/1개월/6개월/1년) 수익률은 get_etf_returns로 조회할 수 있습니다.");
|
|
21
28
|
return lines.join("\n");
|
|
22
29
|
}
|
|
23
30
|
export function registerEtfInfoTool(server) {
|
|
24
31
|
server.registerTool("get_etf_info", {
|
|
25
32
|
title: "ETF 정보 조회",
|
|
26
|
-
description: "ETF의 추적지수, 과세유형, 현재
|
|
33
|
+
description: "ETF의 추적지수, 과세유형, 현재 시세, NAV·괴리율을 조회합니다 (키움 ka40002+ka10001+ka40009). " +
|
|
27
34
|
"종목코드를 모르면 search_stock으로 먼저 찾으세요.",
|
|
28
35
|
inputSchema: {
|
|
29
36
|
stock_code: z
|
|
@@ -34,11 +41,12 @@ export function registerEtfInfoTool(server) {
|
|
|
34
41
|
}, async ({ stock_code }) => runTool(async () => {
|
|
35
42
|
const { client, config } = getKiwoomContext();
|
|
36
43
|
const code = stock_code.toUpperCase();
|
|
37
|
-
// Different TRs — parallel is fine;
|
|
38
|
-
const [etf, quote] = await Promise.all([
|
|
44
|
+
// Different TRs — parallel is fine; quote and NAV are best-effort garnish.
|
|
45
|
+
const [etf, quote, navRows] = await Promise.all([
|
|
39
46
|
fetchEtfInfo(client, code),
|
|
40
47
|
fetchStockInfo(client, code).catch(() => null),
|
|
48
|
+
fetchEtfNav(client, code).catch(() => null),
|
|
41
49
|
]);
|
|
42
|
-
return textResult(formatEtfInfo(etf, quote, code, config.modeLabel));
|
|
50
|
+
return textResult(formatEtfInfo(etf, quote, navRows?.[0] ?? null, code, config.modeLabel));
|
|
43
51
|
}));
|
|
44
52
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getKiwoomContext } from "../context.js";
|
|
3
|
+
import { ETF_RETURN_PERIODS, fetchEtfInfo, fetchEtfReturns } from "../kiwoom/api.js";
|
|
4
|
+
import { formatPercent, formatSigned, parseKiwoomNumber } from "../utils/num.js";
|
|
5
|
+
import { runTool, textResult } from "./helpers.js";
|
|
6
|
+
import { sectorLabel } from "./sector.js";
|
|
7
|
+
/** ka40001의 etfobjt_idex_cd는 필수 — 지정이 없으면 KOSPI200(201)을 기본 벤치마크로 쓴다. */
|
|
8
|
+
const DEFAULT_BENCHMARK = "201";
|
|
9
|
+
export function formatEtfReturns(rows, etfName, stockCode, benchmarkCode, modeLabel) {
|
|
10
|
+
const n = parseKiwoomNumber;
|
|
11
|
+
if (rows.every((r) => r === null || n(r.etfprft_rt) === null)) {
|
|
12
|
+
return (`[${modeLabel}] ${stockCode}의 ETF 수익률 데이터가 없습니다. ` +
|
|
13
|
+
`ETF 종목코드인지 확인해 주세요 (search_stock으로 검색 가능).`);
|
|
14
|
+
}
|
|
15
|
+
const title = etfName ? `${etfName} (${stockCode})` : stockCode;
|
|
16
|
+
const lines = [
|
|
17
|
+
`[${modeLabel}] ${title} ETF 기간 수익률`,
|
|
18
|
+
"",
|
|
19
|
+
"| 기간 | ETF 수익률 | 대상지수 수익률 | 외인 순매수량 | 기관 순매수량 |",
|
|
20
|
+
"|---|---:|---:|---:|---:|",
|
|
21
|
+
];
|
|
22
|
+
for (const [i, period] of ETF_RETURN_PERIODS.entries()) {
|
|
23
|
+
const row = rows[i] ?? null;
|
|
24
|
+
lines.push(`| ${period.label} | ${formatPercent(n(row?.etfprft_rt))} | ${formatPercent(n(row?.cntr_prft_rt))} | ` +
|
|
25
|
+
`${formatSigned(n(row?.for_netprps_qty), 0)} | ${formatSigned(n(row?.orgn_netprps_qty), 0)} |`);
|
|
26
|
+
}
|
|
27
|
+
lines.push("", `※ 대상지수: ${benchmarkCode} (${sectorLabel(benchmarkCode)}) — benchmark_index_code로 변경할 수 ` +
|
|
28
|
+
`있습니다 (get_market_index의 '코드' 값).`);
|
|
29
|
+
return lines.join("\n");
|
|
30
|
+
}
|
|
31
|
+
export function registerEtfReturnsTool(server) {
|
|
32
|
+
server.registerTool("get_etf_returns", {
|
|
33
|
+
title: "ETF 기간 수익률 조회",
|
|
34
|
+
description: "ETF의 기간별(1주/1개월/6개월/1년) 수익률을 대상지수 수익률과 나란히 조회합니다 (키움 ka40001). " +
|
|
35
|
+
`대상지수는 benchmark_index_code로 지정하며 기본값은 ${DEFAULT_BENCHMARK}(KOSPI200)입니다 — ` +
|
|
36
|
+
"코드는 get_market_index의 '코드' 값을 사용하세요 (001 코스피 종합, 101 코스닥 종합 등). " +
|
|
37
|
+
"종목코드를 모르면 search_stock으로 먼저 찾으세요.",
|
|
38
|
+
inputSchema: {
|
|
39
|
+
stock_code: z
|
|
40
|
+
.string()
|
|
41
|
+
.regex(/^[0-9A-Z]{6}$/i, "6자리 종목코드여야 합니다")
|
|
42
|
+
.describe("6자리 ETF 종목코드 (예: 069500)"),
|
|
43
|
+
benchmark_index_code: z
|
|
44
|
+
.string()
|
|
45
|
+
.regex(/^\d{3}$/)
|
|
46
|
+
.optional()
|
|
47
|
+
.describe(`비교할 지수 코드 3자리 (기본값 ${DEFAULT_BENCHMARK} KOSPI200 — get_market_index의 '코드' 값)`),
|
|
48
|
+
},
|
|
49
|
+
}, async ({ stock_code, benchmark_index_code }) => runTool(async () => {
|
|
50
|
+
const { client, config } = getKiwoomContext();
|
|
51
|
+
const code = stock_code.toUpperCase();
|
|
52
|
+
const benchmark = benchmark_index_code ?? DEFAULT_BENCHMARK;
|
|
53
|
+
// 4-period loop (~3.3s, same-TR rate limit) + name garnish in parallel.
|
|
54
|
+
const [rows, etf] = await Promise.all([
|
|
55
|
+
fetchEtfReturns(client, code, benchmark),
|
|
56
|
+
fetchEtfInfo(client, code).catch(() => null),
|
|
57
|
+
]);
|
|
58
|
+
return textResult(formatEtfReturns(rows, etf?.stk_nm || null, code, benchmark, config.modeLabel));
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
@@ -11,21 +11,23 @@ export function formatIndices(items, market, modeLabel) {
|
|
|
11
11
|
const lines = [
|
|
12
12
|
`[${modeLabel}] ${MARKET_LABELS[market]} 업종 지수 (${items.length}개)`,
|
|
13
13
|
"",
|
|
14
|
-
"| 지수 | 현재 | 전일대비 | 등락률 | 상승/보합/하락 |",
|
|
15
|
-
"
|
|
14
|
+
"| 지수 | 코드 | 현재 | 전일대비 | 등락률 | 상승/보합/하락 |",
|
|
15
|
+
"|---|---|---:|---:|---:|---:|",
|
|
16
16
|
];
|
|
17
17
|
for (const item of items) {
|
|
18
|
-
lines.push(`| ${item.stk_nm} | ${formatNumber(parseKiwoomPrice(item.cur_prc))} | ` +
|
|
18
|
+
lines.push(`| ${item.stk_nm} | ${item.stk_cd || "-"} | ${formatNumber(parseKiwoomPrice(item.cur_prc))} | ` +
|
|
19
19
|
`${formatSigned(parseKiwoomNumber(item.pred_pre))} | ${formatPercent(parseKiwoomNumber(item.flu_rt))} | ` +
|
|
20
20
|
`${parseKiwoomNumber(item.rising) ?? "-"}/${parseKiwoomNumber(item.stdns) ?? "-"}/${parseKiwoomNumber(item.fall) ?? "-"} |`);
|
|
21
21
|
}
|
|
22
|
+
lines.push("", "※ 업종 상세·구성종목은 get_sector_price / get_sector_stocks에 '코드' 값을 넣어 조회하세요.");
|
|
22
23
|
return lines.join("\n");
|
|
23
24
|
}
|
|
24
25
|
export function registerMarketIndexTool(server) {
|
|
25
26
|
server.registerTool("get_market_index", {
|
|
26
27
|
title: "시장 지수 조회",
|
|
27
28
|
description: "코스피/코스닥 종합지수와 업종별 지수를 조회합니다 (키움 ka20003). " +
|
|
28
|
-
"첫 행이 시장 종합지수, 이후는 업종 지수입니다."
|
|
29
|
+
"첫 행이 시장 종합지수, 이후는 업종 지수입니다. 각 행의 '코드'는 " +
|
|
30
|
+
"get_sector_price / get_sector_stocks의 sector_code로 사용할 수 있습니다.",
|
|
29
31
|
inputSchema: {
|
|
30
32
|
market: z
|
|
31
33
|
.enum(["kospi", "kosdaq"])
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getKiwoomContext } from "../context.js";
|
|
3
|
+
import { fetchSectorPrice, fetchSectorStocks } from "../kiwoom/api.js";
|
|
4
|
+
import { formatDateDashed } from "../utils/date.js";
|
|
5
|
+
import { formatNumber, formatPercent, formatRatioPercent, formatSigned, parseKiwoomNumber, parseKiwoomPrice, } from "../utils/num.js";
|
|
6
|
+
import { runTool, textResult } from "./helpers.js";
|
|
7
|
+
const DEFAULT_STOCK_LIMIT = 30;
|
|
8
|
+
const MAX_STOCK_LIMIT = 100;
|
|
9
|
+
const TIME_ROWS_SHOWN = 10;
|
|
10
|
+
/** 잘 알려진 업종 코드 라벨 — 전체 코드 목록은 get_market_index로 조회. */
|
|
11
|
+
const SECTOR_LABELS = {
|
|
12
|
+
"001": "코스피 종합",
|
|
13
|
+
"002": "코스피 대형주",
|
|
14
|
+
"101": "코스닥 종합",
|
|
15
|
+
"201": "KOSPI200",
|
|
16
|
+
};
|
|
17
|
+
export const sectorLabel = (code) => SECTOR_LABELS[code] ?? `업종 ${code}`;
|
|
18
|
+
/** "153220" → "15:32:20"; 시각이 아닌 값(장마감 센티널 999999/888888 등)은 null. */
|
|
19
|
+
function formatSectorTime(tm) {
|
|
20
|
+
if (!/^\d{6}$/.test(tm) || Number(tm.slice(0, 2)) > 23)
|
|
21
|
+
return null;
|
|
22
|
+
return `${tm.slice(0, 2)}:${tm.slice(2, 4)}:${tm.slice(4, 6)}`;
|
|
23
|
+
}
|
|
24
|
+
const dashedOr = (yyyymmdd) => /^\d{8}$/.test(yyyymmdd) ? formatDateDashed(yyyymmdd) : "-";
|
|
25
|
+
export function formatSectorPrice(res, sectorCode, modeLabel) {
|
|
26
|
+
const n = parseKiwoomNumber;
|
|
27
|
+
const p = parseKiwoomPrice;
|
|
28
|
+
const index = p(res.cur_prc);
|
|
29
|
+
if (index === null) {
|
|
30
|
+
return (`[${modeLabel}] 업종 ${sectorCode}의 시세 데이터가 없습니다. ` +
|
|
31
|
+
`업종 코드를 확인하세요 (get_market_index의 '코드' 값).`);
|
|
32
|
+
}
|
|
33
|
+
const lines = [
|
|
34
|
+
`[${modeLabel}] 업종 현재가 — ${sectorLabel(sectorCode)} (${sectorCode})`,
|
|
35
|
+
"",
|
|
36
|
+
`지수 ${formatNumber(index)} (전일대비 ${formatSigned(n(res.pred_pre))}, ${formatPercent(n(res.flu_rt))})`,
|
|
37
|
+
`시가 ${formatNumber(p(res.open_pric))} · 고가 ${formatNumber(p(res.high_pric))} · 저가 ${formatNumber(p(res.low_pric))}`,
|
|
38
|
+
`거래량 ${formatNumber(n(res.trde_qty), 0)}천주 · 거래대금 ${formatNumber(n(res.trde_prica), 0)}백만원`,
|
|
39
|
+
`등락 구성: 상한 ${n(res.upl) ?? "-"} · 상승 ${n(res.rising) ?? "-"} · 보합 ${n(res.stdns) ?? "-"} · ` +
|
|
40
|
+
`하락 ${n(res.fall) ?? "-"} · 하한 ${n(res.lst) ?? "-"} ` +
|
|
41
|
+
`(거래형성 ${formatNumber(n(res.trde_frmatn_stk_num), 0)}종목, ${formatRatioPercent(p(res.trde_frmatn_rt))})`,
|
|
42
|
+
`52주 최고 ${formatNumber(p(res["52wk_hgst_pric"]))} (${dashedOr(res["52wk_hgst_pric_dt"])}, ` +
|
|
43
|
+
`현재 대비 ${formatPercent(n(res["52wk_hgst_pric_pre_rt"]))}) · ` +
|
|
44
|
+
`최저 ${formatNumber(p(res["52wk_lwst_pric"]))} (${dashedOr(res["52wk_lwst_pric_dt"])}, ` +
|
|
45
|
+
`현재 대비 ${formatPercent(n(res["52wk_lwst_pric_pre_rt"]))})`,
|
|
46
|
+
];
|
|
47
|
+
const timeRows = [];
|
|
48
|
+
for (const row of res.inds_cur_prc_tm) {
|
|
49
|
+
const time = formatSectorTime(row.tm_n);
|
|
50
|
+
if (time)
|
|
51
|
+
timeRows.push({ time, row });
|
|
52
|
+
if (timeRows.length >= TIME_ROWS_SHOWN)
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
if (timeRows.length > 0) {
|
|
56
|
+
lines.push("", `시간대별 추이 (최근 ${timeRows.length}건)`, "", "| 시각 | 지수 | 전일대비 | 등락률 | 누적거래량(천주) |", "|---|---:|---:|---:|---:|");
|
|
57
|
+
for (const { time, row } of timeRows) {
|
|
58
|
+
lines.push(`| ${time} | ${formatNumber(p(row.cur_prc_n))} | ${formatSigned(n(row.pred_pre_n))} | ` +
|
|
59
|
+
`${formatPercent(n(row.flu_rt_n))} | ${formatNumber(n(row.acc_trde_qty_n), 0)} |`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
lines.push("", "※ 구성 종목별 시세는 get_sector_stocks에 같은 업종 코드를 넣어 조회하세요.");
|
|
63
|
+
return lines.join("\n");
|
|
64
|
+
}
|
|
65
|
+
export function formatSectorStocks(items, truncated, sectorCode, limit, modeLabel) {
|
|
66
|
+
if (items.length === 0) {
|
|
67
|
+
return (`[${modeLabel}] ${sectorLabel(sectorCode)} (${sectorCode})의 구성 종목이 없습니다. ` +
|
|
68
|
+
`업종 코드를 확인하세요 (get_market_index의 '코드' 값).`);
|
|
69
|
+
}
|
|
70
|
+
const n = parseKiwoomNumber;
|
|
71
|
+
const p = parseKiwoomPrice;
|
|
72
|
+
const shown = items.slice(0, limit);
|
|
73
|
+
const lines = [
|
|
74
|
+
`[${modeLabel}] 업종별 주가 — ${sectorLabel(sectorCode)} (${sectorCode}), ${shown.length}종목 (종목코드순)`,
|
|
75
|
+
"",
|
|
76
|
+
"| 종목명 | 코드 | 현재가 | 전일대비 | 등락률 | 거래량 | 고가 | 저가 |",
|
|
77
|
+
"|---|---|---:|---:|---:|---:|---:|---:|",
|
|
78
|
+
];
|
|
79
|
+
for (const item of shown) {
|
|
80
|
+
lines.push(`| ${item.stk_nm || "-"} | ${item.stk_cd || "-"} | ${formatNumber(p(item.cur_prc), 0)} | ` +
|
|
81
|
+
`${formatSigned(n(item.pred_pre), 0)} | ${formatPercent(n(item.flu_rt))} | ` +
|
|
82
|
+
`${formatNumber(n(item.now_trde_qty), 0)} | ${formatNumber(p(item.high_pric), 0)} | ` +
|
|
83
|
+
`${formatNumber(p(item.low_pric), 0)} |`);
|
|
84
|
+
}
|
|
85
|
+
const notes = [];
|
|
86
|
+
if (items.length > shown.length) {
|
|
87
|
+
notes.push(`※ 첫 ${shown.length}종목만 표시했습니다 (limit으로 최대 ${MAX_STOCK_LIMIT}개까지 조정 가능).`);
|
|
88
|
+
}
|
|
89
|
+
if (truncated) {
|
|
90
|
+
notes.push("※ 구성 종목이 더 있습니다 — 이 조회는 첫 페이지(종목코드순 100종목)만 가져옵니다.");
|
|
91
|
+
}
|
|
92
|
+
if (notes.length > 0)
|
|
93
|
+
lines.push("", ...notes);
|
|
94
|
+
return lines.join("\n");
|
|
95
|
+
}
|
|
96
|
+
const sectorCodeSchema = z
|
|
97
|
+
.string()
|
|
98
|
+
.regex(/^\d{3}$/)
|
|
99
|
+
.describe("업종 코드 3자리 (예: 001 코스피 종합, 101 코스닥 종합 — get_market_index의 '코드' 값)");
|
|
100
|
+
export function registerSectorPriceTool(server) {
|
|
101
|
+
server.registerTool("get_sector_price", {
|
|
102
|
+
title: "업종 현재가 조회",
|
|
103
|
+
description: "업종(섹터) 지수의 현재가 상세를 조회합니다 (키움 ka20001) — 지수·시/고/저가·거래량·" +
|
|
104
|
+
"상승/하락 종목수·52주 고저·시간대별 추이. sector_code는 get_market_index가 보여주는 " +
|
|
105
|
+
"업종 코드입니다 (001 코스피 종합, 002 코스피 대형주, 101 코스닥 종합, 201 KOSPI200 등).",
|
|
106
|
+
inputSchema: {
|
|
107
|
+
sector_code: sectorCodeSchema,
|
|
108
|
+
},
|
|
109
|
+
}, async ({ sector_code }) => runTool(async () => {
|
|
110
|
+
const { client, config } = getKiwoomContext();
|
|
111
|
+
const res = await fetchSectorPrice(client, sector_code);
|
|
112
|
+
return textResult(formatSectorPrice(res, sector_code, config.modeLabel));
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
export function registerSectorStocksTool(server) {
|
|
116
|
+
server.registerTool("get_sector_stocks", {
|
|
117
|
+
title: "업종별 종목 시세 조회",
|
|
118
|
+
description: "특정 업종에 속한 종목들의 시세를 조회합니다 (키움 ka20002). 종목코드순 정렬이며 " +
|
|
119
|
+
"첫 페이지(최대 100종목)만 가져옵니다. sector_code는 get_market_index의 업종 코드입니다.",
|
|
120
|
+
inputSchema: {
|
|
121
|
+
sector_code: sectorCodeSchema,
|
|
122
|
+
limit: z
|
|
123
|
+
.number()
|
|
124
|
+
.int()
|
|
125
|
+
.min(1)
|
|
126
|
+
.max(MAX_STOCK_LIMIT)
|
|
127
|
+
.optional()
|
|
128
|
+
.describe(`표시할 종목 수 (기본값 ${DEFAULT_STOCK_LIMIT}, 최대 ${MAX_STOCK_LIMIT})`),
|
|
129
|
+
},
|
|
130
|
+
}, async ({ sector_code, limit }) => runTool(async () => {
|
|
131
|
+
const { client, config } = getKiwoomContext();
|
|
132
|
+
const { items, truncated } = await fetchSectorStocks(client, sector_code);
|
|
133
|
+
return textResult(formatSectorStocks(items, truncated, sector_code, limit ?? DEFAULT_STOCK_LIMIT, config.modeLabel));
|
|
134
|
+
}));
|
|
135
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { getKiwoomContext } from "../context.js";
|
|
3
|
-
import { fetchDailyChart, fetchMinuteChart } from "../kiwoom/api.js";
|
|
3
|
+
import { fetchDailyChart, fetchMinuteChart, fetchTickChart } from "../kiwoom/api.js";
|
|
4
4
|
import { formatDateDashed, todayInKst } from "../utils/date.js";
|
|
5
5
|
import { formatNumber, parseKiwoomNumber, parseKiwoomPrice } from "../utils/num.js";
|
|
6
6
|
import { runTool, textResult } from "./helpers.js";
|
|
@@ -10,7 +10,7 @@ const PERIOD_LABELS = {
|
|
|
10
10
|
day: "일봉",
|
|
11
11
|
week: "주봉",
|
|
12
12
|
month: "월봉",
|
|
13
|
-
|
|
13
|
+
year: "년봉",
|
|
14
14
|
};
|
|
15
15
|
/** "20260703153000" → "2026-07-03 15:30" */
|
|
16
16
|
function formatMinuteTime(cntrTm) {
|
|
@@ -37,13 +37,14 @@ export function formatDailyChart(items, stockCode, period, count, modeLabel) {
|
|
|
37
37
|
];
|
|
38
38
|
return lines.join("\n");
|
|
39
39
|
}
|
|
40
|
-
|
|
40
|
+
/** 분봉과 틱봉이 같은 행 구조(cntr_tm)를 공유 — scopeLabel은 "5분"/"30틱" 형태. */
|
|
41
|
+
export function formatMinuteChart(items, stockCode, scopeLabel, count, modeLabel) {
|
|
41
42
|
if (items.length === 0) {
|
|
42
|
-
return `[${modeLabel}] ${stockCode}
|
|
43
|
+
return `[${modeLabel}] ${stockCode} ${scopeLabel}봉 데이터가 없습니다. 종목코드를 확인해 주세요.`;
|
|
43
44
|
}
|
|
44
45
|
const shown = items.slice(0, count).reverse();
|
|
45
46
|
const lines = [
|
|
46
|
-
`[${modeLabel}] ${stockCode} ${
|
|
47
|
+
`[${modeLabel}] ${stockCode} ${scopeLabel}봉 차트 (최근 ${shown.length}개, 수정주가 반영)`,
|
|
47
48
|
"",
|
|
48
49
|
"| 시각 | 시가 | 고가 | 저가 | 종가 | 거래량 |",
|
|
49
50
|
"|---|---:|---:|---:|---:|---:|",
|
|
@@ -53,17 +54,18 @@ export function formatMinuteChart(items, stockCode, ticScope, count, modeLabel)
|
|
|
53
54
|
}
|
|
54
55
|
export function registerStockChartTool(server) {
|
|
55
56
|
server.registerTool("get_stock_chart", {
|
|
56
|
-
title: "주식 차트 조회 (
|
|
57
|
-
description: "종목의 캔들 차트 데이터를 조회합니다 (키움
|
|
58
|
-
"period: day(일봉, 기본)/week(주봉)/month(월봉)/minute(분봉).
|
|
59
|
-
"분
|
|
57
|
+
title: "주식 차트 조회 (일/주/월/년/분/틱봉)",
|
|
58
|
+
description: "종목의 캔들 차트 데이터를 조회합니다 (키움 ka10079~ka10083/ka10094, 수정주가 반영). " +
|
|
59
|
+
"period: day(일봉, 기본)/week(주봉)/month(월봉)/year(년봉)/minute(분봉)/tick(틱봉). " +
|
|
60
|
+
"분봉은 minute_scope로 분 단위를, 틱봉은 tick_scope로 캔들당 틱 수를 지정합니다. " +
|
|
61
|
+
"종목코드를 모르면 search_stock으로 먼저 찾으세요.",
|
|
60
62
|
inputSchema: {
|
|
61
63
|
stock_code: z
|
|
62
64
|
.string()
|
|
63
65
|
.regex(/^[0-9A-Z]{6}$/i, "6자리 종목코드여야 합니다")
|
|
64
66
|
.describe("6자리 종목코드 (예: 005930)"),
|
|
65
67
|
period: z
|
|
66
|
-
.enum(["day", "week", "month", "minute"])
|
|
68
|
+
.enum(["day", "week", "month", "year", "minute", "tick"])
|
|
67
69
|
.optional()
|
|
68
70
|
.describe("봉 주기 (기본값: day)"),
|
|
69
71
|
count: z
|
|
@@ -77,15 +79,22 @@ export function registerStockChartTool(server) {
|
|
|
77
79
|
.enum(["1", "3", "5", "10", "15", "30", "45", "60"])
|
|
78
80
|
.optional()
|
|
79
81
|
.describe("period=minute일 때 분 단위 (기본값: 5)"),
|
|
82
|
+
tick_scope: z
|
|
83
|
+
.enum(["1", "3", "5", "10", "30"])
|
|
84
|
+
.optional()
|
|
85
|
+
.describe("period=tick일 때 캔들당 틱 수 (기본값: 30)"),
|
|
80
86
|
},
|
|
81
|
-
}, async ({ stock_code, period, count, minute_scope }) => runTool(async () => {
|
|
87
|
+
}, async ({ stock_code, period, count, minute_scope, tick_scope }) => runTool(async () => {
|
|
82
88
|
const { client, config } = getKiwoomContext();
|
|
83
89
|
const code = stock_code.toUpperCase();
|
|
84
90
|
const n = count ?? DEFAULT_COUNT;
|
|
85
|
-
if (period === "minute") {
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
|
|
91
|
+
if (period === "minute" || period === "tick") {
|
|
92
|
+
const isTick = period === "tick";
|
|
93
|
+
const scope = isTick ? (tick_scope ?? "30") : (minute_scope ?? "5");
|
|
94
|
+
const items = isTick
|
|
95
|
+
? await fetchTickChart(client, code, scope)
|
|
96
|
+
: await fetchMinuteChart(client, code, scope);
|
|
97
|
+
return textResult(formatMinuteChart(items, code, `${scope}${isTick ? "틱" : "분"}`, n, config.modeLabel));
|
|
89
98
|
}
|
|
90
99
|
const p = period ?? "day";
|
|
91
100
|
const items = await fetchDailyChart(client, code, p, todayInKst());
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kiwoom-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Read-only MCP server exposing the Kiwoom Securities REST API (market data, account inquiry, ISA tax-allowance calculator) to Claude Desktop / Claude Code.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|