수정사항
This commit is contained in:
@@ -19,6 +19,7 @@ from app.core.market_schedule import is_market_open
|
||||
from app.services.mean_reversion_strategy import (
|
||||
MarketRegime, detect_market_regime, check_buy_signal,
|
||||
calculate_atr, calculate_adx, calculate_rsi, calculate_bollinger, is_reversal_candle,
|
||||
calculate_position_size_v2,
|
||||
)
|
||||
from app.services.mdd_manager import mdd_manager
|
||||
from app.services.loss_tracker import loss_tracker
|
||||
@@ -1319,6 +1320,7 @@ async def get_rule_based_recommendations_from_db(db: AsyncSession):
|
||||
"change_rate": round(float(stock.change_rate), 2) if stock.change_rate else 0,
|
||||
"volume": stock.volume,
|
||||
"rsi": round(float(stock.rsi), 2) if stock.rsi else None,
|
||||
"adx_14": round(float(stock.adx_14), 2) if stock.adx_14 else None,
|
||||
"ma5": int(stock.ma5) if stock.ma5 else None,
|
||||
"ma20": int(stock.ma20) if stock.ma20 else None,
|
||||
"stochastic_k": round(float(stock.stochastic_k), 2) if stock.stochastic_k else None,
|
||||
@@ -1332,10 +1334,8 @@ async def get_rule_based_recommendations_from_db(db: AsyncSession):
|
||||
"rule_name": rule.name,
|
||||
"rule_description": rule.description,
|
||||
"rule_priority": rule.priority,
|
||||
"position_ratio": actions.get("position_sizing", {}).get("target_ratio", 0.10),
|
||||
"take_profit": actions.get("take_profit", 3.0),
|
||||
"stop_loss": actions.get("stop_loss", -1.5),
|
||||
"max_position_pct": actions.get("max_position_pct", 0.12),
|
||||
"max_add_buys": actions.get("max_add_buys", 3),
|
||||
"add_buy_cooldown_min": actions.get("add_buy_cooldown_min", 15),
|
||||
}
|
||||
@@ -1382,6 +1382,7 @@ async def get_rule_based_recommendations_from_db(db: AsyncSession):
|
||||
"change_rate": round(float(stock.change_rate), 2) if stock.change_rate else 0,
|
||||
"volume": stock.volume,
|
||||
"rsi": round(float(stock.rsi), 2) if stock.rsi else None,
|
||||
"adx_14": round(float(stock.adx_14), 2) if stock.adx_14 else None,
|
||||
"ma5": int(stock.ma5) if stock.ma5 else None,
|
||||
"ma20": int(stock.ma20) if stock.ma20 else None,
|
||||
"stochastic_k": round(float(stock.stochastic_k), 2) if stock.stochastic_k else None,
|
||||
@@ -1395,10 +1396,8 @@ async def get_rule_based_recommendations_from_db(db: AsyncSession):
|
||||
"rule_name": "고점수 분석 종목",
|
||||
"rule_description": "매매 규칙 조건 미충족 - 분석 점수 기준 상위 종목",
|
||||
"rule_priority": 99,
|
||||
"position_ratio": 0.10,
|
||||
"take_profit": 3.0,
|
||||
"stop_loss": -1.5,
|
||||
"max_position_pct": 0.10,
|
||||
"max_add_buys": 2,
|
||||
"add_buy_cooldown_min": 30,
|
||||
}
|
||||
@@ -1900,8 +1899,9 @@ async def execute_auto_trade_background(recommendations_response: dict, db: Asyn
|
||||
# continue
|
||||
|
||||
try:
|
||||
# 🐻 하락장 포지션 축소 비율 전달
|
||||
# 시장 레짐 정보 전달 (포지션 사이징에서 regime 복원에 사용)
|
||||
stock["_position_multiplier"] = position_multiplier
|
||||
stock["_regime_str"] = market_regime.get("regime_200d", "bear") if market_regime else "bear"
|
||||
result = await execute_auto_trade(stock, db)
|
||||
total_processed += 1
|
||||
|
||||
@@ -2174,10 +2174,7 @@ async def execute_auto_trade(stock: dict, db: AsyncSession):
|
||||
logger.info(f"🚫 일일 매매 한도 초과: {stock_name} 당일 {daily_buy_count}/{max_daily_trades}회")
|
||||
return {"action": "HOLD", "stock": stock_name, "reason": "daily_trade_limit"}
|
||||
|
||||
# 기본 파라미터 (권장 안전 프로파일, 규칙에서 오버라이드 가능)
|
||||
target_ratio = float(stock.get("position_ratio", 0.10) or 0.10) # 목표 비중(자산 대비)
|
||||
max_position_pct = float(stock.get("max_position_pct", 0.12) or 0.12) # 최대 비중 캡
|
||||
cooldown_min = int(stock.get("add_buy_cooldown_min", 30) or 30) # 추가매수 쿨다운(분) - 30분으로 증가
|
||||
cooldown_min = int(stock.get("add_buy_cooldown_min", 30) or 30) # 추가매수 쿨다운(분)
|
||||
|
||||
# 🔥 쿨다운 체크 (보유 여부와 관계없이 항상 체크)
|
||||
# 포트폴리오 동기화가 느릴 수 있으므로, DB 거래 내역 기준으로 체크
|
||||
@@ -2220,23 +2217,21 @@ async def execute_auto_trade(stock: dict, db: AsyncSession):
|
||||
logger.info(f"🚫 MCP AI 매수 거부: {stock_name} ({mcp_reason})")
|
||||
return {"action": "HOLD", "stock": stock_name, "reason": mcp_reason}
|
||||
|
||||
if total_assets > 0:
|
||||
current_value = (holding.quantity * current_price) if holding else 0.0
|
||||
desired_value = total_assets * target_ratio
|
||||
max_allowed_value = total_assets * max_position_pct
|
||||
add_value = max(0.0, desired_value - current_value)
|
||||
add_value = min(add_value, max(0.0, max_allowed_value - current_value))
|
||||
invest_amount = min(add_value, available_cash)
|
||||
else:
|
||||
# 총자산 미확정 시 보수적으로 현금의 10%
|
||||
invest_amount = available_cash * 0.10
|
||||
|
||||
# 🐻 하락장 포지션 축소 적용
|
||||
pos_mult = float(stock.get("_position_multiplier", 1.0))
|
||||
if pos_mult < 1.0:
|
||||
original_amount = invest_amount
|
||||
invest_amount = invest_amount * pos_mult
|
||||
logger.info(f"🐻 하락장 포지션 축소: {stock_name} {original_amount:,.0f}원 → {invest_amount:,.0f}원 (x{pos_mult})")
|
||||
# 동적 포지션 사이징 (CLAUDE.md 섹션 6.1: 종목당 최대 4%, regime/RSI/ADX 반영)
|
||||
current_value = float(holding.quantity * current_price) if holding else 0.0
|
||||
regime_str = stock.get("_regime_str", "bear")
|
||||
regime = MarketRegime.BULL if regime_str == "bull" else MarketRegime.BEAR
|
||||
invest_amount = calculate_position_size_v2(
|
||||
total_assets=total_assets,
|
||||
available_cash=available_cash,
|
||||
current_strategy_allocation=current_value,
|
||||
regime=regime,
|
||||
rsi=stock.get("rsi"),
|
||||
adx=stock.get("adx_14"),
|
||||
)
|
||||
if invest_amount <= 0 and total_assets <= 0:
|
||||
# 총자산 미확정 시 보수적으로 현금의 4%
|
||||
invest_amount = available_cash * 0.04
|
||||
|
||||
quantity = int(invest_amount / current_price)
|
||||
|
||||
|
||||
@@ -381,6 +381,46 @@ def calculate_position_size(
|
||||
return result
|
||||
|
||||
|
||||
def calculate_position_size_v2(
|
||||
total_assets: float,
|
||||
available_cash: float,
|
||||
current_strategy_allocation: float,
|
||||
regime: MarketRegime,
|
||||
rsi: Optional[float] = None,
|
||||
adx: Optional[float] = None,
|
||||
max_per_position: float = 0.04,
|
||||
max_strategy_capital_bull: float = 0.70,
|
||||
max_strategy_capital_bear: float = 0.30,
|
||||
) -> float:
|
||||
"""CLAUDE.md 섹션 6.1 + RSI/ADX 동적 포지션 사이징
|
||||
|
||||
calculate_position_size 기반으로 RSI·ADX 보정 추가:
|
||||
- RSI ≤ 20: ×1.2 (깊은 과매도 = 더 유리한 진입)
|
||||
- RSI ≤ 25: ×1.1
|
||||
- ADX > 30: ×0.8 (강추세 중 평균회귀 위험 = 포지션 축소)
|
||||
보정 후에도 종목당 4% 상한 재적용 (CLAUDE.md 절대 룰)
|
||||
"""
|
||||
base = calculate_position_size(
|
||||
total_assets, available_cash, current_strategy_allocation,
|
||||
regime, max_per_position, max_strategy_capital_bull, max_strategy_capital_bear,
|
||||
)
|
||||
if base <= 0:
|
||||
return 0.0
|
||||
|
||||
mult = 1.0
|
||||
if rsi is not None:
|
||||
if rsi <= 20:
|
||||
mult *= 1.2
|
||||
elif rsi <= 25:
|
||||
mult *= 1.1
|
||||
|
||||
if adx is not None and adx > 30:
|
||||
mult *= 0.8
|
||||
|
||||
max_allowed = total_assets * max_per_position
|
||||
return min(base * mult, max_allowed, available_cash)
|
||||
|
||||
|
||||
def check_h1_breakout_with_ma5(
|
||||
h1_closes: List[float],
|
||||
h1_current_close: float,
|
||||
|
||||
@@ -73,6 +73,7 @@ async def initialize_watchlist(
|
||||
top_codes = {row.stock_code for row in top_stocks}
|
||||
|
||||
added = 0
|
||||
scanner_added = 0
|
||||
added_codes: set[str] = set()
|
||||
scanner_slots = SCANNER_PRIORITY_SLOTS
|
||||
|
||||
@@ -90,6 +91,7 @@ async def initialize_watchlist(
|
||||
added_codes.add(row.stock_code)
|
||||
scanner_slots -= 1
|
||||
added += 1
|
||||
scanner_added += 1
|
||||
|
||||
# --- 2순위: 스캐너에만 있고 score 랭킹에는 없는 종목 (최대 scanner_slots개) ---
|
||||
extra = list(scanner_tickers - top_codes)
|
||||
@@ -113,6 +115,7 @@ async def initialize_watchlist(
|
||||
))
|
||||
added_codes.add(row.stock_code)
|
||||
added += 1
|
||||
scanner_added += 1
|
||||
|
||||
# --- 3순위: 나머지 슬롯 — score 상위 순 ---
|
||||
remaining = WATCHLIST_INIT_SIZE - added
|
||||
@@ -132,12 +135,11 @@ async def initialize_watchlist(
|
||||
|
||||
await session.commit()
|
||||
|
||||
scanner_added = added - remaining if remaining >= 0 else added
|
||||
logger.info(
|
||||
"📋 워치리스트 초기화: %d개 (scanner_signal=%d, morning_batch=%d, 기준일: %s)",
|
||||
added,
|
||||
len([c for c in added_codes]), # 로깅용 (simplified)
|
||||
remaining if remaining >= 0 else 0,
|
||||
scanner_added,
|
||||
added - scanner_added,
|
||||
analysis_date,
|
||||
)
|
||||
return added
|
||||
|
||||
Reference in New Issue
Block a user