RedoxNet.LsOpenApi.Core
# Release Notes — RedoxNet.LsOpenApi.Core
v1.6.0 (unreleased)
Lockstep release with RedoxNet.Mcp.LsOpenApi 1.6.0. The v1.6 work
is entirely in the Mcp package (ls_account_* family + portfolio.db
schema-split into paper / live registries); Core ships unchanged in
behaviour and surface. Full context in
RELEASENOTES.Mcp.md v1.6.0.
v1.5.1 (2026-05-27)
Chart theme propagation slice. CandlestickChartBuilder.Build accepts
an optional themeHint parameter ("light" / "dark" / null); when
non-null and recognized, the value is embedded in
layout._themeHint so PlotlyTemplate.applyTheme() can override
hostContext.theme / iframe prefers-color-scheme.
CandlestickChartBuilder also now emits transparent layout
backgrounds (paper_bgcolor / plot_bgcolor = "rgba(0,0,0,0)") to
match the other builders, so the host card shows through regardless of
theme signal. Full context in
RELEASENOTES.Mcp.md v1.5.1.
v1.5.0 (2026-05-27)
Lockstep release with RedoxNet.Mcp.LsOpenApi 1.5.0 — no Core
changes. The v1.5 work is entirely in the Mcp package
(_meta.render_status / _meta.do_not_render response metadata +
ServerInstructions narration-honesty paragraph; SPEC v1.5). Core is
re-packaged at v1.5.0 only so the two NuGet packages remain
version-aligned for the MCP Registry publish.
v1.4.0 (2026-05-26)
Trading-calendar + date-envelope primitives for the v1.4 query_date /
data_as_of standardization (consumed by RedoxNet.Mcp.LsOpenApi
1.4.0), one transient-retry expansion, and two Q-Click signal-screener
TRs in the catalog. Surface additions only; public API back-compat.
Added — Trading calendar + date envelope
RedoxNet.LsOpenApi.Core.Time.ITradingCalendar— minimal contract
(IsTradingDay(DateOnly)+PreviousTradingDay(DateOnly)) so a
consumer can plug in KRX-, NYSE-, or any other calendar without
reaching into the core library.WeekendOnlyCalendar— ships in-box, treats Saturday and Sunday as
non-trading days. Sufficient for v1.4's weekend-fallback envelope;
full KRX / NYSE holiday tables remain a v1.5+ slice (see
docs/SPEC-v1.4.md §2.5).DateEnvelope.Resolve(...)— normalizes a caller-supplied
yyyyMMdd(ornull= "latest") into(DateOnly resolvedDate, string resolution)whereresolutionis one ofused/weekend/
holiday/future_date/pre_market. The same shape is used by
every Mcp tool that adopts the envelope, so the model learns the
vocabulary once.- DI:
AddLsOpenApiCore()registersITradingCalendar→
WeekendOnlyCalendaras the default. Swap with a custom
registration to plug in a holiday-aware calendar.
Added — Catalog rows (Q-Click signal pair)
t1825— Q-Click 신호 종목 검색 (signal stocks for a saved
condition expression).t1826— Q-Click 사용자 등록 신호 목록 (the user's saved Q-Click
conditions on the LS server side).
Catalog count 62 → 64 TRs.
Changed — Transient retry
LsApiClient's Polly policy now retries HTTP 500 as transient
alongside the existing 408/429/502/503/504 set. The overseas chart
HTTP 500 v1.3 fixed viacomp_yn="N"was one well-known cause, but
empirical traffic shows HTTP 500 also surfaces on rate-limited TRs
before LS hands back a proper 429 — retrying recovers the call
without surfacing a transient failure to the model.
Versioned in lockstep with RedoxNet.Mcp.LsOpenApi 1.4.0.
v1.3.0 (2026-05-24)
Embedded TR catalog update for first-class overseas stock support in
RedoxNet.Mcp.LsOpenApi 1.3.0, plus one additive chart-builder API
change so non-KRW prices render with the right unit.
Added — Catalog rows (9 new TRs)
- Overseas stock market data:
g3101(current quote),g3102
(time-and-sales / time-sliced quotes),g3104(security profile),
g3106(10-level order book), andg3190(overseas stock master). - Overseas stock charts:
g3103(legacy day/week/month),g3202
(N-tick),g3203(N-minute), andg3204(day/week/month/year).
Catalog count 53 → 62 TRs.
Added — Charting
CandlestickChartBuilder.Buildgains an optionalcurrencyargument.
null/"KRW"keeps the default Korean-market label (주가 (원));
"USD"/"JPY"/"EUR"/"GBP"/"HKD"substitute the matching
symbol ($/¥/€/£/HK$); any other code falls through
to itself. Existing callers are unaffected (parameter is optional and
defaults to KRW). InternalPriceUnitLabel(string?)helper exposed
forInternalsVisibleTotests.
Versioned in lockstep with RedoxNet.Mcp.LsOpenApi 1.3.0.
v1.2.0 (2026-05-22)
Version-only release, kept in lockstep with RedoxNet.Mcp.LsOpenApi
1.2.0. The v1.2 slice — MCP Apps capability negotiation — lives entirely
in the MCP server project; the Core library has no functional changes and
the public Core API is unchanged.
v1.1.0 (2026-05-22)
Program-trading support. The embedded TR catalog gains the seven
/stock/program TRs, and two new internal layers — chart-spec builders
and a footprint analyzer — back the RedoxNet.Mcp.LsOpenApi 1.1.0 tools.
The public Core API is unchanged.
Added — Catalog rows (7 new TRs)
- The
/stock/programfamily:t1662(시간대별 프로그램매매 추이),
t1633(기간별 프로그램매매 추이),t1636(종목별 프로그램매매
동향),t1637(종목별 프로그램매매 추이), plust1631/
t1632/t1640(종합조회 —ls_call_tr-only). Catalog count
46 → 53 TRs.
Added — Charting (internal)
Charting/gains the program-trading chart-spec builders —
ProgramTradeChartBuilder(market intraday flow / basis / intensity /
gross-flow / daily views),ProgramRankingChartBuilder, and
ProgramStockChartBuilder— plus a sharedChartLayouthelper that
unifies title / legend / font conventions across every chart.
PlotlyChartBuilderis renamedCandlestickChartBuilder.
Added — Analysis layer (internal)
- New
Analysis/namespace.ProgramFootprintAnalyzerclassifies a
stock's program-trading footprint into a deterministic verdict —
regime, signals, and plain-language evidence.
All new types are internal, consumed by the MCP server via
InternalsVisibleTo; the public Core surface is unchanged. The
/stock/program amount-unit divergence (t1662 / t1633 백만원 vs
t1636 / t1637 천원) is catalogued in docs/LS-API-QUIRKS.md §3.3.
Versioned in lockstep with RedoxNet.Mcp.LsOpenApi 1.1.0.
v1.0.0 (2026-05-21)
The first stable release. The public Core API and the embedded TR
catalog (46 TRs) are frozen at v1.0.0.
Changed — defaults
LsMarketdefaults toReal(wasVirtual).
LsMarketExtensions.Parse(null), an unrecognized market string, and
LsApiOptions.Marketall now default toLsMarket.Real. The LS
virtual endpoint serves real market data — only orders and balances
are synthetic — so for a read-only data SDKRealis the correct
default. Pass"virtual"/LsMarket.Virtualto opt back in.
No other code changes; the embedded TR catalog is identical to 0.10.1.
Versioned in lockstep with RedoxNet.Mcp.LsOpenApi 1.0.0.
v0.10.1 (2026-05-20)
No Core code changes — a lockstep version bump with
RedoxNet.Mcp.LsOpenApi 0.10.1, which fixes the MCP Registry publish
(an over-long .mcp/server.json description) and caps the
ls_get_etf_holdings default page size. The embedded TR catalog and all
public Core APIs are identical to 0.10.0.
v0.10.0 (2026-05-20)
One embedded-catalog addition: t1716 (외국인기간별추이) under
/stock/frgr-itt — the 금감원 foreign held-share level series
(fsc_listing 보유주식수 / fsc_sjrate 소진율). It backs the new opt-in
foreign section on ls_get_stock_info in RedoxNet.Mcp.LsOpenApi
0.10.0. Catalog count 45 → 46 TRs. No public API change — the only
delta is the additional catalog row. Versioned in lockstep with
RedoxNet.Mcp.LsOpenApi 0.10.0.
v0.9.0 (2026-05-20)
Embedded catalog correction. The t1102 entry carried 93 hand-curation
errors in its OutBlock field descriptions — the broker 매수 / 매도
(s* / d*) groups were swapped, abscnt / vol / jkrate / exhratio
named the wrong concept, bf* quarterly figures were labeled 당기 / 전기
instead of 전분기 / 전전분기, and the 외국계 buy/sell fields were
mislabeled. All verified against the official t1102 spec and corrected, so
ls_describe_tr now reports them accurately. Versioned in lockstep with
RedoxNet.Mcp.LsOpenApi 0.9.0.
v0.8.0 (2026-05-20)
Twelve new TRs in the embedded catalog and one continuation-handling
change. Versioned in lockstep with RedoxNet.Mcp.LsOpenApi 0.8.0.
Added — Catalog rows (12 new TRs)
t3521(해외지수조회 API용) +t3518(해외실시간지수) under
/stock/investinfo— overseas index / FX / futures, snapshot and
day/week/month/min/tick series. Back the newls_get_global_market_quote
wrapper;t3518isls_call_tr-only for now.t3102(뉴스본문) under/stock/investinfo— news body by
sNewsno. Catalog-only:sNewsnodiscovery requires the NWS
WebSocket push (the push payload'srealkeyis thesNewsno).- Nine staged TRs reachable via
ls_call_trand, where wrapped, the
v0.8 Mcp tools:t1105(피봇/디마크),t1305(기간별주가),
t1403(신규상장종목),t1442(신고/신저가),t1475
(체결강도추이),t1927(공매도일별추이),t3401(투자의견),
t8425(전체테마),t8428(증시주변자금추이).
Changed
- Catalog count 33 → 45 TRs.
LsApiClientcontinuation handling: body cursor fields
(LsTrResponse.ContinuationKeys) are now populated from the catalog's
key_fieldseven when a TR also pages by thetr_contheader. Pure
body-paged and pure header-paged TRs are unaffected — only TRs that
use both styles (e.g.t3401) gain the body-cursor map.
Versioned in lockstep with
RedoxNet.Mcp.LsOpenApi0.8.0. The only
public Core change is the additive continuation field above; consumers
that read the catalog or call TRs throughLsApiClientget the new
rows automatically.
v0.7.0 (2026-05-18)
Seven new TRs in the embedded catalog, no public API changes.
Versioned in lockstep with RedoxNet.Mcp.LsOpenApi 0.7.0.
Added — Catalog rows (7 new TRs)
t3320(FNG_요약 / 투자정보 요약) under/stock/investinfo.
Per-stock 시장구분 + 회사 프로필 + 펀더멘털 (PER/PBR/EPS/ROE 등) +
FICS 산업분류 (upgubunnm, "FICS " prefix가 붙은 풀-네임). 1 TPS
per symbol. Input is 6-char shcode (LS doc lists 7-char "A"+code
but live verification confirmed 6-char only). Consumed by the Mcp
package's A1 industry enrichment path; reachable vials_call_tr
for raw access.t3341(재무순위종합) under/stock/investinfo. PER / PBR /
PEG / EPS / BPS / ROE + 매출액/영업이익/세전계속이익 증가율 +
부채비율 + 유보율 ranking. PER/PBR/PEG are forced ascending
LS-side. Pagination viaidx. Consumed byls_get_fundamentals_rank.t1601(투자자별 종합) under/stock/investor. Real-time
market-wide investor-type flow — 6 OutBlocks × 12 investor types
(개인/외국인/기관계/증권/투신/은행/보험/종금/기금/국가/기타/사모펀드).
LS does not label which OutBlock corresponds to which market segment;
consumers should surface them as unlabeled.t1702(외인기관 종목별 동향) under/stock/investor.
Single-stock daily investor flow history.volvalgbcontrols
unit (금액/수량/단가),msmdgbselects net/buy/sell,gubun
toggles per-day vs cumulative. Consumed byls_get_investor_flow.t3202(종목별 증시일정) under/stock/investinfo. 14 LS
corporate-action types (유무상증자 / 배당 / 감자 / 합병/분할 /
매수청구 / 실권주 / 액면교체 / 주주총회 / 상호변경 / 국내/해외 CB
전환 / 해외 BW 행사 / 스톡옵션행사).recdt='00000000'indicates
TBD entries. Consumed byls_get_stock_events.t1404(관리/불성실공시/투자유의) +t1405(투자경고/
매매정지/정리매매/단기과열 …) under/stock/market-data. Two
surveillance screens together cover 13 KRX designations. Both use
cts_shcodebody-paging. Consumed byls_get_market_warnings.
Changed
- Catalog count 26 → 33 TRs.
Versioned in lockstep with
RedoxNet.Mcp.LsOpenApi0.7.0. No public
Core API changes — consumers that read the catalog or call TRs through
LsApiClientget the new rows automatically.
v0.6.0 (2026-05-16)
Six new TRs in the embedded catalog under the new /indtp/market-data (업종) path, plus one rate-limit correction. Catalog-only release — the public Core surface (LsApiClient, TrCatalog, indicator pipeline, etc.) is unchanged from v0.5.
Added — Catalog rows (6 new TRs)
t1511(업종현재가) under/indtp/market-data. Single-index snapshot — current value, OHLC with timestamps, 52-week + YTD range, market breadth (advancers/decliners/limit), four related auxiliary indices infirstj*..fourthj*blocks. Consumed by the Mcp package'sls_get_index_quotewrapper.t8424(전체업종) under/indtp/market-data. Industry/index code catalog (code + name only, no quotes). Consumed internally byls_get_industry_indicesfanout and byindustry_keywordresolution inls_get_industry_stocks.t1516(업종별종목시세) under/indtp/market-data. Stocks inside one industry + the industry's index summary. Body-based continuation paging (lastshcodeechoed). Consumed byls_get_industry_stocks.t1537(테마종목별시세조회) under/stock/sector. Stocks inside one LS curated theme + theme summary (tmcnt/upcnt/uprate). Header-basedtr_cont/tr_cont_keypaging. Consumed byls_get_theme_stocks.t1485(예상지수) under/indtp/market-data. Pre-market expected index. Catalog-only — reachable vials_call_tr.t1514(업종기간별추이) under/indtp/market-data. Industry daily/weekly/monthly time series. Catalog-only — reachable vials_call_tr. v0.7 wrapper candidate.
Changed
t1511rate_limit_per_sec: 1 → 10. The conservative v0.6 placeholder was corrected against the LS [업종] 시세 guide which publishes t1511 at 10 req/sec while keeping the other four TRs in the category at 1. Impact:ls_get_industry_indicescold-cache fanout drops from ~25s to ~2.5s for KOSPI (≈25 upcodes).- Catalog count 18 → 24 TRs.
Versioned in lockstep with
RedoxNet.Mcp.LsOpenApi0.6.0. No public Core API changes — consumers that read the catalog or call TRs throughLsApiClientget the new rows automatically.
v0.5.0 (2026-05-15)
Two theme classification TRs added to the embedded catalog. The Mcp package's portfolio sector quote enrichment depends on t1531.
Added
t1531(테마별종목) andt1532(종목별테마) under/stock/sectorwith full InBlock / OutBlock schemas.tmname/tmcodeont1531are marked optional — an empty input returns the full theme list (LS confirmed behavior).
Changed
- Catalog count 16 → 18 TRs.
Versioned in lockstep with
RedoxNet.Mcp.LsOpenApi0.5.0; the public Core surface is unchanged beyond the catalog rows.
v0.4.0 (2026-05-15)
A compact analytical-summary builder + ZigZag swing detector. Additive — the existing public surface is unchanged; the MCP server in 0.4.0 builds on this layer.
Added
AnalyticalSummary+InflectionPoint+PivotKind+IndicatorCoveragerecords (Models). A token-efficient model-facing snapshot for chart data: latest close + period-relative change %, period-aware moving-average snapshots, MA60 deviation/slope, drawdown from peak, a bounded ZigZag-based key-turn list, and a per-indicator availability map. Usable standalone by any consumer that wants a structured swing/trend summary instead of raw OHLCV.AnalyticalSummaryBuilder(Indicators). Computes the summary above from a candle window. Picks MA periods by chart period (MA5..MA200for day;MA20/60/120for month/year;MA20/60for week), classifies MA60 slope by a least-squares fit over the look-back window (robust to endpoint noise), and emits anIndicatorCoveragewith status flags (ok/insufficient_data/disabled) + a human-readable note when a window is too narrow.displayBarCountandwarmupAppliedflow through so callers can distinguish "narrow window the user pinned" from "this period doesn't support 1Y/5Y change."ZigZagswing detector +ZigZagOptions+ZigZagPivot(Indicators). Threshold-reversal pivot detection with two modes —Percent(literal fraction, e.g.0.12= 12%) andAtrMultiple(Wilder ATR-normalized for adaptive volatility). Triggers on the close but emits pivot prices from the actual swing high/low, so a single wide-range bar cannot self-trigger. Pivots strictly alternate peak/trough; the trailing pivot isIsConfirmed=falseat the latest bar — the in-progress swing's provisional endpoint, observable to callers.
Internal
JsonStringEnumConverteris wired throughRedoxNet.Mcp.LsOpenApi's tool serializer soPivotKindserializes as"peak"/"trough"; Core itself keeps its existingLsCoreJsonoptions untouched.
Versioned independently from
RedoxNet.Mcp.LsOpenApi; the shared 0.4.0 is intentional this time — both packages release together for the chart-tool reshape.
v0.3.0 (2026-05-14)
Five market-ranking TRs added to the catalog; the chart-spec builders now live here.
Added
- Five 주식상위종목 ranking TRs in the embedded catalog —
t1441(등락율 상위),t1444(시가총액 상위),t1452(거래량 상위),t1463(거래대금 상위),t1466(전일동시간대비 거래급증), each with full InBlock / OutBlock schemas andidxcontinuation metadata. The catalog now covers 16 TRs. Charting/chart-spec builders —PlotlyChartBuilderandEtfHoldingsChartBuilder(Plotly v5 candlestick + volume specs, ETF-holdings treemap specs) moved here from the MCP server. They depend only on Core types (Candle,IndicatorSpec) andSystem.Text.Json, so Core is their natural home. Both areinternal, exposed to the MCP server viaInternalsVisibleTo— no public API surface change.
Changed
- Chart-spec output: Naver-style evenly-spaced date ticks (
tickvals/ticktextover the verbatim category x), the Korean MA palette (green / red / orange / purple), period high/low annotations, white-on-deep-blue ETF treemap labels, and an optional stock name in the candlestick title.
Versioned independently from
RedoxNet.Mcp.LsOpenApi; the shared 0.3.0 is a coincidence.
v0.2.0 (2026-05-14)
Packaging fix. No code or API changes — the SDK is equivalent to v0.1.0.
Fixed
- Package README missing on NuGet. v0.1.0 was pushed without its README, so the NuGet package page showed no documentation. v0.2.0 ships with the README included.
The version is bumped to 0.2.0 to land alongside the
RedoxNet.Mcp.LsOpenApi0.2.0 release. The two packages are versioned independently — they only happen to share this number; there is no lockstep policy.
v0.1.0 (2026-05-13)
Initial public release of the LS증권 OpenAPI SDK.
Included
-
Auth — OAuth 2.0
client_credentialstoken issuer with a two-tier cache (in-memory + SQLite WAL). Cache key isSHA256(appkey):marketso the raw app key never lives on disk. Refresh fires 5 min before expiry; concurrent callers share a single in-flight issuance via a per-issuer semaphore. POSIXchmod 0600applied to the database file and its WAL/SHM siblings. -
HTTP client —
LsApiClient.CallTrAsyncwith Polly retries (3 attempts × exponential back-off on 408/429/5xx), per-TR rate limiter (TrRateLimiter), and dual continuation modes: header-basedtr_cont_keyfor newer CSPAQ-style TRs, body-based field continuation for legacy stock TRs (t8410/t8412/t1301). -
TR catalog — 13 testbed-verified seed entries across five categories (시세 / 차트 / ETF / 종목조회 / 기타). Full InBlock / OutBlock schemas with field-by-field Korean and English descriptions:
t1101,t1102,t8407,t8410,t8412,t1301,t1901,t1902,t1903,t1904,t9945,t8430,t8436. Catalog shipped as an embedded resource and exposed viaTrCatalog.DefaultwithFind/Searchhelpers. -
Indicators —
IndicatorServiceoverSkender.Stock.Indicators(SMA, EMA, RSI, MACD, Bollinger Bands). Spec parser accepts compact strings (ma:12,bb:20,2,macd:12,26,9).ChartContextBuilderproduces the pre-computed analysis context (divergence from each MA, volume averages, drawdown from period high, trend per MA, tristatebullish_alignment—nullduring MA warm-up). -
Models —
Candle,Quote,ChartContextwith sub-records (VolumeContext,DrawdownInfo). All records, all snake-case JSON via the sharedLsCoreJsonoptions. -
Hygiene —
SecretMasker.Mask("...XYZW") → "****XYZW". App key and access token are masked in every log line; the app secret is never logged in any form.LsCredentialsis a record but explicitly not intended for serialization.
Targets
net8.0 library. Distributed as a NuGet package; consumers add a single PackageReference and use services.AddLsOpenApiCore() to wire everything into Microsoft.Extensions.DependencyInjection.
Verified
234 unit and integration tests pass on .NET 8 and .NET 10. Live-verified against the LS 모의투자 server on 2026-05-13: token issuance + cache hit/miss, the full catalog of seed TRs, indicator computation against real candle data, and the chart context builder against tristate alignment scenarios.
RedoxNet.Mcp.LsOpenApi
# Release Notes — RedoxNet.Mcp.LsOpenApi
v1.6.0 (unreleased)
Account inquiry family + schema-split live registry. v1.6 introduces
the ten ls_account_* MCP tools (read-only inquiry against the
appkey-bound LS broker account) and corrects a v1.6-dev design flaw that
let manually-tracked paper portfolios shadow the LS live label.
Added — ls_account_* family (10 tools)
ls_account_holdings (t0424), ls_account_orders (t0425),
ls_account_balance (CSPAQ12200), ls_account_bep (CSPAQ12300),
ls_account_credit_limit (CSPAQ00600),
ls_account_max_order_qty (CSPBQ00200),
ls_account_order_history (CSPAQ13700),
ls_account_transactions (CDPCQ04700),
ls_account_performance (FOCCQ33600),
ls_account_daily_pnl (t0150 today / t0151 specific date).
Each call is a fresh REST snapshot — no caching, no daemon. None of the
tools accept an account argument: LS account-inquiry TRs do not take
AcntNo in the request body, the appkey's authenticated session
resolves to one subaccount server-side and only echoes the resolved
number in the response (programgarden's
CSPAQ12300InBlock1 / CSPAQ22200InBlock1 confirms this is by design,
not an omission). v1.7 trading actuators will build on the same routing.
Surface: 40 → 50 standard / 43 → 53 all. Token budget 32k → 38k / 36k →
42k.
Changed (BREAKING) — paper portfolio is LS-mode agnostic
Migration v8 drops the accounts.mode column added by v6. The
mode-keyed indexes (idx_accounts_nickname_mode /
idx_accounts_mode_default) are replaced by a single
nickname-uniqueness index plus a default-promotion index. Paper
portfolios are the user's multi-broker manual book — they have no
LS-side identity to begin with — so segregating them by LS_MARKET
just hid the user's records every time they swapped appkey pairs.
After v8, ls_holdings_list, ls_account(action="list")'s
paper_accounts array, and every paper write (ls_holding,
ls_account upsert/remove/rename_broker) operate on the full paper
book regardless of the loaded appkey. Live LS rows continue to
live in the separate ls_accounts table — which stays mode-keyed,
because the live registry IS appkey-bound by definition.
Migration v8 also defensively disambiguates any duplicate nicknames
left over from v6's cross-mode tolerance (suffixes the non-lowest-id
row with (real) / (virtual)) so the new single-column UNIQUE
index can be created without conflict. AccountInfo / AccountSummary
drop their mode field; the PortfolioExportDto schema drops
account mode too.
Changed (BREAKING) — paper / live account registries are now physically separate
The v1.6-dev iterations stored both paper-portfolio accounts (broker
label as display) and auto-discovered LS broker accounts (broker
hardcoded to "LS") in the same accounts table. A user with a default
paper-portfolio row would see _meta.account_used echo that row's
identity even though the LS data came from a completely unrelated
appkey-bound account. The E2E run in todo/E2E-v1.6-claude.txt
exhibited this exact confusion (paper "유안타-001" shadowed the LS row).
v1.6 release splits the two surfaces into physically distinct stores:
- Paper portfolio stays in
accountsand is the user-curated
multi-broker book accessed vials_account/ls_holding/
ls_holdings_list.brokeris purely a display label —"한투",
"유안타증권","LS증권 페이퍼"are all valid. - LS live broker moves to a new
ls_accountstable, keyed by
(account_no, mode). Auto-discovered from successfulls_account_*
responses. Tools that match this surface read only from
ls_accounts; paper rows can never shadow them.
Migration v7 backfills any v1.6-dev accounts rows with broker='LS'
into ls_accounts and deletes the originals (the labels move with them).
Changed (BREAKING) — ls_account(action="list") response shape
The response is now an object with two keys instead of a flat array:
{
"paper_accounts": [ /* AccountSummary[] */ ],
"live_accounts": [ /* LsLiveAccountInfo[] */ ]
}so the model can tell which side it is looking at without inspecting
broker labels.
Added — ls_account(action="set_live_nickname")
Sets or clears a friendly nickname on a live LS account row. Requires
account_number (the LS-side AcntNo) and nickname (empty clears).
Live broker accounts are NOT created via ls_account — they are
auto-discovered on the first successful ls_account_balance /
ls_account_bep / ls_account_credit_limit / ls_account_max_order_qty
call (the four CSPAQ/CSPBQ TRs that echo AcntNo in their OutBlock1).
ls_account_holdings / ls_account_orders / ls_account_daily_pnl
work fine without prior discovery but echo a synthetic
account_used.account_number = null until one of the CSPAQ tools primes
the registry.
Changed — _meta.account_used shape on ls_account_*
The echo is now LsLiveAccountInfo:
{ account_number, nickname, mode, discovered, branch_name?, account_name? }
No is_default field (the live registry has at most one row per mode
in practice), no broker field (live rows are inherently LS-broker so
the label was always redundant). branch_name / account_name are
populated when CSPAQ12200 has answered at least once (cached
in ls_accounts).
Virtual (모의투자) account coverage matrix
Empirical findings from v1.6 E2E with a virtual appkey pair, captured
in docs/LS-API-QUIRKS.md §4.2f:
- Works:
ls_account_holdings,ls_account_orders,
ls_account_balance,ls_account_bep,ls_account_max_order_qty,
ls_account_order_history,ls_account_transactions,
ls_account_daily_pnl, every read-only market-data TR (quote /
chart / index / industry / theme / investor flow / screeners),
ls_get_global_market_quote(t3521 — overseas indices / FX / futures). - Blocked by LS with
rsp_cd: "01900" / "모의투자에서는 해당업무가 제공되지 않습니다":
ls_account_performance(FOCCQ33600),
ls_account_credit_limit(CSPAQ00600). - Unavailable (separate appkey track required): overseas
individual stocks (ls_search_overseas_stock,ls_get_overseas_quote,
ls_get_overseas_chart). LS issues the해외주식 APIkey track
separately, with no모의투자counterpart — overseas individual
stocks are real-money only.
The wrapper passes the 01900 business-level error through unchanged
so the model can narrate the limitation honestly.
LS mode reality
REST endpoint is identical for real and virtual: https://openapi.ls-sec.co.kr:8080.
Only WSS splits. Mode is determined entirely by which appkey pair
(LS_APPKEY / LS_APPSECRETKEY) is loaded; LS_MARKET is informational
labelling that tags ls_accounts rows so real-mode and virtual-mode
(모의투자) registrations stay separate. To target a different
subaccount, swap to that account's appkey pair. See
docs/LS-API-QUIRKS.md §4.2c / §4.2d for the full story.
Tests
683 Mcp tests pass (was 672); +11 new (SqliteLsLiveAccountRepositoryTests
direct coverage, LsAccountResolverTests rewritten for the
schema-split, paper-portfolio non-shadowing pinned).
v1.5.1 (2026-05-27)
Chart theme propagation across SEP-1865 hosts. Follow-up to v1.5
addressing a v1.5 hosts empirical: hosts that don't propagate
hostContext.theme into the iframe (observed in VS Code Copilot Chat
dark mode) showed white chart cards on dark hosts, breaking visual
fidelity. Three fixes, none of them surface-breaking:
Changed — PlotlyTemplate.html defaults to transparent
The iframe <body> and applyTheme() defaults now use
paper_bgcolor: "rgba(0,0,0,0)" / plot_bgcolor: "rgba(0,0,0,0)" /
background: transparent. The host card's background shows through
regardless of theme signal, so even when a host omits
hostContext.theme and the iframe's prefers-color-scheme disagrees
with the host (the actual root cause), the chart blends with the
surrounding panel.
CandlestickChartBuilder — the one builder that previously did not
set explicit backgrounds — now also emits transparent layout
backgrounds, closing the gap that left dark Cowork / VS Code Chat
sessions with a white candlestick card.
CSS variables for foreground / grid colors still follow the resolved
theme (hostContext.theme ↦ _themeHint ↦ iframe
prefers-color-scheme), and applyTheme() toggles a
body.theme-dark / body.theme-light class so panel / table / badge
styles override the iframe's unreliable @media match.
Added — theme parameter on chart-emitting tools
ls_get_chart, ls_get_overseas_chart, ls_add_indicator,
ls_reframe_chart now accept theme = "auto" | "light" | "dark"
(default auto). When set to light or dark, the spec embeds a
layout._themeHint field that overrides hostContext.theme /
prefers-color-scheme in the iframe. Use it when the user explicitly
asks for a dark / light chart ("다크 차트로", "use light theme").
The hint persists on the dataset, so follow-up
ls_add_indicator / ls_reframe_chart calls on the same dataset_id
inherit the theme without re-passing it; pass theme again to flip it
mid-conversation.
Added — diagnostic console.log in PlotlyTemplate.html
applyTheme() now emits one [theme] line per render with the
resolved theme, the value of hostContext.theme, the iframe's
matchMedia("(prefers-color-scheme: dark)").matches, and the
spec-level _themeHint. This is the Track A empirical lever — open
dev tools in any host and the answer to "did the host send
hostContext.theme, and does my iframe's prefers-color-scheme
actually follow the parent?" prints on every render.
Documentation
ServerInstructionschart-customization paragraph namestheme
alongsidels_add_indicator/ls_reframe_chartas the tool-mediated
customization surface.- New
docs/MCP-APPS-INTEROP.md§3 Q8 records the host-by-host theme
propagation matrix.
Surface
40 standard / 43 all (unchanged). Chart-emitting tool descriptors gain
the optional theme parameter.
v1.5.0 (2026-05-27)
Fidelity-first chart narration. Chart-emitting tools now ship a hard
signal to the model about how the host received the chart, and
ServerInstructions forbids the model from routing around the server's
chart by synthesizing one of its own. Single-slice release — no new
tools, no surface change (40 standard / 43 all).
Added — _meta.render_status on every chart-emitting tool
ls_get_chart, ls_reframe_chart, ls_add_indicator,
ls_get_overseas_chart, ls_get_etf_holdings, and
ls_get_program_trading now ship _meta.render_status on every
CallToolResult:
delivered— the server emittedstructuredContent.chart.specand
the host shows it inline. Verified hosts: AssistStudio,
Claude Desktop Chat, Claude Cowork, VS Code Chat, ext-apps
basic-host.stripped_text_only— the chart payload was withheld because the
host has no SEP-1865 / structured-chart path (Codex, Claude Code
CLI, and other text-only hosts). The model receives only the
analytical summary.
ServerInstructions reads this signal explicitly: on
stripped_text_only the model must not claim it drew / rendered /
표시 the chart — it states the limitation and provides the analytical
summary only.
Changed — ServerInstructions chart-routing paragraph replaced
The v1.4 "wrap the Plotly spec in an HTML scaffold and forward to a
peer visualize MCP" paragraph is gone. The v1.5 paragraph
forbids self-synthesis fallbacks regardless of render_status:
- No fetching raw OHLCV via
output_mode=exportand rendering the
chart in Python / JavaScript / PNG / SVG / HTML. - No forwarding the chart spec or raw OHLCV to a generic
visualization MCP (mcp__visualize__show_widget,create_artifact,
etc.). - No recomputing indicators from raw bars —
summary.moving_averages
/summary.ma60_slope/summary.drawdown_from_peak_pct/context.*
are authoritative.
Chart customization is tool-mediated: indicator add/remove ⇒
ls_add_indicator on the dataset_id; range / period / count adjust
⇒ ls_reframe_chart. Layout-level requests (panel height, sizing,
colors) are identified as host panel constraints and answered
honestly rather than routed around with a self-synthesized chart.
The Codex 2026-05-26 "render_samsung_chart.py 248-line self-synthesis"
case and the Cowork height-customization workaround both motivated
this paragraph.
Added — _meta.do_not_render guard on output_mode=export chart responses
ls_get_chart and ls_get_overseas_chart in output_mode=export
mode now ship an additional inline reminder on the result:
This is a brake the model sees every time it's tempted to use the
OHLCV for chart synthesis. output_mode=export is legitimate only for
data analysis pipelines (pandas / numpy / custom backtests).
Not shipped — KnownIframeRenderingHosts allowlist
An earlier draft of the v1.5 spec planned to tighten Sep1865 mode
behind a hand-curated allowlist on top of the v1.2 capability check.
The 2026-05-27 spike/sep1865-verify session against ext-apps
basic-host exposed three real bugs in our own
PlotlyTemplate.html (postMessage stringify, missing appInfo,
missing ui/notifications/size-changed). Once those landed on
main as 38d4dc2 / 46e20f0 / 01d74e3, Claude Desktop Chat /
Cowork / VS Code Chat all rendered the SEP-1865 iframe correctly —
the original "advertise but don't render" diagnosis was our bug,
not a host shortcoming. v1.5 therefore keeps the v1.2
capability-based gate unchanged. The hook point in
ChartHostSupport.Resolve remains in place if a capability not
advertised but iframe-capable host (basic-host class) ever needs
explicit opt-in.
Tool surface
40 standard / 43 all — unchanged. All additions are non-breaking
response metadata + ServerInstructions text.
Versioned in lockstep with RedoxNet.LsOpenApi.Core 1.5.0 (no Core
changes — version bump only for release alignment).
v1.4.0 (2026-05-26)
Two independent additive slices: (A) a standardized date envelope
so non-trading-day fallbacks are explicit to the model, and (B)
first-class access to LS's Q-Click signal screeners (saved
condition expressions). Both are additive — existing tool signatures
and response shapes are unchanged for callers that ignore the new
fields.
Added — Date envelope (slice A)
Date-bearing tools now accept an optional query_date (yyyyMMdd)
and always echo back data_as_of (the trading day LS actually used)
plus query_date_resolution — one of used / weekend / holiday
/ future_date / pre_market. Saturday queries no longer silently
return Friday's data labeled as "today"; future-dated input no longer
goes through silently — the response makes the fallback explicit.
v1.4 wires the envelope on the two highest-value daily-snapshot tools:
ls_get_market_funds_trend(t8428) — market liquidity time series.ls_get_short_selling_trend(t1927) — daily short selling.
The remaining ~10 date-bearing tools listed in
docs/SPEC-v1.4.md §2.3 follow in v1.5+. The
weekend-only fallback ships now; KRX / NYSE holiday tables remain a
v1.5+ slice.
Underlying calendar lives in RedoxNet.LsOpenApi.Core.Time —
swappable via DI so a future holiday-aware calendar drops in without
touching the Mcp wrappers.
Added — Q-Click signal screeners (slice B)
LS hosts user-defined Q-Click (조건검색) expressions on the broker
side. v1.4 exposes them as three additive tools:
ls_list_screeners— list the user's saved Q-Click conditions
(t1826) so the model can find them by partial Korean name.ls_run_screener— execute a saved condition by its Q-Click id
(t1825) and return the matching stocks with the usuallimit
row cap. The Q-Click runtime occasionally ships a row whose price
reads0for thinly-traded names — the wrapper tags those rows
with arapid_change_noiseflag rather than dropping silently.ls_combine_screeners— intersect or union the results of multiple
saved screeners in one call, deduping inputs (the same id passed
twice is collapsed) and returning the set the model needs for
"give me names appearing on both the 거래대금 급증 and 외국인 매수
screeners"-style asks.
See docs/SPEC-v1.4.md §3 for the design rationale
and three Q-Click response quirks recorded during implementation in
docs/LS-API-QUIRKS.md.
Changed — Server instructions
ServerInstructionscarries an ambiguity-strategy guide so the
model handles low-confidence matches consistently (e.g. when
ls_search_overseas_stockreturns multiple plausible candidates);
the OR-combined keyword path orders by match count; the PER/PBR=0
quirk is now called out so a fundamentals-rank zero isn't read as
"cheap".- The server nudges the model to forward chart specs to a peer
visualize MCP when the host can't renderstructuredContent.chart
itself — same Plotly spec, lifted off the in-process chart side
channel through whichever rendering MCP the user has connected.
Groundwork for the broader chart-host-adaptation work specced in
docs/SPEC-v1.5.md.
Tool surface
37 → 40 in the standard profile (40 → 43 in all). All
additions are new tool names; no existing tool was renamed, removed,
or signature-changed.
Versioned in lockstep with RedoxNet.LsOpenApi.Core 1.4.0.
v1.3.0 (2026-05-24)
First-class overseas stock support for the v1.x line. This release adds
semantic MCP tools for US/overseas individual stocks alongside the
existing overseas index / FX / futures snapshot tool.
Added — Overseas stocks
ls_search_overseas_stock— searches the LS overseas stock master
(g3190) by ticker, Korean name, or English name and returns
keysymbol,exchcd, andsymbolfor follow-up calls. When the
keyword looks like a US ticker (1-5 alphanumeric chars), the wrapper
short-circuits via a directg3104probe by keysymbol, so the actual
ticker (e.g.NVDA → 82NVDA) outranks unrelated ETFs whose Korean
name happens to contain the same substring. Master-scan fallback
threads the body cursor back ascontinuationKey:(setstr_cont: Y),
without which LS silently resets to page 1 on every continuation
request — and scans up to 10 pages (5,000 rows) so a name-only
search like"NVIDIA"reaches symbols deep in the alphabetical
listing (see docs/LS-API-QUIRKS.md §6.1).ls_get_overseas_quote— overseas stock quote snapshot (g3101) with
optional profile fields (g3104) and 10-level order book (g3106).ls_get_overseas_chart— overseas stock OHLCV charts:
day/week/month/year viag3204, minute viag3203, and tick via
g3202, with optional indicators and Plotly rendering through the
same MCP Apps chart side channel. All three TRs are called with
comp_yn="N"— LS's"Y"compression path mangles floating-point
prices to control bytes (rsp_cd=IGW40014), forcing HTTP 500 on
every call (see docs/LS-API-QUIRKS.md §3.4). The summary-warm-up policy matches
ls_get_chart, so long-period MAs (e.g.ma:200) and the 1Y change /
MA-slope fields populate even when the display window is short. The
response carriescurrencyandbar_timezone(e.g.USD/
America/New_Yorkfor Nasdaq/NYSE/AMEX) so the model can read a
"5/22 일봉" as the NYSE trading session, not an Asia/Seoul calendar
date; the chart's price y-axis label switches to the matching currency
symbol viaCandlestickChartBuilder's new optionalcurrencyargument.
Changed — Chart follow-ups now span KR + overseas
ls_add_indicator and ls_reframe_chart now also accept dataset_ids
returned by ls_get_overseas_chart. The "여기에 MA200도 추가해줘" /
"일봉으로 바꿔서 6개월" conversational flow that worked for Korean
stocks now works identically for US stocks; the handle cache is shared.
Tool surface 34 → 37 in the standard profile (37 → 40 in
all). The addition is additive; existing tool names, parameters, and
response shapes are unchanged.
Versioned in lockstep with RedoxNet.LsOpenApi.Core 1.3.0.
v1.2.0 (2026-05-22)
MCP Apps capability negotiation — the chart surface now adapts to what
the connected host can actually render. A correctness slice, not a
feature add: it stops the server from handing a chart-less host a payload
it would bury in the model's context, and from telling the model a chart
exists when none can be shown.
Changed — Capability-gated chart surface
Chart-emitting tools (ls_get_chart, ls_add_indicator,
ls_reframe_chart, ls_get_etf_holdings, ls_get_program_trading) now
shape their output by a chart-rendering mode resolved per connection from
two signals:
- the SEP-1865
io.modelcontextprotocol/uicapability the host
advertises atinitialize(preferred), or - a
clientInfoallowlist of known hosts that render
structuredContent.chartdirectly with their own renderer.
A host that hits neither is treated as text-only:
- text-only host —
include_chartis dropped from the tool schema,
andstructuredContent.chartis stripped from results.
structuredContentis a generic MCP field, so a host that supports
structured output but not MCP Apps would otherwise feed the Plotly spec
straight into the model's context, burying the analytical summary. - chart-rendering host —
include_chartstays; a SEP-1865 host also
gets the_meta.uienvelope and theui://lsopenapi/plotlyresource,
while a host that renders thestructuredContent.chartspec directly
gets neither — it has no use for them.
Removed — chart_available text marker
The chart_available field is gone from every chart-emitting tool's text
response. It had no SEP-1865 basis and, on a host that can't render
charts, was a false signal — telling the model a chart existed when the
user would see nothing. Chart visibility is the host's decision now, never
surfaced in the model-facing text.
Fixed — _meta.ui on the chart follow-up tools
ls_add_indicator and ls_reframe_chart emit structuredContent.chart
but were absent from the _meta.ui set, so MCP Apps hosts could not pair
their updated charts with the inline renderer. Both are now included.
Compatibility
No existing tool, C# parameter signature, or response field — other than
the removed chart_available — changes. On a host that advertises no
chart capability, include_chart disappears from the published
tools/list schema: schema-breaking but behaviorally transparent, since
the model adapts to the advertised schema. A host must advertise the MCP
Apps capability or be on the chart-renderer allowlist to see charts at
all. Design notes:
docs/SPEC-v1.2-mcp-apps-capability.md.
v1.1.0 (2026-05-22)
Program-trading support — the first feature release on the v1.0 stable
line. Two new tools turn the catalogued /stock/program TRs into
natural-language answers, with inline Plotly charts. Additive only: no
existing tool, parameter, or response shape changes.
Added — Program-trading flow (ls_get_program_trading)
One tool, three scopes:
scope=market— market-wide program-trading flow.period=intraday
(t1662 — a ~1-minute 차익 / 비차익 series with the KOSPI200 index and
futures basis) orperiod=daily(t1633 — per-day history).scope=ranking— which stocks programs are net buying / selling
right now (t1636), withmktcap_ratio(net buying ÷ market cap) as a
size-normalized footprint metric.scope=stock— one stock's program-trading flow (t1637), intraday
cumulative or daily.
include_chart=true ships a Plotly v5 spec via structuredContent.chart
for inline rendering on MCP Apps hosts — a flow overview, basis vs
arbitrage, per-5-minute intensity / gross-flow twin panels, a daily
stacked bar, a ranking horizontal bar, or a per-stock price-vs-flow chart.
Added — Footprint analysis (ls_analyze_program_flow)
An Analysis-Layer tool. Given a stock, it classifies the
program-trading footprint into a deterministic verdict — a regime
(accumulation / distribution / churn / neutral), a 0–1 direction
confidence, signals (buy-day persistence and streak, churn ratio,
intensity, intraday pace, price coupling), and plain-language evidence
ready for the model to narrate.
Changed
- Tool surface 32 → 34 in the
standardprofile (35 → 37 in
all);ToolSurfaceFreezeTestsis updated to the new counts. The
addition is purely additive — the v1.0.0 frozen tools, parameter names,
and response shapes are unchanged.
Notes
/stock/programamount units differ by TR (t1662 / t1633 백만원;
t1636 / t1637 천원), and t1637's intraday series is current-session
only — both documented indocs/LS-API-QUIRKS.md§3.3. The tools
normalize all amounts to 억원.
Versioned in lockstep with RedoxNet.LsOpenApi.Core 1.1.0.
v1.0.0 (2026-05-21)
The first stable release. v0.10.1 was the functional v1.0 release
candidate; v1.0.0 freezes the public contract and closes the few surface
warts the v0.10.0 normalization missed.
Frozen contract
From v1.0.0 on, these will not change without a major version bump:
- the model-facing MCP tool surface — 32 tools in the default
standardprofile, 35 inall; - model-facing parameter names;
- default response shapes.
ToolSurfaceFreezeTests pins all three from the live [McpServerTool]
metadata — the tool names per profile, the row-cap parameter name on
every list/screener tool, and a cl100k_base token budget on the
serialized surface — so the frozen surface cannot silently drift or
bloat.
Changed — row-cap normalization (BREAKING)
v0.10.0 unified list/screener row caps on limit but missed two tools.
v1.0.0 finishes the job:
ls_get_etf_holdings—top_n→limit.ls_get_industry_indices—top_n→limit(the parameter and
the field echoed back in the response payload).
All eight list/screener tools now take limit; behavior is unchanged.
As with the v0.10.0 renames the practical breakage is small — the model
reads the live tool schema on every call.
Added
- Server-level routing guidance. The server now ships MCP
ServerInstructions— concise guidance steering structured
market-data questions to LS tools while leaving news / disclosure /
"why did it move" questions to the host's own sources. - NuGet MCP environment metadata.
.mcp/server.jsondeclares the
LS_APPKEY/LS_APPSECRETKEY/LS_MARKETenvironment variables
(with secret flags) so MCP hosts can prompt for credentials at
install time.
Fixed
- Personal-holdings questions no longer trip a refusal. "내 보유
종목" / "내 포트폴리오" style questions route tols_holdings_list
(the local portfolio store) instead of being declined as a brokerage
request. ls_get_industry_indicesname corruption and non-industry
pollution. Long LS index names overflow the fixed-widthhname
field and were truncated mid-character, leaving a U+FFFD replacement
glyph — now stripped via the shared name normalizer. Separately, the
industry board pulled LS's full 250+ index catalog, so KP200 / F-K200
leveraged & inverse products dominated the change-percent ranking. It
now fetches the KOSPI and KOSDAQ catalogs via the trustedgubun1
paths and drops LS index products — leveraged/inverse indices,
KP200 / KP50 GICS sector indices, market-cap composites (KOSPI50/100,
F-KOSPI200) — so the board ranks real 업종 only. Each row's absolute
changeis also recomputed fromvalueandchange_pct: t1511
occasionally reportschangeagainst a stale base, contradicting the
percent. A transient empty catalog leg is retried once; any remaining
single-market gap is surfaced inpartial_errorinstead of silently
dropped.
Changed — defaults
LS_MARKETdefaults toreal(wasvirtual). This is a
read-only market-data server with no order path, and the LS virtual
endpoint serves real market data anyway, sorealis the correct
default. Override withLS_MARKET=virtual.
Versioned in lockstep with RedoxNet.LsOpenApi.Core 1.0.0.
v0.10.1 (2026-05-20)
Patch over v0.10.0 — fixes the MCP Registry publish and one token-budget gap.
Fixed
- MCP Registry publish.
.mcp/server.json'sdescriptionexceeded the
registry's 100-character limit, somcp-publisher publishfailed
validation (HTTP 422 onbody.description). Shortened the description. ls_get_etf_holdingstoken budget.top_ndefaulted to unbounded —
an un-capped call on a 200+-holding ETF could dump 20k+ tokens into
context. The default is now 20 (the largest holdings carry most of an
ETF's weight); passtop_n=-1for the full list. The summary block's
holdings_countstill reports the full constituent count.
Versioned in lockstep with RedoxNet.LsOpenApi.Core 0.10.1 (no Core code changes).
v0.10.0 (2026-05-20)
The last 0.x minor — it clears the entire post-v0.9 SPEC backlog in
one breaking release (see docs/SPEC-v0.10.md): tool-surface
compression, foreign-ownership data, list-tool normalization, and a
generalized dataset cache. The only breaking change is the tool-surface
compression — see Migration below. v1.0.0 is stabilization only (no new
breaking changes; the tool surface and response shapes are frozen).
Changed — Tool-surface compression (BREAKING)
-
Five domain dispatchers. Twenty single-purpose portfolio tools
collapse into five action-routed tools. Each takes anactionargument
and validates the per-action required parameters, returning a
structured, model-recoverable envelope on a miss (error+
details.action+details.missing/details.valid_actions).ls_account— wasls_accounts_list/ls_account_upsert/ls_account_removels_watchlist— wasls_watchlist_add/_remove/_list/_group_create/_group_deletels_watched_themes— wasls_watched_themes_add/_remove/_listls_portfolio_io— wasls_portfolio_export/ls_portfolio_importls_holding— wasls_holdings_set/_buy/_sell/_remove/_corporate_action
ls_holdings_listandls_stocks_refresh_metadatastay standalone —
the holdings read path is the single most common portfolio intent, and
the metadata refresh belongs to no one domain. -
LS_TOOL_PROFILEprofile. A new env var:standard(default) or
all.standardhides the three catalog tools (ls_search_tr/
ls_describe_tr/ls_call_tr) fromtools/list— they are
developer-fallback TR access, not first-line routing candidates;all
exposes them.LS_TOOL_PROFILE_STRICT=trueadditionally rejects a
tools/callfor a profile-hidden tool instead of honoring it.
Net surface: 48 → 32 tools in the standard profile (35 in all).
tools/list JSON shrinks ~8% (65,025 → 60,014 chars).
Changed — List-tool normalization (minor BREAKING)
The row-count parameter on every list / screener tool is unified to
limit (was top_n or count), and tools that can cheaply know the
unfiltered total now emit total_available.
| Tool | Was | Now |
|---|---|---|
ls_get_top_stocks |
top_n |
limit |
ls_get_high_low_stocks |
top_n |
limit |
ls_get_industry_stocks |
top_n |
limit |
ls_get_theme_stocks |
top_n |
limit |
ls_get_fundamentals_rank |
count |
limit |
ls_get_market_warnings |
(unbounded) | limit (default 50, 1–200) |
Added — Foreign-ownership data
ls_get_stock_infogains an opt-inforeignsection (six sections
total). Sourced from the newly catalogued t1716, it carries the
금감원 foreign held-share level —held_shares, a derived
ownership_percent, and a normalizedexhaustion_rate_percent. The
defaultsectionsis unchanged, so existing calls are unaffected and
pay no extra TR call. (Daily foreign net flow remains
ls_get_investor_flow; t1716's unique value is the holding level.)
Added — Index-history export (dataset handle)
ls_get_index_historygainsoutput_mode(summarydefault,
export).exportcaches the whole series behind adataset_idand
returns only the digest; a follow-up call with thatdataset_id(plus
optionalfrom/to/recent_n) slices the cached bars with no
further API call.countmay reach 2,500 in export mode (vs 500 for
summary). The chart-onlyDatasetHandleCachewas generalized into a
kind-tagged store to back this.
Migration
The compression is the only breaking change — the merged tools are gone.
Re-map each old call to its dispatcher action:
| v0.9 tool | v0.10 call |
|---|---|
ls_accounts_list |
ls_account(action="list") |
ls_account_upsert(…) |
ls_account(action="upsert", …) |
ls_account_remove(…) |
ls_account(action="remove", …) |
ls_watchlist_add(…) |
ls_watchlist(action="add", …) |
ls_watchlist_remove(…) |
ls_watchlist(action="remove", …) |
ls_watchlist_list(…) |
ls_watchlist(action="list", …) |
ls_watchlist_group_create(…) |
ls_watchlist(action="group_upsert", …) |
ls_watchlist_group_delete(…) |
ls_watchlist(action="group_delete", …) |
ls_watched_themes_add(…) |
ls_watched_themes(action="add", …) |
ls_watched_themes_remove(…) |
ls_watched_themes(action="remove", …) |
ls_watched_themes_list |
ls_watched_themes(action="list") |
ls_portfolio_export(…) |
ls_portfolio_io(action="export", …) |
ls_portfolio_import(…) |
ls_portfolio_io(action="import", …) |
ls_holdings_set(…) |
ls_holding(action="set", …) |
ls_holdings_buy(…) |
ls_holding(action="buy", …) |
ls_holdings_sell(…) |
ls_holding(action="sell", …) |
ls_holdings_remove(…) |
ls_holding(action="remove", …) |
ls_holdings_corporate_action(…) |
ls_holding(action="corporate_action", …) |
ls_search_tr / ls_describe_tr / ls_call_tr |
same names — set LS_TOOL_PROFILE=all to expose them |
ls_get_{top_stocks,high_low_stocks,industry_stocks,theme_stocks}(top_n=…) |
(limit=…) |
ls_get_fundamentals_rank(count=…) |
ls_get_fundamentals_rank(limit=…) |
ls_holdings_list, ls_stocks_refresh_metadata, and every market-data
tool keep their names. ls_get_stock_info and ls_get_index_history
keep their names too — the new foreign section and output_mode are
additive, so existing calls are unaffected.
Lockstep version bump with RedoxNet.LsOpenApi.Core 0.10.0.
v0.9.0 (2026-05-20)
Response-shape / token-economy refactor — Phase 1 of the work deferred
from the v0.8 line (see docs/SPEC-v0.9-response-shapes.md). Three
high-traffic tools are reshaped so the default call carries only what
the common question needs; the heavy payload becomes opt-in. This is an
intentional breaking release — see Migration below.
Changed — Response shapes (BREAKING)
ls_get_index_history— newverbosityargument
(summary|compact|full), defaultsummary.summary
returns only the aggregate digest (period extremes, totals, average
breadth / flows,_metaunits);compactadds the 5 most recent bars;
fullis the pre-v0.9 all-points shape. cl100k-measured 60-bar KOSPI:
summary 299 / compact 1,051 / full 8,979 tokens (96.7% / 88% / 0%
reduction).ls_get_stock_info— newsectionsargument, default
["snapshot","fundamentals"]of five (snapshot,fundamentals,
periods,brokers,flags). Unselected sections are omitted;
sections_shownechoes the result; an unknown section name is a
validation error. Measured: default 565 / all five sections 1,484
tokens (62% reduction). Alongside the split, several long-standing
t1102 field-mapping errors are corrected: thebrokersbuy/sell
numbers were swapped (LS'sd*= 매도,s*= 매수);fundamentals
quarterly figures are the two latest settled periods (latest/
previous), not a current quarter;snapshot.turnover_ratio_percent
now reads 회전율;fundamentals.capital.margin_ratio_percentis the
margin rate (was mislabeled equity ratio). The v0.8foreignsection
is removed — t1102 carries no foreign-investor data; use
ls_get_investor_flowfor foreign / institutional flow.ls_holdings_list— newthemes_limit(default 5;0= count only,
-1= all),include_industry,include_quote(both defaulttrue).
Each holding'sthemesis now a{count, shown, items}object, not a
bare array;include_quote=falsedrops the per-holding quote /
valuation block (account and total summaries are still computed).
Measured 10-holding: default 3,664 / lightest call
(themes_limit=0, include_*=false) 804 tokens (78% reduction).
Changed — Infrastructure
- ModelContextProtocol SDK
1.2.0→1.3.0. TargetFrameworksnet8.0;net9.0→net8.0;net10.0.- New shared
ResponseShapehelper (VerbosityMode,Slice<T>,
TryParseVerbosity,ParseSections) backing the three reshaped tools. - Test-only
TokenEstimator(cl100k_base viaMicrosoft.ML.Tokenizers)
pins every reshaped tool against a measured token budget.
Added — MCP Registry
- An
mcp-namemarker in the package README and a manually-triggered
publish-mcp-registryworkflow, preparing the listing on the Official
MCP Registry asio.github.redoxnet/lsopenapi.
Migration
Every reshaped tool's default response changed. To restore the pre-v0.9
(v0.8-equivalent) payload:
| Tool | v0.9 default | Restore the v0.8 shape |
|---|---|---|
ls_get_index_history |
verbosity="summary" (digest only, no bars) |
verbosity="full" |
ls_get_stock_info |
sections=["snapshot","fundamentals"] |
sections=["snapshot","fundamentals","periods","brokers","flags"] (the v0.8 foreign section is gone — it was wrong data) |
ls_holdings_list |
themes_limit=5 |
themes_limit=-1 |
ls_holdings_list themes_limit=0 omits the theme items entirely (count
only) — it is not the full-restore value; use -1.
Lockstep version bump with RedoxNet.LsOpenApi.Core 0.9.0.
v0.8.0 (2026-05-20)
Overseas market data + per-stock analytics. Tool surface 43 → 48
(+5). The v0.8 line split in two: this release ships the wrapper +
catalog work, while the response-shape / token-economy refactor moves to
v0.9. Five new wrappers turn natural questions — "나스닥 지수",
"원달러 환율", "삼성전자 투자의견", "SK하이닉스 공매도 추이",
"오늘 52주 신고가", "요즘 예탁금 추이" — into one tool call each.
Added — Overseas market data (1 tool)
ls_get_global_market_quote(kind?, symbol?)— one-shot overseas
index / FX / futures snapshot viat3521. An alias table maps
nasdaq→NAS@IXIC,sp500→SPI@SPX, plusdow/soxx/
usdkrw/wti/goldand more; raw LS symbols pass through
unchanged.kind∈index/fx/futures.
Added — Per-stock analytics & screener (4 tools)
ls_get_analyst_opinions(shcode, count?)— brokerage (sell-side)
investment-opinion history viat3401. Each entry carries the
opinion-day date, 회원사, the rating before/after the change, the
target price before/after, and the opinion-day close, plus a
current-price snapshot. Returns the ~20 most recent changes.ls_get_short_selling_trend(shcode, from?, to?, count?)— per-stock
daily short-selling (공매도) viat1927. Short volume / value
(백만원), short ratio, average short price, cumulative short volume,
and the uptick-rule applied vs. exempt split, with a period summary
(totals + highest-ratio day).ls_get_market_funds_trend(market?, count?)— market-liquidity
series viat8428. Per day: index, 고객예탁금, 예탁증감, 신용잔고,
미수금, 선물예수금, and equity / mixed / bond / MMF fund money.
All monetary fields in 억원.ls_get_high_low_stocks(direction?, period?, maintained?, market?, top_n?, exclude_etf?)
— 신고가 / 신저가 screener viat1442. Defaults tuned from live
E2E:maintained=true(돌파유지) and ETF/ETN excluded server-side,
so the result is the clean "currently at a new high" list.period
look-back spans 전일 ~ 52주 ~ 년중.
Changed
- Embedded TR catalog 33 → 45:
t3102/t3518/t3521plus the
nine staged TRs (t1105/t1305/t1403/t1442/t1475/
t1927/t3401/t8425/t8428), all reachable vials_call_tr. ls_call_trnow surfaces body continuation cursors. A TR that pages
bytr_contheader yet also carries a body cursor field (e.g.
t3401'scts_date) previously returnedcontinuation.keys: {}; the
keys map is now populated from the catalog'skey_fieldsso the
caller knows which field to advance for the next page.
Notes
- News (
t3102) is catalog-only. The LS news pipeline discovers
article IDs (sNewsno) through the NWS WebSocket push — the push
payload'srealkeyis thesNewsno— which this server does not yet
implement. A first-class news wrapper waits on WebSocket transport
(v2.0).
v0.7.0 (2026-05-18)
Screeners + index history + industry filter. Tool surface 40 → 43
(net +3 after Tier 2 compression). The headline pivot is from
"snapshot one stock" toward natural screener questions — "PER 낮은
종목", "오늘 외인 매수 누구 들어왔어", "다음 주주총회 언제",
"내 보유 중 관리종목 있어?" — answerable in one tool call. v0.7 also
moves portfolio data hygiene forward: avg_price no longer drifts
under split/reverse-split round-trips, ETF holdings stop reporting
perpetual "themes pending", and a new industry filter classifies
holdings by FICS industry name without a manual refresh.
Added — Screeners (4 tools)
ls_get_fundamentals_rank(field, market?, count?)— fundamental-metric
ranking viat3341(재무순위종합). One TR call covers PER / PBR /
PEG / EPS / BPS / ROE plus four growth metrics + 부채비율 + 유보율.
PER / PBR / PEG are forced ascending (LS-side, undervalued first);
the other metrics use LS's default ordering.fieldaccepts English
snake_case (per,pbr, …) or Korean labels (매출액증가율, …).
Each row carries the full fundamental snapshot so the model can
compare two metrics on the same stock without a follow-up call.ls_get_investor_flow(shcode?, …)— investor-type flow dispatcher
acrosst1601(intraday market-wide) andt1702(single-stock
daily). Omitshcode→ market snapshot per segment (KOSPI / KOSDAQ /
선물 / 옵션; LS does not label segments, so the wrapper surfaces
them asblock_index = 1..6). Passshcode→ daily time series
withmetric(volume/value/price) +direction(net/buy/
sell) +cumulativetoggle. Twelve investor types unified across
both modes: 개인 / 외국인 / 기관계 / 증권 / 투신 / 은행 / 보험 / 종금
/ 기금 / 국가 / 기타 / 사모펀드.ls_get_stock_events(shcode, from?, to?, kinds?)— corporate-action
calendar viat3202(종목별 증시일정). Covers all 14 LS event types
(유무상증자, 배당, 감자, 합병/분할, 매수청구, 실권주, 액면교체,
주주총회, 상호변경, 국내/해외 CB 전환, 해외 BW 행사, 스톡옵션행사).
kindsaccepts English snake_case, Korean labels, or raw two-char
upgu codes. TBD entries (recdt='00000000') survive date filtering
so "다음 주총 언제" surfaces even when LS hasn't fixed the date.ls_get_market_warnings(kinds?, shcodes?, market?)— KRX surveillance
list viat1404+t1405. Covers 13 designations: 관리, 불성실공시,
투자유의, 투자환기, 투자경고, 매매정지, 정리매매, 투자주의,
투자위험, 위험예고, 단기과열지정, 이상급등, 상장주식수부족. Default
kind set is 관리 (designated_admin) only — pass an explicit list
(e.g.["관리", "매매정지", "단기과열"]) for wider sweep.shcodes
clips against holdings so "내 보유 중 관리종목" takes one call.
Per-kind seen-shcode dedup absorbs the LS quirk wherects_shcode
echoes the same cursor when a single-page screen has more candidates
to enumerate, so the response stays the size of the actual unique
set instead of fan-out × pages.
Added — Index history + metadata refresh (2 tools)
ls_get_index_history(upcode, period_type?, count?, cts_date?)— daily/
weekly/monthly time series for a Korean index viat1514. Aliases
mirrorls_get_index_quote(kospi→001etc.). Per-bar OHLC,
volume, transaction value, market breadth (advance/decline/limit-up/
limit-down), and foreign/institutional net flow.cts_datepagination
surfaced when more pages exist; dataset-handle integration (so
ls_add_indicatorcan pipe off this series) is deferred to v0.8.ls_stocks_refresh_metadata(shcodes?, kinds?)— synchronous refresh
for theme and FICS industry enrichment. Default scope = holdings ∪
watchlist symbols whenshcodesomitted;kinds∈themes/
industry. Blocks until the LS-side TPS-1 calls finish (≈N seconds
for N symbols), then echoes per-symbolthemes_updated/
industry_updatedflags and any errors. Use case: the user just
imported a fresh portfolio and wants the metadata caches warm.
Added — Industry filter + industry_* columns (A1)
ls_holdings_list(industry?)— new optional filter. Case-insensitive
substring match against the normalized FICS industry label.
"내 보유 중 반도체 종목" now resolves to aWHERE industry LIKE '%반도체%'. The response carries amatching_industriesecho
(alphabetized distinct labels) so LIKE false positives are visible
the same waymatched_themesworks.- New
stockscolumns (industry_raw,industry,industry_fetched_at)
populated byt3320(FNG_요약).industry_rawkeeps the LS-shipped
"FICS …" prefix verbatim;industryis the normalized label without
prefix. ETF / SPAC / no-profile stocks recordindustry_fetched_at
with NULL industry — the same "fetched-but-empty" pattern v0.7 also
applies to stock_themes (B2) so the column doesn't loop "pending"
forever. - Enrichment is fire-and-forget on holdings / watchlist writes (joining
the existing v0.6 themes path) and synchronous via
ls_stocks_refresh_metadata(kinds=["industry"]). 1 TPS per symbol,
so cold-fill ≈ N seconds for N symbols;industry_fetched_at IS NOT NULLis treated as a permanent cache hit (산업 변경은 분기/연 단위
이벤트).
Fixed — Storage precision (B1) + ETF perpetual-pending (B2)
holdings.avg_pricemigrated fromREALtoINTEGERfractional
won (×10000) — schema v4. Public API response shape unchanged
(avg_pricestill ships asdouble); corporate-action round-trips
(split N then reverse_split N) now exact down to the 1/10000 won
instead of drifting by ~1e-10 per cycle. Internal corporate-action
API switched from(double qtyMultiplier, double priceMultiplier)
to rational(long qtyNum, long qtyDen)so the cost basis is exact
even for non-integer ratios (e.g. bonus(1+r)).stock_themessentinel row for ETFs and SPACs that LS reports
with an emptyt1532array. Before: cache treated "0 rows" as "not
yet fetched" → 60s cooldown loop forever. After:Replace…([])
inserts(symbol, "__NONE__", "")so the cache-hit check matches
and thethemes_status: pendingflag clears. SELECT side filters
out the sentinel, so callers see empty themes (consistent with v0.6).
Changed — Tier 2 compression (BREAKING, −3 tools)
LLM routing burden mitigation; service+repository layers untouched.
ls_watchlist_group_renameremoved. Renames now flow through
ls_watchlist_group_create(name, description?, rename_from?).
Whenrename_fromis set, the existing group is renamed (and
descriptionoverrides if provided); otherwise the call upserts
as before. Conflicts raiseValidationError.ls_watchlist_groups_listremoved. Group-meta listing now flows
throughls_watchlist_list(scope?: "items" | "groups" = "items").
scope="groups"returns{ groups: [{name, description, sort_order, item_count}, …] };scope="items"keeps v0.6 behavior.ls_broker_renameremoved. Broker label rename now flows through
ls_account_upsert(rename_broker_from?, broker?). When
rename_broker_fromis set, every account with that broker label
has its broker field updated tobroker(other args are ignored);
otherwise the call upserts as before.
Added — Catalog v0.7 (7 new TRs)
t3320(FNG_요약 / 투자정보) under/stock/investinfo— FICS
industry name + 시장구분 + 회사 프로필 + 시가총액 + 외국인비율 +
PER/PBR/EPS/ROE 등. Internal-only (A1 enrichment source). 1 TPS;
6-char shcode only (LS doc's "A"+6 prefix is incorrect, confirmed
by live verify).t3341(재무순위종합) —ls_get_fundamentals_rankbackend.t1601(투자자별 종합) under/stock/investor—ls_get_investor_flow
intraday backend. Six unlabeled OutBlocks, twelve investor types each.t1702(외인기관 종목별 동향) under/stock/investor—
ls_get_investor_flowdaily backend.t3202(종목별 증시일정) —ls_get_stock_eventsbackend.t1404(관리/불성실공시/투자유의) +t1405(투자경고/매매정지/
정리매매/단기과열 …) under/stock/market-data—
ls_get_market_warningsbackend. Both usects_shcodecontinuation.
Deferred — v0.8 candidates
- Dataset-handle integration for
ls_get_index_history. v0.7 ships
as a thin wrapper;ls_add_indicator/ls_reframe_chartpiping is
v0.8. - t1511 firdiff non-self slot fix (B3). v0.6
d0b765eself-entry
override stands; the other side (other upcodes' responses that include
KRX 100 as a related index) still ships LS-as-shipped because no
top-leveldiffjisuis available for client-side correction. LS bug
report filed; v0.8 will re-evaluate after LS responds. - FICS → KRX standard industry mapping. v0.7 stores FICS labels
verbatim (withindustry_rawkeeping the "FICS " prefix and
industrynormalized). KRX standard isn't directly published in
LS OpenAPI; bridging is a v0.8 problem.
Verified
- 531 tests pass on net8.0 (181 Core + 350 Mcp; was 408 at the end
of v0.6 — net +123 across schema v4/v5 migration, sentinel-row
semantics, FICS industry normalization, integer-arithmetic
corporate actions, 6 new tool wrappers, and E2E polish below). - Live E2E (
todo/Test_v0.7.0.txt) against the LS 모의투자
server caught and fixed three issues before tag:t3341idxwas being serialized as a JSON string when LS
expects a number — HTTP 500 on every call. C-1 wrapper now
keepsidxasintend-to-end.t1702lives under/stock/frgr-itt, not/stock/investor
like t1601 (LS splits [주식] 외인/기관 from [주식] 투자자).
Catalog path corrected; thetodo/t1702.txtreference sheet
was staged without the URL header so v0.7 prep guessed wrong.ls_get_market_warningshad a cursor loop ont1404/t1405
when the surveillance screen fit in one page — LS kept echoing
the samects_shcodeand the wrapper paged 6× through the same
rows. Fixed with per-kind seen-shcode dedup.ls_get_investor_flowdaily mode shipped ~12k tokens for one
30-day call (30 × 12 investor types × {kind, korean_label,
value} objects). Diet applied: default 3 macro categories
(investors=["all"]opts back in), flows array → map shape,
redundantsign/changedropped,summaryblock added with
period totals + per-investor extremes. ~75% token reduction.
- Live verifications also confirmed: t3320 6-char shcode behavior,
FICS prefix consistency for two semiconductor stocks (005930 +
000660), ETF empty-OutBlock detection. See
scripts/verify-t3320.ps1.
v0.6.0 (2026-05-16)
Market context + theme wrappers + portfolio I/O. The third big surface
release: 37 → 39 tools (net +2 after Tier 1 compression). v0.5's
"watched sectors" naming was a misnomer — LS API splits 업종 (KRX
industry classification) from 테마 (LS curated themes); v0.6 lines up
both DB and tool surface with that split. The portfolio module also
gets its first migration story (export/import + before-import auto-backup).
Added — Market context (3 tools)
ls_get_index_quote(index_code)— single Korean index snapshot via
TRt1511. Aliaseskospi→001,kosdaq→301,kospi200→101,
krx100→501. Envelope nestsvalue/previous_close/
change/change_pct, OHLC with timestamps, 52-week and YTD range
blocks, market breadth (up/down/unchanged/limit), and four related
auxiliary indices (e.g. KOSPI 종합 returns 대형주/중형주/소형주
alongside).ls_get_industry_indices(market, top_n)— sorted top-N industry
indices for kospi/kosdaq/all. Internally fans outt8424(industry
catalog) +t1511per upcode; the catalog defaultrate_limit_per_sec=10
fort1511(confirmed against the LS [업종] 시세 guide) keeps the
cold-cache cost ≈2.5s for KOSPI's ~25 codes. 60-second in-process
cache sotop_n=5thentop_n=30is one fanout, not two.ls_get_industry_stocks(upcode | industry_keyword, market, top_n)
— stocks inside one industry with the industry's index summary, via
t1516. Body-based continuation paging (last shcode echo). Keyword
resolution against the cached catalog: 0 matches →IndustryNotFound,
1 →resolved.matched_via="keyword"echo, 2+ →AmbiguousIndustry
with candidates.
Added — LS themes (2 tools)
ls_get_theme_stocks(theme_code | theme_keyword, top_n)— stocks
inside one LS curated theme + the theme's roll-up summary, via
t1537. Header-based continuation paging (tr_cont/tr_cont_key
echo). Keyword resolution same 0/1/N branches as the industry tool.ls_get_stock_themes(shcode)— every theme a stock belongs to via
t1532. Empty array is a valid response — not every stock is
pinned to a theme.
Added — Portfolio I/O (2 tools)
ls_portfolio_export(path?)— versioned JSON snapshot covering
accounts/holdings/watchlists/watched_themes. Default path writes
<db-parent>/exports/portfolio-YYYY-MM-DDTHHmmss.jsonso backups
sit alongsideportfolio.db.stocksandstock_themescaches are
intentionally excluded — quote/theme enrichment rebuilds them after
import.ls_portfolio_import(path, mode, confirm)—mode=merge(default)
skips duplicates with explicit reason codes per domain
(duplicate_account_number,duplicate_theme_code, …);
mode=replacewipes the export-covered domains first, requires
confirm=true, and silently writes abefore-import-*.json
auto-backup so a wrong file is recoverable. Unsupported
schema_version→ImportSchemaMismatch.
Added — Theme enrichment + freshness hint
t1532fire-and-forget enrichment onls_holdings_set/_buy/
ls_watchlist_add. Per-stock theme memberships cached in a new
stock_themestable. Best-effort: LS errors are absorbed; on stdio
shutdown a half-finished enrichment retries on next session's first
write.ls_holdings_listnow emits ametadata_freshnessblock:
{ fully_enriched, pending: { themes: N }, hint }. Each holding row
carriesthemes(array) and optionalthemes_status("pending"
when enrichment hasn't caught up yet; omitted when"ok"to save
tokens).ls_holdings_listgains optionaltheme_code(exact) and
theme_keyword(LIKE on name) filters with AND-combine semantics.
Responses include afilterecho andmatched_themes(alphabetized
unique names) so LIKE false positives are visible.
Added — Catalog v0.6 (6 new TRs)
t1511업종현재가 (wrapper) —rate_limit_per_sec=10.t1485예상지수 /t1514업종기간별추이 /t8424전체업종 —
catalog-only; reachable vials_call_tr.t8424is consumed
internally byls_get_industry_indices.t1516업종별종목시세 (wrapper).t1537테마종목별시세조회 (wrapper).
Changed — Renamed (BREAKING)
ls_watched_sectors_{add,remove,list}→ls_watched_themes_{add,remove,list}.
Paramsector_code→theme_code,sector_name→theme_name.
v0.5 was caching LS theme tmcodes under the "sector" label; v0.6
lines names up with the actual concept.- portfolio.db schema v2 → v3 migration:
watched_sectorstable
renamed towatched_themes(columnssector_code/sector_name→
theme_code/theme_name). Newstock_themescache table.
v0.5 data (e.g. tmcode0012,0064) is preserved.
Removed — Tier 1 compression (BREAKING, −5 tools)
LLM routing burden mitigation; service+repository layers untouched.
ls_account_get— derive fromls_accounts_list[].is_default.ls_account_set_default— same effect via
ls_account_upsert(set_default=true).ls_holdings_split/ls_holdings_reverse_split/
ls_holdings_bonus— collapsed intols_holdings_corporate_action( shcode, type, ratio, account?). Open enum: v0.6 supports
split/reverse_split/bonus; v0.7+ adds
stock_dividend/spin_off/mergerby extending the enum
without growing the tool surface.
Deferred — v0.7 candidates
stocks.krx_sectorenrichment +industry?filter. SPEC §4.4
/ §10 Q7 — confirmed during v0.6 implementation thatt1102does
not return a KRX industry classification field. LS's [주식 섹터]
category is all theme TRs. v0.6 leaves the column NULL; v0.7 will
identify an enrichment source (LS 마스터 TR / t8424+t1516 reverse
lookup / static KRX table).ls_get_index_history(t1514wrapper) — catalog-only in v0.6.ls_stocks_refresh_metadatasynchronous refresh tool.
Verified
- 230 unit + fixture tests pass on net8.0 (was 178). New coverage:
IndustryDataCache fanout sort + cache reuse, t1516 body-paging,
t1537 header-paging withtr_cont_keyecho, keyword resolution
0/1/N branches for industry + theme, schema v3 migration round-trip,
ReplaceStockThemes / GetStockThemesBatch, EnrichStockMetadataAsync
store + LS-error fallback, ListHoldings metadata_freshness +
per-row themes, theme filters (exact / LIKE / AND-combine),
portfolio I/O round-trip + replace auto-backup + schema_version
mismatch, corporate_action dispatch + unknown-type rejection. - 48-case stdio smoke (
scripts/portfolio-smoke.py) extended with
Tier 1 compression regressions (removed tools absent from
tools/list, calling a removed name returns__rpc_error),
watched_themes rename, portfolio export/import round-trip with
schema_version=99 → ImportSchemaMismatch, all 5 new tools route
offline.
v0.5.0 (2026-05-15)
Local-only portfolio module — multi-account holdings, buy/sell/corporate-action semantics, watchlists, watched sectors. The biggest tool-surface expansion since v0.1: 13 → 37 tools. Stored alongside token.db; no broker sync, no data leaves the user's machine.
Added — Portfolio module (24 new tools)
Accounts
ls_accounts_list— every registered account with holdings count and the default flag (empty array when no accounts exist).ls_account_get— default account, ornullwhen none registered.ls_account_upsert(account_number, nickname, broker, set_default)— create or update byaccount_number.nicknameis globally UNIQUE; first registration auto-promotes to default;set_default=truedisplaces the existing default within a single transaction.ls_account_set_default(account)— promote byaccount_numberornickname.ls_account_remove(account, confirm)— two-step cascade.confirm=falsewith holdings returnsRequiresConfirmationcarryingholding_count+market_valuepreview;confirm=trueproceeds. When the removed account was default and others remain, the oldest account (id ASC) is auto-promoted.ls_broker_rename(from, to)— rename a broker label across every matching account (free text; no merge conflicts since nickname is the unique key).
Holdings (account-aware)
ls_holdings_list(account?)— grouped response:accounts[]with per-accountsummary+ atotal_summaryroll-up across all accounts. Optionalaccountfilter narrows to one. Each row carries awarningfield whencurrent_price/avg_pricediverges 5×+ (likely missed corporate action).ls_holdings_set— replace state.quantity=0isValidationError("use ls_holdings_remove").ls_holdings_buy(shcode, quantity, price)— incremental buy with weighted-average merge:new_avg = (old.qty*old.avg + qty*price) / (old.qty + qty).ls_holdings_sell(shcode, quantity)— partial sell; row auto-removes when remaining quantity reaches zero; raisesInsufficientQuantityabove the position withapplied_toecho.ls_holdings_remove— drop a row outright;removed=falsewhen not held anywhere.ls_holdings_split(ratio)/_reverse_split(ratio)/_bonus(ratio)— corporate actions. With noaccountspecified, applied across every account holding the symbol (single corporate event affects all owners). Reverse-split rejects non-divisible quantities withValidationError; bonus rejects non-integer results from a single-share holder.
Watchlists
ls_watchlist_groups_list/_group_create/_group_delete/_group_rename(new).ls_watchlist_add/_remove/_list— items inside groups; list enriches with live quotes when credentials are present, falls back toquote_errorenvelope otherwise.
Watched sectors / themes
ls_watched_sectors_add/_remove/_list—t1531theme codes with avg percent change. 60-second in-process cache on the theme table so adding multiple watched themes doesn't burn the LS rate limit.
Added — Cross-cutting
- Multi-account ambiguity policy. Reads fall back; writes require an explicit target when ambiguous. Documented in docs/SPEC-portfolio-multi-account.md.
- 0 accounts →
RequiresAccounterror. - 1 account → auto with
applied_toecho. - 2+ accounts →
AmbiguousAccountwithcandidates[]for set/buy. For sell/remove, only ambiguous when the symbol exists in multiple accounts.
- 0 accounts →
applied_toecho on every write response. Single{account_number, nickname, broker, is_default}for one-account writes; array of before/after snapshots for corporate actions across multiple accounts. Safety net for the soft single-account fallback.- Typed error envelopes —
RequiresAccount,AmbiguousAccount,AccountNotFound,RequiresConfirmation,InsufficientQuantity,ValidationError. Each carries structured fields (candidates, identifier, holding count, market value, current/requested quantity) so the LLM can re-call with the correct argument without prompting the user. LSOPENAPI_DB_PATHenv var — override the local portfolio SQLite path. Defaults to%LOCALAPPDATA%\RedoxNet\LsOpenApi\portfolio.db(next totoken.db).- 6-character alphanumeric stock codes. Validation relaxed across
ls_get_multi_quoteand the portfolio tools so ETF codes with an uppercase letter (e.g. TIGER 코리아AI전력기기TOP3 =0117V0) are accepted. Lowercase input is uppercased on the way in so storage stays case-insensitive. - Split/bonus warning on
ls_holdings_list. Whencurrent_price / avg_pricediverges by 5× or more in either direction, the holding row carrieswarning: "분할/무상증자 가능성: 현재가/평단 비율 N배. 분할 도구로 보정하세요.".
Changed
- Tool count 13 → 37. New tools listed above.
ls_holdings_listresponse shape. Always grouped (accounts: [...]+total_summary); single-account responses have length 1. Field renames:current_value → market_value,total_cost → cost_basis.- Schema migration v2 on the local SQLite store. Drops the v0.4 placeholder
'UNSET' / '기본 계좌'account when no holdings reference it, and addsUNIQUE(nickname). The zero-account empty state is now valid and surfaced asRequiresAccounton first write.
Removed
ls_account_set(single-account default updater). Replaced byls_account_upsert— same first-run UX, but explicit about creating named accounts.ls_holdings_add/ls_holdings_update. Replaced byls_holdings_set(replace),_buy(weighted-average merge),_sell(subtract + auto-remove). The split makes the intent explicit in the tool name so the LLM routes by meaning rather than guessing whether an "add" call meant "more shares bought" or "current state is now this".
Verified
- 178 unit + fixture tests pass on net8.0 (up from 157), including new multi-account / weighted-average / split-divisibility / cascade-confirmation / default-succession cases.
- 28-case stdio smoke (
scripts/portfolio-smoke.py) covers empty state, account upsert + default toggle, nickname collision, multi-account ambiguity, weighted-average buy, sell with auto-remove + over-sell guard,set(qty=0)validation, grouped holdings_list with total_summary, split across all holders, non-divisible reverse-split, two-step account cascade with auto succession, broker rename, watchlist group rename. Live-verified against the LS real server. - E2E via Claude Code against the live LS API (12 natural-language scenarios): multi-account registration in one turn, "유안타 LG전자 24주 익절", "민테크 10:1 분할" auto-propagated to both accounts, "유안타증권 계좌 지워줘" with
RequiresConfirmation→ confirm → cascade + auto-succession to 카카오페이.
v0.4.0 (2026-05-15)
Token-efficient chart payloads, two follow-up tools that operate on a dataset handle, a ZigZag-based swing detector for key_turns, and an IndicatorCoverage block that lets the model explain why an indicator is null.
Added
ls_add_indicator— follow-up tool that adds an indicator (e.g."ma:200") to an existingdataset_idand refetches the frame with that indicator's warm-up window. Updates the samedataset_id, returns the new summary + optional Plotly chart spec. Lets the model honor "MA200도 추가해줘" without sending raw OHLCV back through context.ls_reframe_chart— follow-up tool that reframes a dataset to a different period/count using the cached symbol + indicator specs. Replaces the current view in the samedataset_idso a subsequentls_add_indicatordoesn't need aperiod_typeargument.output_modeonls_get_chart—display|analyze|export|reference.display/analyze/referencekeep raw OHLCV and full indicator arrays out of the model's text; onlyexportreturns them. Defaults todisplaywheninclude_chart=true, otherwiseanalyze. Existingsummary_onlystays as a legacy flag and maps toanalyzewhenoutput_modeis omitted.dataset_idis returned on every successful chart call — opaqueds_*handle backed by a process-local LRU (16 datasets, 5 MB per dataset). Used by the two follow-up tools.with_warmup— explicit opt-in for the analytical-summary warm-up policy.null(default) auto-applies warm-up whenfromis unspecified and skips it whenfromis given;trueforces warm-up even with explicitfrom(analyze long-period indicators inside a narrow window);falseskips warm-up even whenfromis null (fastest, narrowest read). The tool description spells out the three cases so the model toggles it without being told.summary.coverageon every chart response — per-indicator availability (MA5..MA200,ma60_slope,change_1y,change_5y,key_turns) reported asok/insufficient_data/disabled, pluswarmup_applied,analytical_bar_count,display_bar_count, and a human-readablenotewhen something is missing. The narrow-windownoteliterally tells the model "pass with_warmup=true to populate them" so it can self-correct in the next turn.
Changed
ls_get_charttext payload is summary-first. The text content now carriesdataset_id+summary+ (foranalyze) the existingcontextblock; rawcandlesand full indicator arrays are present only whenoutput_mode='export'. Plotly chart specs continue to ship viastructuredContent.chart— zero token cost. Long-range chart requests no longer blow tens of thousands of tokens into the conversation.AnalyticalSummaryis computed over a warm-up-inclusive series, not the trimmed display window. Default warm-up: 240 day bars / 120 week+month / 200 min / 10 year. Withcount=60,summary.moving_averagesnow includes a populatedMA200,ma60_slopeis non-null, andchange_pct1_yis populated — without changing what the user sees on the chart.key_turnsuse a threshold-reversal ZigZag instead of a 5-bar fractal. Reversal triggers on the close (not on intrabar high/low), so a single wide-range bar can't self-trigger a spurious pivot. Period-aware percent thresholds: day 4%, week 8%, month 12%, year 20%, min 1.5%, tick 1%. Pivots strictly alternate peak/trough; the trailing pivot isis_confirmed=falseat the latest bar and represents the in-progress swing. The fix also closes a stale-index bug in the old fractal detector that could emit duplicate pivots on the same bar.InflectionPointshape:(date, price, kind: peak|trough, change_pct_from_prev, is_confirmed)—kindis now a typed enum (serialized as"peak"/"trough"via a snake-case enum converter), and each turn carries its leg size + confirmation status.- MA60 slope (
rising/flat/falling) is classified by a least-squares fit over the lookback window of MA values, not a two-point delta. A single noisy endpoint no longer flips the verdict.
Tool count
- 11 → 13 (added
ls_add_indicator,ls_reframe_chart).
Verified
322 unit and fixture tests pass on .NET 8. Live-verified end-to-end against the LS real-market server: default summary populates MA5..200 + slope + 1Y change over a 300-bar analytical window; narrow explicit-from window with with_warmup=true forces padding and re-populates long indicators; with_warmup=false skips it (coverage.note then guides the model to re-call with true); output_mode='export' brings back raw OHLCV; ls_add_indicator(ma:200) preserves dataset_id and returns the latest value; ls_reframe_chart swaps day→week in-place; ZigZag pivots on 035720 monthly alternate strictly with a trailing tentative pivot.
v0.3.0 (2026-05-14)
A new market-screener tool, a search-parameter rename, and Naver-style chart polish.
Added
ls_get_top_stocks— market-wide ranking screener wrapping five TRs behind onekindparameter:gainers/losers/unchanged(t1441),market_cap(t1444),volume(t1452),amount(t1463),volume_surge(t1466). Supportsmarket(all / kospi / kosdaq),basis(today / previous_day),exchange(unified / krx / nxt), and price/volume floor filters; paginates via the LSidxcontinuation key and merges KOSPI + KOSDAQ formarket_capwhenmarket=all. Brings the tool count to 11.
Fixed
ls_get_top_stocksprice filters were silently ignored. LS's ranking TRs (t1441/t1452/t1463/t1466) treateprice=0as "no price filter" and — the non-obvious part — suppress thespricefloor along with it. Somin_pricewith nomax_pricereturned the full unfiltered list. The tool now sends the 8-digit ceiling (99999999) asepricewhenever amin_pricefloor is set without amax_pricecap.min_volumeand the both-bounds case were already correct. Verified end-to-end against the live LS server. (Sending the fields as JSON strings instead of numbers, an early hypothesis, makes LS return HTTP 500 — the field types were never the problem.)
Changed
ls_search_stock'squeryparameter renamed tokeyword.ls_search_tralready tookkeyword, and models generalize the sibling tool's parameter name — sols_search_stockcalls came in withkeywordand were rejected by the .NET MCP SDK's parameter binder with an opaque "An error occurred invoking 'ls_search_stock'." (the real "missing required parameter" detail only reaches the server's stderr log). Both search tools' keyword parameters are now also optional at the protocol level, so a missing or misnamed argument reaches the in-body validation and returns a clear "keyword is required." instead.ls_get_chartgained an optionalnameparameter. When supplied, the inline chart title reads "삼성전자 (005930) — 일봉" instead of just the code. The chart TRs do not carry the stock name, so the caller passes it through; omitted, the title falls back to the code as before.- Inline chart specs polished toward the Naver Finance look. Candlestick x-axis labels are now an evenly-spaced ~8-tick subset (
MM/dd) rather than one label per candle; MA / EMA overlays use the Korean retail palette (green / red / orange / purple); the period high and low get최고/최저annotations; the ETF-holdings treemap uses white labels on a deeper blue so they stay legible regardless of the host theme.
Internal
- The Plotly chart-spec builders moved into
RedoxNet.LsOpenApi.Core(see the Core 0.3.0 notes) — no effect on the tool surface.
v0.2.0 (2026-05-14)
MCP host interoperability fixes. No tool surface or behavior changes — the 10 tools, their inputs, and their outputs are identical to v0.1.0. This release only reshapes the published JSON schema and UI-resource metadata so MCP hosts accept and render them correctly.
Fixed
-
Optional array/string parameters were rejected by strict MCP host validators. The .NET MCP SDK emits JSON Schema 2020-12's
"type": ["array","null"]form for nullable parameters. Claude Desktop, Claude Code, and cowork reject that shape, so everyls_get_chartcall that passedindicatorsfailed before reaching the server (from/towere affected the same way). Atools/listrequest filter —SchemaNormalizer— now rewrites["X","null"]→"X"and drops the orphaneddefault: null, leaving the C#T?signatures untouched. Verified end-to-end against the live LS server. -
MCP Apps (SEP-1865) UI-resource CSP metadata was the wrong shape.
_meta.ui.cspused raw CSP directive names (script-src,style-src, …); the spec expects domain lists (resourceDomains,connectDomains). Hosts readcsp.resourceDomainsto allowlist the Plotly CDN inside the sandbox iframe — with the old shape the CDN was never allowed. The_metablock is now also attached to theresources/readcontent (the host reads CSP from there, not only from theresources/listentry),prefersBorder: trueis declared, and the HTML template declaresavailableDisplayModes: ["inline"]in itsui/initializehandshake.
Added
LS_LOG_LEVELenvironment variable — sets the minimum log level (Trace/Debug/Information/Warning/Error/Critical/None, defaultInformation). WithTrace, every JSON-RPC message — including fulltools/callpayloads — is written to stderr, for diagnosing host-side interop issues.
Host support note
Inline Plotly rendering still depends on the host. It works on Claude.ai (web). Claude Desktop / cowork currently cannot embed the MCP Apps sandbox iframe — the claudemcpcontent.com sandbox serves a CSP frame-ancestors that lists only the web origins, not the desktop app — so on those hosts the tool degrades gracefully to the structured candles / indicators / context payload. This is an Anthropic-side limitation, not a server issue.
v0.1.0 (2026-05-13)
Initial public release of the MCP server for LS증권 OpenAPI.
Tools (10)
-
Meta —
ls_search_tr,ls_describe_tr,ls_call_tr.ls_call_traccepts both real JSON objects and JSON-stringifiedin_blockpayloads as a robustness fallback for clients whose MCP schema inference omitstype: objecton theJsonElementparameter. -
Quotes & info
ls_get_quote(t1101) — current price + 10-level order book + session OHLC.ls_get_multi_quote(t8407) — up to 50 stocks per call.ls_get_stock_info(t1102) — PER/PBR/EPS, quarterly financials, 52-week + YTD ranges, top-5 buy/sell brokerages, foreign-investor activity, SPAC / 관리종목 flags.
-
Charts —
ls_get_chart(t8410 / t8412 / t1301)- Period types:
day/week/month/year/min/tick. Comma-separated period strings ("day,week,month") return aframes[]array — one frame per timeframe, each with its own candles / indicators / context. - Optional technical indicators:
ma:N,ema:N,rsi:N,macd:F,S,Sig,bb:N,SD. - Pre-computed
contextblock:divergence_from_ma,volume.{avg20,ratio20,avg60,ratio60},drawdown.{period_high,date,pct},ma_trend, tristatebullish_alignment(nullwhen MA warm-up makes the stack undecidable). include_chart=trueattaches a Plotly v5 JSON spec underchart.spec. Korean broker color convention applied (rising = red, falling = blue).ma/ema/bboverlay the price subplot;rsi/macdare emitted inindicatorsbut not plotted (would need separate subplots).summary_only=truekeeps only the last 5 candles + final indicator scalars while preserving the full context — useful for multi-timeframe screening passes that would otherwise blow past inline token budgets.
- Period types:
-
Discovery & ETF
ls_search_stock(t8436) — name → code search with aninstrumentfilter (all/stock/etf).ls_get_etf_info(t1901) — NAV + divergence (괴리율, fromkasis) + tracking error (추적오차율, fromcocrate) + reference index (참고지수 — e.g. KOSPI 200 for KODEX 200) + AUM + LP list + 52-week / year ranges + related futures. Foreign-ownership ratio (소진율) surfaced asforeign_ownership_percent.listing_sharesconverted from LS's 천-주 unit to raw share count.ls_get_etf_holdings(t1904) — PDF (구성종목) array sorted by weight, with a top-N cap (top_n=10etc.) for ETFs with 200+ constituents. Summary block always reflects the full ETF (NAV, AUM, constituent count, cash); only the holdings array is truncated. The InBlock correctly sends LS's requireddate+sgbfields — omitting either produces a misleadingrsp_msg="해당자료가 없습니다"response.
Credentials
- Environment variable only.
LS_APPKEYandLS_APPSECRETKEYare accepted via the process environment only — never through chat, tool arguments, or MCP elicitation. This is the strictest reading of the MCP spec's "Servers MUST NOT use elicitation to request sensitive information" guidance. Rationale is documented indocs/ADR-001-credential-management.md. - Logs, errors, and tool responses never echo credentials. Diagnostics show
****+ the last four characters of the app key; the secret key is never logged in any form.
Packaging
- dotnet tool —
dnx RedoxNet.Mcp.LsOpenApi. Dual-targetednet8.0+net9.0. - MCP Server package type with
.mcp/server.jsonfor the registry. MSBuildVerifyServerJsonVersiontarget catches drift between csproj<Version>and.mcp/server.jsonat pack time.
Out of scope (v0.1.0)
- Real-time (WebSocket) subscriptions — planned for v0.2.x (separate package
RedoxNet.Mcp.LsOpenApi.Realtime). - Account balance, order history, unfilled orders (read-only) — planned for v0.2.x.
- Order placement (매수 / 매도) — explicitly deferred to a future major release with elicitation-gated confirmation; this v0.1.x line stays read-only.
Verified
Live-verified against the LS 모의투자 server on 2026-05-13: current quote (Samsung), multi-timeframe chart (SK하이닉스 일·주·월 with ma:5/20/60), search-then-quote (카카오), bio-only ETF discovery (instrument="etf", 16 results), KODEX 200 PDF (201 holdings, AUM ≈ 25조원), and KODEX 200 ETF info (NAV / 괴리율 0.00% / 추적오차율 0.01% / KOSPI 200 reference / 외인 보유율 23.39%). 234 unit and fixture tests pass on .NET 8 and .NET 10.