Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
# KIS API Credentials
# Broker selection: kis or yfinance
BROKER=yfinance

# Trading mode: paper or live. yfinance supports paper only.
TRADING_MODE=paper
Comment thread
riroan marked this conversation as resolved.

# KIS API Credentials (required only when BROKER=kis)
KIS_APP_KEY=your_app_key_here
KIS_APP_SECRET=your_app_secret_here
KIS_ACCOUNT_NO=12345678-01

# Mode: real or paper
KIS_PAPER_TRADING=true

# KIS HTS ID — required for WebSocket execution-notice subscriptions.
# Find it in KIS HTS/MTS login screen (보통 영문+숫자 5~12자). Without this,
# the bot can place orders but cannot detect fills in real time and
Expand Down
46 changes: 39 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,17 @@ US 전용 스케줄러는 KST 기준으로 KIS가 지원하는 미국 주식 세
`.env` 파일 생성:

```env
# KIS API
# Broker selection: kis or yfinance
BROKER=yfinance

# Trading mode: paper or live. yfinance supports paper only.
TRADING_MODE=paper

# KIS API (BROKER=kis일 때 필요)
KIS_APP_KEY=your_app_key
KIS_APP_SECRET=your_app_secret
KIS_ACCOUNT_NO=your_account_number

# 모드: true=페이퍼트레이딩, false=실거래
KIS_PAPER_TRADING=true

# MySQL
MYSQL_ROOT_PASSWORD=your_root_password
MYSQL_DATABASE=quant
Expand Down Expand Up @@ -196,14 +199,43 @@ DB가 source of truth인 데이터:
- RSI 기간, 손절 %, 쿨다운 일수
- 매수/매도 레벨 및 비율

### yfinance paper mode (KIS 없이 실행)

KIS API 키 없이 미국/한국 종목을 paper trading으로 테스트하려면 `.env`에서:

```env
BROKER=yfinance
TRADING_MODE=paper
DASHBOARD_USERNAME=admin
DASHBOARD_PASSWORD=change_me
```

이 모드는 yfinance에서 시세/일봉 데이터를 가져오고, 주문은 로컬 paper 계좌에서 즉시 체결 처리합니다. 실제 주문은 발생하지 않습니다. Paper cash/positions/orders/fills 상태는 기본적으로 `data/yfinance_paper_state.json`에 저장되어 재시작 후에도 유지됩니다.

실행:

```bash
uv run python scripts/run_bot.py --web --web-port 9095
```

주의:

- yfinance 데이터는 지연/누락될 수 있어 실거래 판단용으로 쓰면 안 됩니다.
- 한국 종목은 yfinance suffix가 필요할 수 있습니다. 예: `005930.KS`, `091990.KQ`.
- suffix 없는 6자리 KRX 코드는 기본적으로 `.KS`로 조회합니다.
- yfinance broker는 live trading을 지원하지 않습니다.

### 페이퍼 트레이딩

`.env`에서 `KIS_PAPER_TRADING=true`로 설정하면:
- 실제 시세 조회 (KIS API)
- 주문은 가상 체결 (실제 돈 사용 안 함)
`.env`에서 `BROKER=yfinance`, `TRADING_MODE=paper`로 설정하면:
- yfinance 시세/일봉 데이터 사용
- 주문은 로컬 paper 계좌에서 가상 체결 (실제 돈 사용 안 함)
- paper 상태는 `data/yfinance_paper_state.json`에 유지
- 워밍업 없이 즉시 시작
- 대시보드에 `PAPER` 배지 표시

KIS 기반 paper trading을 쓰려면 `BROKER=kis`, `TRADING_MODE=paper`와 유효한 KIS API 키가 필요합니다.

## 테스트

```bash
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ dependencies = [
"pandas>=2.0",
"numpy>=1.24",
"pandas-ta>=0.4.71b0",
"yfinance>=0.2.40",
"yfinance>=0.2.50",

# Database
"sqlalchemy[asyncio]>=2.0",
Expand Down
42 changes: 4 additions & 38 deletions src/bot/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@

from src.core.events import EventBus
from src.core.types import Market
from src.broker.kis.client import KISBroker
from src.broker.base import Broker
from src.broker.factory import create_broker
from src.data.storage import Storage
from src.strategy.engine import StrategyEngine
from src.execution.order_manager import OrderManager
Expand Down Expand Up @@ -36,7 +37,7 @@ def __init__(self, config_dir: str = "config", warmup_hours: int = 0):
self.event_bus = EventBus()

self.storage: Storage | None = None
self.broker: KISBroker | None = None
self.broker: Broker | None = None
self.strategy_engine: StrategyEngine | None = None
self.order_manager: OrderManager | None = None
self.risk_manager: RiskManager | None = None
Expand Down Expand Up @@ -178,42 +179,7 @@ async def initialize(self) -> None:

async def _initialize_broker(self) -> None:
"""Initialize broker connection"""
kis_config = self.config.get_kis_config()
real_broker = KISBroker(
event_bus=self.event_bus,
app_key=kis_config["app_key"],
app_secret=kis_config["app_secret"],
account_no=kis_config["account_no"],
paper_trading=kis_config["paper_trading"],
hts_id=kis_config.get("hts_id", ""),
slippage_buffer=float(
self.config.get("trading.slippage_buffer_pct", 0.01)
),
)

# Use PaperBroker if KIS_PAPER_TRADING=true
if kis_config["paper_trading"]:
from src.broker.paper import PaperBroker
initial_cash_usd = Decimal(
str(self.config.get("trading.paper_cash_usd", 10000))
)
initial_cash_krw = Decimal(
str(self.config.get("trading.paper_cash_krw", 0))
)
self.broker = PaperBroker(
event_bus=self.event_bus,
real_broker=real_broker,
initial_cash_usd=initial_cash_usd,
initial_cash_krw=initial_cash_krw,
)
logger.info(
f"Paper trading mode | "
f"USD: ${initial_cash_usd:,.0f}, "
f"KRW: {initial_cash_krw:,.0f}"
)
else:
self.broker = real_broker

self.broker = create_broker(self.config, self.event_bus)
await self.broker.connect()

# Get account balances (populates dashboard.balance_usd/krw;
Expand Down
4 changes: 3 additions & 1 deletion src/broker/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from .base import Broker
from .kis.client import KISBroker
from .yfinance import YFinanceBroker

__all__ = ["KISBroker"]
__all__ = ["Broker", "KISBroker", "YFinanceBroker"]
65 changes: 65 additions & 0 deletions src/broker/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""Broker protocol shared by trading components."""

from decimal import Decimal
from typing import Protocol

from src.core.types import Bar, Market, Order, Position


class Broker(Protocol):
"""Async broker interface used by TradingBot, StrategyEngine, and OrderManager."""

paper_trading: bool

@property
def is_connected(self) -> bool:
"""Whether the broker is connected."""
...

@property
def account_no(self) -> str:
"""Account identifier for logs/dashboard."""
...

async def connect(self) -> None:
...

async def disconnect(self) -> None:
...

async def get_current_price(
self,
symbol: str,
market: Market = Market.KRX,
) -> Decimal:
...

async def get_historical_bars(
self,
symbol: str,
market: Market = Market.KRX,
days: int = 100,
) -> list[Bar]:
...

async def get_positions(
self,
market: Market,
) -> list[Position]:
...

async def get_account_balance(
self,
market: Market,
) -> dict:
...

async def submit_order(self, order: Order) -> str:
...

async def cancel_order(
self,
order_id: str,
market: Market,
) -> bool:
...
47 changes: 47 additions & 0 deletions src/broker/factory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""Broker factory for selecting runtime broker implementations."""

from decimal import Decimal

from src.broker.base import Broker
from src.broker.kis.client import KISBroker
from src.broker.paper import PaperBroker
from src.broker.yfinance import YFinanceBroker
from src.core.events import EventBus
from src.utils.config import Config


def create_broker(config: Config, event_bus: EventBus) -> Broker:
"""Create the configured broker implementation."""
broker_name = config.get_broker_name()
trading_mode = config.get_trading_mode()

if broker_name == "yfinance":
if trading_mode != "paper":
raise ValueError("BROKER=yfinance only supports TRADING_MODE=paper")
return YFinanceBroker(
event_bus=event_bus,
initial_cash_usd=Decimal(str(config.get("trading.paper_cash_usd", 10000))),
initial_cash_krw=Decimal(str(config.get("trading.paper_cash_krw", 0))),
)

if broker_name == "kis":
kis_config = config.get_kis_config()
real_broker = KISBroker(
event_bus=event_bus,
app_key=kis_config["app_key"],
app_secret=kis_config["app_secret"],
account_no=kis_config["account_no"],
paper_trading=kis_config["paper_trading"],
hts_id=kis_config.get("hts_id", ""),
slippage_buffer=float(config.get("trading.slippage_buffer_pct", 0.01)),
)
if kis_config["paper_trading"]:
return PaperBroker(
event_bus=event_bus,
real_broker=real_broker,
initial_cash_usd=Decimal(str(config.get("trading.paper_cash_usd", 10000))),
initial_cash_krw=Decimal(str(config.get("trading.paper_cash_krw", 0))),
)
return real_broker

raise ValueError(f"Unsupported BROKER: {broker_name}")
Loading
Loading