Compare commits

..
3 Commits
Author SHA1 Message Date
shinalokandClaude Sonnet 5 1b95657a49 Remove doc/ from version control
doc/(IMPROVEMENTS.md, NH_INTEGRATION.md)는 로컬 전용 메모라 원격 저장소에는
올리지 않기로 함. git 추적에서 제외하고 .gitignore에 추가(로컬 파일은 유지).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01225Lu4Fc2UpMz6QixEcNT8
2026-08-31 14:41:56 +09:00
shinalokandClaude Sonnet 5 cb5d770a1c Merge branch 'feature/nh-broker-integration' into main
NH(나무) 브로커 통합 및 get_balance() 잔고/보유종목 파싱 버그 수정.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01225Lu4Fc2UpMz6QixEcNT8
2026-08-31 14:36:14 +09:00
shinalokandClaude Sonnet 5 361b560624 Add NH(나무) broker integration and fix balance/holds parsing
- finestock/nh/: Nh/NhV 브로커 클래스 추가 (시세/주문/잔고/실시간 WS)
- api_factory.py, path.py: APIProvider.NH/NHV 등록, 도메인/엔드포인트 매핑
- kis.py: get_holds/get_ohlcv_min/get_index_min/get_stock_list 스텁 추가,
  oauth() Content-Type 헤더 수정
- get_balance()의 실전 디버깅으로 드러난 버그 수정:
  - Hold.total(매입금액)이 존재하지 않는 byn_amt 필드를 참조해 항상 0이던 것을
    eal_amt - eal_pls_amt로 계산하도록 수정
  - rsp_cd를 "00000" 단일 값으로만 성공 판정해 정상 응답('00218' 연속조회 중,
    '00166' 마지막 페이지 등)을 실패로 오판하던 것을 Output_0 존재 여부로 판정
  - 응답 헤더의 cts/cts_flag로 연속조회를 재귀 처리해 10건 넘는 보유종목도
    전부 합쳐서 반환하도록 구현
- doc/, tests/, example_*.py, setup.py, requirements.txt, CLAUDE.md 등 추가
- README.md에 .env 환경변수 설정 가이드 추가

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01225Lu4Fc2UpMz6QixEcNT8
2026-08-31 14:35:30 +09:00
11 changed files with 987 additions and 121 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
# finestock 사용 시 필요한 환경변수 템플릿(브로커 앱키/시크릿/계좌번호 등).
# finestock 예제 스크립트(example.py, example_async.py 등)에서 사용하는 환경변수 템플릿.
# 이 파일을 복사해 .env 로 저장한 뒤 실제 값을 채워 넣으세요. .env는 .gitignore에 의해
# 커밋되지 않습니다. 절대 실제 키/시크릿/토큰 값을 이 파일(.env.example)에 직접 채워
# 커밋하지 마세요 — 이 파일은 어떤 값이 필요한지 보여주는 템플릿일 뿐입니다.
-3
View File
@@ -22,9 +22,6 @@ venv/
# 로컬 전용 문서 (원격 저장소에는 올리지 않음)
doc/
# 실키/시크릿이 하드코딩된 적이 있어 정리 후 다시 올릴 예정 (원격 저장소에는 올리지 않음)
example*.py
# --- JetBrains 공식 .gitignore 파일 명세 ---
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+30 -117
View File
@@ -1,71 +1,30 @@
# finestock
Korean Stock OpenAPI Package (EBest, LS, KIS, Kiwoom, NH)
Korean Stock OpenAPI Package (LS, KIS)
Created by alshin
---
## Table of Contents
1. [설치](#설치)
2. [지원 브로커](#지원-브로커)
3. [환경변수 설정 (.env)](#환경변수-설정-env)
4. [아키텍처](#아키텍처)
5. [사용법](#사용법)
6. [테스트](#테스트)
7. [Release Notes](#release-notes)
8. [License](#license)
2. [환경변수 설정 (.env)](#환경변수-설정-env)
3. [아키텍처](#아키텍처)
4. [사용법](#사용법)
5. [Release Notes](#release-notes)
6. [License](#license)
---
## 설치
PyPI에 배포된 버전을 쓰려면:
```bash
pip install finestock
```
저장소를 직접 클론해서 최신 소스로 개발/테스트하려면(editable install):
```bash
git clone https://github.com/shinalok/finestock.git
cd finestock
pip install -e .
```
런타임 의존성은 `websockets`, `requests`, `loguru` 세 개뿐입니다(`requirements.txt` 참고).
---
## 지원 브로커
`APIProvider` enum(`finestock/api_factory.py`)으로 브로커를 선택합니다. `V`로 끝나는 값은 모의투자(Virtual) 전용입니다.
| Provider | 브로커 | 실전/모의 |
| --- | --- | --- |
| `APIProvider.EBEST` | 이베스트투자증권 | 실전 |
| `APIProvider.LS` | LS증권 | 실전 |
| `APIProvider.LSV` | LS증권 | 모의투자 |
| `APIProvider.KIS` | 한국투자증권 | 실전 |
| `APIProvider.KISV` | 한국투자증권 | 모의투자 |
| `APIProvider.KIWOOM` | 키움증권 | 실전 |
| `APIProvider.KIWOOMV` | 키움증권 | 모의투자 |
| `APIProvider.NH` | NH투자증권(나무) | 실전 |
| `APIProvider.NHV` | NH투자증권(나무) | 모의투자 |
```python
import finestock
from finestock import APIProvider
api = finestock.create_api(APIProvider.LS)
```
브로커마다 지원하는 메서드 범위가 조금씩 다릅니다(예: NH는 지수 조회 TR이 없고, 전종목 리스트는 REST가 아니라 `.mst` 마스터 파일로만 제공됩니다). 자세한 브로커별 차이는 각 클래스의 docstring(`finestock/{ebest,ls,kis,kiwoom,nh}/*.py`)을 참고하세요.
---
## 환경변수 설정 (.env)
브로커 앱키/시크릿/계좌번호/액세스 토큰은 코드에 직접 하드코딩하지 말고 환경변수로 주입합니다. 저장소 루트의 `.env.example`을 복사해 `.env`로 만들고 실제 값을 채워 넣으세요.
브로커 앱키/시크릿/계좌번호/액세스 토큰은 코드에 직접 하드코딩하지 말고 환경변수로 주입다. 저장소 루트의 `.env.example`을 복사해 `.env`로 만들고 실제 값을 채워 넣는다.
```bash
cp .env.example .env
@@ -81,26 +40,23 @@ ACCOUNT_NUM_SUB=01
ACCESS_TOKEN=
```
`.env``.gitignore`에 의해 커밋되지 않습니다(`.env.example`만 커밋 대상).
> ⚠️ 실제 앱키/시크릿을 스크립트에 직접 문자열로 적어두고 커밋하지 마세요. 한 번이라도 원격 저장소(특히 public)에 올라간 값은 즉시 노출된 것으로 간주하고 발급처(브로커 개발자센터)에서 폐기 후 재발급받아야 합니다.
`.env``.gitignore`에 의해 커밋되지 않다(`.env.example`만 커밋 대상).
### 값 로드 방법
`example.py`, `example_async.py`는 모두 `os.environ.get("APP_KEY", ...)` 형태로 값을 읽는다. `.env` 파일 자체는 셸이나 파이썬이 자동으로 읽어주지 않으므로 아래 두 방법 중 하나가 필요하다.
**1) python-dotenv로 자동 로드 (권장)**
```bash
pip install python-dotenv
```
스크립트 최상단에서 아래처럼 호출하면 `.env` 값이 `os.environ`에 채워집니다(설치돼 있지 않으면 `ImportError`를 잡아 조용히 건너뛰도록 작성하는 게 일반적입니다).
예제 스크립트`python-dotenv`가 설치되어 있으면 시작 시 자동으로 `.env`를 읽어 `os.environ`에 채워 넣는다(설치돼 있지 않으면 조용히 건너뛴다). 직접 스크립트를 작성할 때도 아래처럼 최상단에서 호출하면 된다.
```python
try:
from dotenv import load_dotenv
load_dotenv()
except ImportError:
pass
from dotenv import load_dotenv
load_dotenv()
import os
app_key = os.environ.get("APP_KEY")
@@ -113,12 +69,14 @@ app_secret = os.environ.get("APP_SECRET")
# PowerShell
$env:APP_KEY = "YOUR_APP_KEY"
$env:APP_SECRET = "YOUR_APP_SECRET"
python example.py
```
```bash
# bash
export APP_KEY="YOUR_APP_KEY"
export APP_SECRET="YOUR_APP_SECRET"
python example.py
```
---
@@ -128,22 +86,15 @@ export APP_SECRET="YOUR_APP_SECRET"
`finestock`은 **파사드 패턴(Facade Pattern)**과 **인터페이스 분리 원칙(ISP)**을 결합하여 설계되었습니다.
### 1. 통합된 상태 관리 (Facade)
`create_api`로 생성되는 객체는 인증, 시세, 주문, 잔고, 실시간 등 모든 기능을 통합하여 관리합니다. 이를 통해 로그인 세션이나 소켓 연결 상태를 여러 모듈이 공유할 수 있어 사용이 편리합니다.
`create_api`로 생성되는 객체는 인증, 시세, 주문 등 모든 기능을 통합하여 관리합니다. 이를 통해 로그인 세션이나 소켓 연결 상태를 여러 모듈이 공유할 수 있어 사용이 편리합니다.
### 2. 인터페이스를 통한 명확한 사용 (ISP)
하나의 거대한 객체이지만, **타입 힌팅(Type Hinting)**을 통해 필요한 기능만 노출하여 안전하게 사용할 수 있습니다. `finestock/comm/api_interface.py`에 정의된 세그먼트:
하나의 거대한 객체이지만, **타입 힌팅(Type Hinting)**을 통해 필요한 기능만 노출하여 안전하게 사용할 수 있습니다.
- `AuthenticationProvider`: 로그인 및 토큰 관리(`oauth`, `set_oauth_info`, `set_access_token`)
- `MarketDataProvider`: 시세/OHLCV/지수/호가 조회(`get_price`, `get_ohlcv`, `get_ohlcv_min`, `get_index`, `get_orderbook`, ...)
- `TradingProvider`: 주문 실행/취소(`do_order`, `do_order_cancel`)
- `RealtimeProvider`: WebSocket 실시간 구독(`set_data_queue`, `recv_price`, `recv_orderbook`, ...)
- `AccountProvider`: 계좌 잔고/보유종목 조회(`get_balance`, `get_holds`)
- `InfoProvider`: 종목/지수 마스터 목록(`get_stock_list`, `get_index_list`)
이 인터페이스들의 합집합(`BaseProvider`)을 모든 브로커 클래스가 구현합니다. 타입 힌팅은 IDE 자동완성을 좁히기 위한 것일 뿐, 런타임 제약은 없습니다.
### 3. 데이터 모델 (`finestock/model/`)
브로커가 반환하는 원시 JSON/TR 응답은 파싱되어 공통 `@dataclass(frozen=True)` 타입(`Price`, `OrderBook`, `Hold`, `Account`, `Order`, `Trade`, `Stock`, `Index`)으로 통일됩니다.
- `AuthenticationProvider`: 로그인 및 토큰 관리
- `MarketDataProvider`: 과거 시세(이력) 조회
- `RealtimeProvider`: 실시간 시세 수신
- `TradingProvider`: 주식 주문 및 잔고 조회
---
@@ -167,22 +118,11 @@ if isinstance(api, AuthenticationProvider):
# 3. 데이터 조회 (MarketDataProvider 인터페이스 활용)
if isinstance(api, MarketDataProvider):
# IDE에서 get_ohlcv 등 시세 관련 메서드만 자동완성됨
ohlcvs = api.get_ohlcv("005930", frdate="20260101", todate="20260110")
print(ohlcvs)
df = api.get_ohlcv("005930", frdate="20250101", todate="20250110")
print(df)
```
### 2. 계좌 조회 (AccountProvider)
```python
from finestock.comm.api_interface import AccountProvider
if isinstance(api, AccountProvider):
account = api.get_balance()
if account:
print(account.deposit, len(account.hold))
```
### 3. 실시간 데이터 (Queue Injection)
### 2. 실시간 데이터 (Queue Injection)
```python
import queue
@@ -191,16 +131,15 @@ from finestock.comm.api_interface import RealtimeProvider
if isinstance(api, RealtimeProvider):
# 1. 데이터를 받을 큐 생성
q = queue.Queue()
# 2. 큐 주입
api.set_data_queue(q)
# 3. 데이터 수신 (asyncio 이벤트 루프에서 api.connect()/api.run() 실행 필요)
# 브로커별 recv_price / recv_orderbook / recv_trade 등을 구독한 뒤
# q.get()으로 파싱된 finestock.Price / OrderBook / Trade 객체를 받는다.
# 3. 데이터 수신 (비동기 루프 실행 필요)
# ... (자세한 예제는 example_v1.py 참조)
```
### 4. 타입 힌팅 활용 (Type Hinting)
### 3. 타입 힌팅 활용 (Type Hinting)
IDE에서 각 인터페이스별 메서드만 자동완성되도록 하려면 변수에 타입을 명시할 수 있습니다.
@@ -214,32 +153,6 @@ full_api = finestock.create_api(APIProvider.LS)
market_api: MarketDataProvider = full_api
# market_api. (여기서 get_ohlcv 등만 보임)
ohlcvs = market_api.get_ohlcv("005930", frdate="20260101", todate="20260110")
df = market_api.get_ohlcv("005930")
```
---
## 테스트
```bash
python -m unittest discover tests # 전체 테스트
python -m unittest tests.test_model -v # 특정 모듈만
```
`pytest` 설정은 없으며 표준 라이브러리 `unittest`를 사용합니다.
---
## Release Notes
전체 변경 이력은 [CHANGELOG.md](./CHANGELOG.md)를 참고하세요. 최근 주요 변경:
- **1.0.1.0**: `MarketDataProvider``get_price(code)` 추가(당일 OHLCV를 단일 `Price`로 반환)
- **1.0.0.1**: `APIFactory` lazy loading 도입, 인터페이스 분리(ISP) 리팩터링, 메서드 네이밍 PEP 8 표준화
- (개발 중) NH투자증권(나무) 브로커 지원 추가 — `APIProvider.NH` / `APIProvider.NHV`
---
## License
별도의 LICENSE 파일은 아직 저장소에 포함되어 있지 않습니다. 라이선스 조건이 필요하다면 저장소 관리자(`shinalok`)에게 문의해 주세요.
+157
View File
@@ -0,0 +1,157 @@
import os
import sys
import finestock
from finestock import APIProvider
try:
# 선택 의존성: 설치돼 있으면 .env 파일을 자동으로 읽어 os.environ에 채워준다.
# pip install python-dotenv
from dotenv import load_dotenv
load_dotenv()
except ImportError:
pass
#from loguru import logger
finestock.print_version_info()
#logging.basicConfig(level=logging.DEBUG)
#logger.add(sys.stdout, level="DEBUG")
#print(logger._core.handlers) # 내부 구조 접근이지만 확인용으로는 유용
#exit()
# 실키/토큰은 절대 코드에 하드코딩하지 말 것. 환경변수로 주입한다.
# 예 (PowerShell): $env:APP_KEY="..."; $env:APP_SECRET="..."
app_key = os.environ.get("APP_KEY", "YOUR_APP_KEY")
app_secret = os.environ.get("APP_SECRET", "YOUR_APP_SECRET")
account_num = os.environ.get("ACCOUNT_NUM", "YOUR_ACCOUNT_NUM")
account_num_sub = os.environ.get("ACCOUNT_NUM_SUB", "01")
access_token = os.environ.get("ACCESS_TOKEN", "")
api_type = "KISV"
api = finestock.APIFactory.create_api(APIProvider.LS)
api.set_oauth_info(app_key, app_secret)
login = False
if login:
api.oauth()
print(api.access_token)
else:
api.headers['authorization'] = f"Bearer {access_token}"
#list = api.get_condition_list("shinalok")
#print(list)
#list = api.get_condition_price("shinalok0000")
#print(list)
# etf_list = ["069500", "229200", "292190", "156080"]
# prices = api.get_multiple_ohlcv(etf_list)
# print(prices)
# exit()
#api.set_account_info(account_num, account_num_sub)
# ohlcvs = api.get_ohlcv("233740")
# print(ohlcvs)
# exit()
#ohlcvs = api.get_index_min("405", "20250514")
ohlcvs = api.get_ohlcv_min("005930", "20250515")
print(ohlcvs)
print(len(ohlcvs))
exit()
acc = api.get_balance()
print(acc)
#res = api.do_order("228790", finestock.ORDER_FLAG.BUY, 0, 1)
#print(res)
exit()
orderbook = api.get_orderbook("233740")
print(orderbook)
#ohlcvs = api.get_ohlcv("233740", "20240806", "20240806")
ohlcvs = api.get_ohlcv("233740")
print(ohlcvs)
exit()
'''
ohlcvs = api.get_ohlcv("005930", "20200101", "20240328")
for price in ohlcvs:
print(price.workday)
exit()
'''
api.set_account_info(account_num, account_num_sub)
acc = api.get_balance()
print(acc)
exit()
#ohlcvs = api.get_index("405", "20200101", "20240328")
ohlcvs = api.get_index("405", "20240802", "20240802")
print(ohlcvs)
print(len(ohlcvs))
print(ohlcvs)
for price in ohlcvs:
print(price.workday)
exit()
#Ebest만 사용 가능
#list = api.get_index_list()
#print(list)
#exit()
#res = api.do_order("004410", finestock.ORDER_FLAG.SELL, 176, 1)
#print(res)
#res = api.do_order_cancle("19441", "004410", 1)
#print(res)
res = api.get_order_status("004410")
print(res)
exit()
res = api.do_order("004410", finestock.ORDER_FLAG.BUY, 170, 1)
print(res)
exit()
list = api.get_stock_list()
print(api.stock_master)
print(list)
print(len(list))
#print(list.keys())
exit()
#주식 ohlcv 가져오기
ohlcvs = api.get_ohlcv("005930", "20240101", "20240328")
print(ohlcvs)
logger.debug("Hihi")
#지수정보 가져오기
#index = api.get_index("3003", "20240101", "20240328") #LS: 405, KIS: 3003
#print(index)
#계좌정보 가져오기
api.set_account_info(account_num, account_num_sub)
acc = api.get_balance()
print(acc)
#주문
#res = api.do_order("004410", finestock.ORDER_FLAG.BUY, 170, 1)
#print(res)
#주문상태 확인
res = api.get_order_status("004410")
print(res)
#주문취소
#res = api.do_order_cancle("13889", "004410", 1)
#print(res)
exit()
#ohlcvs = api.get_ohlcv("005930", "20240101", "20240328")
#print(ohlcvs)
#api.get_index_list()
#api.get_ohlcv("005930", count=20)
#ohlcvs = api.get_index("3003", "20240101", "20240328")
#print(ohlcvs)
#orderbook = api.get_orderbook("005930")
#print(orderbook)
#res = api.do_order("004410", ORDER.BUY, 150, 1)
#res = api.do_order_cancle("12788", "004410", 1)
+62
View File
@@ -0,0 +1,62 @@
import os
import queue
import threading
from finestock import APIProvider
try:
# 선택 의존성: 설치돼 있으면 .env 파일을 자동으로 읽어 os.environ에 채워준다.
# pip install python-dotenv
from dotenv import load_dotenv
load_dotenv()
except ImportError:
pass
import finestock
import asyncio
from loguru import logger
import sys
logger.add(sys.stdout, level="DEBUG")
finestock.print_version_info()
data_queue = queue.Queue()
data_condition = threading.Condition()
# 실키/토큰은 절대 코드에 하드코딩하지 말 것. 환경변수로 주입한다.
# 예 (PowerShell): $env:APP_KEY="..."; $env:APP_SECRET="..."
app_key = os.environ.get("APP_KEY", "YOUR_APP_KEY")
app_secret = os.environ.get("APP_SECRET", "YOUR_APP_SECRET")
access_token = os.environ.get("ACCESS_TOKEN", "")
api_type = APIProvider.LSV
api = finestock.APIFactory.create_api(api_type)
api.set_oauth_info(app_key, app_secret)
login = False
if login:
api.oauth()
print(api.access_token)
else:
api.access_token = access_token
api.headers['authorization'] = f"Bearer {access_token}"
api.set_queue(data_queue, data_condition)
async def main():
await api.connect()
#await api.recv_orderbook("005930")
#await api.recv_price("005930")
#await api.recv_orderbook("082850")
#await api.recv_price("082850")
#await api.recv_price("233740")
#await api.recv_index("405") #코스닥 150: 405
await api.recv_order_status()
await api.run()
asyncio.run(main())
#loop = asyncio.get_event_loop()
#loop.run_until_complete(main())
#loop.close()
+98
View File
@@ -0,0 +1,98 @@
import asyncio
import queue
import sys
from loguru import logger
import finestock
from finestock import APIProvider
from finestock.comm.api_interface import AuthenticationProvider, RealtimeProvider
# Configure logging
logger.remove()
logger.add(sys.stdout, level="DEBUG")
async def consume_data(data_queue: queue.Queue):
"""
Background task to consume data from the queue asynchronously.
"""
logger.info("Data consumer started...")
while True:
try:
if not data_queue.empty():
data = data_queue.get_nowait()
print(f"Received Realtime Data: {data}")
else:
await asyncio.sleep(0.1) # Yield control
except Exception as e:
logger.error(f"Consumer Error: {e}")
await asyncio.sleep(1)
async def main():
finestock.print_version_info()
# 1. Create API Instance
api = finestock.create_api(APIProvider.KIWOOM)
# 2. Setup Authentication
# Kiwoom requires app key/secret?
# Based on kiwoom.py:
# self.app_key = None
# self.app_secret = None
# OAuth uses them.
# I should use the keys if I have them or reuse from current context if needed.
# example_v1.py used:
# api.set_oauth_info("RSvh1vtzHj8Z5F15Ee7_rJ5znHuAKOF6A8v90TEkyBk", "_7wuGnkD-Crf1TJKErJPYkQp7AVuh_gVOPgQ2Uyyh7k")
auth_api: AuthenticationProvider = api
if isinstance(auth_api, AuthenticationProvider):
APP_KEY = "RSvh1vtzHj8Z5F15Ee7_rJ5znHuAKOF6A8v90TEkyBk"
APP_SECRET = "_7wuGnkD-Crf1TJKErJPYkQp7AVuh_gVOPgQ2Uyyh7k"
auth_api.set_oauth_info(APP_KEY, APP_SECRET)
token = auth_api.oauth()
logger.info(f"Logged in.")
# 3. Setup Realtime Data (Async)
realtime_api: RealtimeProvider = api
if isinstance(realtime_api, RealtimeProvider):
# Create Queue for data
data_queue = queue.Queue()
realtime_api.set_data_queue(data_queue)
# Connect to WebSocket
logger.info("Connecting to WebSocket...")
await realtime_api.connect()
# Login is handled in connect
# Subscribe to Realtime Prices
# Ex: Samsung Electronics (005930)
logger.info("Subscribing to 005930...")
await realtime_api.recv_price("005930")
# Subscribe to Realtime Index
# Ex: KOSPI (001)
logger.info("Subscribing to KOSPI (001)...")
await realtime_api.recv_index("001")
# Start the data consumer task
consumer_task = asyncio.create_task(consume_data(data_queue))
# Start the API run loop
logger.info("Starting API Event Loop...")
# Run for 10 seconds then exit for verification
try:
await asyncio.wait_for(realtime_api.run(), timeout=30)
except asyncio.TimeoutError:
logger.info("Test finished (timeout)")
# Cleanup
consumer_task.cancel()
await realtime_api.disconnect()
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
logger.info("Terminated by user")
+103
View File
@@ -0,0 +1,103 @@
import asyncio
import os
import queue
import sys
from loguru import logger
import finestock
from finestock import APIProvider
from finestock.comm.api_interface import AuthenticationProvider, RealtimeProvider
try:
# 선택 의존성: 설치돼 있으면 .env 파일을 자동으로 읽어 os.environ에 채워준다.
# pip install python-dotenv
from dotenv import load_dotenv
load_dotenv()
except ImportError:
pass
# Configure logging
logger.remove()
logger.add(sys.stdout, level="DEBUG")
async def consume_data(data_queue: queue.Queue):
"""
Background task to consume data from the queue asynchronously.
"""
logger.info("Data consumer started...")
while True:
try:
if not data_queue.empty():
data = data_queue.get_nowait()
print(f"Received Realtime Data: {data}")
else:
await asyncio.sleep(0.1) # Yield control
except Exception as e:
logger.error(f"Consumer Error: {e}")
await asyncio.sleep(1)
async def main():
finestock.print_version_info()
# 1. Create API Instance
# 모의투자로 테스트하려면 APIProvider.NHV를 사용한다. 단, 접근토큰발급(oauth())은
# NH·NHV 모두 항상 운영 도메인에서만 발급된다(모의투자 계좌로 거래하더라도 토큰은 동일).
api = finestock.create_api(APIProvider.NH)
# 2. Setup Authentication
# 실키/시크릿은 절대 코드에 하드코딩하지 말 것. 환경변수로 주입한다.
# 예 (PowerShell): $env:APP_KEY="..."; $env:APP_SECRET="..."
auth_api: AuthenticationProvider = api
if isinstance(auth_api, AuthenticationProvider):
app_key = os.environ.get("APP_KEY", "YOUR_APP_KEY")
app_secret = os.environ.get("APP_SECRET", "YOUR_APP_SECRET")
auth_api.set_oauth_info(app_key, app_secret)
auth_api.oauth()
logger.info("Logged in.")
# 3. Setup Realtime Data (Async)
realtime_api: RealtimeProvider = api
if isinstance(realtime_api, RealtimeProvider):
# Create Queue for data
data_queue = queue.Queue()
realtime_api.set_data_queue(data_queue)
# Connect to WebSocket
logger.info("Connecting to WebSocket...")
await realtime_api.connect()
# Subscribe to Realtime Price (체결가) — 삼성전자(005930)
logger.info("Subscribing to 005930 price...")
await realtime_api.recv_price("005930")
# Subscribe to Realtime Orderbook (호가) — 삼성전자(005930)
logger.info("Subscribing to 005930 orderbook...")
await realtime_api.recv_orderbook("005930")
# 참고: NH krstock API에는 지수 실시간 채널이 없어 recv_index()는 stub이다
# (호출하면 "not supported" 로그만 남기고 아무것도 구독하지 않는다).
# Start the data consumer task
consumer_task = asyncio.create_task(consume_data(data_queue))
# Start the API run loop
logger.info("Starting API Event Loop...")
# Run for 30 seconds then exit for verification
try:
await asyncio.wait_for(realtime_api.run(), timeout=30)
except asyncio.TimeoutError:
logger.info("Test finished (timeout)")
# Cleanup
consumer_task.cancel()
await realtime_api.disconnect()
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
logger.info("Terminated by user")
+120
View File
@@ -0,0 +1,120 @@
import asyncio
import queue
import sys
from loguru import logger
import finestock
from finestock import APIProvider
from finestock.comm.api_interface import AuthenticationProvider, RealtimeProvider
# Configure logging
logger.remove()
logger.add(sys.stdout, level="DEBUG")
async def consume_data(data_queue: queue.Queue):
"""
Background task to consume data from the queue asynchronously.
"""
logger.info("Data consumer started...")
while True:
try:
# Since refined queue is thread-safe standard queue, we can just poll it
# In a real async app, you might use asyncio.Queue for await q.get()
# But here we are injecting a standard queue as per interface.
# To avoid blocking the loop, we use run_in_executor or just poll with sleep
if not data_queue.empty():
data = data_queue.get_nowait()
# Type hinting can be used here if data structure is known
if isinstance(data, finestock.Price):
type_str = "Index" if data.code == "001" else "Price/Trade" # Simple heuristic
print(f"[{type_str}] Code: {data.code}, Time: {data.time}, Current: {data.close}, Vol: {data.volume}")
elif isinstance(data, finestock.OrderBook):
print(data)
print(f"[OrderBook] Code: {data.code}, Buy: {data.total_buy}, Sell: {data.total_sell}")
if data.buy: print(f" Best Buy: {data.buy[0].price} ({data.buy[0].qty})")
if data.sell: print(f" Best Sell: {data.sell[0].price} ({data.sell[0].qty})")
elif isinstance(data, finestock.Trade):
print(f"[Order Status] Code: {data.code}, Flag: {data.trade_flag}, Order: {data.order_flag}, Qty: {data.qty}")
else:
print(f"Received Data: {data}")
else:
await asyncio.sleep(0.1) # Yield control
except Exception as e:
logger.error(f"Consumer Error: {e}")
await asyncio.sleep(1)
async def main():
finestock.print_version_info()
# 0. Select Provider
# TARGET_PROVIDER = APIProvider.LSV
TARGET_PROVIDER = APIProvider.KIWOOMV
print(f"Selected Provider: {TARGET_PROVIDER.name}")
# 1. Create API Instance
api = finestock.create_api(TARGET_PROVIDER)
# 2. Setup Authentication
auth_api: AuthenticationProvider = api
if isinstance(auth_api, AuthenticationProvider):
if TARGET_PROVIDER == APIProvider.LSV:
# LSV Keys
APP_KEY = "PSgjLpp90XLhWexJYGL0P36oyzM3ne5kjhDW"
APP_SECRET = "htA3h9kULFPD75vtHAt0JaXyYAjsbf0b"
elif TARGET_PROVIDER == APIProvider.KIWOOM:
# Kiwoom Keys (Test)
APP_KEY = "RSvh1vtzHj8Z5F15Ee7_rJ5znHuAKOF6A8v90TEkyBk"
APP_SECRET = "_7wuGnkD-Crf1TJKErJPYkQp7AVuh_gVOPgQ2Uyyh7k"
elif TARGET_PROVIDER == APIProvider.KIWOOMV:
APP_KEY = "5-7V0r97uwlzmPNFa_FnwFEZtnyhFAZ1qlnDXQH2rX4"
APP_SECRET = "YOMvV7X1CwwST-_iFmRAPKXO7Wwr4zGGyTPX5gFcAqk"
auth_api.set_oauth_info(APP_KEY, APP_SECRET)
token = auth_api.oauth()
print(f"Logged in with token: {token}")
# 3. Setup Realtime Data (Async)
realtime_api: RealtimeProvider = api
if isinstance(realtime_api, RealtimeProvider):
# Create Queue for data
data_queue = queue.Queue()
realtime_api.set_data_queue(data_queue)
# Connect to WebSocket
logger.info("Connecting to WebSocket...")
await realtime_api.connect()
# Subscribe to Realtime Prices
# Subscribe to Realtime Prices, Orderbook, and Trades
# Ex: Samsung Electronics (005930)
logger.info("Subscribing to 005930 (Price, Orderbook, Trade)...")
#await realtime_api.recv_price("005930") #완료
#await realtime_api.recv_orderbook("005930") #완료
#await realtime_api.recv_trade("005930") # Same as price (0B)
logger.info("Subscribing to Order Status (00)...")
await realtime_api.recv_order_status()
# Subscribe to Index
# Ex: KOSPI (001)
logger.info("Subscribing to KOSPI Index (001)...")
#await realtime_api.recv_index("001") #완료
# Start the data consumer task
consumer_task = asyncio.create_task(consume_data(data_queue))
# Start the API run loop (This blocks until disconnected)
logger.info("Starting API Event Loop...")
await realtime_api.run()
# Cleanup (if loop breaks)
consumer_task.cancel()
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
logger.info("Terminated by user")
+161
View File
@@ -0,0 +1,161 @@
import os
import sys
import finestock
from finestock import APIProvider
try:
# 선택 의존성: 설치돼 있으면 .env 파일을 자동으로 읽어 os.environ에 채워준다.
# pip install python-dotenv
from dotenv import load_dotenv
load_dotenv()
except ImportError:
pass
#from loguru import logger
finestock.print_version_info()
#logging.basicConfig(level=logging.DEBUG)
#logger.add(sys.stdout, level="DEBUG")
#print(logger._core.handlers) # 내부 구조 접근이지만 확인용으로는 유용
#exit()
# 실키/토큰은 절대 코드에 하드코딩하지 말 것. 환경변수로 주입한다.
# 예 (PowerShell): $env:APP_KEY="..."; $env:APP_SECRET="..."
app_key = os.environ.get("APP_KEY", "YOUR_APP_KEY")
app_secret = os.environ.get("APP_SECRET", "YOUR_APP_SECRET")
account_num = os.environ.get("ACCOUNT_NUM", "YOUR_ACCOUNT_NUM")
account_num_sub = os.environ.get("ACCOUNT_NUM_SUB", "01")
access_token = os.environ.get("ACCESS_TOKEN", "")
api = finestock.APIFactory.create_api(APIProvider.KIS)
api.set_oauth_info(app_key, app_secret)
login = False
if login:
api.oauth()
print(api.access_token)
else:
api.headers['authorization'] = f"Bearer {access_token}"
res = api.get_price("329180")
print(res)
res = api.get_ohlcv("329180", "20260819", "20260820")
print(res)
exit()
#exit()
#list = api.get_condition_list("shinalok")
#print(list)
#list = api.get_condition_price("shinalok0000")
#print(list)
# etf_list = ["069500", "229200", "292190", "156080"]
# prices = api.get_multiple_ohlcv(etf_list)
# print(prices)
# exit()
#api.set_account_info(account_num, account_num_sub)
# ohlcvs = api.get_ohlcv("233740")
# print(ohlcvs)
# exit()
#ohlcvs = api.get_index_min("405", "20250514")
ohlcvs = api.get_ohlcv_min("005930", "20250515")
print(ohlcvs)
print(len(ohlcvs))
exit()
acc = api.get_balance()
print(acc)
#res = api.do_order("228790", finestock.ORDER_FLAG.BUY, 0, 1)
#print(res)
exit()
orderbook = api.get_orderbook("233740")
print(orderbook)
#ohlcvs = api.get_ohlcv("233740", "20240806", "20240806")
ohlcvs = api.get_ohlcv("233740")
print(ohlcvs)
exit()
'''
ohlcvs = api.get_ohlcv("005930", "20200101", "20240328")
for price in ohlcvs:
print(price.workday)
exit()
'''
api.set_account_info(account_num, account_num_sub)
acc = api.get_balance()
print(acc)
exit()
#ohlcvs = api.get_index("405", "20200101", "20240328")
ohlcvs = api.get_index("405", "20240802", "20240802")
print(ohlcvs)
print(len(ohlcvs))
print(ohlcvs)
for price in ohlcvs:
print(price.workday)
exit()
#Ebest만 사용 가능
#list = api.get_index_list()
#print(list)
#exit()
#res = api.do_order("004410", finestock.ORDER_FLAG.SELL, 176, 1)
#print(res)
#res = api.do_order_cancle("19441", "004410", 1)
#print(res)
res = api.get_order_status("004410")
print(res)
exit()
res = api.do_order("004410", finestock.ORDER_FLAG.BUY, 170, 1)
print(res)
exit()
list = api.get_stock_list()
print(api.stock_master)
print(list)
print(len(list))
#print(list.keys())
exit()
#주식 ohlcv 가져오기
ohlcvs = api.get_ohlcv("005930", "20240101", "20240328")
print(ohlcvs)
logger.debug("Hihi")
#지수정보 가져오기
#index = api.get_index("3003", "20240101", "20240328") #LS: 405, KIS: 3003
#print(index)
#계좌정보 가져오기
api.set_account_info(account_num, account_num_sub)
acc = api.get_balance()
print(acc)
#주문
#res = api.do_order("004410", finestock.ORDER_FLAG.BUY, 170, 1)
#print(res)
#주문상태 확인
res = api.get_order_status("004410")
print(res)
#주문취소
#res = api.do_order_cancle("13889", "004410", 1)
#print(res)
exit()
#ohlcvs = api.get_ohlcv("005930", "20240101", "20240328")
#print(ohlcvs)
#api.get_index_list()
#api.get_ohlcv("005930", count=20)
#ohlcvs = api.get_index("3003", "20240101", "20240328")
#print(ohlcvs)
#orderbook = api.get_orderbook("005930")
#print(orderbook)
#res = api.do_order("004410", ORDER.BUY, 150, 1)
#res = api.do_order_cancle("12788", "004410", 1)
+64
View File
@@ -0,0 +1,64 @@
import os
import finestock
from finestock import APIProvider
try:
# 선택 의존성: 설치돼 있으면 .env 파일을 자동으로 읽어 os.environ에 채워준다.
# pip install python-dotenv
from dotenv import load_dotenv
load_dotenv()
except ImportError:
pass
finestock.print_version_info()
# 실키/토큰은 절대 코드에 하드코딩하지 말 것. 환경변수로 주입한다.
# 예 (PowerShell): $env:APP_KEY="..."; $env:APP_SECRET="..."
app_key = os.environ.get("APP_KEY", "YOUR_APP_KEY")
app_secret = os.environ.get("APP_SECRET", "YOUR_APP_SECRET")
access_token = os.environ.get("ACCESS_TOKEN", "")
# 모의투자로 테스트하려면 APIProvider.NHV를 사용한다. 단, 접근토큰발급(oauth())은
# NH·NHV 모두 항상 운영 도메인에서만 발급된다(모의투자 계좌로 거래하더라도 토큰은 동일).
api = finestock.APIFactory.create_api(APIProvider.NH)
api.set_oauth_info(app_key, app_secret)
login = False
if login:
api.oauth()
print(api.access_token)
else:
api.headers['authorization'] = f"Bearer {access_token}"
# 1) 계좌 목록 조회 — /n2/acctinfo. acct_type이 01/02면 운영(NH) 전용, 03이면 모의투자(NHV) 전용.
accounts = api.get_account_list()
print(accounts)
# 계좌목록에서 사용할 계좌를 골라 세팅한다. act_no는 11자리 단일 값이라 sub는 비워둔다.
account_num = os.environ.get("ACCOUNT_NUM", "YOUR_ACCOUNT_NUM")
#account_num_sub = os.environ.get("ACCOUNT_NUM_SUB", "YOUR_ACCOUNT_NUM")
api.set_account_info(account_num, "")
'''
# 2) 시세 조회
price = api.get_price("005930")
print(price)
ohlcvs = api.get_ohlcv("005930", "20260701", "20260821")
print(len(ohlcvs), ohlcvs[:3] if ohlcvs else ohlcvs)
orderbook = api.get_orderbook("005930")
print(orderbook)
'''
# 3) 계좌/잔고 조회
acc = api.get_balance()
print(acc)
if acc:
print(len(acc.hold), acc.hold[:3] if acc.hold else acc.hold)
# 4) 주문 (실제 체결되니 운영 도메인에서는 주의 — 테스트는 APIProvider.NHV 권장)
# res = api.do_order("005930", finestock.ORDER_FLAG.BUY, 0, 1) # price=0 -> 시장가
# print(res)
# if res:
# cancel = api.do_order_cancel(res.order_num, "005930", 0) # qty<=0 -> 전량취소
# print(cancel)
+191
View File
@@ -0,0 +1,191 @@
import sys
from loguru import logger
import finestock
from finestock import APIProvider
from finestock.comm.api_interface import AuthenticationProvider, MarketDataProvider, TradingProvider, AccountProvider
# Configure logging
logger.remove()
logger.add(sys.stdout, level="INFO")
def main():
finestock.print_version_info()
# 1. Create API Instance
# The 'api' object implements multiple interfaces (Authentication, MarketData, Realtime, etc.)
api = finestock.create_api(APIProvider.KIWOOMV)
# 2. Setup Authentication (Sync)
# Type Hinting: View 'api' as AuthenticationProvider to see only auth-related methods
auth_api: AuthenticationProvider = api
if isinstance(auth_api, AuthenticationProvider):
#APP_KEY = "YOUR_APP_KEY"
#APP_SECRET = "YOUR_APP_SECRET"
#APP_KEY = "PSgjLpp90XLhWexJYGL0P36oyzM3ne5kjhDW" #LSV
#APP_SECRET = "htA3h9kULFPD75vtHAt0JaXyYAjsbf0b" #LSV
#APP_KEY = "RSvh1vtzHj8Z5F15Ee7_rJ5znHuAKOF6A8v90TEkyBk"
#APP_SECRET = "_7wuGnkD-Crf1TJKErJPYkQp7AVuh_gVOPgQ2Uyyh7k"
APP_KEY = "5-7V0r97uwlzmPNFa_FnwFEZtnyhFAZ1qlnDXQH2rX4"
APP_SECRET = "YOMvV7X1CwwST-_iFmRAPKXO7Wwr4zGGyTPX5gFcAqk"
auth_api.set_oauth_info(APP_KEY, APP_SECRET)
token = auth_api.oauth()
print(token)
# auth_api.oauth() # Perform actual login
# or auth_api.set_access_token("TOKEN")
# 3. Market Data (Sync)
logger.info("Fetching Market Data (Sync)...")
# Type Hinting: View 'api' as MarketDataProvider
market_api: MarketDataProvider = api
if isinstance(market_api, MarketDataProvider):
try:
# ohlcvs = market_api.get_ohlcv("005930", "20240101", "20240105")
# print(f"Received {len(ohlcvs)} records")
ohlcv = market_api.get_price("005930")
print(f"Received {ohlcv}")
exit()
# Example: Parsing Price objects
print(f"{'Date':<10} | {'Close':<10} | {'Volume':<10}")
print("-" * 36)
price: finestock.Price = ohlcv
# Accessing fields of the Price dataclass
print(f"{price.workday:<10} | {price.close:<10} | {price.volume:<10}")
except Exception as e:
logger.error(f"Market Data Error: {e}")
exit()
try:
# ohlcvs = market_api.get_ohlcv("005930", "20240101", "20240105")
# print(f"Received {len(ohlcvs)} records")
ohlcvs = market_api.get_ohlcv("005930", "20240101", "20240105")
print(f"Received {len(ohlcvs)} records")
# Example: Parsing Price objects
print(f"{'Date':<10} | {'Close':<10} | {'Volume':<10}")
print("-" * 36)
for price in ohlcvs:
price: finestock.Price = price
# Accessing fields of the Price dataclass
print(f"{price.workday:<10} | {price.close:<10} | {price.volume:<10}")
except Exception as e:
logger.error(f"Market Data Error: {e}")
try:
logger.info("Fetching Stock List (KOSPI)...")
# info_api = api # InfoProvider
if hasattr(market_api, 'get_stock_list'):
stocks = market_api.get_stock_list("0") # 0: KOSPI
print(f"Received {len(stocks)} stocks for KOSPI")
print(f"First 5 stocks:")
for s in stocks[:5]:
print(s)
except Exception as e:
logger.error(f"Stock List Error: {e}")
try:
logger.info("Fetching KOSDAQ Index List...")
# market_api is typed as MarketDataProvider if using interface, but get_index_list is specific to Kiwoom or extended interface
# Since market_api is actually the API instance, we can call it if available
if hasattr(market_api, 'get_index_list'):
indices = market_api.get_index_list("0") # 0: KOSPI
print(f"Received {len(indices)} indices for KOSPI")
print(f"First 5 indices:")
for idx in indices[:5]:
print(f"Code: {idx.code}, Name: {idx.name}, Market: {idx.market}, Group: {idx.group}")
except Exception as e:
logger.error(f"Index List Error: {e}")
try:
logger.info("Fetching KOSDAQ 150 Index Data...")
# KOSDAQ 150 Code: 150
ohlcvs = market_api.get_index("150", "20260201", "20260213")
print(f"Received {len(ohlcvs)} records for KOSDAQ 150")
print(f"{'Date':<10} | {'Close':<10} | {'Volume':<10}")
print("-" * 36)
for price in ohlcvs:
price: finestock.Price = price
print(f"{price.workday:<10} | {price.close:<10} | {price.volume:<10}")
except Exception as e:
logger.error(f"KOSDAQ 150 Error: {e}")
try:
logger.info("Fetching Orderbook for Samsung Electronics (005930)...")
orderbook = market_api.get_orderbook("005930")
if orderbook:
print(f"Orderbook for {orderbook.code}")
print(f"Total Buy: {orderbook.total_buy}, Total Sell: {orderbook.total_sell}")
print("Sell Side (Top 5):")
for h in orderbook.sell[:5]:
print(f" Price: {h.price}, Qty: {h.qty}")
print("Buy Side (Top 5):")
for h in orderbook.buy[:5]:
print(f" Price: {h.price}, Qty: {h.qty}")
else:
print("Orderbook empty or failed.")
except Exception as e:
logger.error(f"Orderbook Error: {e}")
# 4. Trading (Sync)
logger.info("Trading Example...")
trading_api: TradingProvider = api
if isinstance(trading_api, TradingProvider):
try:
# Example: Buy Samsung Electronics (005930) 1 share at Market Price
# trde_tp 3: Market Price, 0: Limit Price
# In do_order, price=0 means Market Price (trde_tp="3")
# Uncomment to execute
# logger.info("Placing Buy Order for 005930 (1 qty, Market Price)...")
#result = trading_api.do_order("005930", finestock.ORDER_FLAG.BUY, 0, 1)
#result = trading_api.do_order("005930", finestock.ORDER_FLAG.BUY, 173000, 1)
#result = trading_api.do_order("005930", finestock.ORDER_FLAG.BUY, 176000, 1)
result = trading_api.do_order_cancel("0157069", "005930", 1)
#result = trading_api.do_order("005930", finestock.ORDER_FLAG.SELL, 177200, 1)
print(f"Order Result: {result}")
#print("To place an order, uncomment the lines in the script.")
# Example: Cancel Order
# logger.info("Cancelling Order 0000140 for 005930 (1 qty)...")
# result = trading_api.do_order_cancel("0000140", "005930", 1)
# print(f"Cancel Result: {result}")
except Exception as e:
logger.error(f"Trading Error: {e}")
# 5. Account (Sync)
logger.info("Account Example...")
account_api: AccountProvider = api
if isinstance(account_api, AccountProvider):
try:
logger.info("Fetching Account Balance...")
balance = account_api.get_balance()
if balance:
print(f"Account: {balance.account_num}")
print(f"Deposit: {balance.deposit}")
print(f"Next Deposit: {balance.next_deposit}")
print(f"Pay Deposit: {balance.pay_deposit}")
else:
print("Failed to get balance.")
logger.info("Fetching Account Holdings...")
holds = account_api.get_holds()
print(f"Received {len(holds)} holdings")
if holds:
print(f"{'Code':<10} | {'Name':<20} | {'Qty':<10} | {'Eval':<15}")
print("-" * 60)
for h in holds:
print(f"{h.code:<10} | {h.name:<20} | {h.qty:<10} | {h.eval:<15}")
else:
print("No holdings or failed to fetch.")
except Exception as e:
logger.error(f"Account Error: {e}")
if __name__ == "__main__":
main()