feat: add minimum trade amount filter to rebalancing calculation
Add min_trade_amount parameter (default 10,000 KRW) to rebalance/calculate endpoint. Trades below this threshold are converted to hold actions to avoid inefficient micro-trades during rebalancing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
01f86298c4
commit
62ac92eaaf
@ -366,6 +366,7 @@ async def calculate_rebalance_manual(
|
||||
strategy=data.strategy,
|
||||
manual_prices=data.prices,
|
||||
additional_amount=data.additional_amount,
|
||||
min_trade_amount=data.min_trade_amount,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -208,6 +208,7 @@ class RebalanceCalculateRequest(BaseModel):
|
||||
strategy: str = Field(..., pattern="^(full_rebalance|additional_buy)$")
|
||||
prices: Optional[dict[str, Decimal]] = None
|
||||
additional_amount: Optional[Decimal] = Field(None, ge=0)
|
||||
min_trade_amount: Optional[Decimal] = Field(default=Decimal("10000"), ge=0)
|
||||
|
||||
|
||||
class RebalanceCalculateItem(BaseModel):
|
||||
|
||||
@ -193,6 +193,7 @@ class RebalanceService:
|
||||
strategy: str,
|
||||
manual_prices: Optional[Dict[str, Decimal]] = None,
|
||||
additional_amount: Optional[Decimal] = None,
|
||||
min_trade_amount: Optional[Decimal] = None,
|
||||
):
|
||||
"""Calculate rebalance with optional manual prices and strategy selection."""
|
||||
from app.schemas.portfolio import RebalanceCalculateItem, RebalanceCalculateResponse
|
||||
@ -228,17 +229,29 @@ class RebalanceService:
|
||||
current_values, total_assets, stock_names,
|
||||
prev_prices, start_prices,
|
||||
)
|
||||
return RebalanceCalculateResponse(
|
||||
portfolio_id=portfolio.id,
|
||||
total_assets=total_assets,
|
||||
items=items,
|
||||
)
|
||||
else: # additional_buy
|
||||
items = self._calc_additional_buy(
|
||||
all_tickers, targets, holdings, current_prices,
|
||||
current_values, total_assets, additional_amount,
|
||||
stock_names, prev_prices, start_prices,
|
||||
)
|
||||
|
||||
# Filter out trades below min_trade_amount
|
||||
if min_trade_amount and min_trade_amount > 0:
|
||||
for item in items:
|
||||
if item.action != "hold":
|
||||
trade_value = abs(item.diff_quantity) * item.current_price
|
||||
if trade_value < min_trade_amount:
|
||||
item.diff_quantity = 0
|
||||
item.action = "hold"
|
||||
|
||||
if strategy == "full_rebalance":
|
||||
return RebalanceCalculateResponse(
|
||||
portfolio_id=portfolio.id,
|
||||
total_assets=total_assets,
|
||||
items=items,
|
||||
)
|
||||
else:
|
||||
return RebalanceCalculateResponse(
|
||||
portfolio_id=portfolio.id,
|
||||
total_assets=total_assets,
|
||||
|
||||
@ -158,3 +158,48 @@ def test_apply_rebalance_insufficient_quantity(client: TestClient, auth_headers)
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
def test_min_trade_amount_filters_small_trades(client: TestClient, auth_headers):
|
||||
"""min_trade_amount 미만 거래는 hold로 변경된다."""
|
||||
pid = _setup_portfolio_with_holdings(client, auth_headers)
|
||||
|
||||
# With very high min_trade_amount, all trades should become hold
|
||||
response = client.post(
|
||||
f"/api/portfolios/{pid}/rebalance/calculate",
|
||||
json={
|
||||
"strategy": "full_rebalance",
|
||||
"prices": {"069500": 50000, "148070": 110000},
|
||||
"min_trade_amount": 99999999,
|
||||
},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
for item in data["items"]:
|
||||
assert item["action"] == "hold"
|
||||
assert item["diff_quantity"] == 0
|
||||
|
||||
|
||||
def test_min_trade_amount_allows_large_trades(client: TestClient, auth_headers):
|
||||
"""min_trade_amount 이상 거래는 정상 처리된다."""
|
||||
pid = _setup_portfolio_with_holdings(client, auth_headers)
|
||||
|
||||
# Use skewed prices to create a meaningful rebalancing diff
|
||||
# 069500: 10 * 30000 = 300,000 / 148070: 5 * 200000 = 1,000,000
|
||||
# total = 1,300,000, target each 50% = 650,000
|
||||
# 069500 needs buy: (650000-300000)/30000 = 11 shares => 330,000 trade value
|
||||
response = client.post(
|
||||
f"/api/portfolios/{pid}/rebalance/calculate",
|
||||
json={
|
||||
"strategy": "full_rebalance",
|
||||
"prices": {"069500": 30000, "148070": 200000},
|
||||
"min_trade_amount": 1,
|
||||
},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
# At least one item should have buy or sell action
|
||||
actions = [item["action"] for item in data["items"]]
|
||||
assert "buy" in actions or "sell" in actions
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user