5분 AI.
TRENDSHIFT #8 딥다이브 · 2026-08-24 분석

Financial-API 딥다이브
— 데이터 회사가 직접 만든 “에이전트용 창구”

중국 증권정보 회사 同花順(통화순, HiThink)이 자사 A주 데이터를 공식 공개한 저장소입니다. 그런데 이 저장소에서 배울 것은 금융 데이터가 아닙니다 — 데이터 하나를 REST·MCP·CLI·파이썬 SDK·로컬 DB·에이전트 스킬 여섯 갈래로 동시에 내보내면서, 계약을 어떻게 한 벌만 유지하는가가 진짜 주제입니다. (저장소: HiThink-Tech/Financial-API · ⭐1.48k · TypeScript+Python · MIT · CLI v0.1.5 · 2026-08-24 기준)
목차
  1. 프로젝트 한줄 요약
  2. 왜 주목받는가
  3. 기술 스택 전체 지도
  4. 아키텍처 심화 분석
  5. 디렉토리 구조 해부
  6. 학습 포인트 (기술별)
  7. 하드웨어 / 시스템 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 학습 로드맵
  10. 핵심 키워드 사전
  11. 참고 링크

1프로젝트 한줄 요약

Financial-API가 정확히 무엇을 하는 물건인가

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엔 없고, 문서에 적힌 파라미터 이름이 실제와 다르고. 이 저장소가 흥미로운 이유는 문 여섯 개의 규격을 사람이 아니라 코드 한 벌이 지키게 만들어 뒀다는 점입니다.

용어
thscode (통화순 종목 코드)
이 서비스가 종목을 가리키는 표준 식별자. 600519.SH(상하이) · 000001.SZ(선전) · 430047.BJ(베이징) 형태로 6자리 숫자 + 거래소 접미사입니다. 지수·판(板)은 .TI, 펀드는 .OF/.SH/.SZ를 씁니다. 모든 API가 이 코드를 요구하므로, 사람이 아는 이름(“귀주모태”)을 먼저 thscode로 바꾸는 단계가 항상 앞에 옵니다.
용어
복권 (復權, 전복권 qfq / 후복권 hfq)
배당·액면분할 같은 기업 행동이 있으면 주가 차트에 인위적인 계단이 생깁니다. 그 계단을 없애려고 과거 가격에 보정 계수를 곱하는 게 복권입니다. 과거를 현재 기준으로 당기면 전복권(前復權, qfq), 현재를 과거 기준으로 밀면 후복권(後復權, hfq). 백테스트 결과가 통째로 달라지는 지점이라 이 저장소도 뷰를 따로 만들어 둡니다.

2왜 주목받는가

트렌딩 이유와 경쟁 제품 대비 강점

2026년 6월 9일 첫 공개, 두 달 반 만에 별 1.48k. 중국어 문서 일색인 저장소치고는 빠른 속도입니다. 이유는 셋으로 갈립니다.

① 비공식 스크레이퍼 생태계에 공식 사업자가 들어왔다. A주 데이터는 오래도록 akshare·tushare·baostock 같은 커뮤니티 프로젝트가 여기저기서 긁어모아 채우던 영역입니다. 소스가 바뀌면 조용히 깨지고, 품질 보증도 없습니다. 통화순은 중국 증권 단말기 시장의 주요 사업자이고, README는 “官方提供和維護(공식 제공·유지보수)”를 반복해서 강조합니다.

② MCP와 Agent Skill을 처음부터 1급 시민으로 뒀다. 기존 금융 데이터 라이브러리에도 서드파티 MCP 래퍼는 있지만, 사업자가 직접 MCP 엔드포인트 4개·도구 55개를 운영하는 사례는 드뭅니다.

③ 접근 경로를 여섯 개 깔아 두고 “고르지 말라”고 한다. README의 권장 진입점은 CLI도 SDK도 아니고 Agent Skill입니다. 사용자는 “귀주모태 오늘 가격 알려줘”라고만 말하고, 어느 문으로 갈지는 에이전트가 고르라는 설계입니다.

경계할 점
별 개수만 보고 clone하면 어긋나는 세 가지

① 데이터는 공짜가 아니다. 저장소 코드는 MIT지만 실제 호출에는 API Key가 필요하고, 키는 fuyao.aicubes.cn/admin에서 발급받습니다. 권한·호출 빈도는 계정 등급에 달렸다고만 적혀 있을 뿐 가격표가 README에 없습니다. 즉 이 저장소는 유료 서비스의 공식 클라이언트입니다.

② 문서가 사실상 전량 중국어다. 영어는 GitHub 저장소 설명 한 줄뿐. 코드 주석도 중국어라, 한국어권 학습자가 읽으려면 번역 부담이 붙습니다.

③ 라이선스 표기가 어긋나 있다. 루트 LICENSE는 MIT인데 python/pyproject.tomllicense = { text = "Proprietary" }로 적혀 있습니다. 오기일 가능성이 높지만, 회사 업무에 넣기 전에 확인이 필요한 항목입니다.

커뮤니티 대안과의 실제 차이

프로젝트데이터 출처강점약점
akshare여러 공개 사이트 스크레이핑무료 · 범위 압도적(매크로·뉴스 포함)소스 변경 시 침묵 고장, SLA 없음
tushare자체 수집 + 포인트제정제 품질 · 파이썬 친화고급 데이터는 포인트 과금
baostock자체 수집완전 무료 · 안정적 일봉커버리지 좁고 갱신 느림
yfinanceYahoo Finance 비공식글로벌 · 설치 즉시 사용A주 커버 부실, 차단 위험
hithink-finance통화순 공식 API공식 계약 · MCP/Skill 네이티브 · 크로스플랫폼 CLI유료 키 필수 · 중국어 문서 · A주 전용

범위도 정직하게 좁혀 놨습니다. README가 지원하지 않는다고 명시한 것은 분봉·틱 데이터, 홍콩/미국 시장, 거시경제 지표, 뉴스·공시 원문, 리서치 리포트 원문입니다. akshare가 커버하는 영역 일부를 의도적으로 비워 둔 셈이라, “akshare를 대체한다”가 아니라 “A주 코어 데이터를 계약 있는 형태로 준다”가 정확한 위치입니다.

3기술 스택 전체 지도

두 언어 · 두 서브프로젝트 · 한 개의 계약

모노레포지만 워크스페이스 도구(pnpm workspace, turborepo 등)를 쓰지 않습니다. 독립적인 두 서브프로젝트가 나란히 놓여 있고, 둘을 묶는 것은 빌드 도구가 아니라 docs/의 계약 문서입니다.

① Node.js CLI — hithink-finance-cli/

항목값 · 용도
패키지@hithink-tech/hithink-finance-cli v0.1.5 · ESM("type":"module")
Nodeengines.node >= 22.12.0 — 내장 fetch와 최신 ESM 해석 전제
commander 15CLI 명령 트리. 서브커맨드를 런타임에 동적 등록하는 데 씀
zod 4.4.3입력 검증 + 응답 검증. 이 저장소 설계의 중심축
@duckdb/node-api 1.5.4로컬 임베디드 OLAP DB. 서버 없이 파일 하나로 동작
@napi-rs/keyring 1.3.0OS 자격증명 저장소 추상화 (Keychain / Credential Manager / Secret Service)
skills 1.5.15Agent Skill 설치 도구. CLI가 스킬을 직접 깔아 줌
빌드 · 검증tscdist/. npm run verify = format + lint + typecheck + build + test
테스트vitest. tests/가 unit / contract / integration / e2e / performance / security / release 7층

② Python — python/

항목값 · 용도
패키지marketdb v0.1.0 · requires-python >= 3.11
duckdb / pyarrow로컬 DB와 컬럼 포맷. 대용량 덤프를 Parquet로 캐시
pandas 2.2+조회 결과를 DataFrame으로 반환
typer + richmarketdb 명령(엔트리포인트 자동 등록)과 터미널 출력
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이 없을 수 없습니다.

③ CI — .github/workflows/ 4개

워크플로하는 일
cli-ciWindows · Ubuntu(x64/arm64) · macOS(Intel/ARM) 5개 OS × Node 22·24 매트릭스로 npm run verify
cli-live-canary매주 월요일 cron: 17 2 * * 1실제 API 키로 최소 호출. 서버 계약이 깨졌는지 감시
cli-releasev* 태그 푸시 시 npm 자동 배포
python-ci파이썬 쪽 lint · test

live-canary는 흔치 않은 장치입니다. 보통 CI는 목(mock)을 씁니다. 이 저장소는 그 위에 “주 1회 진짜 서버를 찔러 보는” 잡을 하나 더 얹었습니다. 클라이언트 라이브러리는 내 코드가 멀쩡해도 상대가 바뀌면 깨진다는 특성이 있고, 그걸 테스트로 잡으려는 시도입니다.

4아키텍처 심화 분석

요청 하나가 어디를 거쳐 가는가 · 이 저장소의 핵심 설계 패턴

전체 구조도

사용자 / AI 에이전트 │ ┌──────────┼──────────┬───────────┬────────────┐ │ │ │ │ │ REST MCP(4개) CLI Python SDK Agent Skill curl 도구 55개 hithink- marketdb SKILL.md │ (원격 호스팅) finance │ (라우터) │ │ │ │ │ └──────────┴────┬─────┴───────────┘ "어느 문으로 갈지" │ 대신 고름 X-api-key 헤더 (모두 동일한 키 1개) │ ▼ ┌────────────────────────────┐ │ https://fuyao.aicubes.cn │ ← 저장소에 코드 없음 │ (통화순 데이터 백엔드) │ └────────────┬───────────────┘ │ 응답 봉투 { code, message, request_id?, data } ▼ ┌───────────────────────────────────────────┐ │ CLI 내부 파이프라인 │ │ ① zod inputSchema ─ 파라미터 검증 │ │ ② FuyaoClient.request() ─ fetch + 재시도 │ │ ③ envelopeSchema ─ 봉투 형태 검증 │ │ ④ outputSchema ─ data 내용 검증 │ │ ⑤ renderer ─ json / csv / table │ └───────────────────────────────────────────┘ │ ▼ (선택) 로컬 DuckDB data/market.duckdb raw_* → calc_* → v_daily_qfq (안정 뷰)

패턴 ① — 능력을 코드가 아니라 “데이터”로 적는다

이 저장소에서 가장 배울 만한 대목입니다. 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
});

dataunknown으로 둔 게 의도적입니다. 봉투 형태가 깨졌는지(UPSTREAM_INVALID_RESPONSE)와 내용물 형태가 깨졌는지(UPSTREAM_INVALID_DATA)를 다른 에러로 구분하기 위해서입니다. 전자는 서버 전체가 이상한 것이고, 후자는 그 엔드포인트 하나의 스키마가 바뀐 것 — 원인이 완전히 다르니 에러도 달라야 합니다.

여기서 배울 것
“계약 드리프트”를 런타임에 잡는 습관

남의 API를 부르는 코드는 타입스크립트 타입만으로는 안전하지 않습니다. 타입은 컴파일 때 사라지고, 서버가 필드 이름을 바꾸면 undefined가 조용히 흘러다닙니다. zod처럼 런타임에도 살아 있는 스키마로 응답을 한 번 통과시키면, 깨진 지점이 데이터가 아니라 경계선에서 드러납니다.

패턴 ③ — 에러 코드를 종료 코드로 번역한다

src/infrastructure/fuyao/client.tsbusinessError()가 서버 코드 대역을 CLI 종료 코드로 매핑합니다. 스크립트에서 $?만 보고 대응을 나눌 수 있게 하려는 배려입니다.

서버 code분류exit code재시도힌트 메시지
1000~1999validation2“명령 스키마를 확인하고 파라미터를 고치세요”
2000~2999authentication3auth status로 키 권한을 확인하세요”
4xxx / 5xxxupstream4일부 ○“나중에 재시도하고 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 날짜 두 형식을 모두 받습니다.

용어
지터 (jitter) · 천둥소떼 문제 (thundering herd)
여러 클라이언트가 동시에 실패하면, 순수 지수 백오프는 모두 같은 순간에 재시도하게 만들어 서버를 다시 무너뜨립니다. 대기 시간에 무작위 폭(여기서는 0~20%)을 섞어 재시도 시점을 흩뿌리는 게 지터입니다. 코드 한 줄이지만 이게 없으면 장애가 파도처럼 반복됩니다.

패턴 ⑤ — 긴 기간 요청을 자동으로 쪼갠다

서버가 한 번에 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' 한 줄이 이 동작을 켜는 스위치입니다 — 패턴 ①과 맞물리는 지점입니다.

패턴 ⑥ — 로컬 DB의 3층 스키마 (raw → calc → view)

파이썬 쪽 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 CLIPython
1--api-key 명시 전달HITHINK_FINANCE_API_KEY 환경변수
2HITHINK_FINANCE_API_KEY 환경변수사용자 레벨 credentials.env 파일
3OS 자격증명 저장소(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)로 실패하고, 환경변수로 우회하라는 힌트를 띄웁니다.

5디렉토리 구조 해부

clone 후 어디부터 열어야 하는가
Financial-API/ ├── README.md 중국어. 이 저장소의 지도 ├── AGENTS.md AI 에이전트가 이 레포에서 일할 때의 규칙 ├── LICENSE MIT (※ python/pyproject.toml 은 Proprietary — 불일치) ├── .env.example HITHINK_FINANCE_API_KEY 샘플 │ ├── docs/ ★ 단일 진실원(SSOT) — 모든 계약이 여기서 나온다 │ ├── api/ REST 계약 12개 md (prices · financials · fund · special-data …) │ └── mcp/ MCP 계약 5개 md (capability-map + 서비스 4개) │ ├── skills/hithink-finance/ │ └── SKILL.md 153줄. 자연어 의도 → 접근 방식 라우팅 규칙 │ ├── hithink-finance-cli/ ★ Node.js CLI (파이썬 비의존, 독립 배포) │ └── src/ │ ├── cli/ main.ts · program.ts · context.ts · i18n.ts │ ├── commands/ auth · market · financials · valuation · index · │ │ fund · special · symbol · data · db · config · │ │ skills · system + remote.ts(제네릭 실행기) │ ├── contracts/ remote-capabilities.ts(1,514줄) · envelope · errors │ ├── infrastructure/ fuyao(client·retry·windowing) · credentials(keyring) │ │ · duckdb · filesystem · skills · updater │ ├── application/ config · source-policy · use-cases │ ├── domains/ 도메인 타입 │ └── output/ json / table / renderer │ └── tests/ unit · contract · integration · e2e · performance │ · security · release (7층, 45개 파일) │ ├── python/ ★ 파이썬 (로컬 DB + 원격 래퍼) │ ├── marketdb/ cli.py · sdk.py · db.py · credentials.py │ │ ├── sql/ schema.sql · views.sql ← 3층 스키마의 정의 │ │ ├── importers/ providers/ updaters/ calculations/ checks/ │ ├── toolkit/fuyao/scripts/ │ │ ├── fuyao_client.py 1,536줄. 엔드포인트별 타입드 함수 │ │ └── fuyao.py 837줄. 그 위의 CLI 래퍼 │ ├── examples/ 01_quickstart · 02_cross_section · 03_fundamentals_join │ └── tests/ 25개 (계약 문서 검증 테스트 포함) │ ├── examples/inspirations/ 16개 "영감" 세트 (프롬프트 + 미리보기 + 단일 HTML 대시보드) ├── scripts/ sync_skill_contracts.py — docs/ → skills/ 미러링 └── .github/workflows/ cli-ci · cli-release · cli-live-canary · python-ci

읽는 순서 추천

  1. docs/api/capability-map.md — 이 서비스가 뭘 주는지 한 장에 정리돼 있음
  2. hithink-finance-cli/src/contracts/remote-capabilities.ts32개 명령의 정의 전부. 여기만 읽어도 CLI 전체를 안다
  3. hithink-finance-cli/src/commands/remote.ts — 위 배열을 실행으로 바꾸는 유일한 실행기
  4. hithink-finance-cli/src/infrastructure/fuyao/ — client · retry · envelope · windowing 네 파일. 클라이언트 설계의 정수
  5. python/marketdb/sql/schema.sql, views.sql — 데이터 계층 설계
  6. skills/hithink-finance/SKILL.md — “에이전트에게 도구를 어떻게 설명할 것인가”의 실물 예제
주의

README가 언급하는 internal/sdd-docs/public clone에 존재하지 않습니다. 내부 거버넌스 문서로 따로 관리되는 듯합니다. 문서를 따라가다 없는 경로를 만나면 이 경우입니다.

6학습 포인트

이 저장소에서 실제로 가져갈 수 있는 것
학습 1 — 설계

선언적 레지스트리 vs 개별 핸들러

명령이 5개일 때는 핸들러 5개가 낫습니다. 30개가 되면 선언적 배열이 이깁니다. 손익분기점이 어디인지, 그리고 배열로 표현 못 하는 예외를 어떻게 흡수하는지(paging·window 같은 정책 필드)를 실물로 볼 수 있는 사례입니다.

비슷한 발상을 이미 쓰는 곳: OpenAPI 스펙에서 클라이언트 생성, Django admin의 ModelAdmin, Terraform provider의 리소스 스키마. 이 저장소는 그걸 손으로 쓴 TypeScript 배열로 한 버전입니다.

학습 2 — 신뢰성

남의 서버를 부르는 코드의 표준 장비 5종

① 타임아웃(30_000ms 기본) ② 재시도 가능 코드 집합의 명시적 화이트리스트 ③ 지수 백오프 + 지터 ④ 서버 Retry-After 우선 ⑤ 응답 런타임 검증. 다섯 개가 각각 어느 파일에 어떻게 들어가는지 이 저장소에서 그대로 베낄 수 있습니다.

특히 ②가 중요합니다. “5xx면 재시도” 같은 뭉뚱그린 규칙은 재시도하면 안 되는 실패(중복 주문 등)까지 재시도합니다. 여기서는 {4001, 5001, 5002, 5003} 넷만 명시했습니다.

학습 3 — 데이터

raw / calc / view 3층 분리

“원본은 절대 안 건드린다 · 파생은 언제든 다시 만든다 · 사용자는 뷰만 본다”. 이 세 줄이면 파이프라인이 망가져도 복구 경로가 명확합니다. dbt·메달리온 아키텍처(bronze/silver/gold)의 축소판이고, DuckDB 파일 하나로 실습할 수 있어 학습 비용이 낮습니다.

학습 4 — AI 도구화

MCP 계약을 문서로 고정하는 방식

docs/mcp/capability-map.md는 이렇게 시작합니다 — “현재 연결된 tools/list는 실제 가용성과 호출 스키마 확인용이며, 이 페이지의 작업 의미론을 대체하지 않는다.

서버가 알려주는 도구 목록과, 사람이 정리한 “어떤 의도일 때 어떤 도구를 쓰는가”를 분리했습니다. 도구가 55개쯤 되면 에이전트가 목록만 보고는 못 고릅니다. 이 라우팅표가 그 간극을 메웁니다 — MCP 서버를 만들 계획이 있다면 이 파일이 가장 값진 참고자료입니다.

학습 5 — 문서 운영

SSOT + 동기화 스크립트 + --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 같은 공용 파일로 빼고 양쪽이 읽게 하는 편이 낫습니다. 지금 구조는 한쪽만 고치고 다른 쪽을 잊는 사고가 시간문제입니다.

7하드웨어 / 시스템 요구사항

돌려 보려면 무엇이 필요한가
항목요구비고
Node.js22.12.0 이상CLI 필수. 20.x에서는 설치 자체가 거부됨
Python3.11 이상SDK · marketdb 필수
OSWindows / 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-apipyarrow네이티브 바이너리를 포함합니다. 사내 프록시 환경이나 흔치 않은 아키텍처에서는 prebuilt 다운로드가 막혀 소스 빌드로 넘어가고, 그때 컴파일러가 필요해집니다. Linux ARM 컨테이너에서 특히 자주 겪는 지점입니다.

Linux 데스크톱이 아닌 헤드리스 서버에서는 keyring이 붙을 Secret Service 데몬이 없습니다. 이 경우 auth login 대신 HITHINK_FINANCE_API_KEY 환경변수를 쓰는 게 정상 경로입니다.

8직접 해볼 수 있는 실습 과제

API 키가 없어도 절반은 할 수 있습니다
난이도 ★☆☆

과제 1 — 계약 배열만 읽고 CLI 명령표를 복원하기키 불필요 · 30분

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)과 정확히 대응합니다. 폴더가 명령을 만드는 게 아니라, 배열이 만든다는 걸 눈으로 확인하는 게 목적입니다.

난이도 ★☆☆

과제 2 — 재시도 함수 단위 테스트 짜기키 불필요 · 1시간

retryDelayMs(attempt, random)난수 생성기를 인자로 받습니다. 덕분에 테스트에서 () => 0.5를 넣으면 결과가 결정적입니다. 이 성질을 이용해 다음을 검증해 보세요.

배우는 것: 난수·시계처럼 “바깥 세계”에 의존하는 값을 인자로 빼면(의존성 주입) 테스트가 갑자기 쉬워진다는 것.

난이도 ★★☆

과제 3 — 가짜 서버를 세워 CLI 파이프라인 통과시키기키 불필요 · 반나절

봉투 형식({code, message, data})만 맞춘 30줄짜리 Node/Python HTTP 서버를 띄우고, CLI의 baseUrl을 그쪽으로 돌려 보세요. 그다음 일부러 깨뜨립니다.

  1. code: 0 + 정상 data → 통과하는지
  2. code: 2003 → exit code 3이 나오는지
  3. code: 5001재시도가 3번 일어나는지 (서버 쪽 로그로 확인)
  4. code: 4002 → 재시도 없이 즉시 실패하는지
  5. data에 엉뚱한 필드 → UPSTREAM_INVALID_DATA가 나오는지

배우는 것: 계약 기반 클라이언트의 방어선이 실제로 어디서 작동하는지. 문서 열 번 읽는 것보다 한 번 깨뜨려 보는 게 빠릅니다.

난이도 ★★☆

과제 4 — 3층 스키마를 다른 데이터로 재현하기키 불필요 · 하루

무료로 구할 수 있는 아무 시계열(암호화폐 일봉, 공공데이터 포털 지표)로 raw_*calc_*v_* 구조를 DuckDB에 그대로 만들어 보세요. schema.sql·views.sql이 그대로 템플릿입니다.

핵심 확인점: calc_* 테이블을 통째로 DELETE한 뒤 재계산 스크립트만 돌려서 완전히 복구되는가. 복구되지 않는다면 raw에 있어야 할 정보가 calc에 섞여 들어간 것 — 계층 분리가 실패한 신호입니다.

난이도 ★★★

과제 5 — 같은 패턴으로 내 API의 CLI를 만들기키 불필요 · 며칠

사내 API든 공개 API(예: 공공데이터 포털)든, 엔드포인트 5~10개를 골라 서술자 배열 + 제네릭 실행기 구조로 CLI를 짜 보세요. commander + zod 조합이면 remote.ts의 골격을 거의 그대로 쓸 수 있습니다.

단계: ① 서술자 인터페이스 정의 → ② 엔드포인트 3개를 배열로 → ③ 실행기 하나 작성 → ④ 나머지 엔드포인트는 배열에만 추가 → ⑤ 4번에서 실행기를 안 고쳤다면 설계 성공.

여기서 진짜 배우는 것: ④에서 실행기를 고쳐야 했다면 고쳐야 했는지. 그 이유가 곧 서술자에 추가해야 할 정책 필드입니다 — 원본이 paging·window를 갖게 된 경위와 같습니다.

난이도 ★★★

과제 6 — 라우팅표를 갖춘 MCP 서버 만들기키 불필요 · 며칠

docs/mcp/capability-map.md를 본떠서, 도구 10개 이상인 MCP 서버에 “의도 → 도구” 라우팅표를 문서로 붙여 보세요. 그다음 라우팅표를 준 경우와 안 준 경우로 나눠, 같은 질문 10개를 에이전트에게 던져 도구 선택 정확도를 비교합니다.

원본의 라우팅표는 “이름 → 데이터”, “펀드 이름 → 데이터” 같은 조합 흐름도 순서대로 적어 둡니다 — 도구 하나로 안 끝나는 작업에서 특히 효과가 납니다.

9관련 기술 심화 학습 로드맵

이 저장소를 “읽을 수 있는 상태”로 만드는 6주
주차주제할 것
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.mdcapability-map.md“LLM에게 주는 매뉴얼”의 잘 쓴 예로 놓고 분해해 보세요.
지름길

목적에 따라 갈라집니다. 클라이언트 라이브러리를 잘 짜고 싶다면 1·2·4주차만. AI 도구화가 목적이면 1·6주차만. 데이터 파이프라인이 목적이면 5주차 하나로 충분합니다 — 이 저장소는 세 관심사가 우연히 한 곳에 모여 있는 형태라 전부 볼 필요가 없습니다.

10핵심 키워드 사전

이 문서와 저장소에 반복해서 나오는 말들
용어
A주 (A股)중국 본토 증시(상하이·선전·베이징)에서 위안화로 거래되는 보통주. 이 서비스의 유일한 대상 시장
同花順 / HiThink통화순. 중국의 증권 정보 단말·데이터 사업자. 이 저장소의 발행 주체
thscode통화순 종목 식별자. 600519.SH 형태. 모든 조회의 출발점
복권 (qfq / hfq)배당·분할로 생긴 가격 계단을 보정하는 것. 전복권(과거를 당김) / 후복권(현재를 밀어냄)
집합경매 (集合競價)장 시작·마감 전 주문을 모아 한 가격으로 체결하는 방식. 별도 엔드포인트가 있음
涨停 / 跌停 (창팅·뎨팅)상한가 / 하한가. 중국 증시는 일일 등락 제한이 있어 “상한가 종목 풀”이 독립 데이터로 유통됨
龙虎榜 (룽후방)대량 거래 상위 창구 공시. 큰손 자금 흐름 추적에 쓰이는 중국 특유의 공개 데이터
봉투 (envelope)응답을 {code, message, data}로 감싸는 규약. HTTP 상태와 별개로 업무 성패를 표현
SSOTSingle Source of Truth. 같은 사실이 여러 곳에 있으면 반드시 갈라지므로 원본을 하나만 둔다는 원칙
계약 드리프트서버 API와 클라이언트가 조용히 어긋나는 현상. 이 저장소는 런타임 검증 + live-canary로 잡음
zodTypeScript용 스키마 검증 라이브러리. 런타임에도 살아 있는 타입이 핵심 가치
서술자 (descriptor)동작을 코드가 아니라 데이터로 적은 명세 객체. 여기서는 CLI 명령 하나 = 서술자 하나
지수 백오프재시도 간격을 1s → 2s → 4s처럼 배로 늘리는 것. 실패한 서버에 몰리지 않게 함
지터 (jitter)백오프에 섞는 무작위 폭. 여러 클라이언트의 재시도 시점을 흩뿌려 천둥소떼를 막음
Retry-After“이만큼 기다렸다 오라”는 서버 헤더. 초 단위 숫자 또는 HTTP 날짜
keyringOS의 자격증명 금고(macOS Keychain · Windows Credential Manager · Linux Secret Service)
DuckDB서버 없이 파일 하나로 도는 분석용(OLAP) DB. “분석계의 SQLite”
Parquet컬럼 지향 파일 포맷. 큰 표 데이터를 압축해 두고 필요한 열만 읽을 수 있음
MCPModel Context Protocol. AI 모델에 외부 도구를 붙이는 표준. 여기서는 HTTP transport로 4개 서비스
Agent Skill에이전트에게 “이런 상황엔 이렇게 하라”를 알려주는 마크다운 문서. 이 저장소의 권장 진입점
live canary주기적으로 진짜 서버를 최소 호출해 계약 파손을 감지하는 CI 잡
메달리온 아키텍처데이터를 bronze(원본) / silver(정제) / gold(소비) 층으로 나누는 방식. 이 저장소의 raw/calc/view와 같은 발상

11참고 링크

원본과 이어서 볼 것들

이 저장소 · 서비스

비교해 볼 만한 저장소

배경 지식