Skip to content

Commit 18ee8d6

Browse files
committed
feat: 소액 트랙 v2 — 유휴 현금 절반을 CD금리 파킹 ETF로 + ETF 매도세 면제 반영
알파가 없다는 결론 아래에서 수익을 더 끌어올 정직한 원천은 예측이 아니라 구조다. v1(지수 50/현금 50)은 절반이 이자 0으로 잠들어 있었고, 체결 모델은 거래세 면제인 ETF에 개별주 매도세 0.20%를 물리고 있었다. - kr_pocket v2: KODEX200 47.5 + TIGER CD금리투자KIS 47.5 + 현금 5. 위험자산 노출은 종전 절반 원칙 그대로, 무수익 절반만 이자 자산으로(연 +1%대). min_cash_ratio 바스켓별 오버라이드 신설(전역 20%는 개별주용 안전판 — 현금성 ETF 바스켓엔 과잉이라 5%). - 클램프 규칙을 effective_stock_fraction 한 곳으로 통합 — 종전 5벌 복제 (리밸런서/평가/헬스/대시보드/시뮬레이터)라 표면마다 설계 비중이 어긋날 구조. - transaction_costs.tax_exempt_symbols + calculate_transaction_costs(symbol=) — ETF 매도세 0. 체결 경로 8곳 전달, 미전달 호출부는 기존 동작. - 구조 배치 허용 오차를 보유 슬롯별 1주 가격의 합으로 교정 — 최고가 1주만 재면 다중 슬롯 정상 적립(4/9/10개월차)에 몇 주씩 거짓 ATTENTION. 단일 슬롯 동작 불변, 빈 슬롯은 실패로 그대로 경보. 검증: 스위트 1677 통과. 격리 DB E2E 3사이클(지수 1주 -> 파킹 2주 -> 정착 0주문 조용) + 시뮬레이터 실CLI 일치. 분배금 미수령은 보수적 편향으로 문서화.
1 parent 4170fd1 commit 18ee8d6

17 files changed

Lines changed: 282 additions & 80 deletions

config/baskets.yaml

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -87,24 +87,34 @@ baskets:
8787

8888
# --- 소액 적립 트랙 (용돈 엔진) — docs/POCKET_TRACK_PLAN.md ---
8989
# 소액(30만 시작 + 월 10만 적립)의 유일한 분산 수단은 ETF다: KODEX 200 1주가
90-
# 그 자체로 200종목 분산이고, 국내 주식형 ETF는 매도 거래세 면제라 비용도 유리.
91-
# 주식 50/현금 50으로 낙폭을 절반으로 누른다(STATIC_ALLOCATION.md).
90+
# 그 자체로 200종목 분산이고, 국내 주식형 ETF는 매도 거래세 면제라 비용도 유리
91+
# (transaction_costs.tax_exempt_symbols에 등록 — 체결 모델도 면제 반영).
92+
#
93+
# v2 (2026-07-07): 위험자산 절반 원칙은 유지하되 "나머지 절반"을 무수익 현금 대신
94+
# CD금리 파킹 ETF로 보유한다. 종전 50/50에서 현금 50%는 이자 0으로 잠들어 있었는데,
95+
# 357870은 CD 91일물 금리를 일할 누적하는 무분배(TR형) ETF라 가격 변동이 사실상
96+
# 없이 연 3%대가 쌓인다 — 위험은 거의 그대로, 유휴분만 회수(연 +1%대 기대).
97+
# - 주가지수(069500) 47.5% + 파킹(357870) 47.5% + 현금 5%(수수료 여유)
98+
# - min_cash_ratio 0.05: 전역 20% 하한은 개별 주식 바스켓용 안전판 — 보유분
99+
# 절반이 현금성인 이 바스켓엔 과잉이라 바스켓별 오버라이드로 낮춘다.
100+
# - 한계 고지: KODEX200 분배금은 페이퍼가 못 받는다(연 ~1.5-2% × 47.5% 보수적
101+
# 편향). 357870은 무분배라 누락 없음. 합성 ETF의 스왑 상대방 위험은 미미.
92102
# 파라미터 주의(소액 특화):
93-
# - max_turnover_ratio 0.6: 15%면 회전 한도(30만×15%=4.5만)가 ETF 1주가(약 12.6만)보다
94-
# 작아 영원히 매수 불가. 0.6이면 첫 사이클에 주식 슬리브(50%)를 채울 수 있다.
95-
# - min_trade_amount 50000: 기본 20만이면 슬롯(15만)이 최소거래 미달로 미체결 판정.
96-
# 월 적립은 tools/record_deposit.py --basket kr_pocket --amount 100000 으로 기록
97-
# (입금은 TWR이 중화 — 수익률 왜곡 없음). 다음 사이클이 새 현금을 흡수한다.
103+
# - max_turnover_ratio 0.6: 15%면 회전 한도(30만×15%=4.5만)가 ETF 1주가(약 12.4만)보다
104+
# 작아 영원히 매수 불가. 0.6이면 첫 사이클에 큰 슬롯부터 채운다(잔여는 다음 날).
105+
# - min_trade_amount 50000: 기본 20만이면 슬롯(약 14만)이 최소거래 미달로 미체결 판정.
106+
# 월 적립은 웹 대시보드 입금 버튼 또는 tools/record_deposit.py --basket kr_pocket
107+
# --amount 100000 으로 기록(입금은 TWR이 중화 — 수익률 왜곡 없음). 다음 사이클이 흡수한다.
98108
kr_pocket:
99-
name: "소액 적립 (KODEX200 50/50)"
109+
name: "소액 적립 (지수+CD금리 파킹)"
100110
enabled: true
101111
initial_capital: 300000 # 시작 자본 30만원 (paper — live 전환 시 실자본과 일치)
102-
target_stock_weight: 0.5 # 주식 50% / 현금 50% — 낙폭 절반이 목적
103-
# 소액 구조적 절사: ETF 1주(약 12.8만) 단위라 배치율 미달이 상시 존재하고, 적립
104-
# 직후에는 '부족분 < 1주'로 매수가 보류돼 미달이 최대 1주가/총액(잔고 40만 시점
105-
# 32%p)까지 벌어진다(docs/POCKET_TRACK_PLAN.md §3 — 정상, 적립 누적 시 수렴).
106-
# 이 값은 '하한(floor)'이고, 헬스가 구조 하한(1주가/총액)과의 max로 자동 완화·
107-
# 조임한다 — 잔고가 커지면 이 값이 다시 지배한다.
112+
target_stock_weight: 0.95 # 투자 95% (지수 47.5 + 파킹 47.5) / 현금 5%
113+
min_cash_ratio: 0.05 # 바스켓별 현금 하한 오버라이드 (전역 0.20 대체)
114+
# 소액 구조적 절사: ETF 1주(약 12.4만) 단위라 배치율 미달이 상시 존재하고, 적립
115+
# 직후에는 '부족분 < 1주'로 매수가 보류돼 미달이 벌어진다(docs/POCKET_TRACK_PLAN.md
116+
# §3 — 정상, 적립 누적 시 수렴). 이 값은 '하한(floor)'이고, 헬스가 구조 하한
117+
# (1주가/총액)과의 max로 자동 완화·조임한다 — 잔고가 커지면 이 값이 다시 지배한다.
108118
monitoring:
109119
deployment_tolerance: 0.10
110120
rebalance:
@@ -113,7 +123,8 @@ baskets:
113123
min_trade_amount: 50000
114124
max_turnover_ratio: 0.6
115125
holdings:
116-
"069500": 1.0 # KODEX 200 — 주식 슬리브 전체(슬리브 자체가 총자산의 50%)
126+
"069500": 0.5 # KODEX 200 — 위험자산 슬리브(총자산의 47.5%)
127+
"357870": 0.5 # TIGER CD금리투자KIS(합성) — 파킹 슬리브(총자산의 47.5%)
117128

118129
# --- 예시 2: 신호 가중 리밸런싱 바스켓 ---
119130
# signal_weighted: true 시, 스코어링 전략 점수에 따라 목표 비중을 동적 조정

config/risk_params.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,12 @@ paper_backtest_compare:
158158
transaction_costs:
159159
commission_rate: 0.00015 # 편도 수수료 (0.015%)
160160
tax_rate: 0.0020 # 증권거래세+농특세: 매도 금액의 0.20% (2026년~ 코스피·코스닥 동일)
161+
# 국내 상장 ETF는 증권거래세 비과세 — 개별 주식 세율을 일괄 적용하면 ETF 전용 바스켓
162+
# (kr_pocket)의 매도 비용이 0.20%p 과대계상돼 승격 게이트의 비용 상한(연 1%) 판정까지
163+
# 왜곡된다. 여기 등록된 종목은 매도세 0으로 계산한다(수수료·슬리피지는 동일 적용).
164+
tax_exempt_symbols:
165+
- "069500" # KODEX 200
166+
- "357870" # TIGER CD금리투자KIS(합성)
161167
slippage: 0.0005 # 슬리피지 기본 비율 (0.05%). 동적 슬리피지 배수 적용 시 이 값 위에 곱해짐
162168
slippage_ticks: 1 # 호가 단위 기준 슬리피지 틱 수 (지정가 주문 기준 1틱이 현실적)
163169
# 양도소득세 (대주주 해당 시에만 적용; 일반 투자자는 해당 없음)

core/basket_deploy.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,31 @@
1515
DEFAULT_COMMISSION_RATE = 0.00015 # 0.015% 양방향
1616
DEFAULT_TAX_RATE = 0.0020 # 0.20% 매도만
1717

18+
DEFAULT_MIN_CASH_RATIO = 0.20 # diversification.min_cash_ratio 기본값과 동일
19+
20+
21+
def effective_stock_fraction(basket_cfg: dict[str, Any], risk_params: dict[str, Any]) -> float:
22+
"""바스켓의 유효 투자 비중(총자산 대비). 리밸런서·평가·헬스가 같은 규칙을 써야
23+
'설계 비중'이 서로 어긋나지 않으므로 여기 한 곳에 둔다.
24+
25+
- target_stock_weight: 바스켓이 명시한 투자 목표 비중. 미지정이면 (1 - 현금 하한).
26+
- min_cash_ratio: 전역(diversification.min_cash_ratio, 기본 20%)이 하한이지만
27+
바스켓별로 오버라이드할 수 있다. 전역 20%는 개별 주식 바스켓의 안전판인데,
28+
보유분 절반이 현금성(CD금리 파킹 ETF)인 바스켓에는 과잉이라 유휴 현금만 남긴다.
29+
"""
30+
div_cfg = (risk_params or {}).get("diversification", {}) or {}
31+
mcr_raw = basket_cfg.get("min_cash_ratio", div_cfg.get("min_cash_ratio", DEFAULT_MIN_CASH_RATIO))
32+
try:
33+
mcr = float(mcr_raw)
34+
except (TypeError, ValueError):
35+
mcr = float(div_cfg.get("min_cash_ratio", DEFAULT_MIN_CASH_RATIO))
36+
mcr = max(0.0, min(1.0, mcr))
37+
max_stock = 1.0 - mcr
38+
tsw = basket_cfg.get("target_stock_weight")
39+
if tsw is None:
40+
return max_stock
41+
return max(0.0, min(float(tsw), max_stock))
42+
1843

1944
def estimate_order_costs(
2045
orders: list[Any],

core/basket_evaluation.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -494,11 +494,8 @@ def _d(v):
494494
# 성과 귀속(실행 격차/구성 격차) — 종목별 조회(네트워크)라 기본 off.
495495
# 일일 사이클(리포트 부가필드)은 호출하지 않고, CLI 평가 도구에서만 켠다.
496496
if include_attribution:
497-
max_stock = 1.0 - (
498-
config.risk_params.get("diversification", {}).get("min_cash_ratio", 0.20)
499-
)
500-
tsw = basket_cfg.get("target_stock_weight")
501-
design_fraction = max_stock if tsw is None else max(0.0, min(float(tsw), max_stock))
497+
from core.basket_deploy import effective_stock_fraction
498+
design_fraction = effective_stock_fraction(basket_cfg, config.risk_params)
502499
holdings = basket_cfg.get("holdings") or {}
503500
design_return_pct = None
504501
if snaps and holdings:

core/basket_rebalancer.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -129,13 +129,11 @@ def __init__(
129129
self.data_collector = DataCollector(self.config)
130130

131131
self._risk_params = self.config.risk_params
132-
self._min_cash_ratio = (
133-
self._risk_params.get("diversification", {}).get("min_cash_ratio", 0.20)
134-
)
135132
# target_stock_weight: 바스켓이 명시하는 주식 목표 비중(총자산 대비). 나머지는 현금으로
136133
# 보유한다. 미지정(None)이면 기존 동작(min_cash_ratio만 현금 유보)을 유지한다.
137134
# 정적 자산배분(docs/STATIC_ALLOCATION.md) 결론을 실행 기능으로 구현: 예) 0.5면 주식 50%
138-
# 보유 + 50% 현금 → 낙폭을 절반으로 줄인다. min_cash_ratio가 더 큰 현금을 요구하면 그쪽을 따른다.
135+
# 보유 + 50% 현금 → 낙폭을 절반으로 줄인다. min_cash_ratio가 더 큰 현금을 요구하면 그쪽을
136+
# 따르되, 하한 자체는 바스켓별 오버라이드 가능(effective_stock_fraction 참고).
139137
tsw = self.basket.get("target_stock_weight")
140138
self._target_stock_weight = float(tsw) if tsw is not None else None
141139

@@ -146,12 +144,10 @@ def __init__(
146144
)
147145

148146
def _stock_fraction(self) -> float:
149-
"""총자산 중 주식에 배정할 비중. target_stock_weight가 있으면 그것을(단, min_cash_ratio
150-
가 요구하는 현금 하한은 항상 지킴), 없으면 (1 - min_cash_ratio)."""
151-
max_stock = 1.0 - self._min_cash_ratio
152-
if self._target_stock_weight is None:
153-
return max_stock
154-
return max(0.0, min(self._target_stock_weight, max_stock))
147+
"""총자산 중 주식에 배정할 비중 — 규칙은 effective_stock_fraction 한 곳에만 둔다
148+
(리밸런서·평가·헬스가 각자 계산하면 '설계 비중'이 서로 어긋난다)."""
149+
from core.basket_deploy import effective_stock_fraction
150+
return effective_stock_fraction(self.basket, self._risk_params)
155151

156152
def _is_live(self) -> bool:
157153
"""실전(live) 모드 여부."""

core/operator_health.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -135,28 +135,32 @@ def summarize_blockers(blockers: dict[str, Any] | None) -> dict[str, Any]:
135135

136136

137137
def structural_deployment_tolerance(
138-
max_share_price: float | None,
138+
truncation_unit_value: float | None,
139139
total_value: float | None,
140140
floor_tolerance: float = 0.05,
141141
) -> float:
142142
"""배치율 허용 오차의 구조 하한 — 정수 주식 절사가 만드는 불가피한 미달을 반영(순수).
143143
144-
적립 직후에는 '부족분 < 1주 가격'인 동안 매수가 보류되므로(설계), 미달폭이 최대
145-
1주 가격/총액까지 벌어진다(예: 잔고 40만·ETF 1주 12.8만 → 32%p). 이 구간을 고정
146-
허용치로 재면 정상 적립 과정이 매일 ATTENTION을 울린다(경보 피로). 반대로 잔고가
147-
커지면 구조 하한이 저절로 조여져(200만 → 6.4%p) floor가 다시 지배한다 —
148-
진짜 이상(예: 매수 자체 불능 → 미달 50%p)은 어느 시점에도 잡힌다.
149-
150-
반환: max(floor_tolerance, max_share_price/total_value). 입력 불충분 시 floor.
144+
적립 직후에는 '부족분 < 1주 가격'인 동안 매수가 보류되므로(설계), 미달폭이
145+
슬롯마다 최대 1주 가격까지 벌어진다. truncation_unit_value는 그 상한의 합 —
146+
**보유 슬롯별 1주 가격의 합**이다(예: 잔고 40만·ETF 1주 12.8만 단일 슬롯 → 32%p;
147+
지수 12.4만+파킹 5.8만 2슬롯 잔고 70만 → 26%p). 최고가 1주만 재면 다중 슬롯
148+
바스켓이 정상 적립 중(모든 슬롯이 부족분 < 1주)에도 허용을 초과해 몇 주씩
149+
거짓 ATTENTION이 울린다. 잔고가 커지면 구조 하한이 저절로 조여져 floor가 다시
150+
지배한다 — 진짜 이상(미달이 절사 상한을 초과)은 규모가 커지는 즉시 잡힌다.
151+
완전히 빈 슬롯은 절사가 아니라 실패이므로 합산에 넣지 않는다(호출부는 '보유'
152+
포지션의 1주 가격만 합산할 것).
153+
154+
반환: max(floor_tolerance, truncation_unit_value/total_value). 입력 불충분 시 floor.
151155
"""
152156
try:
153-
price = float(max_share_price or 0)
157+
unit = float(truncation_unit_value or 0)
154158
total = float(total_value or 0)
155159
except (TypeError, ValueError):
156160
return float(floor_tolerance)
157-
if price <= 0 or total <= 0:
161+
if unit <= 0 or total <= 0:
158162
return float(floor_tolerance)
159-
return max(float(floor_tolerance), price / total)
163+
return max(float(floor_tolerance), unit / total)
160164

161165

162166
def summarize_deployment(

core/order_executor.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -779,6 +779,7 @@ def _execute_buy_impl(
779779
1,
780780
"BUY",
781781
avg_daily_volume=avg_daily_volume,
782+
symbol=symbol,
782783
)
783784
sizing_entry_price = float(sizing_costs.get("execution_price", price) or price)
784785

@@ -867,7 +868,9 @@ def _execute_buy_impl(
867868
return {"success": False, "reason": perf_check.get("reason", "성과 열화로 매수 중단")}
868869

869870
# 거래 비용 계산 (avg_daily_volume 있으면 거래량 기반 동적 슬리피지 적용)
870-
costs = self.risk_manager.calculate_transaction_costs(price, quantity, "BUY", avg_daily_volume=avg_daily_volume)
871+
costs = self.risk_manager.calculate_transaction_costs(
872+
price, quantity, "BUY", avg_daily_volume=avg_daily_volume, symbol=symbol,
873+
)
871874
available_cash = capital if available_cash is None else available_cash
872875
estimated_fill_price = float(costs.get("execution_price", price) or price)
873876

@@ -984,7 +987,7 @@ def _execute_buy_impl(
984987
order.transition(OrderStatus.ACKED)
985988
# Paper에서는 슬리피지 적용 후 즉시 FILLED (simulated fill)
986989
costs = self.risk_manager.calculate_transaction_costs(
987-
expected_price, quantity, "BUY", avg_daily_volume=avg_daily_volume,
990+
expected_price, quantity, "BUY", avg_daily_volume=avg_daily_volume, symbol=symbol,
988991
)
989992
fill_price = costs["execution_price"]
990993
order.transition(OrderStatus.FILLED, fill_qty=quantity, fill_price=fill_price)
@@ -994,7 +997,7 @@ def _execute_buy_impl(
994997

995998
if self.mode == "live":
996999
costs = self.risk_manager.calculate_transaction_costs(
997-
fill_price, quantity, "BUY", avg_daily_volume=avg_daily_volume,
1000+
fill_price, quantity, "BUY", avg_daily_volume=avg_daily_volume, symbol=symbol,
9981001
)
9991002

10001003
stop_loss = self.risk_manager.calculate_stop_loss(
@@ -1128,7 +1131,7 @@ def _execute_buy_quantity_impl(
11281131
return {"success": False, "reason": "장 초반/마감 진입 차단 시간대"}
11291132

11301133
costs = self.risk_manager.calculate_transaction_costs(
1131-
price, quantity, "BUY", avg_daily_volume=avg_daily_volume,
1134+
price, quantity, "BUY", avg_daily_volume=avg_daily_volume, symbol=symbol,
11321135
)
11331136
expected_price = float(price)
11341137
fill_price = float(costs["execution_price"])
@@ -1232,7 +1235,7 @@ def _execute_buy_quantity_impl(
12321235
OrderGuard.clear(symbol)
12331236
# 체결가 기준 비용 재계산
12341237
costs = self.risk_manager.calculate_transaction_costs(
1235-
fill_price, quantity, "BUY", avg_daily_volume=avg_daily_volume,
1238+
fill_price, quantity, "BUY", avg_daily_volume=avg_daily_volume, symbol=symbol,
12361239
)
12371240
else:
12381241
order.transition(OrderStatus.SUBMITTED)
@@ -1464,6 +1467,7 @@ def _execute_sell_impl(
14641467
"SELL",
14651468
avg_daily_volume=avg_daily_volume,
14661469
avg_price=float(position.avg_price),
1470+
symbol=symbol,
14671471
)
14681472
fill_price = float(costs["execution_price"])
14691473
order.transition(OrderStatus.SUBMITTED)
@@ -1480,6 +1484,7 @@ def _execute_sell_impl(
14801484
"SELL",
14811485
avg_daily_volume=avg_daily_volume,
14821486
avg_price=float(position.avg_price),
1487+
symbol=symbol,
14831488
)
14841489
total_tax = costs["tax"] + costs.get("capital_gains_tax", 0)
14851490
pnl = (fill_price - position.avg_price) * sell_qty - costs["commission"] - total_tax

core/risk_manager.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,7 @@ def calculate_transaction_costs(
667667
action: str = "BUY",
668668
avg_daily_volume: float = None,
669669
avg_price: float = None,
670+
symbol: str = None,
670671
) -> dict:
671672
"""
672673
거래 비용 계산 (수수료 + 증권거래세 + 슬리피지 + 양도소득세(선택))
@@ -677,6 +678,10 @@ def calculate_transaction_costs(
677678
action: "BUY" 또는 "SELL"
678679
avg_daily_volume: 일평균 거래량 (동적 슬리피지용)
679680
avg_price: 매도 시 평균 매입 단가 (양도소득세 계산용; 대주주 해당 시)
681+
symbol: 종목코드 (선택). transaction_costs.tax_exempt_symbols에 있으면
682+
매도세를 면제한다 — 국내 상장 ETF는 증권거래세 비과세인데 개별 주식
683+
세율을 일괄 적용하면 ETF 바스켓의 비용이 과대계상된다(승격 게이트의
684+
비용 상한 판정까지 왜곡). 미전달 시 기존 동작(일괄 과세) 유지.
680685
681686
Returns:
682687
commission(수수료), tax(증권거래세+농특세 매도 시 0.20%), capital_gains_tax(양도소득세, 설정 시),
@@ -685,6 +690,11 @@ def calculate_transaction_costs(
685690
costs = self.risk_params.get("transaction_costs", {})
686691
amount = price * quantity
687692

693+
sell_tax_rate = costs.get("tax_rate", 0.0020)
694+
exempt = costs.get("tax_exempt_symbols") or []
695+
if symbol is not None and str(symbol) in {str(s) for s in exempt}:
696+
sell_tax_rate = 0.0
697+
688698
commission = amount * costs.get("commission_rate", 0.00015)
689699
dynamic = costs.get("dynamic_slippage", {})
690700
slippage_rate_fixed = costs.get("slippage", 0.0005)
@@ -713,10 +723,10 @@ def calculate_transaction_costs(
713723
slippage = slippage_per_share * quantity
714724
slippage_rate_effective = slippage_per_share / price if price > 0 else 0
715725

716-
# 증권거래세+농특세: 매도 금액의 0.20% (2026년~ 코스피·코스닥 동일)
726+
# 증권거래세+농특세: 매도 금액의 0.20% (2026년~ 코스피·코스닥 동일; ETF는 면제)
717727
tax = 0
718728
if action.upper() == "SELL":
719-
tax = amount * costs.get("tax_rate", 0.0020)
729+
tax = amount * sell_tax_rate
720730

721731
# 양도소득세 (대주주 해당 시만; enabled 시 실현 이익에 대해 부과)
722732
capital_gains_tax = 0
@@ -735,7 +745,7 @@ def calculate_transaction_costs(
735745
execution_price = price + slippage_per_share
736746
else:
737747
effective_price = price * (
738-
1 - costs.get("commission_rate", 0) - slippage_rate_effective - costs.get("tax_rate", 0)
748+
1 - costs.get("commission_rate", 0) - slippage_rate_effective - sell_tax_rate
739749
)
740750
execution_price = max(0, price - slippage_per_share)
741751

0 commit comments

Comments
 (0)