수정사항 반영
This commit is contained in:
@@ -33,6 +33,18 @@ logger = logging.getLogger(__name__)
|
||||
# 한국 시간대
|
||||
KST = pytz.timezone('Asia/Seoul')
|
||||
|
||||
# 해외 거래소 코드 목록
|
||||
OVERSEAS_MARKETS = {"NASD", "NYSE", "AMEX", "TSEX", "SEHK", "SHAA", "SZAA", "HASE", "VNSE"}
|
||||
|
||||
def _is_domestic(stock_code: str, market: str = None) -> bool:
|
||||
"""국내 종목 여부 판별.
|
||||
market 컬럼이 있으면 우선 사용, 없으면 종목코드 패턴으로 폴백.
|
||||
"""
|
||||
if market:
|
||||
return market not in OVERSEAS_MARKETS
|
||||
# 폴백: 6자리 숫자이면 국내(KRX)
|
||||
return stock_code.isdigit() and len(stock_code) == 6
|
||||
|
||||
# Request/Response Models
|
||||
class SellRequest(BaseModel):
|
||||
stock_code: str
|
||||
@@ -144,12 +156,42 @@ class PortfolioCache:
|
||||
self.next_retry_at = None
|
||||
self.last_sync_success = True
|
||||
|
||||
def cache_summary(self, balance_data: dict = None):
|
||||
def cache_summary(self, balance_data: dict = None, overseas_balance_data: dict = None, exchange_rate: float = 0.0):
|
||||
"""포트폴리오 요약 캐시 업데이트.
|
||||
|
||||
Args:
|
||||
balance_data: 국내 잔고 KIS API 응답
|
||||
overseas_balance_data: 해외 잔고 KIS API 응답
|
||||
exchange_rate: USD/KRW 환율 (해외 종목 원화 환산용, 0이면 미제공)
|
||||
"""
|
||||
holdings = self.holdings
|
||||
|
||||
def pick_float(d: dict, keys: list) -> float:
|
||||
for k in keys:
|
||||
if k in d and d.get(k) not in (None, ""):
|
||||
try:
|
||||
return float(d[k])
|
||||
except Exception:
|
||||
continue
|
||||
return 0.0
|
||||
|
||||
total_stocks = len(holdings)
|
||||
total_value = sum(float(h.current_price or 0) * h.quantity for h in holdings)
|
||||
|
||||
# 국내/해외 분리하여 평가금액 계산
|
||||
domestic_value = sum(
|
||||
float(h.current_price or 0) * h.quantity
|
||||
for h in holdings if _is_domestic(h.stock_code, getattr(h, "market", None))
|
||||
)
|
||||
overseas_value_usd = sum(
|
||||
float(h.current_price or 0) * h.quantity
|
||||
for h in holdings if not _is_domestic(h.stock_code, getattr(h, "market", None))
|
||||
)
|
||||
# 해외 원화 환산 (환율 미제공 시 overseas_balance_data output2의 tot_evlu_amt 사용)
|
||||
overseas_value_krw = overseas_value_usd * exchange_rate if exchange_rate > 0 else 0.0
|
||||
|
||||
total_value = domestic_value + overseas_value_krw
|
||||
|
||||
total_investment = sum(float(h.average_price or 0) * h.quantity for h in holdings)
|
||||
# 평가손익은 현재가 기준으로 직접 계산 (KIS evlu_pfls_amt는 지연될 수 있음)
|
||||
total_profit_loss = total_value - total_investment
|
||||
total_profit_loss_rate = (total_profit_loss / total_investment * 100) if total_investment > 0 else 0
|
||||
|
||||
@@ -168,15 +210,6 @@ class PortfolioCache:
|
||||
if balance_data and "output2" in balance_data:
|
||||
output2 = balance_data["output2"][0] if isinstance(balance_data["output2"], list) else balance_data["output2"]
|
||||
|
||||
def pick_float(d: dict, keys: list[str]) -> float:
|
||||
for k in keys:
|
||||
if k in d and d.get(k) not in (None, ""):
|
||||
try:
|
||||
return float(d.get(k))
|
||||
except Exception:
|
||||
continue
|
||||
return 0.0
|
||||
|
||||
available_cash = pick_float(output2, [
|
||||
"dnca_tot_amt",
|
||||
"d2_depo_amt",
|
||||
@@ -189,7 +222,6 @@ class PortfolioCache:
|
||||
if kis_total_assets > 0:
|
||||
total_assets = kis_total_assets
|
||||
calculated_assets = total_value + available_cash
|
||||
|
||||
if abs(kis_total_assets - calculated_assets) > 1000:
|
||||
logger.debug(
|
||||
f"💰 총자산 - KIS API: {kis_total_assets:,.0f}원, "
|
||||
@@ -214,12 +246,23 @@ class PortfolioCache:
|
||||
# D+2 정산액 및 기타 상세 정보 추출
|
||||
d2_settlement = 0.0
|
||||
stock_value_from_api = 0.0
|
||||
|
||||
if balance_data and "output2" in balance_data:
|
||||
output2 = balance_data["output2"][0] if isinstance(balance_data["output2"], list) else balance_data["output2"]
|
||||
d2_settlement = pick_float(output2, ["nxdy_excc_amt", "d2_auto_rdpt_amt"])
|
||||
stock_value_from_api = pick_float(output2, ["scts_evlu_amt", "evlu_amt_smtl_amt"])
|
||||
|
||||
# 해외 잔고 합산 (원화 환산 총자산에 포함)
|
||||
overseas_total_krw = 0.0
|
||||
if overseas_balance_data and "output2" in overseas_balance_data:
|
||||
ovrs_out2 = overseas_balance_data["output2"]
|
||||
if isinstance(ovrs_out2, list):
|
||||
ovrs_out2 = ovrs_out2[0] if ovrs_out2 else {}
|
||||
# tot_evlu_amt: 해외주식 평가금액 합계 (원화)
|
||||
overseas_total_krw = pick_float(ovrs_out2, ["tot_evlu_amt", "frcr_evlu_tota"])
|
||||
if overseas_total_krw > 0:
|
||||
total_assets += overseas_total_krw
|
||||
logger.debug(f"💵 해외 잔고 합산: {overseas_total_krw:,.0f}원")
|
||||
|
||||
self.summary = {
|
||||
"total_stocks": total_stocks,
|
||||
"total_value": total_value,
|
||||
@@ -229,7 +272,11 @@ class PortfolioCache:
|
||||
"available_cash": available_cash,
|
||||
"total_assets": total_assets,
|
||||
"d2_settlement": d2_settlement,
|
||||
"stock_value_from_api": stock_value_from_api
|
||||
"stock_value_from_api": stock_value_from_api,
|
||||
# 해외 관련 추가 필드
|
||||
"overseas_value_usd": overseas_value_usd,
|
||||
"overseas_value_krw": overseas_value_krw if exchange_rate > 0 else overseas_total_krw,
|
||||
"exchange_rate": exchange_rate,
|
||||
}
|
||||
|
||||
portfolio_cache = PortfolioCache()
|
||||
@@ -247,6 +294,7 @@ class PortfolioResponse(BaseModel):
|
||||
current_price: Decimal = None
|
||||
profit_loss: Decimal = None
|
||||
profit_loss_rate: Decimal = None
|
||||
market: str = "KRX"
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
@@ -305,7 +353,18 @@ async def sync_portfolio(db: AsyncSession = Depends(get_db), force: bool = True)
|
||||
try:
|
||||
portfolio_cache.last_attempt = datetime.now()
|
||||
try:
|
||||
balance_data = await korea_investment_api.get_balance()
|
||||
# 국내·해외 잔고 병렬 조회
|
||||
balance_data, overseas_balance_data = await asyncio.gather(
|
||||
korea_investment_api.get_balance(),
|
||||
korea_investment_api.get_overseas_balance(),
|
||||
return_exceptions=True,
|
||||
)
|
||||
if isinstance(balance_data, Exception):
|
||||
logger.warning(f"⚠️ 국내 잔고 조회 실패: {balance_data}")
|
||||
balance_data = {}
|
||||
if isinstance(overseas_balance_data, Exception):
|
||||
logger.warning(f"⚠️ 해외 잔고 조회 실패: {overseas_balance_data}")
|
||||
overseas_balance_data = {}
|
||||
|
||||
# 디버깅: KIS API 전체 응답 로깅
|
||||
logger.info(f"🔍 KIS API balance response keys: {balance_data.keys() if balance_data else 'None'}")
|
||||
@@ -430,6 +489,7 @@ async def sync_portfolio(db: AsyncSession = Depends(get_db), force: bool = True)
|
||||
h.current_price = current_price
|
||||
h.profit_loss = profit_loss
|
||||
h.profit_loss_rate = profit_loss_rate
|
||||
h.market = "KRX"
|
||||
new_holdings.append(h)
|
||||
else:
|
||||
new_holding = Portfolio(
|
||||
@@ -439,10 +499,87 @@ async def sync_portfolio(db: AsyncSession = Depends(get_db), force: bool = True)
|
||||
average_price=average_price,
|
||||
current_price=current_price,
|
||||
profit_loss=profit_loss,
|
||||
profit_loss_rate=profit_loss_rate
|
||||
profit_loss_rate=profit_loss_rate,
|
||||
market="KRX",
|
||||
)
|
||||
db.add(new_holding)
|
||||
new_holdings.append(new_holding)
|
||||
|
||||
# ── 해외 잔고 처리 ─────────────────────────────────────────────────
|
||||
if overseas_balance_data and overseas_balance_data.get("rt_cd") == "0" and "output1" in overseas_balance_data:
|
||||
ovrs_items = overseas_balance_data["output1"] or []
|
||||
# 해외 현재가 병렬 조회
|
||||
_ovrs_codes = [
|
||||
item.get("ovrs_pdno") for item in ovrs_items
|
||||
if item.get("ovrs_pdno") and int(item.get("ord_psbl_qty", 0)) > 0
|
||||
]
|
||||
if _ovrs_codes:
|
||||
_ovrs_price_results = await asyncio.gather(
|
||||
*[korea_investment_api.get_overseas_price(c, item.get("ovrs_excg_cd", "NASD"))
|
||||
for c, item in zip(_ovrs_codes, ovrs_items)],
|
||||
return_exceptions=True,
|
||||
)
|
||||
_ovrs_price_map = dict(zip(_ovrs_codes, _ovrs_price_results))
|
||||
else:
|
||||
_ovrs_price_map = {}
|
||||
|
||||
for item in ovrs_items:
|
||||
stock_code = item.get("ovrs_pdno")
|
||||
if not stock_code:
|
||||
continue
|
||||
quantity = int(item.get("ord_psbl_qty", 0))
|
||||
if quantity <= 0:
|
||||
continue
|
||||
|
||||
stock_name = item.get("ovrs_item_name", stock_code)
|
||||
exchange_code = item.get("ovrs_excg_cd", "NASD")
|
||||
average_price = Decimal(str(item.get("pchs_avg_pric", 0)))
|
||||
|
||||
# 실시간 현재가
|
||||
price_resp = _ovrs_price_map.get(stock_code)
|
||||
try:
|
||||
if price_resp and not isinstance(price_resp, Exception) and "output" in price_resp:
|
||||
realtime_price = float(price_resp["output"].get("last", 0))
|
||||
current_price = Decimal(str(realtime_price)) if realtime_price > 0 else Decimal(str(item.get("now_pric2", 0)))
|
||||
else:
|
||||
current_price = Decimal(str(item.get("now_pric2", 0)))
|
||||
except Exception:
|
||||
current_price = Decimal(str(item.get("now_pric2", 0)))
|
||||
|
||||
profit_loss = Decimal(str(item.get("evlu_pfls_amt", 0)))
|
||||
profit_loss_rate = Decimal(str(item.get("evlu_pfls_rt2", 0)))
|
||||
|
||||
logger.info(
|
||||
f"[해외] Portfolio sync - {stock_name}({stock_code}/{exchange_code}): "
|
||||
f"수량={quantity}, 평균단가={average_price}, 현재가={current_price}"
|
||||
)
|
||||
|
||||
processed_codes.add(stock_code)
|
||||
if stock_code in existing_all:
|
||||
h = existing_all[stock_code]
|
||||
h.stock_name = stock_name
|
||||
h.quantity = quantity
|
||||
h.average_price = average_price
|
||||
h.current_price = current_price
|
||||
h.profit_loss = profit_loss
|
||||
h.profit_loss_rate = profit_loss_rate
|
||||
h.market = exchange_code
|
||||
new_holdings.append(h)
|
||||
else:
|
||||
new_holding = Portfolio(
|
||||
stock_code=stock_code,
|
||||
stock_name=stock_name,
|
||||
quantity=quantity,
|
||||
average_price=average_price,
|
||||
current_price=current_price,
|
||||
profit_loss=profit_loss,
|
||||
profit_loss_rate=profit_loss_rate,
|
||||
market=exchange_code,
|
||||
)
|
||||
db.add(new_holding)
|
||||
new_holdings.append(new_holding)
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
|
||||
# 제거된 종목 반영 (DB에서 완전 삭제)
|
||||
for code, h in existing_all.items():
|
||||
if code not in processed_codes:
|
||||
@@ -456,7 +593,7 @@ async def sync_portfolio(db: AsyncSession = Depends(get_db), force: bool = True)
|
||||
await db.delete(h)
|
||||
await db.commit()
|
||||
portfolio_cache.holdings = [h for h in new_holdings if h.quantity > 0]
|
||||
portfolio_cache.cache_summary(balance_data) # balance_data 전달
|
||||
portfolio_cache.cache_summary(balance_data, overseas_balance_data) # 국내·해외 잔고 전달
|
||||
portfolio_cache.last_synced = datetime.now()
|
||||
portfolio_cache.change_flag = False
|
||||
portfolio_cache.last_trade_id = await _get_latest_executed_trade_id(db)
|
||||
@@ -638,12 +775,19 @@ async def check_and_execute_exit_trades(db: AsyncSession):
|
||||
stock_name = holding.stock_name
|
||||
quantity = holding.quantity
|
||||
avg_price = float(holding.average_price or 0)
|
||||
market = getattr(holding, "market", None) or ("KRX" if _is_domestic(stock_code) else "NASD")
|
||||
domestic = _is_domestic(stock_code, market)
|
||||
|
||||
# 실시간 가격 확인
|
||||
price_resp = await korea_investment_api.get_current_price(stock_code)
|
||||
# ── 실시간 가격 확인 (국내/해외 분기) ───────────────────────
|
||||
latest_price = float(holding.current_price or 0)
|
||||
if price_resp and price_resp.get("output"):
|
||||
latest_price = int(price_resp["output"].get("stck_prpr", latest_price))
|
||||
if domestic:
|
||||
price_resp = await korea_investment_api.get_current_price(stock_code)
|
||||
if price_resp and price_resp.get("output"):
|
||||
latest_price = int(price_resp["output"].get("stck_prpr", latest_price))
|
||||
else:
|
||||
price_resp = await korea_investment_api.get_overseas_price(stock_code, market)
|
||||
if price_resp and price_resp.get("output"):
|
||||
latest_price = float(price_resp["output"].get("last", latest_price))
|
||||
|
||||
if latest_price <= 0 or avg_price <= 0:
|
||||
continue
|
||||
@@ -660,43 +804,77 @@ async def check_and_execute_exit_trades(db: AsyncSession):
|
||||
active_trade = at_result.scalar_one_or_none()
|
||||
remaining_pct = float(active_trade.remaining_pct) if active_trade and active_trade.remaining_pct else 1.0
|
||||
|
||||
# ATR 기반 청산 시도 (차트 데이터 필요)
|
||||
# ── ATR 기반 청산 시도 (국내만, 해외는 고정 비율 폴백) ──────
|
||||
exit_signal = None
|
||||
try:
|
||||
chart_resp = await korea_investment_api.get_daily_chart(stock_code, "D", limit=30)
|
||||
chart_data = chart_resp.get("output2", [])
|
||||
if len(chart_data) >= 20:
|
||||
closes = [float(d.get("stck_clpr", 0)) for d in reversed(chart_data)]
|
||||
highs = [float(d.get("stck_hgpr", 0)) for d in reversed(chart_data)]
|
||||
lows = [float(d.get("stck_lwpr", 0)) for d in reversed(chart_data)]
|
||||
if domestic:
|
||||
try:
|
||||
chart_resp = await korea_investment_api.get_daily_chart(stock_code, "D", limit=30)
|
||||
chart_data = chart_resp.get("output2", [])
|
||||
if len(chart_data) >= 20:
|
||||
closes = [float(d.get("stck_clpr", 0)) for d in reversed(chart_data)]
|
||||
highs = [float(d.get("stck_hgpr", 0)) for d in reversed(chart_data)]
|
||||
lows = [float(d.get("stck_lwpr", 0)) for d in reversed(chart_data)]
|
||||
|
||||
atr = calculate_atr(highs, lows, closes)
|
||||
rsi = calculate_rsi(closes)
|
||||
bb = calculate_bollinger(closes)
|
||||
atr = calculate_atr(highs, lows, closes)
|
||||
rsi = calculate_rsi(closes)
|
||||
bb = calculate_bollinger(closes)
|
||||
|
||||
if atr and rsi is not None and bb:
|
||||
bb_upper, bb_middle, bb_lower = bb
|
||||
lowest_since = min(lows[-5:]) if lows else avg_price
|
||||
if atr and rsi is not None and bb:
|
||||
bb_upper, bb_middle, bb_lower = bb
|
||||
lowest_since = min(lows[-5:]) if lows else avg_price
|
||||
trailing_high = max(highs[-10:]) if highs else None
|
||||
regime = MarketRegime.BULL
|
||||
|
||||
trailing_high = max(highs[-10:]) if highs else None
|
||||
exit_signal = check_exit_signal(
|
||||
regime=regime,
|
||||
rsi=rsi,
|
||||
close=latest_price,
|
||||
bb_upper=bb_upper,
|
||||
bb_middle=bb_middle,
|
||||
bb_lower=bb_lower,
|
||||
entry_price=avg_price,
|
||||
atr=atr,
|
||||
lowest_since_entry=lowest_since,
|
||||
remaining_pct=remaining_pct,
|
||||
trailing_high=trailing_high,
|
||||
)
|
||||
except Exception as chart_err:
|
||||
logger.debug(f"ATR 청산 계산 실패 ({stock_code}): {chart_err}")
|
||||
else:
|
||||
# 해외: 해외 차트로 ATR 계산 시도
|
||||
try:
|
||||
chart_resp = await korea_investment_api.get_overseas_daily_chart(stock_code, market)
|
||||
chart_data = chart_resp.get("output2", [])
|
||||
if len(chart_data) >= 20:
|
||||
closes = [float(d.get("clos", 0)) for d in reversed(chart_data)]
|
||||
highs = [float(d.get("high", 0)) for d in reversed(chart_data)]
|
||||
lows = [float(d.get("low", 0)) for d in reversed(chart_data)]
|
||||
|
||||
regime = MarketRegime.BULL
|
||||
atr = calculate_atr(highs, lows, closes)
|
||||
rsi = calculate_rsi(closes)
|
||||
bb = calculate_bollinger(closes)
|
||||
|
||||
exit_signal = check_exit_signal(
|
||||
regime=regime,
|
||||
rsi=rsi,
|
||||
close=latest_price,
|
||||
bb_upper=bb_upper,
|
||||
bb_middle=bb_middle,
|
||||
bb_lower=bb_lower,
|
||||
entry_price=avg_price,
|
||||
atr=atr,
|
||||
lowest_since_entry=lowest_since,
|
||||
remaining_pct=remaining_pct,
|
||||
trailing_high=trailing_high,
|
||||
)
|
||||
except Exception as chart_err:
|
||||
logger.debug(f"ATR 청산 계산 실패 ({stock_code}): {chart_err}")
|
||||
if atr and rsi is not None and bb:
|
||||
bb_upper, bb_middle, bb_lower = bb
|
||||
lowest_since = min(lows[-5:]) if lows else avg_price
|
||||
trailing_high = max(highs[-10:]) if highs else None
|
||||
regime = MarketRegime.BULL
|
||||
|
||||
exit_signal = check_exit_signal(
|
||||
regime=regime,
|
||||
rsi=rsi,
|
||||
close=latest_price,
|
||||
bb_upper=bb_upper,
|
||||
bb_middle=bb_middle,
|
||||
bb_lower=bb_lower,
|
||||
entry_price=avg_price,
|
||||
atr=atr,
|
||||
lowest_since_entry=lowest_since,
|
||||
remaining_pct=remaining_pct,
|
||||
trailing_high=trailing_high,
|
||||
)
|
||||
except Exception as chart_err:
|
||||
logger.debug(f"[해외] ATR 청산 계산 실패 ({stock_code}): {chart_err}")
|
||||
|
||||
should_exit = False
|
||||
exit_reason = ""
|
||||
@@ -717,24 +895,37 @@ async def check_and_execute_exit_trades(db: AsyncSession):
|
||||
exit_reason = f"고정 익절 ({profit_loss_rate:.2f}% >= {settings.TAKE_PROFIT_PERCENT}%)"
|
||||
|
||||
if should_exit:
|
||||
logger.info(f"{exit_reason} - {stock_name}({stock_code})")
|
||||
try:
|
||||
order_result = await korea_investment_api.place_order(
|
||||
stock_code=stock_code,
|
||||
logger.info(f"{exit_reason} - {stock_name}({stock_code}) [{'국내' if domestic else '해외/' + market}]")
|
||||
# ── 주문 실행 (국내/해외 분기) ───────────────────────────
|
||||
if domestic:
|
||||
try:
|
||||
order_result = await korea_investment_api.place_order(
|
||||
stock_code=stock_code,
|
||||
order_type="SELL",
|
||||
quantity=sell_quantity,
|
||||
price=order_price
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"지정가 실패, 시장가 대체: {e}")
|
||||
order_result = await korea_investment_api.place_order(
|
||||
stock_code=stock_code,
|
||||
order_type="SELL",
|
||||
quantity=sell_quantity,
|
||||
price=None
|
||||
)
|
||||
else:
|
||||
order_result = await korea_investment_api.place_overseas_order(
|
||||
symbol=stock_code,
|
||||
order_type="SELL",
|
||||
quantity=sell_quantity,
|
||||
price=order_price
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"지정가 실패, 시장가 대체: {e}")
|
||||
order_result = await korea_investment_api.place_order(
|
||||
stock_code=stock_code,
|
||||
order_type="SELL",
|
||||
quantity=sell_quantity,
|
||||
price=None
|
||||
price=order_price,
|
||||
exchange_code=market,
|
||||
)
|
||||
|
||||
executed_price = Decimal(str(order_result.get("output", {}).get("ord_prc", order_price)))
|
||||
# 해외 주문 응답은 ord_prc 대신 ovrs_ord_unpr 사용
|
||||
_out = order_result.get("output", {})
|
||||
_raw_price = _out.get("ord_prc") or _out.get("ovrs_ord_unpr") or order_price
|
||||
executed_price = Decimal(str(_raw_price))
|
||||
total_amount = Decimal(str(executed_price)) * sell_quantity
|
||||
profit_loss = (float(executed_price) - avg_price) * sell_quantity
|
||||
|
||||
|
||||
@@ -118,6 +118,7 @@ class Portfolio(Base):
|
||||
current_price = Column(Numeric(15, 2))
|
||||
profit_loss = Column(Numeric(15, 2))
|
||||
profit_loss_rate = Column(Numeric(10, 4))
|
||||
market = Column(String(10), nullable=True, default="KRX") # KRX, NASD, NYSE, AMEX
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
|
||||
|
||||
@@ -774,6 +774,38 @@ class KoreaInvestmentAPI:
|
||||
logger.error(f"Error fetching balance: {e}")
|
||||
raise
|
||||
|
||||
async def get_overseas_balance(self) -> Dict[str, Any]:
|
||||
"""해외주식 잔고 조회 (미국 기준)
|
||||
|
||||
실전: TTTS3012R / 모의: VTTS3012R
|
||||
응답 output1: 보유 종목 목록 (ovrs_pdno, ovrs_item_name, ovrs_excg_cd, ord_psbl_qty, pchs_avg_pric, now_pric2, evlu_pfls_amt, evlu_pfls_rt2)
|
||||
응답 output2: 계좌 요약 (tot_evlu_amt, frcr_evlu_tota, usd_amt, ovrs_tot_pfls)
|
||||
"""
|
||||
try:
|
||||
account_number_prefix = self.account_number[:8]
|
||||
account_number_suffix = self.account_number[8:]
|
||||
|
||||
is_virtual = "vts" in self.base_url.lower()
|
||||
tr_id = "VTTS3012R" if is_virtual else "TTTS3012R"
|
||||
|
||||
result, _ = await self._make_request(
|
||||
"GET",
|
||||
"/uapi/overseas-stock/v1/trading/inquire-balance",
|
||||
tr_id,
|
||||
params={
|
||||
"CANO": account_number_prefix,
|
||||
"ACNT_PRDT_CD": account_number_suffix,
|
||||
"OVRS_EXCG_CD": "NASD", # 미국 전체 (NASD 조회 시 NYSE·AMEX 포함)
|
||||
"TR_CRCY_CD": "USD",
|
||||
"CTX_AREA_FK200": "",
|
||||
"CTX_AREA_NK200": "",
|
||||
}
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching overseas balance: {e}")
|
||||
return {}
|
||||
|
||||
async def place_order(
|
||||
self,
|
||||
stock_code: str,
|
||||
|
||||
Reference in New Issue
Block a user