Financial-API(패키지명 hithink-finance)는 중국 A주 금융 데이터를 여섯 가지 방법으로 꺼내 쓰게 해 주는 공식 클라이언트 모노레포입니다. 데이터 자체는 fuyao.aicubes.cn이라는 원격 서버에 있고, 이 저장소에 담긴 것은 그 서버를 부르는 클라이언트들과 계약 문서입니다.
여기서 오해하기 쉬운 지점을 먼저 정리합니다. 이름이 “Financial-API”라서 API 서버 코드가 들어 있을 것 같지만, 서버 코드는 없습니다. 저장소를 clone하면 나오는 것은 ① REST 계약을 적은 마크다운 문서, ② Node.js로 짠 CLI, ③ 파이썬 SDK와 로컬 DuckDB 도구, ④ MCP 서버에 붙는 설정과 도구 목록, ⑤ AI 에이전트용 스킬 문서입니다. MCP 서버 구현조차 원격에 호스팅돼 있어 이 저장소에는 URL과 도구 계약만 있습니다.
물건(A주 시세·재무제표)은 통화순 서버라는 창고에 있습니다. 이 저장소가 제공하는 건 창고 자체가 아니라 문입니다 — 브라우저로 두드리는 문(REST), AI가 두드리는 문(MCP), 터미널에서 두드리는 문(CLI), 파이썬에서 두드리는 문(SDK), 물건을 미리 옮겨 두는 개인 창고(로컬 DuckDB), 그리고 어느 문으로 갈지 대신 골라 주는 안내원(Agent Skill).
문이 여섯 개면 보통 규격이 어긋나기 시작합니다. CLI에는 있는 옵션이 SDK엔 없고, 문서에 적힌 파라미터 이름이 실제와 다르고. 이 저장소가 흥미로운 이유는 문 여섯 개의 규격을 사람이 아니라 코드 한 벌이 지키게 만들어 뒀다는 점입니다.
600519.SH(상하이) · 000001.SZ(선전) · 430047.BJ(베이징) 형태로 6자리 숫자 + 거래소 접미사입니다. 지수·판(板)은 .TI, 펀드는 .OF/.SH/.SZ를 씁니다. 모든 API가 이 코드를 요구하므로, 사람이 아는 이름(“귀주모태”)을 먼저 thscode로 바꾸는 단계가 항상 앞에 옵니다.2026년 6월 9일 첫 공개, 두 달 반 만에 별 1.48k. 중국어 문서 일색인 저장소치고는 빠른 속도입니다. 이유는 셋으로 갈립니다.
① 비공식 스크레이퍼 생태계에 공식 사업자가 들어왔다. A주 데이터는 오래도록 akshare·tushare·baostock 같은 커뮤니티 프로젝트가 여기저기서 긁어모아 채우던 영역입니다. 소스가 바뀌면 조용히 깨지고, 품질 보증도 없습니다. 통화순은 중국 증권 단말기 시장의 주요 사업자이고, README는 “官方提供和維護(공식 제공·유지보수)”를 반복해서 강조합니다.
② MCP와 Agent Skill을 처음부터 1급 시민으로 뒀다. 기존 금융 데이터 라이브러리에도 서드파티 MCP 래퍼는 있지만, 사업자가 직접 MCP 엔드포인트 4개·도구 55개를 운영하는 사례는 드뭅니다.
③ 접근 경로를 여섯 개 깔아 두고 “고르지 말라”고 한다. README의 권장 진입점은 CLI도 SDK도 아니고 Agent Skill입니다. 사용자는 “귀주모태 오늘 가격 알려줘”라고만 말하고, 어느 문으로 갈지는 에이전트가 고르라는 설계입니다.
① 데이터는 공짜가 아니다. 저장소 코드는 MIT지만 실제 호출에는 API Key가 필요하고, 키는 fuyao.aicubes.cn/admin에서 발급받습니다. 권한·호출 빈도는 계정 등급에 달렸다고만 적혀 있을 뿐 가격표가 README에 없습니다. 즉 이 저장소는 유료 서비스의 공식 클라이언트입니다.
② 문서가 사실상 전량 중국어다. 영어는 GitHub 저장소 설명 한 줄뿐. 코드 주석도 중국어라, 한국어권 학습자가 읽으려면 번역 부담이 붙습니다.
③ 라이선스 표기가 어긋나 있다. 루트 LICENSE는 MIT인데 python/pyproject.toml은 license = { text = "Proprietary" }로 적혀 있습니다. 오기일 가능성이 높지만, 회사 업무에 넣기 전에 확인이 필요한 항목입니다.
| 프로젝트 | 데이터 출처 | 강점 | 약점 |
|---|---|---|---|
| akshare | 여러 공개 사이트 스크레이핑 | 무료 · 범위 압도적(매크로·뉴스 포함) | 소스 변경 시 침묵 고장, SLA 없음 |
| tushare | 자체 수집 + 포인트제 | 정제 품질 · 파이썬 친화 | 고급 데이터는 포인트 과금 |
| baostock | 자체 수집 | 완전 무료 · 안정적 일봉 | 커버리지 좁고 갱신 느림 |
| yfinance | Yahoo Finance 비공식 | 글로벌 · 설치 즉시 사용 | A주 커버 부실, 차단 위험 |
| hithink-finance | 통화순 공식 API | 공식 계약 · MCP/Skill 네이티브 · 크로스플랫폼 CLI | 유료 키 필수 · 중국어 문서 · A주 전용 |
범위도 정직하게 좁혀 놨습니다. README가 지원하지 않는다고 명시한 것은 분봉·틱 데이터, 홍콩/미국 시장, 거시경제 지표, 뉴스·공시 원문, 리서치 리포트 원문입니다. akshare가 커버하는 영역 일부를 의도적으로 비워 둔 셈이라, “akshare를 대체한다”가 아니라 “A주 코어 데이터를 계약 있는 형태로 준다”가 정확한 위치입니다.
모노레포지만 워크스페이스 도구(pnpm workspace, turborepo 등)를 쓰지 않습니다. 독립적인 두 서브프로젝트가 나란히 놓여 있고, 둘을 묶는 것은 빌드 도구가 아니라 docs/의 계약 문서입니다.
hithink-finance-cli/| 항목 | 값 · 용도 |
|---|---|
| 패키지 | @hithink-tech/hithink-finance-cli v0.1.5 · ESM("type":"module") |
| Node | engines.node >= 22.12.0 — 내장 fetch와 최신 ESM 해석 전제 |
| commander 15 | CLI 명령 트리. 서브커맨드를 런타임에 동적 등록하는 데 씀 |
| zod 4.4.3 | 입력 검증 + 응답 검증. 이 저장소 설계의 중심축 |
| @duckdb/node-api 1.5.4 | 로컬 임베디드 OLAP DB. 서버 없이 파일 하나로 동작 |
| @napi-rs/keyring 1.3.0 | OS 자격증명 저장소 추상화 (Keychain / Credential Manager / Secret Service) |
| skills 1.5.15 | Agent Skill 설치 도구. CLI가 스킬을 직접 깔아 줌 |
| 빌드 · 검증 | tsc → dist/. npm run verify = format + lint + typecheck + build + test |
| 테스트 | vitest. tests/가 unit / contract / integration / e2e / performance / security / release 7층 |
python/| 항목 | 값 · 용도 |
|---|---|
| 패키지 | marketdb v0.1.0 · requires-python >= 3.11 |
| duckdb / pyarrow | 로컬 DB와 컬럼 포맷. 대용량 덤프를 Parquet로 캐시 |
| pandas 2.2+ | 조회 결과를 DataFrame으로 반환 |
| typer + rich | marketdb 명령(엔트리포인트 자동 등록)과 터미널 출력 |
| requests | 원격 API 호출. TS 쪽 fetch와 대응 |
| 구성 | marketdb/(로컬 DB) + toolkit/fuyao/(원격 API 래퍼 1,536줄) |
Dockerfile이 없습니다. find . -iname "dockerfile*" 결과가 0건입니다. 컨테이너 배포 경로가 아예 없고, 설치는 npm i -g(CLI)와 pip install -e ./python(파이썬)뿐입니다. “서버가 아니라 클라이언트”라는 정체성이 여기서도 드러납니다 — 서버라면 Dockerfile이 없을 수 없습니다.
.github/workflows/ 4개| 워크플로 | 하는 일 |
|---|---|
| cli-ci | Windows · Ubuntu(x64/arm64) · macOS(Intel/ARM) 5개 OS × Node 22·24 매트릭스로 npm run verify |
| cli-live-canary | 매주 월요일 cron: 17 2 * * 1에 실제 API 키로 최소 호출. 서버 계약이 깨졌는지 감시 |
| cli-release | v* 태그 푸시 시 npm 자동 배포 |
| python-ci | 파이썬 쪽 lint · test |
live-canary는 흔치 않은 장치입니다. 보통 CI는 목(mock)을 씁니다. 이 저장소는 그 위에 “주 1회 진짜 서버를 찔러 보는” 잡을 하나 더 얹었습니다. 클라이언트 라이브러리는 내 코드가 멀쩡해도 상대가 바뀌면 깨진다는 특성이 있고, 그걸 테스트로 잡으려는 시도입니다.
이 저장소에서 가장 배울 만한 대목입니다. CLI가 지원하는 원격 명령은 32개(market snapshot, financials indicators, special limit-up-pool …). 보통이라면 명령마다 핸들러 함수를 하나씩 짭니다. 여기서는 그러지 않습니다.
src/contracts/remote-capabilities.ts(1,514줄) 안에 서술자(descriptor) 배열이 있고, 항목 하나가 명령 하나입니다.
export interface RemoteCapabilityDescriptor {
id: string; // 'market.snapshot'
command: readonly [string, string]; // ['market', 'snapshot']
description: string;
endpoint: string; // '/api/a-share/prices/snapshot'
method: 'GET';
inputSchema: ZodType<Record<string, unknown>>;
outputSchema: ZodType<unknown>;
options: readonly RemoteOptionDescriptor[]; // --thscodes 등
paging: 'none' | 'offset' | 'page';
pagingEnd?: 'short-page' | 'has-more';
window: 'none' | 'ten-years' | 'five-years' | 'one-year' | 'today-only';
}
실제 항목 하나는 이렇게 생겼습니다.
{
id: 'market.snapshot',
command: ['market', 'snapshot'],
description: 'Query A-share price snapshots',
endpoint: '/api/a-share/prices/snapshot',
method: 'GET',
inputSchema: z.object({
thscodes: commaCodes.optional(),
limit: z.number().int().min(1).max(10000).default(100),
offset: z.number().int().nonnegative().default(0),
}).strict(),
outputSchema: itemOutput,
options: [
{ flags: '--thscodes <codes>', description: 'comma-separated A-share thscodes',
type: 'string' },
{ flags: '--limit <number>', description: 'page size', type: 'integer',
defaultValue: 100 },
{ flags: '--offset <number>', description: 'row offset', type: 'integer',
defaultValue: 0 },
],
paging: 'offset',
window: 'none',
},
그리고 src/commands/remote.ts(403줄) 단 하나의 제네릭 실행기가 이 배열을 순회하며 commander 명령을 등록하고, 옵션을 파싱하고, 페이징을 돌리고, 결과를 렌더합니다. 새 엔드포인트를 추가하는 작업이 “함수를 짜는 일”에서 “배열에 항목 하나를 넣는 일”로 바뀐 것이 이 설계의 전부이자 핵심입니다.
식당으로 치면, 메뉴가 30개일 때 요리사 30명을 뽑는 대신 레시피 카드 30장을 만들고 요리사 한 명이 카드를 읽게 한 것입니다. 새 메뉴는 카드 한 장 추가. 요리사(실행기) 코드는 안 늘어납니다.
대가도 있습니다. 카드로 표현할 수 없는 특이한 요리(엔드포인트마다 다른 전처리)는 실행기에 분기를 넣어야 하고, 그게 쌓이면 실행기가 도로 복잡해집니다. 실제로 paging·window 같은 필드가 이미 그 “분기 흡수용” 슬롯입니다.
서버 응답은 항상 같은 봉투(envelope)에 담겨 옵니다.
export const fuyaoEnvelopeSchema = z.object({
code: z.number().int(), // 0=성공, 1xxx=파라미터, 2xxx=인증, 4xxx/5xxx=서버
message: z.string(),
request_id: z.string().optional(),
data: z.unknown(), // 여기는 일부러 unknown
});
data를 unknown으로 둔 게 의도적입니다. 봉투 형태가 깨졌는지(UPSTREAM_INVALID_RESPONSE)와 내용물 형태가 깨졌는지(UPSTREAM_INVALID_DATA)를 다른 에러로 구분하기 위해서입니다. 전자는 서버 전체가 이상한 것이고, 후자는 그 엔드포인트 하나의 스키마가 바뀐 것 — 원인이 완전히 다르니 에러도 달라야 합니다.
남의 API를 부르는 코드는 타입스크립트 타입만으로는 안전하지 않습니다. 타입은 컴파일 때 사라지고, 서버가 필드 이름을 바꾸면 undefined가 조용히 흘러다닙니다. zod처럼 런타임에도 살아 있는 스키마로 응답을 한 번 통과시키면, 깨진 지점이 데이터가 아니라 경계선에서 드러납니다.
src/infrastructure/fuyao/client.ts의 businessError()가 서버 코드 대역을 CLI 종료 코드로 매핑합니다. 스크립트에서 $?만 보고 대응을 나눌 수 있게 하려는 배려입니다.
| 서버 code | 분류 | exit code | 재시도 | 힌트 메시지 |
|---|---|---|---|---|
| 1000~1999 | validation | 2 | ✗ | “명령 스키마를 확인하고 파라미터를 고치세요” |
| 2000~2999 | authentication | 3 | ✗ | “auth status로 키 권한을 확인하세요” |
| 4xxx / 5xxx | upstream | 4 | 일부 ○ | “나중에 재시도하고 request ID를 보관하세요” |
// src/infrastructure/fuyao/retry.ts
export const RETRYABLE_BUSINESS_CODES = new Set([4001, 5001, 5002, 5003]);
export function retryDelayMs(attempt: number, random: () => number): number {
const base = Math.min(1000 * 2 ** attempt, 8000); // 1s → 2s → 4s → 8s(상한)
return Math.round(base + base * 0.2 * random()); // +0~20% 지터
}
여기에 parseRetryAfter()가 붙습니다. 서버가 Retry-After 헤더를 주면 그쪽을 우선하고, 헤더는 초 단위 숫자와 HTTP 날짜 두 형식을 모두 받습니다.
서버가 한 번에 10년치까지만 준다면, 30년치를 원하는 사용자는 직접 세 번 불러 합쳐야 합니다. 이 CLI는 그걸 windowing.ts가 대신합니다.
export const TEN_YEARS_MS = Math.floor(10 * 365.25 * 24 * 60 * 60 * 1000);
export function sliceTimeWindow(start: number, end: number,
size = TEN_YEARS_MS): TimeSlice[] {
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end)
|| end < start || size < 1) throw new RangeError('Invalid time window.');
const slices: TimeSlice[] = [];
let cursor = start;
while (cursor <= end) {
const sliceEnd = Math.min(cursor + size, end);
slices.push({ start: cursor, end: sliceEnd });
cursor = sliceEnd + 1; // +1 로 경계 중복 방지
}
return slices;
}
합칠 때는 deduplicateAndSort()가 Map으로 키 중복을 없애고 정렬합니다. 서술자의 window: 'ten-years' 한 줄이 이 동작을 켜는 스위치입니다 — 패턴 ①과 맞물리는 지점입니다.
파이썬 쪽 marketdb는 받아 온 데이터를 DuckDB 파일에 쌓습니다. 테이블 이름 접두사가 곧 계층입니다.
| 층 | 테이블 | 성격 |
|---|---|---|
stg_* | stg_kline_daily, stg_symbols | 적재 중간 단계(staging). 검증 전 원자료 |
raw_* | raw_kline_daily, raw_adjustment_events | 손대지 않은 사실. 절대 덮어쓰지 않음 |
dim_* | dim_symbol | 종목 차원 테이블(이름·거래소·자산유형) |
calc_* | calc_adjust_factor_daily | 재계산 가능한 파생값. 지워도 복구됨 |
v_* | v_daily, v_daily_qfq, v_daily_hfq | 사용자가 보는 안정 인터페이스 |
CREATE OR REPLACE VIEW v_daily_qfq AS
SELECT
k.thscode, k.date,
k.open * COALESCE(f.forward_factor, 1.0) AS open,
k.high * COALESCE(f.forward_factor, 1.0) AS high,
k.low * COALESCE(f.forward_factor, 1.0) AS low,
k.close * COALESCE(f.forward_factor, 1.0) AS close,
k.volume, k.turnover,
COALESCE(f.forward_factor, 1.0) AS forward_factor,
k.currency, k.interval
FROM raw_kline_daily k
LEFT JOIN calc_adjust_factor_daily f
ON f.thscode = k.thscode AND f.date = k.date;
COALESCE(..., 1.0)이 이 뷰의 안전장치입니다. 보정 계수가 아직 계산되지 않은 날짜라도 1.0을 곱해 원가격 그대로 나오므로 조회가 통째로 비지 않습니다. 데이터 파이프라인에서 “아직 안 채워진 파생 테이블”을 다루는 정석적인 방법입니다.
API 키를 찾는 순서가 두 언어에서 각각 구현돼 있는데, 정책은 같습니다.
| 순위 | Node CLI | Python |
|---|---|---|
| 1 | --api-key 명시 전달 | HITHINK_FINANCE_API_KEY 환경변수 |
| 2 | HITHINK_FINANCE_API_KEY 환경변수 | 사용자 레벨 credentials.env 파일 |
| 3 | OS 자격증명 저장소(keyring) | 레거시 FUYAO_TOKEN / API_KEY |
파일 위치도 OS 규약을 따릅니다 — Windows %APPDATA%\hithink-finance\, macOS ~/Library/Application Support/hithink-finance/, Linux ${XDG_CONFIG_HOME:-~/.config}/hithink-finance/. Linux에서 Secret Service 데몬이 없으면 AUTH_CREDENTIAL_STORE_UNAVAILABLE(exit 3)로 실패하고, 환경변수로 우회하라는 힌트를 띄웁니다.
docs/api/capability-map.md — 이 서비스가 뭘 주는지 한 장에 정리돼 있음hithink-finance-cli/src/contracts/remote-capabilities.ts — 32개 명령의 정의 전부. 여기만 읽어도 CLI 전체를 안다hithink-finance-cli/src/commands/remote.ts — 위 배열을 실행으로 바꾸는 유일한 실행기hithink-finance-cli/src/infrastructure/fuyao/ — client · retry · envelope · windowing 네 파일. 클라이언트 설계의 정수python/marketdb/sql/schema.sql, views.sql — 데이터 계층 설계skills/hithink-finance/SKILL.md — “에이전트에게 도구를 어떻게 설명할 것인가”의 실물 예제README가 언급하는 internal/과 sdd-docs/는 public clone에 존재하지 않습니다. 내부 거버넌스 문서로 따로 관리되는 듯합니다. 문서를 따라가다 없는 경로를 만나면 이 경우입니다.
명령이 5개일 때는 핸들러 5개가 낫습니다. 30개가 되면 선언적 배열이 이깁니다. 손익분기점이 어디인지, 그리고 배열로 표현 못 하는 예외를 어떻게 흡수하는지(paging·window 같은 정책 필드)를 실물로 볼 수 있는 사례입니다.
비슷한 발상을 이미 쓰는 곳: OpenAPI 스펙에서 클라이언트 생성, Django admin의 ModelAdmin, Terraform provider의 리소스 스키마. 이 저장소는 그걸 손으로 쓴 TypeScript 배열로 한 버전입니다.
① 타임아웃(30_000ms 기본) ② 재시도 가능 코드 집합의 명시적 화이트리스트 ③ 지수 백오프 + 지터 ④ 서버 Retry-After 우선 ⑤ 응답 런타임 검증. 다섯 개가 각각 어느 파일에 어떻게 들어가는지 이 저장소에서 그대로 베낄 수 있습니다.
특히 ②가 중요합니다. “5xx면 재시도” 같은 뭉뚱그린 규칙은 재시도하면 안 되는 실패(중복 주문 등)까지 재시도합니다. 여기서는 {4001, 5001, 5002, 5003} 넷만 명시했습니다.
“원본은 절대 안 건드린다 · 파생은 언제든 다시 만든다 · 사용자는 뷰만 본다”. 이 세 줄이면 파이프라인이 망가져도 복구 경로가 명확합니다. dbt·메달리온 아키텍처(bronze/silver/gold)의 축소판이고, DuckDB 파일 하나로 실습할 수 있어 학습 비용이 낮습니다.
docs/mcp/capability-map.md는 이렇게 시작합니다 — “현재 연결된 tools/list는 실제 가용성과 호출 스키마 확인용이며, 이 페이지의 작업 의미론을 대체하지 않는다.”
즉 서버가 알려주는 도구 목록과, 사람이 정리한 “어떤 의도일 때 어떤 도구를 쓰는가”를 분리했습니다. 도구가 55개쯤 되면 에이전트가 목록만 보고는 못 고릅니다. 이 라우팅표가 그 간극을 메웁니다 — MCP 서버를 만들 계획이 있다면 이 파일이 가장 값진 참고자료입니다.
--check 모드docs/가 계약의 단일 진실원이고, scripts/sync_skill_contracts.py가 그걸 스킬 배포본으로 미러링합니다. --check 모드로 돌리면 어긋났는지만 검사하고 실패시킵니다 — CI에 걸어 두면 문서 드리프트가 리뷰 단계에서 잡힙니다.
파이썬 테스트에도 test_api_contract_docs.py·test_mcp_contract_docs.py가 있어, 문서 자체를 테스트 대상으로 삼습니다. 문서가 코드와 함께 썩는 문제를 구조로 막은 사례입니다.
자격증명 우선순위와 재시도 정책이 TypeScript와 Python에 각각 독립 구현돼 있습니다. 그리고 이미 미세하게 갈라졌습니다 — TS의 retryDelayMs는 지터를 넣지만, 파이썬 fuyao_client.py의 재시도는 1.0 × 2^attempt 단순 백오프로 지터가 없습니다. 재시도 코드 집합은 같은데 대기 방식이 다릅니다.
정책을 여러 언어에 복제할 거라면, 최소한 정책 값을 JSON 같은 공용 파일로 빼고 양쪽이 읽게 하는 편이 낫습니다. 지금 구조는 한쪽만 고치고 다른 쪽을 잊는 사고가 시간문제입니다.
| 항목 | 요구 | 비고 |
|---|---|---|
| Node.js | 22.12.0 이상 | CLI 필수. 20.x에서는 설치 자체가 거부됨 |
| Python | 3.11 이상 | SDK · marketdb 필수 |
| OS | Windows / Linux(x64·arm64) / macOS(Intel·ARM) | CI가 5종 전부 검증 — 크로스플랫폼 공식 지원 |
| GPU | 불필요 | 모델을 돌리는 프로젝트가 아님 |
| 메모리 | 일반 PC 수준 | DuckDB가 디스크 기반이라 RAM 요구가 낮음 |
| 디스크 | 수백 MB ~ GB (전 종목 히스토리 적재 시) | 정확한 수치는 문서에 없음 — 종목 수·기간에 비례 |
| 네트워크 | fuyao.aicubes.cn HTTPS 아웃바운드 | 로컬 DuckDB 조회만 한다면 오프라인 가능 |
| API Key | 필수 (fuyao.aicubes.cn/admin) | 키 없이는 아무 데이터도 못 받음 |
@duckdb/node-api와 pyarrow는 네이티브 바이너리를 포함합니다. 사내 프록시 환경이나 흔치 않은 아키텍처에서는 prebuilt 다운로드가 막혀 소스 빌드로 넘어가고, 그때 컴파일러가 필요해집니다. Linux ARM 컨테이너에서 특히 자주 겪는 지점입니다.
Linux 데스크톱이 아닌 헤드리스 서버에서는 keyring이 붙을 Secret Service 데몬이 없습니다. 이 경우 auth login 대신 HITHINK_FINANCE_API_KEY 환경변수를 쓰는 게 정상 경로입니다.
remote-capabilities.ts에서 id·command·endpoint만 뽑아 32행짜리 표를 만들어 보세요. 정규식이나 간단한 스크립트면 됩니다.
git clone --depth 1 https://github.com/HiThink-Tech/Financial-API
cd Financial-API
grep -oE "id: '[a-z0-9.-]+'" \
hithink-finance-cli/src/contracts/remote-capabilities.ts
확인할 것 — 이 목록이 src/commands/ 하위 폴더 이름(market · financials · fund · special · index · symbol · valuation)과 정확히 대응합니다. 폴더가 명령을 만드는 게 아니라, 배열이 만든다는 걸 눈으로 확인하는 게 목적입니다.
retryDelayMs(attempt, random)는 난수 생성기를 인자로 받습니다. 덕분에 테스트에서 () => 0.5를 넣으면 결과가 결정적입니다. 이 성질을 이용해 다음을 검증해 보세요.
parseRetryAfter('120') = 120000, parseRetryAfter(null) = undefined배우는 것: 난수·시계처럼 “바깥 세계”에 의존하는 값을 인자로 빼면(의존성 주입) 테스트가 갑자기 쉬워진다는 것.
봉투 형식({code, message, data})만 맞춘 30줄짜리 Node/Python HTTP 서버를 띄우고, CLI의 baseUrl을 그쪽으로 돌려 보세요. 그다음 일부러 깨뜨립니다.
code: 0 + 정상 data → 통과하는지code: 2003 → exit code 3이 나오는지code: 5001 → 재시도가 3번 일어나는지 (서버 쪽 로그로 확인)code: 4002 → 재시도 없이 즉시 실패하는지data에 엉뚱한 필드 → UPSTREAM_INVALID_DATA가 나오는지배우는 것: 계약 기반 클라이언트의 방어선이 실제로 어디서 작동하는지. 문서 열 번 읽는 것보다 한 번 깨뜨려 보는 게 빠릅니다.
무료로 구할 수 있는 아무 시계열(암호화폐 일봉, 공공데이터 포털 지표)로 raw_* → calc_* → v_* 구조를 DuckDB에 그대로 만들어 보세요. schema.sql·views.sql이 그대로 템플릿입니다.
핵심 확인점: calc_* 테이블을 통째로 DELETE한 뒤 재계산 스크립트만 돌려서 완전히 복구되는가. 복구되지 않는다면 raw에 있어야 할 정보가 calc에 섞여 들어간 것 — 계층 분리가 실패한 신호입니다.
사내 API든 공개 API(예: 공공데이터 포털)든, 엔드포인트 5~10개를 골라 서술자 배열 + 제네릭 실행기 구조로 CLI를 짜 보세요. commander + zod 조합이면 remote.ts의 골격을 거의 그대로 쓸 수 있습니다.
단계: ① 서술자 인터페이스 정의 → ② 엔드포인트 3개를 배열로 → ③ 실행기 하나 작성 → ④ 나머지 엔드포인트는 배열에만 추가 → ⑤ 4번에서 실행기를 안 고쳤다면 설계 성공.
여기서 진짜 배우는 것: ④에서 실행기를 고쳐야 했다면 왜 고쳐야 했는지. 그 이유가 곧 서술자에 추가해야 할 정책 필드입니다 — 원본이 paging·window를 갖게 된 경위와 같습니다.
docs/mcp/capability-map.md를 본떠서, 도구 10개 이상인 MCP 서버에 “의도 → 도구” 라우팅표를 문서로 붙여 보세요. 그다음 라우팅표를 준 경우와 안 준 경우로 나눠, 같은 질문 10개를 에이전트에게 던져 도구 선택 정확도를 비교합니다.
원본의 라우팅표는 “이름 → 데이터”, “펀드 이름 → 데이터” 같은 조합 흐름도 순서대로 적어 둡니다 — 도구 하나로 안 끝나는 작업에서 특히 효과가 납니다.
| 주차 | 주제 | 할 것 |
|---|---|---|
| 1주차 | zod와 런타임 검증 | 기본 스키마 → .strict()·.passthrough()의 차이 → superRefine으로 커스텀 검증 → .transform()으로 정규화. 원본의 valuationCodes가 검증과 정규화(중복 제거·대문자화)를 한 스키마에서 하는 게 이 단계를 마치면 읽힙니다. |
| 2주차 | 타입에서 스키마로, 스키마에서 타입으로 | z.infer<typeof schema>로 스키마가 타입의 원본이 되는 방향. 반대 방향(타입 먼저, 검증은 손으로)과 비교해 보면 왜 이 저장소가 스키마 우선인지 납득됩니다. |
| 3주차 | CLI 설계 (commander) | 서브커맨드 트리 · 옵션 타입 · 종료 코드 규약. 그다음 명령을 런타임에 동적 등록하는 예제를 직접 짜 보면 remote.ts가 열립니다. |
| 4주차 | 회복탄력성 패턴 | 타임아웃 · 재시도 · 백오프 · 지터 · 서킷 브레이커 · 벌크헤드. 이 저장소는 앞의 넷을 쓰고 뒤의 둘은 안 씁니다 — 왜 안 써도 되는지(단일 사용자 CLI)를 설명할 수 있으면 이해한 것. |
| 5주차 | DuckDB와 분석용 데이터 모델링 | DuckDB 기본 → Parquet 읽기 → 뷰 설계 → 메달리온(bronze/silver/gold) 개념. schema.sql·views.sql을 열어 각 테이블이 어느 층인지 스스로 분류해 보기. |
| 6주차 | MCP와 Agent Skill | MCP 사양(도구·리소스·프롬프트) → 직접 서버 만들기 → 도구 설명문(description) 쓰기 연습. SKILL.md와 capability-map.md를 “LLM에게 주는 매뉴얼”의 잘 쓴 예로 놓고 분해해 보세요. |
목적에 따라 갈라집니다. 클라이언트 라이브러리를 잘 짜고 싶다면 1·2·4주차만. AI 도구화가 목적이면 1·6주차만. 데이터 파이프라인이 목적이면 5주차 하나로 충분합니다 — 이 저장소는 세 관심사가 우연히 한 곳에 모여 있는 형태라 전부 볼 필요가 없습니다.
| 용어 | 뜻 |
|---|---|
| A주 (A股) | 중국 본토 증시(상하이·선전·베이징)에서 위안화로 거래되는 보통주. 이 서비스의 유일한 대상 시장 |
| 同花順 / HiThink | 통화순. 중국의 증권 정보 단말·데이터 사업자. 이 저장소의 발행 주체 |
| thscode | 통화순 종목 식별자. 600519.SH 형태. 모든 조회의 출발점 |
| 복권 (qfq / hfq) | 배당·분할로 생긴 가격 계단을 보정하는 것. 전복권(과거를 당김) / 후복권(현재를 밀어냄) |
| 집합경매 (集合競價) | 장 시작·마감 전 주문을 모아 한 가격으로 체결하는 방식. 별도 엔드포인트가 있음 |
| 涨停 / 跌停 (창팅·뎨팅) | 상한가 / 하한가. 중국 증시는 일일 등락 제한이 있어 “상한가 종목 풀”이 독립 데이터로 유통됨 |
| 龙虎榜 (룽후방) | 대량 거래 상위 창구 공시. 큰손 자금 흐름 추적에 쓰이는 중국 특유의 공개 데이터 |
| 봉투 (envelope) | 응답을 {code, message, data}로 감싸는 규약. HTTP 상태와 별개로 업무 성패를 표현 |
| SSOT | Single Source of Truth. 같은 사실이 여러 곳에 있으면 반드시 갈라지므로 원본을 하나만 둔다는 원칙 |
| 계약 드리프트 | 서버 API와 클라이언트가 조용히 어긋나는 현상. 이 저장소는 런타임 검증 + live-canary로 잡음 |
| zod | TypeScript용 스키마 검증 라이브러리. 런타임에도 살아 있는 타입이 핵심 가치 |
| 서술자 (descriptor) | 동작을 코드가 아니라 데이터로 적은 명세 객체. 여기서는 CLI 명령 하나 = 서술자 하나 |
| 지수 백오프 | 재시도 간격을 1s → 2s → 4s처럼 배로 늘리는 것. 실패한 서버에 몰리지 않게 함 |
| 지터 (jitter) | 백오프에 섞는 무작위 폭. 여러 클라이언트의 재시도 시점을 흩뿌려 천둥소떼를 막음 |
| Retry-After | “이만큼 기다렸다 오라”는 서버 헤더. 초 단위 숫자 또는 HTTP 날짜 |
| keyring | OS의 자격증명 금고(macOS Keychain · Windows Credential Manager · Linux Secret Service) |
| DuckDB | 서버 없이 파일 하나로 도는 분석용(OLAP) DB. “분석계의 SQLite” |
| Parquet | 컬럼 지향 파일 포맷. 큰 표 데이터를 압축해 두고 필요한 열만 읽을 수 있음 |
| MCP | Model Context Protocol. AI 모델에 외부 도구를 붙이는 표준. 여기서는 HTTP transport로 4개 서비스 |
| Agent Skill | 에이전트에게 “이런 상황엔 이렇게 하라”를 알려주는 마크다운 문서. 이 저장소의 권장 진입점 |
| live canary | 주기적으로 진짜 서버를 최소 호출해 계약 파손을 감지하는 CI 잡 |
| 메달리온 아키텍처 | 데이터를 bronze(원본) / silver(정제) / gold(소비) 층으로 나누는 방식. 이 저장소의 raw/calc/view와 같은 발상 |