feat(stock): market_events (급변·거래량Z·외인순매도)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-31 22:02:10 +09:00
parent c756b20c77
commit 241c24943f
2 changed files with 63 additions and 0 deletions

View File

@@ -124,6 +124,54 @@ def exit_rules(holding: dict, ticker_prices: "pd.DataFrame", params: dict) -> di
ADD_SCORE = 70.0 # 이 이상이면 추가매수 후보
# ---- Task 3.1: market_events ----
_DEFAULT_EVENT_PARAMS = {"move_pct": 7.0, "vol_z": 2.5}
def market_events(ticker: str, ticker_prices: "pd.DataFrame",
ticker_flow: "pd.DataFrame | None", params: dict) -> list[dict]:
"""일봉/flow 기반 시장 이벤트 (급변·거래량 Z·외인 순매도)."""
p = {**_DEFAULT_EVENT_PARAMS, **(params or {})}
events = []
if ticker_prices is None or ticker_prices.empty or len(ticker_prices) < 2:
return events
tp = ticker_prices.sort_values("date").reset_index(drop=True)
close = tp["close"].astype(float)
pct = (close.iloc[-1] - close.iloc[-2]) / close.iloc[-2] * 100.0 if close.iloc[-2] else 0.0
if abs(pct) >= p["move_pct"]:
events.append({
"type": "price_move",
"severity": "high" if abs(pct) >= p["move_pct"] * 1.5 else "med",
"summary": f"전일 대비 {pct:+.1f}%",
})
vol = tp["volume"].astype(float)
if len(vol) >= 21:
base = vol.iloc[-21:-1]
mu, sd = base.mean(), base.std(ddof=0)
last_vol = vol.iloc[-1]
if mu > 0 and (
(sd and (last_vol - mu) / sd >= p["vol_z"])
or (not sd and last_vol >= mu * p["vol_z"]) # sd=0 fallback: plain ratio
):
z_txt = f"{(last_vol - mu) / sd:.1f}" if sd else f"ratio={last_vol / mu:.1f}x"
events.append({
"type": "volume_surge",
"severity": "med",
"summary": f"거래량 평소 대비 급증(Z={z_txt})",
})
if ticker_flow is not None and not ticker_flow.empty:
tf = ticker_flow.sort_values("date")
recent = tf["foreign_net"].astype(float).iloc[-3:]
if len(recent) >= 3 and (recent < 0).all():
events.append({
"type": "foreign_selling",
"severity": "med",
"summary": "외국인 3일 연속 순매도",
})
return events
def decide_action(tech_score: float, exit_flags: dict, pnl: float | None,
add_score: float = ADD_SCORE) -> tuple[str, str]:
"""액션 결정 매트릭스: sell > trim > add > hold (우선순위 순).

View File

@@ -188,3 +188,18 @@ def test_technical_posture_empty_kospi_not_penalized():
scores = hi.technical_posture(ctx, ["005930"])
# ma_alignment+momentum만으로 정규화 → 상승추세면 충분히 높은 점수
assert scores["005930"] > 50.0
# ---- Phase 3 tests ----
DEFAULT_EVENT = {"move_pct": 7.0, "vol_z": 2.5}
def test_market_events_detects_move_and_volume():
closes = [1000]*30 + [1100] # 마지막날 +10%
vols = [1000]*30 + [10000] # 거래량 급증
df = _ticker_prices(closes, vols)
evts = hi.market_events("005930", df, None, DEFAULT_EVENT)
types = {e["type"] for e in evts}
assert "price_move" in types
assert "volume_surge" in types