Files
auto-stock/backend/app/services/trade_cost_core.py
shuim 45677c8fbb fix: 리스크 결함 일괄 수정 — 누적 4% 상한·스윙 소유권·일일한도 분리·DT 순손익/체결가·시간손절 밴 제외·체결조회 경로
trade_executor
- 종목당 4% 상한을 기존 보유분 포함 누적으로 (sizing_core.remaining_position_room)
- 추천 경로는 스윙 소유 행만 매수·매도 (external/manual/daytrading/us_swing skip, 대칭), 고아 DT 행 warning
- 일일 매수한도는 스윙 소유 매수만 집계, 집계 실패 시 fail-closed
- 부분매도 반복 차단에서만 DT 매도 제외 (재매수 쿨다운은 유지)
- 체결 확인: 부분체결 잔량 자동 재주문 제거(가드 전부 우회·취소/거부 시 발동), PARTIAL→COMPLETED+체결수량, 초과체결 캡

korea_investment_api
- get_order_execution 이 존재하지 않는 /trading/inquire-ccnl 을 호출하던 결함 → inquire-daily-ccld
  (TTTC0081R 우선, 구 TR 폴백, 페이지네이션, ODNO 매칭, EXCG_ID_DVSN_CD=KRX), 실패 로그 warning

daytrading
- 시간손절은 loss_tracker 중립 (42일 밴이 스윙까지 막던 문제)
- 실현손익을 수수료·거래세 차감 순손익으로 (trade_cost_core), 체결가 백그라운드 보정 (fill_reconciler)
- legacy _record_trade strategy_type=daytrading 명시

db_worker
- update_trade_history_by_order_id 를 당일(KST)+최신 1행으로 한정 (일별 재사용 ODNO 로 과거 행 덮어쓰기 방지)

alembic 0013: trade_journals strategy_name='unknown' 을 trade_history.strategy_type 으로 보정 (백업 테이블 포함)
CLAUDE.md: 변경 동작 반영, MT 예약 리비전 0013→0014

998 passed

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 17:37:29 +09:00

73 lines
2.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""매매 비용·순손익 순수 계산 모듈.
settings·DB·네트워크를 참조하지 않는다(indicators_core / sizing_core 와 같은 원칙).
**비용률 숫자는 이 모듈에 없다** — 호출부가 settings(KR_COMMISSION_RATE,
KR_SELL_TAX_RATE_KOSPI/KOSDAQ)에서 꺼내 넘긴다.
비용 정의는 스윙 집행 경로(`trade_helpers.calculate_trade_costs` →
`trade_executor` 매도 기록)와 동일하다:
- 매수 수수료 = 진입가 × 수량 × 수수료율
- 매도 수수료 = 청산가 × 수량 × 수수료율
- 매도 거래세 = 청산가 × 수량 × 거래세율
- 각 항목을 원 단위로 반올림한 뒤 합산
슬리피지는 비용률에 넣지 않는다 — 실제 체결가를 쓰면 이미 손익에 반영되어 있고,
`DAY_TRADE_COST_PCT`(수수료+슬리피지 추정치)는 진입/청산 **판정 게이트**용이라 기록에 쓰지 않는다.
"""
from __future__ import annotations
def round_trip_costs(
entry_price: float,
exit_price: float,
quantity: int,
commission_rate: float,
sell_tax_rate: float,
) -> dict:
"""왕복 매매 비용(원). `trade_helpers.calculate_trade_costs` 와 같은 반올림 규칙.
Returns:
{"buy_commission", "sell_commission", "sell_tax", "total_cost"}
"""
qty = max(0, int(quantity))
buy_commission = float(entry_price) * qty * float(commission_rate)
sell_commission = float(exit_price) * qty * float(commission_rate)
sell_tax = float(exit_price) * qty * float(sell_tax_rate)
total_cost = buy_commission + sell_commission + sell_tax
return {
"buy_commission": round(buy_commission, 0),
"sell_commission": round(sell_commission, 0),
"sell_tax": round(sell_tax, 0),
"total_cost": round(total_cost, 0),
}
def net_realized_pnl(
entry_price: float,
exit_price: float,
quantity: int,
commission_rate: float,
sell_tax_rate: float,
) -> dict:
"""비용 차감 실현손익.
Returns:
{
"gross_profit_loss": (청산가 − 진입가) × 수량,
"total_cost": 왕복 비용(원),
"net_profit_loss": gross − total_cost,
"net_profit_loss_rate": net / (진입가 × 수량) × 100 (진입 원금 대비 %, 진입가<=0 이면 0.0),
}
"""
qty = max(0, int(quantity))
gross = (float(exit_price) - float(entry_price)) * qty
costs = round_trip_costs(entry_price, exit_price, qty, commission_rate, sell_tax_rate)
net = gross - costs["total_cost"]
invested = float(entry_price) * qty
rate = (net / invested * 100.0) if invested > 0 else 0.0
return {
"gross_profit_loss": gross,
"total_cost": costs["total_cost"],
"net_profit_loss": net,
"net_profit_loss_rate": rate,
}