Files
QuantEngineByItz/.gitea/workflows/kis_data_collection.yml
T
kjh2064 4cb206a269 KIS Open API 조회전용 연동 + 직접매매 절대금지 안전게이트
매수/매도 주문 및 계좌 잔고조회를 API로 직접 실행하지 않는다는 원칙을
코드 레벨에서 강제하는 안전게이트(governance/rules/06, 07)와 함께,
시세/호가/공매도거래비중 등 조회전용 KIS Open API 연동 및 SQLite
수집 파이프라인을 추가한다.

- kis_api_client_v1: 모든 요청이 _assert_read_only를 통과해야 하며
  /trading/ 경로·주문 TR_ID는 RuntimeError로 즉시 차단
- kis_data_collection_v1: KIS 우선 + Naver 폴백, 네트워크 실패는
  개별 ticker 단위로 흡수(배치 전체 중단 없음)
- data_collection_store_v1 / storage_backend_v1: SQLite 캐노니컬
  저장소, PostgreSQL 전환 대비 백엔드 추상화
- Gitea 영업일 스케줄(2시간 간격) + CI 강제 게이트
  (validate_no_direct_api_trading_v1, validate_kis_api_credentials_v1)
2026-06-21 20:04:44 +09:00

132 lines
5.9 KiB
YAML

name: KIS Data Collection (SQLite Canonical Feed)
# ─────────────────────────────────────────────────────────────────
# [중요] 이 워크플로우는 KIS Open API를 코어로 하는 read-only 데이터 수집만 수행한다.
# xlsx를 직접 읽지 않고 GatherTradingData.json + live read-only APIs를 통해
# SQLite canonical store를 갱신한다. 매수/매도 주문은 어떤 경우에도 실행하지 않는다.
#
# 스케줄: 영업일(월~금) 08:00~17:00 KST, 2시간 간격(08/10/12/14/16시).
# Gitea Actions의 schedule cron은 UTC 기준으로 평가된다(서버 타임존이 별도
# 설정되어 있지 않은 경우의 기본값). 아래 cron은 UTC로 작성했다:
# KST 08:00 = UTC 전날 23:00 → 요일은 "한국 기준 평일"에 맞춰 UTC 0-4(일~목)로 이동
# KST 10/12/14/16:00 = UTC 01/03/05/07:00, 같은 날(UTC 월~금, 1-5)
#
# [실제 Gitea 서버 타임존이 Asia/Seoul로 설정되어 있다면] 아래 cron을 그대로
# "0 8,10,12,14,16 * * 1-5" 한 줄로 교체하면 된다 — 첫 실행 후 Actions 실행
# 기록의 타임스탬프를 확인해 KST 08시 전후로 도는지 검증할 것(추정하지 말고 확인).
#
# 스케줄 주기 변경: 아래 schedule 목록의 cron 줄을 추가/삭제/수정하면 된다.
# 예) 1시간 간격으로 바꾸려면 09,11,13,15시 슬롯을 추가.
# ─────────────────────────────────────────────────────────────────
on:
schedule:
- cron: "0 23 * * 0-4" # KST 월~금 08:00 (UTC 일~목 23:00)
- cron: "0 1 * * 1-5" # KST 월~금 10:00 (UTC 01:00)
- cron: "0 3 * * 1-5" # KST 월~금 12:00 (UTC 03:00)
- cron: "0 5 * * 1-5" # KST 월~금 14:00 (UTC 05:00)
- cron: "0 7 * * 1-5" # KST 월~금 16:00 (UTC 07:00)
workflow_dispatch: # 수동 실행 — 스케줄 검증/즉시 재시도용
jobs:
collect-kis-data:
runs-on: self-hosted
steps:
- name: Checkout Code
run: |
if [ -d .git ]; then
git remote set-url origin http://x-access-token:${{ secrets.GITHUB_TOKEN }}@192.168.123.100:8418/KimJaeHyun/myfinance.git
else
git init
git remote add origin http://x-access-token:${{ secrets.GITHUB_TOKEN }}@192.168.123.100:8418/KimJaeHyun/myfinance.git
fi
git fetch origin main --depth=1
git reset --hard FETCH_HEAD
if [ ! -f GatherTradingData.json ]; then
echo "::error::GatherTradingData.json 없음 — canonical seed snapshot이 필요합니다."
exit 1
fi
- name: Configure Runtime Paths
run: |
export PATH=/usr/local/bin:$PATH
echo "/usr/local/bin" >> $GITHUB_PATH
/usr/bin/python3 --version
- name: Setup Python Environment
run: |
VENV_BASE=/volume1/gitea/python_venv
REQ_HASH=$(md5sum tools/run_kis_data_collection_v1.py 2>/dev/null | cut -d' ' -f1 || echo "kis-default")
VENV="$VENV_BASE/$REQ_HASH"
if [ ! -f "$VENV/bin/python" ]; then
mkdir -p "$VENV_BASE"
/usr/bin/python3 -m venv "$VENV"
if [ ! -f "$VENV/bin/pip" ]; then
curl -sS https://bootstrap.pypa.io/pip/3.8/get-pip.py -o get-pip.py
"$VENV/bin/python" get-pip.py --quiet
rm get-pip.py
fi
"$VENV/bin/pip" install --upgrade pip --quiet
"$VENV/bin/pip" install requests beautifulsoup4 pyyaml --quiet
ls -dt "$VENV_BASE"/*/ 2>/dev/null | tail -n +3 | xargs rm -rf 2>/dev/null || true
fi
echo "$VENV/bin" >> $GITHUB_PATH
- name: "[CRITICAL] No Direct API Trading Gate"
run: python3 tools/validate_no_direct_api_trading_v1.py
- name: "[CRITICAL] Validate KIS API Credentials (mock)"
env:
KIS_APP_Key_TEST: ${{ secrets.KIS_APP_KEY_TEST }}
KIS_APP_Secret_TEST: ${{ secrets.KIS_APP_SECRET_TEST }}
run: |
python3 tools/validate_kis_api_credentials_v1.py \
--account mock \
--ticker 005930
- name: Collect KIS Market Data to SQLite (read-only)
env:
KIS_APP_Key: ${{ secrets.KIS_APP_KEY }}
KIS_APP_Secret: ${{ secrets.KIS_APP_SECRET }}
run: |
python3 tools/run_kis_data_collection_v1.py \
--input-json GatherTradingData.json \
--sqlite-db outputs/kis_data_collection/kis_data_collection.db \
--output-json Temp/kis_data_collection_v1.json \
--kis-account real
- name: Validate SQLite Artifact
run: |
python3 - <<'PY'
import json, sqlite3
from pathlib import Path
db = Path("outputs/kis_data_collection/kis_data_collection.db")
report = Path("Temp/kis_data_collection_v1.json")
assert db.exists(), f"missing db: {db}"
assert report.exists(), f"missing report: {report}"
conn = sqlite3.connect(db)
try:
run_count = conn.execute("SELECT COUNT(*) FROM collection_runs").fetchone()[0]
snap_count = conn.execute("SELECT COUNT(*) FROM collection_snapshots").fetchone()[0]
print(json.dumps({"run_count": run_count, "snapshot_count": snap_count}, ensure_ascii=False))
assert run_count >= 1
assert snap_count >= 1
finally:
conn.close()
PY
- name: Notify Run Result
if: always()
run: |
STATUS="${{ job.status }}"
RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
SUMMARY_FILE="Temp/kis_data_collection_v1.json"
SUMMARY_TEXT="(요약 파일 없음)"
[ -f "$SUMMARY_FILE" ] && SUMMARY_TEXT=$(cat "$SUMMARY_FILE")
echo "=== KIS Data Collection Result ==="
echo "status: $STATUS"
echo "summary: $SUMMARY_TEXT"
echo "run log: $RUN_URL"