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>
This commit is contained in:
72
backend/app/services/trade_cost_core.py
Normal file
72
backend/app/services/trade_cost_core.py
Normal file
@@ -0,0 +1,72 @@
|
||||
"""매매 비용·순손익 순수 계산 모듈.
|
||||
|
||||
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,
|
||||
}
|
||||
Reference in New Issue
Block a user