update auto trading client
This commit is contained in:
parent
692a55dfd5
commit
8cd0a4248c
99
cli/main.py
99
cli/main.py
|
|
@ -1112,5 +1112,104 @@ def run_swing_pipeline():
|
|||
console.print(f"\n[green]\u2713 Reports saved:[/green] {base_path.resolve()}")
|
||||
|
||||
|
||||
# ─── Phase 4: Order Execution (선택적 주문 실행) ───
|
||||
if buy_signals and config.get("broker_enabled"):
|
||||
console.print()
|
||||
console.print(Rule("Phase 4: Order Execution (주문 실행)", style="bold red"))
|
||||
|
||||
is_paper = config.get("kiwoom_is_paper", True)
|
||||
dry_run = config.get("broker_dry_run", True)
|
||||
|
||||
mode_label = "모의투자" if is_paper else "실전투자"
|
||||
run_label = "DRY RUN (검증만)" if dry_run else "실제 주문"
|
||||
console.print(f"[dim]Mode: {mode_label} / {run_label}[/dim]\n")
|
||||
|
||||
order_table = Table(
|
||||
title="Pending Orders",
|
||||
show_header=True,
|
||||
header_style="bold",
|
||||
box=box.ROUNDED,
|
||||
)
|
||||
order_table.add_column("Ticker", justify="center", width=10)
|
||||
order_table.add_column("Name", width=20)
|
||||
order_table.add_column("Action", justify="center", width=8)
|
||||
order_table.add_column("Entry Price", justify="right", width=12)
|
||||
order_table.add_column("Stop Loss", justify="right", width=12)
|
||||
order_table.add_column("Take Profit", justify="right", width=12)
|
||||
|
||||
for r in buy_signals:
|
||||
sig = r["swing_signal"]
|
||||
entry_str = f"{int(sig['entry_price']):,}" if sig.get("entry_price") else "-"
|
||||
sl_str = f"{int(sig['stop_loss']):,}" if sig.get("stop_loss") else "-"
|
||||
tp_str = f"{int(sig['take_profit']):,}" if sig.get("take_profit") else "-"
|
||||
order_table.add_row(
|
||||
r["ticker"], r["name"], "[green]BUY[/green]",
|
||||
entry_str, sl_str, tp_str,
|
||||
)
|
||||
|
||||
console.print(order_table)
|
||||
|
||||
execute = typer.prompt(
|
||||
f"\n{len(buy_signals)}건 주문을 실행하시겠습니까? ({mode_label}/{run_label})",
|
||||
default="N",
|
||||
).strip().upper()
|
||||
|
||||
if execute in ("Y", "YES"):
|
||||
from tradingagents.broker.kiwoom_client import KiwoomClient
|
||||
from tradingagents.broker.executor import BrokerExecutor
|
||||
from tradingagents.portfolio import load_portfolio, save_portfolio
|
||||
|
||||
client = KiwoomClient(
|
||||
app_key=config["kiwoom_app_key"],
|
||||
app_secret=config["kiwoom_app_secret"],
|
||||
account_no=config["kiwoom_account_no"],
|
||||
is_paper=is_paper,
|
||||
)
|
||||
portfolio = load_portfolio(
|
||||
portfolio_id=config.get("portfolio_id", "default"),
|
||||
results_dir=config.get("results_dir", "./results"),
|
||||
defaults={
|
||||
"total_capital": config.get("total_capital", 100_000_000),
|
||||
"max_positions": config.get("max_positions", 5),
|
||||
"max_position_pct": config.get("max_position_pct", 0.20),
|
||||
},
|
||||
)
|
||||
executor = BrokerExecutor(client=client, portfolio=portfolio, dry_run=dry_run)
|
||||
|
||||
exec_results = []
|
||||
for r in buy_signals:
|
||||
with console.status(f"[bold cyan]Executing {r['ticker']}...[/bold cyan]"):
|
||||
result = executor.execute_signal(
|
||||
ticker=r["ticker"],
|
||||
swing_signal=r["swing_signal"],
|
||||
market=selections["market"],
|
||||
)
|
||||
if result:
|
||||
exec_results.append(result)
|
||||
status = result.get("status", "unknown")
|
||||
color = {"filled": "green", "dry_run": "yellow", "rejected": "red", "failed": "red"}.get(status, "dim")
|
||||
msg = f" [{color}]{r['ticker']}: {status}[/{color}]"
|
||||
if result.get("quantity"):
|
||||
msg += f" (x{result['quantity']} @ {result.get('price', 0):,.0f})"
|
||||
if result.get("broker_msg"):
|
||||
msg += f" - {result['broker_msg']}"
|
||||
if result.get("reason"):
|
||||
msg += f" - {result['reason']}"
|
||||
console.print(msg)
|
||||
|
||||
save_path = save_portfolio(
|
||||
portfolio,
|
||||
results_dir=config.get("results_dir", "./results"),
|
||||
)
|
||||
console.print(f"\n[green]Portfolio saved:[/green] {save_path}")
|
||||
console.print(f"\n{portfolio.summary()}")
|
||||
|
||||
elif buy_signals and not config.get("broker_enabled"):
|
||||
console.print(
|
||||
"\n[dim]Tip: 주문 실행을 원하시면 config에서 broker_enabled=True 설정 후 "
|
||||
"KIWOOM_APP_KEY, KIWOOM_APP_SECRET, KIWOOM_ACCOUNT_NO 환경변수를 설정하세요.[/dim]"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
from tradingagents.broker.kiwoom_client import KiwoomClient
|
||||
from tradingagents.broker.executor import BrokerExecutor
|
||||
|
||||
__all__ = ["KiwoomClient", "BrokerExecutor"]
|
||||
|
|
@ -0,0 +1,260 @@
|
|||
"""Broker executor: bridges trading agent decisions to actual orders.
|
||||
|
||||
Converts swing_signal dicts from the trading pipeline into
|
||||
real orders via the Kiwoom REST API.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import math
|
||||
from typing import Optional
|
||||
|
||||
from tradingagents.broker.kiwoom_client import KiwoomClient
|
||||
from tradingagents.portfolio.state import Order, PortfolioState
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BrokerExecutor:
|
||||
"""Executes trading decisions through Kiwoom Securities API."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
client: KiwoomClient,
|
||||
portfolio: PortfolioState,
|
||||
dry_run: bool = False,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
client: Authenticated KiwoomClient instance
|
||||
portfolio: Current portfolio state
|
||||
dry_run: If True, validate but don't send orders to broker
|
||||
"""
|
||||
self.client = client
|
||||
self.portfolio = portfolio
|
||||
self.dry_run = dry_run
|
||||
|
||||
def execute_signal(
|
||||
self,
|
||||
ticker: str,
|
||||
swing_signal: dict,
|
||||
market: str = "KRX",
|
||||
) -> Optional[dict]:
|
||||
"""Execute a swing trading signal.
|
||||
|
||||
Args:
|
||||
ticker: Stock code (e.g. '005930')
|
||||
swing_signal: Dict from signal_processing with action, entry_price,
|
||||
stop_loss, take_profit, position_size_pct, etc.
|
||||
market: 'KRX' or 'US'
|
||||
|
||||
Returns:
|
||||
Execution result dict or None if skipped.
|
||||
"""
|
||||
action = swing_signal.get("action", "PASS").upper()
|
||||
|
||||
if action == "BUY":
|
||||
return self._execute_buy(ticker, swing_signal, market)
|
||||
elif action == "SELL":
|
||||
return self._execute_sell(ticker, swing_signal, market)
|
||||
else:
|
||||
logger.info(f"PASS for {ticker}: {swing_signal.get('rationale', '')}")
|
||||
return None
|
||||
|
||||
def _execute_buy(self, ticker: str, signal: dict, market: str) -> dict:
|
||||
"""Execute a BUY order."""
|
||||
# Validate portfolio capacity
|
||||
if not self.portfolio.can_add_position():
|
||||
logger.warning(f"Cannot buy {ticker}: max positions reached ({self.portfolio.max_positions})")
|
||||
return {"status": "rejected", "reason": "max_positions_reached", "ticker": ticker}
|
||||
|
||||
if self.portfolio.has_position(ticker):
|
||||
logger.warning(f"Cannot buy {ticker}: already holding position")
|
||||
return {"status": "rejected", "reason": "already_holding", "ticker": ticker}
|
||||
|
||||
# Calculate position size
|
||||
position_size_pct = signal.get("position_size_pct") or 0.10
|
||||
position_size_pct = min(position_size_pct, self.portfolio.max_position_pct)
|
||||
max_capital = self.portfolio.total_capital * position_size_pct
|
||||
available = self.portfolio.available_capital
|
||||
|
||||
capital_to_use = min(max_capital, available)
|
||||
if capital_to_use <= 0:
|
||||
logger.warning(f"Cannot buy {ticker}: insufficient capital")
|
||||
return {"status": "rejected", "reason": "insufficient_capital", "ticker": ticker}
|
||||
|
||||
# Get current price from broker
|
||||
current_price = self.client.get_current_price_value(ticker)
|
||||
entry_price = signal.get("entry_price") or current_price
|
||||
|
||||
if not entry_price or entry_price <= 0:
|
||||
logger.warning(f"Cannot buy {ticker}: no valid price")
|
||||
return {"status": "rejected", "reason": "no_price", "ticker": ticker}
|
||||
|
||||
# Calculate quantity
|
||||
quantity = math.floor(capital_to_use / entry_price)
|
||||
if quantity <= 0:
|
||||
logger.warning(f"Cannot buy {ticker}: price {entry_price} exceeds available capital")
|
||||
return {"status": "rejected", "reason": "price_too_high", "ticker": ticker}
|
||||
|
||||
# Build order
|
||||
order = Order(
|
||||
action="BUY",
|
||||
ticker=ticker,
|
||||
market=market,
|
||||
price=entry_price,
|
||||
stop_loss=signal.get("stop_loss") or entry_price * 0.95,
|
||||
take_profit=signal.get("take_profit") or entry_price * 1.15,
|
||||
quantity=quantity,
|
||||
position_size_pct=position_size_pct,
|
||||
max_hold_days=signal.get("max_hold_days") or 20,
|
||||
rationale=signal.get("rationale", ""),
|
||||
)
|
||||
|
||||
result = {
|
||||
"status": "pending",
|
||||
"ticker": ticker,
|
||||
"action": "BUY",
|
||||
"quantity": quantity,
|
||||
"price": entry_price,
|
||||
"total_cost": quantity * entry_price,
|
||||
"position_size_pct": position_size_pct,
|
||||
"stop_loss": order.stop_loss,
|
||||
"take_profit": order.take_profit,
|
||||
}
|
||||
|
||||
if self.dry_run:
|
||||
result["status"] = "dry_run"
|
||||
logger.info(
|
||||
f"[DRY RUN] BUY {ticker} x{quantity} @ {entry_price:,.0f} "
|
||||
f"(SL: {order.stop_loss:,.0f} / TP: {order.take_profit:,.0f})"
|
||||
)
|
||||
# Still update portfolio state for tracking
|
||||
self.portfolio.add_position(order)
|
||||
return result
|
||||
|
||||
# Execute via Kiwoom API
|
||||
try:
|
||||
broker_result = self.client.buy(
|
||||
stock_code=ticker,
|
||||
quantity=quantity,
|
||||
price=0, # Market order
|
||||
order_type="market",
|
||||
)
|
||||
|
||||
return_code = int(broker_result.get("return_code", -1))
|
||||
|
||||
if return_code == 0:
|
||||
result["status"] = "filled"
|
||||
result["order_no"] = broker_result.get("ord_no")
|
||||
result["broker_msg"] = broker_result.get("return_msg")
|
||||
self.portfolio.add_position(order)
|
||||
logger.info(f"BUY filled: {ticker} x{quantity} @ market (order #{result['order_no']})")
|
||||
else:
|
||||
result["status"] = "failed"
|
||||
result["broker_msg"] = broker_result.get("return_msg", "Unknown error")
|
||||
logger.error(f"BUY failed for {ticker}: {result['broker_msg']}")
|
||||
|
||||
except Exception as e:
|
||||
result["status"] = "error"
|
||||
result["error"] = str(e)
|
||||
logger.error(f"BUY error for {ticker}: {e}")
|
||||
|
||||
return result
|
||||
|
||||
def _execute_sell(self, ticker: str, signal: dict, market: str) -> dict:
|
||||
"""Execute a SELL order."""
|
||||
if not self.portfolio.has_position(ticker):
|
||||
logger.warning(f"Cannot sell {ticker}: no position held")
|
||||
return {"status": "rejected", "reason": "no_position", "ticker": ticker}
|
||||
|
||||
position = self.portfolio.positions[ticker]
|
||||
quantity = position.quantity
|
||||
|
||||
# Get current price
|
||||
current_price = self.client.get_current_price_value(ticker)
|
||||
exit_price = current_price or signal.get("entry_price") or position.current_price
|
||||
|
||||
result = {
|
||||
"status": "pending",
|
||||
"ticker": ticker,
|
||||
"action": "SELL",
|
||||
"quantity": quantity,
|
||||
"price": exit_price,
|
||||
"entry_price": position.entry_price,
|
||||
"pnl": (exit_price - position.entry_price) * quantity,
|
||||
"pnl_pct": (exit_price / position.entry_price - 1) * 100 if position.entry_price else 0,
|
||||
}
|
||||
|
||||
if self.dry_run:
|
||||
result["status"] = "dry_run"
|
||||
logger.info(
|
||||
f"[DRY RUN] SELL {ticker} x{quantity} @ {exit_price:,.0f} "
|
||||
f"(PnL: {result['pnl']:+,.0f} / {result['pnl_pct']:+.1f}%)"
|
||||
)
|
||||
self.portfolio.close_position(ticker, exit_price, "agent_decision")
|
||||
return result
|
||||
|
||||
# Execute via Kiwoom API
|
||||
try:
|
||||
broker_result = self.client.sell(
|
||||
stock_code=ticker,
|
||||
quantity=quantity,
|
||||
price=0,
|
||||
order_type="market",
|
||||
)
|
||||
|
||||
return_code = int(broker_result.get("return_code", -1))
|
||||
|
||||
if return_code == 0:
|
||||
result["status"] = "filled"
|
||||
result["order_no"] = broker_result.get("ord_no")
|
||||
result["broker_msg"] = broker_result.get("return_msg")
|
||||
self.portfolio.close_position(ticker, exit_price, "agent_decision")
|
||||
logger.info(f"SELL filled: {ticker} x{quantity} @ market (order #{result['order_no']})")
|
||||
else:
|
||||
result["status"] = "failed"
|
||||
result["broker_msg"] = broker_result.get("return_msg", "Unknown error")
|
||||
logger.error(f"SELL failed for {ticker}: {result['broker_msg']}")
|
||||
|
||||
except Exception as e:
|
||||
result["status"] = "error"
|
||||
result["error"] = str(e)
|
||||
logger.error(f"SELL error for {ticker}: {e}")
|
||||
|
||||
return result
|
||||
|
||||
def check_exit_conditions(self, trade_date: str) -> list[dict]:
|
||||
"""Check all positions for exit conditions (SL/TP/max hold).
|
||||
|
||||
Returns list of execution results for positions that were closed.
|
||||
"""
|
||||
results = []
|
||||
|
||||
for ticker, position in list(self.portfolio.positions.items()):
|
||||
current_price = self.client.get_current_price_value(ticker)
|
||||
if not current_price:
|
||||
continue
|
||||
|
||||
position.current_price = current_price
|
||||
exit_reason = None
|
||||
|
||||
if current_price <= position.stop_loss:
|
||||
exit_reason = "stop_loss"
|
||||
elif current_price >= position.take_profit:
|
||||
exit_reason = "take_profit"
|
||||
elif position.days_held >= position.max_hold_days:
|
||||
exit_reason = "max_hold"
|
||||
|
||||
if exit_reason:
|
||||
logger.info(
|
||||
f"Exit trigger [{exit_reason}] for {ticker}: "
|
||||
f"price={current_price:,.0f} SL={position.stop_loss:,.0f} "
|
||||
f"TP={position.take_profit:,.0f} days={position.days_held}"
|
||||
)
|
||||
signal = {"action": "SELL", "entry_price": current_price}
|
||||
result = self._execute_sell(ticker, signal, position.market)
|
||||
if result:
|
||||
result["exit_reason"] = exit_reason
|
||||
results.append(result)
|
||||
|
||||
return results
|
||||
|
|
@ -0,0 +1,244 @@
|
|||
"""Kiwoom Securities REST API client.
|
||||
|
||||
Supports both 실전투자 (real) and 모의투자 (paper trading).
|
||||
API docs: https://openapi.kiwoom.com
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
import requests
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Base URLs
|
||||
REAL_BASE_URL = "https://api.kiwoom.com"
|
||||
PAPER_BASE_URL = "https://mockapi.kiwoom.com"
|
||||
|
||||
|
||||
class KiwoomClient:
|
||||
"""Kiwoom Securities REST API client."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
app_key: str,
|
||||
app_secret: str,
|
||||
account_no: str,
|
||||
is_paper: bool = True,
|
||||
):
|
||||
self.app_key = app_key
|
||||
self.app_secret = app_secret
|
||||
self.account_no = account_no
|
||||
self.is_paper = is_paper
|
||||
self.base_url = PAPER_BASE_URL if is_paper else REAL_BASE_URL
|
||||
|
||||
self._token: Optional[str] = None
|
||||
self._token_expires: Optional[datetime] = None
|
||||
|
||||
# ── Authentication ──────────────────────────────────────────
|
||||
|
||||
def _ensure_token(self) -> str:
|
||||
"""Get valid access token, refreshing if needed."""
|
||||
if self._token and self._token_expires and datetime.now() < self._token_expires:
|
||||
return self._token
|
||||
|
||||
resp = requests.post(
|
||||
f"{self.base_url}/oauth2/token",
|
||||
json={
|
||||
"grant_type": "client_credentials",
|
||||
"appkey": self.app_key,
|
||||
"secretkey": self.app_secret,
|
||||
},
|
||||
headers={"Content-Type": "application/json;charset=UTF-8"},
|
||||
timeout=10,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
|
||||
self._token = data["token"]
|
||||
self._token_expires = datetime.strptime(data["expires_dt"], "%Y-%m-%d %H:%M:%S")
|
||||
logger.info(f"Kiwoom token acquired, expires: {data['expires_dt']}")
|
||||
return self._token
|
||||
|
||||
def _headers(self, api_id: str, cont_yn: str = "N", next_key: str = "") -> dict:
|
||||
"""Build request headers."""
|
||||
token = self._ensure_token()
|
||||
headers = {
|
||||
"Content-Type": "application/json;charset=UTF-8",
|
||||
"authorization": f"Bearer {token}",
|
||||
"api-id": api_id,
|
||||
}
|
||||
if cont_yn:
|
||||
headers["cont-yn"] = cont_yn
|
||||
if next_key:
|
||||
headers["next-key"] = next_key
|
||||
return headers
|
||||
|
||||
def _post(self, path: str, api_id: str, body: dict, **kwargs) -> dict:
|
||||
"""Make authenticated POST request."""
|
||||
headers = self._headers(api_id, **kwargs)
|
||||
resp = requests.post(
|
||||
f"{self.base_url}{path}",
|
||||
json=body,
|
||||
headers=headers,
|
||||
timeout=15,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
result = resp.json()
|
||||
|
||||
if result.get("return_code") and int(result["return_code"]) != 0:
|
||||
logger.error(f"Kiwoom API error [{api_id}]: {result.get('return_msg')}")
|
||||
|
||||
return result
|
||||
|
||||
# ── Orders ──────────────────────────────────────────────────
|
||||
|
||||
def buy(
|
||||
self,
|
||||
stock_code: str,
|
||||
quantity: int,
|
||||
price: int = 0,
|
||||
order_type: str = "market",
|
||||
) -> dict:
|
||||
"""Place a buy order.
|
||||
|
||||
Args:
|
||||
stock_code: 6-digit stock code (e.g. '005930')
|
||||
quantity: Number of shares
|
||||
price: Limit price (0 for market order)
|
||||
order_type: 'market' or 'limit'
|
||||
|
||||
Returns:
|
||||
Order response with ord_no, return_code, return_msg
|
||||
"""
|
||||
body = {
|
||||
"dmst_stex_tp": "SOR",
|
||||
"stk_cd": stock_code.zfill(6),
|
||||
"ord_qty": str(quantity),
|
||||
"ord_uv": str(price),
|
||||
"trde_tp": "3" if order_type == "market" else "0",
|
||||
}
|
||||
result = self._post("/api/dostk/ordr", "kt10000", body)
|
||||
logger.info(
|
||||
f"BUY {stock_code} x{quantity} @ {'시장가' if order_type == 'market' else price}: "
|
||||
f"{result.get('return_msg', '')}"
|
||||
)
|
||||
return result
|
||||
|
||||
def sell(
|
||||
self,
|
||||
stock_code: str,
|
||||
quantity: int,
|
||||
price: int = 0,
|
||||
order_type: str = "market",
|
||||
) -> dict:
|
||||
"""Place a sell order.
|
||||
|
||||
Args:
|
||||
stock_code: 6-digit stock code
|
||||
quantity: Number of shares
|
||||
price: Limit price (0 for market order)
|
||||
order_type: 'market' or 'limit'
|
||||
|
||||
Returns:
|
||||
Order response with ord_no, return_code, return_msg
|
||||
"""
|
||||
body = {
|
||||
"dmst_stex_tp": "SOR",
|
||||
"stk_cd": stock_code.zfill(6),
|
||||
"ord_qty": str(quantity),
|
||||
"ord_uv": str(price),
|
||||
"trde_tp": "3" if order_type == "market" else "0",
|
||||
}
|
||||
result = self._post("/api/dostk/ordr", "kt10001", body)
|
||||
logger.info(
|
||||
f"SELL {stock_code} x{quantity} @ {'시장가' if order_type == 'market' else price}: "
|
||||
f"{result.get('return_msg', '')}"
|
||||
)
|
||||
return result
|
||||
|
||||
def cancel_order(self, original_order_no: str, stock_code: str, quantity: int) -> dict:
|
||||
"""Cancel an existing order."""
|
||||
body = {
|
||||
"dmst_stex_tp": "SOR",
|
||||
"stk_cd": stock_code.zfill(6),
|
||||
"ord_qty": str(quantity),
|
||||
"orgn_ord_no": original_order_no,
|
||||
}
|
||||
return self._post("/api/dostk/ordr", "kt10003", body)
|
||||
|
||||
# ── Account / Balance ───────────────────────────────────────
|
||||
|
||||
def get_account_no(self) -> str:
|
||||
"""Retrieve account number from API."""
|
||||
result = self._post("/api/dostk/acnt", "ka00001", {})
|
||||
acct = result.get("acctNo", self.account_no)
|
||||
logger.info(f"Account number: {acct}")
|
||||
return acct
|
||||
|
||||
def get_deposit(self) -> dict:
|
||||
"""Get deposit/cash balance details (예수금상세현황).
|
||||
|
||||
Returns:
|
||||
Dict with deposit info including available cash.
|
||||
"""
|
||||
return self._post("/api/dostk/acnt", "kt00001", {})
|
||||
|
||||
def get_balance(self) -> dict:
|
||||
"""Get account valuation and holdings (계좌평가현황).
|
||||
|
||||
Returns:
|
||||
Dict with total valuation, P&L, and individual holdings.
|
||||
"""
|
||||
return self._post("/api/dostk/acnt", "kt00004", {})
|
||||
|
||||
def get_holdings(self) -> dict:
|
||||
"""Get filled/settled positions (체결잔고).
|
||||
|
||||
Returns:
|
||||
Dict with list of current stock holdings.
|
||||
"""
|
||||
return self._post("/api/dostk/acnt", "kt00005", {})
|
||||
|
||||
# ── Market Data ─────────────────────────────────────────────
|
||||
|
||||
def get_current_price(self, stock_code: str) -> dict:
|
||||
"""Get current price and basic stock info (주식기본정보).
|
||||
|
||||
Args:
|
||||
stock_code: 6-digit stock code
|
||||
|
||||
Returns:
|
||||
Dict with current price, volume, bid/ask, etc.
|
||||
"""
|
||||
body = {"stk_cd": stock_code.zfill(6)}
|
||||
return self._post("/api/dostk/mrkcond", "ka10001", body)
|
||||
|
||||
def get_orderbook(self, stock_code: str) -> dict:
|
||||
"""Get order book / bid-ask data (호가).
|
||||
|
||||
Args:
|
||||
stock_code: 6-digit stock code
|
||||
|
||||
Returns:
|
||||
Dict with bid/ask prices and volumes.
|
||||
"""
|
||||
body = {"stk_cd": stock_code.zfill(6)}
|
||||
return self._post("/api/dostk/mrkcond", "ka10004", body)
|
||||
|
||||
# ── Helpers ──────────────────────────────────────────────────
|
||||
|
||||
def get_current_price_value(self, stock_code: str) -> Optional[int]:
|
||||
"""Get just the current price as an integer."""
|
||||
try:
|
||||
result = self.get_current_price(stock_code)
|
||||
# Price field varies; try common field names
|
||||
for field in ("cur_prc", "stk_prpr", "현재가"):
|
||||
if field in result:
|
||||
return abs(int(str(result[field]).replace(",", "")))
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to get price for {stock_code}: {e}")
|
||||
return None
|
||||
|
|
@ -34,6 +34,13 @@ DEFAULT_CONFIG = {
|
|||
"screening_max_candidates": 5,
|
||||
"us_universe": "sp500", # "sp500", "nasdaq100", or "custom"
|
||||
"custom_watchlist": [], # custom ticker list for manual universe
|
||||
# Broker settings (Kiwoom Securities REST API)
|
||||
"broker_enabled": False, # True to enable order execution
|
||||
"broker_dry_run": True, # True = validate only, False = real orders
|
||||
"kiwoom_app_key": os.getenv("KIWOOM_APP_KEY", ""),
|
||||
"kiwoom_app_secret": os.getenv("KIWOOM_APP_SECRET", ""),
|
||||
"kiwoom_account_no": os.getenv("KIWOOM_ACCOUNT_NO", ""),
|
||||
"kiwoom_is_paper": True, # True = 모의투자, False = 실전투자
|
||||
# Data vendor configuration
|
||||
"data_vendors": {
|
||||
"core_stock_apis": "yfinance",
|
||||
|
|
|
|||
Loading…
Reference in New Issue