Harden scheduler and stale breakout reentry
This commit is contained in:
@@ -150,14 +150,17 @@ Highest-leverage fixes:
|
||||
After /morning exits, run_morning.ps1 calls python scripts/start_bot.py.
|
||||
|
||||
08:30 Bot loads daily_context.json and builds the watch universe.
|
||||
08:50 Bot calculates volatility breakout targets.
|
||||
08:50 Bot calculates volatility breakout targets. If restarted after 08:50,
|
||||
the bot recalculates targets immediately; open=0 is ignored.
|
||||
09:00 Morning trading session starts.
|
||||
09:00-15:05 StockBot_Watchdog checks bot liveness every 5 minutes.
|
||||
11:00 New entries pause if midday_context.json has not loaded.
|
||||
11:20 StockBot_Midday -> scripts/run_midday.ps1 -> /midday
|
||||
Build data/midday_context.json; bot detects it and starts lunch controls.
|
||||
14:00 New entries stop; exits continue.
|
||||
14:50 Force exit all positions. This time is immutable.
|
||||
15:10 Daily settlement and Discord summary.
|
||||
Watchdog must not restart the bot at or after 15:10.
|
||||
15:30 StockBot_Evening -> scripts/run_evening.ps1 -> /evening
|
||||
Write daily report and proposal report when needed.
|
||||
16:00 StockBot_Training -> scripts/run_training_pipeline.ps1
|
||||
|
||||
@@ -150,14 +150,17 @@ Highest-leverage fixes:
|
||||
After /morning exits, run_morning.ps1 calls python scripts/start_bot.py.
|
||||
|
||||
08:30 Bot loads daily_context.json and builds the watch universe.
|
||||
08:50 Bot calculates volatility breakout targets.
|
||||
08:50 Bot calculates volatility breakout targets. If restarted after 08:50,
|
||||
the bot recalculates targets immediately; open=0 is ignored.
|
||||
09:00 Morning trading session starts.
|
||||
09:00-15:05 StockBot_Watchdog checks bot liveness every 5 minutes.
|
||||
11:00 New entries pause if midday_context.json has not loaded.
|
||||
11:20 StockBot_Midday -> scripts/run_midday.ps1 -> /midday
|
||||
Build data/midday_context.json; bot detects it and starts lunch controls.
|
||||
14:00 New entries stop; exits continue.
|
||||
14:50 Force exit all positions. This time is immutable.
|
||||
15:10 Daily settlement and Discord summary.
|
||||
Watchdog must not restart the bot at or after 15:10.
|
||||
15:30 StockBot_Evening -> scripts/run_evening.ps1 -> /evening
|
||||
Write daily report and proposal report when needed.
|
||||
16:00 StockBot_Training -> scripts/run_training_pipeline.ps1
|
||||
|
||||
@@ -24,8 +24,10 @@ AI에게 맡기지는 않습니다. AI는 장 전/장중/장후 시장을 분석
|
||||
| 시간 | 흐름 | 내용 |
|
||||
|---|---|---|
|
||||
| 08:15 | 장 전 분석 | 뉴스, 수급, 업종 분위기를 분석해 `daily_context.json` 생성 |
|
||||
| 08:30 | 봇 시작/준비 | 유니버스 선정, 목표가 계산, Discord 알림 |
|
||||
| 08:30 | 봇 시작/준비 | AI 컨텍스트 로드, 유니버스 선정 |
|
||||
| 08:50 | 목표가 계산 | 전일 고저와 당일 시가 기반, `open=0`이면 계산 제외 |
|
||||
| 09:00 | 오전 매매 | 변동성 돌파 조건과 AI 컨텍스트 필터를 함께 확인 |
|
||||
| 09:00-15:05 | Watchdog | 5분마다 봇 생존 감시, 15:10 결산 직후 재시작 금지 |
|
||||
| 11:20 | 장중 분석 | 오전 결과와 현재 시장을 비교해 `midday_context.json` 생성 |
|
||||
| 14:00 | 신규 진입 종료 | 새 진입은 막고 보유 포지션 청산 감시만 계속 |
|
||||
| 14:50 | 강제 청산 | 모든 포지션 정리 |
|
||||
@@ -38,6 +40,8 @@ AI에게 맡기지는 않습니다. AI는 장 전/장중/장후 시장을 분석
|
||||
- 전략: 변동성 돌파(`K=0.5`)
|
||||
- 진입 시작: `09:20`
|
||||
- 강제 청산: `14:50`
|
||||
- 일일 결산: `15:10`
|
||||
- Watchdog: `09:00-15:05`, 5분 간격
|
||||
- DB: SQLite (`data/stockbot.db`)
|
||||
- 알림: Discord Webhook
|
||||
- AI/ML: 시장 분석과 관찰용 점수 기록까지만 사용
|
||||
|
||||
@@ -55,9 +55,17 @@ class KISClient:
|
||||
)
|
||||
self._load_token_from_file()
|
||||
|
||||
# rate limit: 모의투자 1건/초보다 보수적, 실거래 5건/초 이하
|
||||
self._rate_limit = 1 if self.is_mock else 5
|
||||
self._request_spacing = 1.2 if self.is_mock else 0.22
|
||||
# rate limit: KIS occasionally rejects even nominally safe bursts.
|
||||
# Keep defaults conservative and allow local override from .env.
|
||||
self._rate_limit = int(os.getenv(
|
||||
"KIS_MOCK_RATE_LIMIT" if self.is_mock else "KIS_REAL_RATE_LIMIT",
|
||||
"1" if self.is_mock else "3",
|
||||
))
|
||||
self._request_spacing = float(os.getenv(
|
||||
"KIS_MOCK_REQUEST_SPACING" if self.is_mock else "KIS_REAL_REQUEST_SPACING",
|
||||
"1.7" if self.is_mock else "0.35",
|
||||
))
|
||||
self._cooldown_until = 0.0
|
||||
self._semaphore = asyncio.Semaphore(1)
|
||||
self._req_times : list = []
|
||||
|
||||
@@ -175,6 +183,9 @@ class KISClient:
|
||||
|
||||
async with self._semaphore:
|
||||
now = time.monotonic()
|
||||
if now < self._cooldown_until:
|
||||
await asyncio.sleep(self._cooldown_until - now)
|
||||
now = time.monotonic()
|
||||
if self._req_times:
|
||||
wait = self._request_spacing - (now - self._req_times[-1])
|
||||
if wait > 0:
|
||||
@@ -204,6 +215,8 @@ class KISClient:
|
||||
rt_cd = data.get("rt_cd", "")
|
||||
if rt_cd != "0":
|
||||
msg = data.get("msg1", "알 수 없는 오류")
|
||||
if "초당" in msg or "거래건수" in msg or "rate" in msg.lower():
|
||||
self._cooldown_until = time.monotonic() + max(2.5, self._request_spacing * 2)
|
||||
logger.error(f"KIS API 오류 [{tr_id}]: {rt_cd} - {msg}")
|
||||
raise RuntimeError(f"KIS API 오류: {msg}")
|
||||
|
||||
|
||||
+20
-3
@@ -844,15 +844,28 @@ class StockBot:
|
||||
async def calc_targets(self):
|
||||
"""당일 시가 기반 목표가 계산"""
|
||||
logger.info("목표가 계산 시작")
|
||||
self.strategy.targets.clear()
|
||||
self.strategy.today_open.clear()
|
||||
now_str = datetime.now().strftime("%H:%M")
|
||||
valid_count = 0
|
||||
for ticker in self.universe:
|
||||
try:
|
||||
price_info = await self._get_price_with_retry(ticker, "TARGET")
|
||||
self.strategy.set_today_open(ticker, price_info["open"])
|
||||
target = self.strategy.get_target(ticker)
|
||||
open_price = price_info.get("open") or 0
|
||||
name = self.ticker_names.get(ticker, ticker)
|
||||
if open_price <= 0:
|
||||
current = price_info.get("current") or 0
|
||||
if now_str >= "09:00" and current > 0:
|
||||
open_price = current
|
||||
logger.warning(f"시가 0 감지({name}/{ticker}) → 현재가 {current:,}를 임시 시가로 사용")
|
||||
else:
|
||||
logger.info(f"목표가 제외({name}/{ticker}): 시가 미확정(open=0)")
|
||||
await asyncio.sleep(1.1)
|
||||
continue
|
||||
self.strategy.set_today_open(ticker, open_price)
|
||||
target = self.strategy.get_target(ticker)
|
||||
if target > 0:
|
||||
logger.info(f"목표가: {name}({ticker}) {target:,.0f}원 [시가 {price_info['open']:,}]")
|
||||
logger.info(f"목표가: {name}({ticker}) {target:,.0f}원 [시가 {open_price:,}]")
|
||||
valid_count += 1
|
||||
await asyncio.sleep(1.1)
|
||||
except Exception as e:
|
||||
@@ -1329,6 +1342,9 @@ async def run():
|
||||
ctx = bot.strategy.load_ai_context()
|
||||
bot.risk.set_risk_level(ctx.get("risk_level", "보통"))
|
||||
await bot.update_universe()
|
||||
if now >= "08:50":
|
||||
logger.info("08:50 이후 장 전 재시작 감지 → 목표가 즉시 계산")
|
||||
await bot.calc_targets()
|
||||
|
||||
while True:
|
||||
now = datetime.now().strftime("%H:%M")
|
||||
@@ -1354,6 +1370,7 @@ async def run():
|
||||
|
||||
# 09:00 매매 루프 시작
|
||||
elif now == "09:00":
|
||||
await bot.calc_targets()
|
||||
await bot.trading_loop()
|
||||
|
||||
# 15:10 결산
|
||||
|
||||
@@ -47,6 +47,7 @@ class VolatilityBreakout:
|
||||
self._entry_times: dict = {} # ticker → 마지막 진입 datetime (쿨다운 추적)
|
||||
self._exit_times: dict = {} # ticker -> 마지막 최종 청산 datetime (쿨다운 추적)
|
||||
self._tp_closed_tickers: set[str] = set() # TP로 전량 청산된 당일 재진입 차단
|
||||
self._rebreak_required_tickers: set[str] = set() # TIME/FORCE 후 목표가 재돌파 대기
|
||||
|
||||
# ── AI 컨텍스트 로드 ──
|
||||
|
||||
@@ -103,6 +104,9 @@ class VolatilityBreakout:
|
||||
if not prev:
|
||||
logger.info(f"목표가 제외({ticker}): 전일 데이터 없음")
|
||||
return
|
||||
if open_price <= 0:
|
||||
logger.info(f"목표가 제외({ticker}): 당일 시가 미확정({open_price})")
|
||||
return
|
||||
if prev["amount"] < MIN_TRADE_AMOUNT:
|
||||
logger.info(
|
||||
f"목표가 제외({ticker}): 전일 거래대금 {prev['amount']/1e8:.0f}억"
|
||||
@@ -126,8 +130,10 @@ class VolatilityBreakout:
|
||||
exit_time = exit_time or datetime.now()
|
||||
if reason in ("TIME", "FORCE"):
|
||||
self._exit_times[ticker] = exit_time
|
||||
self._rebreak_required_tickers.add(ticker)
|
||||
elif reason in ("TP1", "TP2"):
|
||||
self._tp_closed_tickers.add(ticker)
|
||||
self._rebreak_required_tickers.discard(ticker)
|
||||
|
||||
# ── 진입 신호 판단 ──
|
||||
|
||||
@@ -151,6 +157,16 @@ class VolatilityBreakout:
|
||||
result["reason"] = "TP 당일 재진입 차단"
|
||||
return result
|
||||
|
||||
# 목표가 확인
|
||||
target = self.targets.get(ticker, 0)
|
||||
if target <= 0:
|
||||
result["reason"] = "목표가 없음"
|
||||
return result
|
||||
|
||||
# TIME/FORCE 이후 쿨다운 중이라도 목표가 아래로 내려온 사실은 기록한다.
|
||||
if ticker in self._rebreak_required_tickers and current_price < target:
|
||||
self._rebreak_required_tickers.discard(ticker)
|
||||
|
||||
# TIME/FORCE 청산 후 쿨다운은 진입 시각이 아니라 청산 시각 기준이다.
|
||||
last_exit = self._exit_times.get(ticker)
|
||||
if last_exit is not None:
|
||||
@@ -159,10 +175,10 @@ class VolatilityBreakout:
|
||||
result["reason"] = f"재진입 쿨다운 ({elapsed:.0f}분 / {TICKER_REENTRY_COOLDOWN_MIN}분)"
|
||||
return result
|
||||
|
||||
# 목표가 확인
|
||||
target = self.targets.get(ticker, 0)
|
||||
if target <= 0:
|
||||
result["reason"] = "목표가 없음"
|
||||
# TIME/FORCE 청산 뒤에는 남아 있는 당일 돌파 신호를 그대로 재사용하지 않는다.
|
||||
# 목표가 아래로 식은 뒤 다시 돌파해야 새로운 진입 신호로 인정한다.
|
||||
if ticker in self._rebreak_required_tickers:
|
||||
result["reason"] = f"재돌파 대기 ({current_price:,} >= {target:,.0f})"
|
||||
return result
|
||||
|
||||
# 기술적 조건: 현재가 >= 목표가
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
|
||||
### 5. 시스템 이슈
|
||||
|
||||
- **15:10 봇 재시작 감지**: 로그에서 결산(15:10:01) 직후 봇이 재초기화됨(15:10:04). 이후 KIS API "초당 거래건수 초과" 오류 발생(15:10:04). 결산 완료 후 자동 또는 수동 재시작이 있었던 것으로 보임. 정산 직후 재시작 시 KIS 토큰 재사용 정상 동작 확인.
|
||||
- **15:10 봇 재시작 감지(수정 완료)**: 로그에서 결산(15:10:01) 직후 watchdog이 종료된 봇을 재시작함(15:10:04). 이후 KIS API "초당 거래건수 초과" 오류 발생. 원인은 watchdog 감시 조건이 `15:10`을 포함한 것이며, `scripts/_watchdog.py`, `scripts/run_watchdog.ps1`, `scripts/setup_scheduler.ps1`을 수정해 watchdog을 09:00-15:05로 제한했다.
|
||||
- **점심 섹터 변경**: 오전 어보이드(건설, 금융) → 점심 어보이드(기계, 운수창고, 2차전지). 흥아해운(해운업/운수창고)은 오전 진입이므로 룰 위반 없음. 그러나 점심 이후 해당 종목 재진입 시 차단이 정상 작동했는지 확인 필요.
|
||||
|
||||
---
|
||||
@@ -90,5 +90,5 @@
|
||||
## 다음 체크 포인트
|
||||
|
||||
- 대한광통신 형태(고변동성 즉시 SL) 재발 시 진입 슬리피지/변동성 필터 강화 검토
|
||||
- 15:10 봇 재시작 원인 확인 및 필요 시 결산 후 자동 재시작 억제
|
||||
- 2026-06-10 15:10에 watchdog이 결산 직후 봇을 재시작하지 않는지 확인
|
||||
- KIS API 초당 거래건수 초과 오류 빈도 모니터링
|
||||
|
||||
@@ -73,15 +73,16 @@ TP 청산 비율 66.7%, FORCE/TIME 0%. 강제청산 없이 깔끔하게 마감.
|
||||
## AI 필터 품질
|
||||
|
||||
- 오늘 전 거래 `ai_boosted = 0`. AI 모델은 관찰 모드만.
|
||||
- 학습 데이터 부족(53 봇 거래 행)으로 AI 판단 의존은 아직 부적절.
|
||||
- 학습 데이터는 아직 대부분 외부 분봉 기반 후보 행이며, 실제 봇 거래 표본은 부족해 AI 판단 의존은 아직 부적절.
|
||||
- 오후 신호 진단: 목표가 미달 종목 다수, SL 차단 2종, TP 재진입 차단 2종 — 필터링 정상.
|
||||
|
||||
## 실행 품질
|
||||
|
||||
- 제로 가격 행 없음, 가격 불일치 없음.
|
||||
- 라이콤/광전자 진입 후 초단시간 TP 달성 — 변동성 돌파 로직 정상.
|
||||
- KIS 율한도 초과 경고 2회 (13:39, 13:42): `ENTRY price retry 1/4` 로그 확인.
|
||||
- KIS 율한도 초과 경고가 장중 여러 차례 발생: `ENTRY price retry 1/4` 로그 확인.
|
||||
- retry 후 정상 재개. 치명적 장애 아님.
|
||||
- 장 마감 후 KISClient 기본 조회 간격 확대 및 rate-limit 전역 쿨다운 추가.
|
||||
- 14:00 이후 ENTRY 차단 정상 동작 확인.
|
||||
- 14:50 강제 청산 시작 → 완료 정상 (미청산 포지션 없음).
|
||||
- 결산 중복 처리 방어 정상: `결산 이미 처리됨: 2026-06-10` 로그 확인.
|
||||
@@ -89,9 +90,10 @@ TP 청산 비율 66.7%, FORCE/TIME 0%. 강제청산 없이 깔끔하게 마감.
|
||||
## 운영 이슈
|
||||
|
||||
### KIS 율한도 초과 (경미)
|
||||
- 13:39, 13:42 가격 조회 시 `초당 거래건수를 초과하였습니다` 2회 발생.
|
||||
- 가격 조회 시 `초당 거래건수를 초과하였습니다`가 여러 차례 발생.
|
||||
- 현행 retry 로직으로 자동 복구됨.
|
||||
- 오후 감시 루프에서 다수 종목 동시 조회 시 빈도 집중 가능성. 지속 모니터링 필요.
|
||||
- 오후 감시 루프에서 다수 종목 순차 조회 시 빈도 집중 가능성.
|
||||
- 2026-06-10 장 마감 후 `app/execution/kis_client.py`에서 기본 조회 간격을 보수화하고 rate-limit 응답 후 전역 쿨다운을 추가함.
|
||||
|
||||
## 30일 누적 지표 (2거래일)
|
||||
|
||||
@@ -117,6 +119,6 @@ TP 청산 비율 66.7%, FORCE/TIME 0%. 강제청산 없이 깔끔하게 마감.
|
||||
|
||||
## 다음 체크사항
|
||||
|
||||
- KIS 율한도 초과 빈도가 주 2회 이상 지속되면 조회 간격 확대 검토.
|
||||
- 2026-06-11에 KIS 율한도 초과 빈도가 줄었는지 확인.
|
||||
- 대우건설 80분 보유 후 SL: `MAX_HOLD_MIN=90` 경계에 근접. 현행 유지.
|
||||
- 운영 데이터 누적 지속. 30거래일 도달 시 라이브 준비 재점검.
|
||||
|
||||
@@ -120,20 +120,22 @@ TP 청산 비율 66.7%, SL 비율 11.1%. 강제청산 없음.
|
||||
- KIS 타임아웃: 라이콤(388790) 12:30 2회 재시도, 현대건설(000720) 12:57 1회 재시도.
|
||||
모두 정상 복구. 실거래 영향 없음.
|
||||
|
||||
## 구조 이슈 — 삼성전자 진입가 > 목표가
|
||||
## 구조 이슈 — 삼성전자 TIME 후 동일 신호 재진입
|
||||
|
||||
| 항목 | 1차 진입 | 2차 진입 |
|
||||
|---|---|---|
|
||||
| 진입가 | 340,000 | 338,500 |
|
||||
| 목표가(TP) | 334,000 | 334,000 |
|
||||
| 돌파 목표가 | 334,000 | 334,000 |
|
||||
| 차이 | **-6,000원** | **-4,500원** |
|
||||
| 청산 | TIME | TIME |
|
||||
|
||||
- 아침 변동성 돌파 목표가는 전일 종가 기준 계산. 삼성전자가 개장 시 갭업하여
|
||||
목표가(334,000)를 이미 초과한 가격(340,000)에 진입.
|
||||
- 이 구조에서는 TP 달성이 원천 불가. 결국 TIME 또는 SL만 가능.
|
||||
- 현행 코드에 `current_price < tp_target` 진입 차단 로직 없음.
|
||||
- **별도 제안서 작성** (`reports/proposals/2026-06-15_strategy_proposal.md`).
|
||||
- 코드상 `목표가`는 익절가가 아니라 변동성 돌파 진입 기준가.
|
||||
따라서 `현재가 >= 목표가` 자체는 정상 진입 조건.
|
||||
- 실제 문제는 1차 `TIME` 청산 후에도 가격이 목표가 위에 머물러,
|
||||
60분 쿨다운 종료만으로 같은 돌파 신호를 재사용해 2차 진입한 점.
|
||||
- **적용 완료**: `TIME/FORCE` 청산 후에는 목표가 아래로 한 번 내려왔다가
|
||||
다시 돌파해야 재진입 가능하도록 `재돌파 대기` 필터 추가.
|
||||
- 적용 문서: `reports/proposals/2026-06-15_strategy_proposal.md`.
|
||||
|
||||
## 30일 누적 지표 (5거래일)
|
||||
|
||||
@@ -151,7 +153,7 @@ TP 청산 비율 66.7%, SL 비율 11.1%. 강제청산 없음.
|
||||
|
||||
## 다음 체크사항
|
||||
|
||||
- `진입가 > 목표가` 필터 제안서 검토 및 수동 승인 여부 결정.
|
||||
- `TIME/FORCE` 후 재돌파 대기 필터 내일 로그에서 정상 차단 여부 확인.
|
||||
- AI 부스트 누적 손익 별도 집계 시작 권장.
|
||||
- 에이팩트 대량 포지션 사이징(193주) — 리스크 대비 포지션 계산 재확인.
|
||||
- 운영 데이터 누적 지속. 30거래일 도달 시 라이브 준비 재점검.
|
||||
|
||||
@@ -1,5 +1,32 @@
|
||||
# Implementation Log
|
||||
|
||||
## 2026-06-10
|
||||
|
||||
- Enabled wake-from-sleep behavior for Scheduler tasks:
|
||||
- `scripts/setup_scheduler.ps1` now registers stock tasks with `WakeToRun`.
|
||||
- Re-registered tasks and verified `WakeToRun=True` and `StartWhenAvailable=True`.
|
||||
- Hardened KIS request throttling:
|
||||
- Mock request spacing default: 1.7s.
|
||||
- Real request spacing default: 0.35s, rate limit default: 3/sec.
|
||||
- Added local `.env` override support for request spacing/rate limits.
|
||||
- Added global cooldown after rate-limit responses.
|
||||
- Updated the 2026-06-10 daily report to reflect repeated KIS rate-limit retries.
|
||||
|
||||
## 2026-06-09
|
||||
|
||||
- Re-registered all Windows Scheduler tasks from the live project path:
|
||||
- `C:\Users\whdwo\Desktop\coding\stockbot_v3`
|
||||
- Verified every task action script exists at that path.
|
||||
- Fixed watchdog end-of-day behavior:
|
||||
- `StockBot_Watchdog` now runs 09:00-15:05 every 5 minutes.
|
||||
- `scripts/_watchdog.py` excludes 15:10 so normal daily settlement shutdown is not restarted.
|
||||
- `scripts/run_watchdog.ps1` skips after 15:09:59.
|
||||
- Hardened target calculation:
|
||||
- Targets are cleared before recalculation.
|
||||
- `open=0` is ignored before market open.
|
||||
- Delayed restarts after 08:50 recalculate targets immediately.
|
||||
- Updated operational docs and the 2026-06-09 daily report.
|
||||
|
||||
## 2026-05-28
|
||||
|
||||
- Applied the approved 2026-05-28 strategy update:
|
||||
@@ -120,3 +147,18 @@ Open risks:
|
||||
- Verification:
|
||||
- Python compile check passed.
|
||||
- Runtime import confirmed `ENTRY_START == "09:15"`.
|
||||
|
||||
## 2026-06-15
|
||||
|
||||
- Applied a stale breakout re-entry guard after reviewing the Samsung Electronics `TIME` re-entry.
|
||||
- Changed `app/strategy/volatility_breakout.py`:
|
||||
- `TIME` and `FORCE` final exits now mark the ticker as requiring a fresh breakout.
|
||||
- While that marker is active, a ticker is blocked with `재돌파 대기` if it remains above the same volatility breakout target.
|
||||
- The marker clears only after price moves back below the target, allowing a later fresh breakout entry.
|
||||
- Rationale:
|
||||
- The existing `current_price >= target` condition is the normal volatility breakout entry rule.
|
||||
- The bug was reusing a still-active same-day breakout signal after `TIME/FORCE` cooldown, not the first breakout itself.
|
||||
- This would have blocked the 2026-06-15 Samsung Electronics second entry after the first `TIME` exit.
|
||||
- Updated docs:
|
||||
- `reports/daily/2026-06-15.md`
|
||||
- `reports/proposals/2026-06-15_strategy_proposal.md`
|
||||
|
||||
@@ -2,39 +2,44 @@
|
||||
|
||||
## 요약
|
||||
|
||||
진입 시점에 현재가가 목표가(TP)를 이미 초과한 경우 진입을 차단하는 필터 추가.
|
||||
`TIME/FORCE` 청산 후에는 같은 당일 돌파 신호를 그대로 재사용하지 않고,
|
||||
목표가 아래로 한 번 식었다가 다시 돌파할 때만 재진입하도록 필터 추가.
|
||||
|
||||
**수동 승인 필수.**
|
||||
**적용 완료:** 2026-06-15
|
||||
|
||||
---
|
||||
|
||||
## 관찰된 문제
|
||||
|
||||
오늘(2026-06-15) 삼성전자(005930)가 두 차례 진입됐으나 두 번 모두 목표가(334,000)가
|
||||
진입가(340,000 / 338,500)보다 낮았다.
|
||||
오늘(2026-06-15) 삼성전자(005930)가 두 차례 진입됐다. 1차는 09:20 돌파 진입이었고,
|
||||
10:51 `TIME` 청산 후 60분 쿨다운이 끝난 11:51에 다시 진입됐다.
|
||||
|
||||
| 진입 | 진입가 | 목표가(TP) | 차이 | 결과 |
|
||||
|---|---|---|---|---|
|
||||
| 1차 09:20 | 340,000 | 334,000 | -6,000 | TIME -21,681원 |
|
||||
| 2차 11:51 | 338,500 | 334,000 | -4,500 | TIME -1,422원 |
|
||||
|
||||
원인: 변동성 돌파 목표가는 전일 종가 기준으로 계산되는데, 삼성전자가 개장 시 갭업하여
|
||||
목표가를 이미 상회한 가격에 진입 트리거가 발동했다.
|
||||
주의: 코드상 `목표가`는 익절가가 아니라 변동성 돌파 진입 기준가다.
|
||||
따라서 `현재가 >= 목표가`를 무조건 막으면 전략 전체 진입이 중단된다.
|
||||
|
||||
이 구조에서는 TP 달성이 원천 불가능하다. TIME 또는 SL 청산만 남는다.
|
||||
실제 구조적 문제는 `TIME/FORCE` 청산 후에도 현재가가 목표가 위에 머물면,
|
||||
새로운 돌파가 없는데도 쿨다운 종료만으로 같은 신호를 재사용해 재진입할 수 있다는 점이다.
|
||||
|
||||
---
|
||||
|
||||
## 제안 내용
|
||||
|
||||
### 진입 차단 조건 추가
|
||||
### TIME/FORCE 후 재돌파 조건 추가
|
||||
|
||||
`check_entry()` 내부에 다음 조건을 hard gate로 추가:
|
||||
`mark_final_exit()`에서 `TIME` 또는 `FORCE` 청산 종목을 재돌파 대기 목록에 넣고,
|
||||
`check_entry()`에서 해당 종목이 목표가 아래로 내려오기 전까지 진입을 차단한다.
|
||||
|
||||
```python
|
||||
# 현재가가 TP1 목표가 이상이면 진입 차단 (갭업 후 목표가 무효화)
|
||||
if current_price >= tp_target:
|
||||
return False, f"현재가({current_price:,})가 목표가({tp_target:,}) 이상 — 진입 차단"
|
||||
if ticker in self._rebreak_required_tickers:
|
||||
if current_price >= target:
|
||||
result["reason"] = f"재돌파 대기 ({current_price:,} >= {target:,.0f})"
|
||||
return result
|
||||
self._rebreak_required_tickers.discard(ticker)
|
||||
```
|
||||
|
||||
적용 위치: `app/strategy/volatility_breakout.py` — `check_entry()` 함수 내
|
||||
@@ -44,19 +49,17 @@ if current_price >= tp_target:
|
||||
|
||||
## 기대 효과
|
||||
|
||||
- 오늘 기준: 삼성전자 2건(-23,103원) 방어 가능.
|
||||
- 갭업 종목이 TP를 이미 소화한 상태로 진입하는 구조적 실수 차단.
|
||||
- 오늘 기준: 삼성전자 2차 TIME 재진입(-1,422원) 방어 가능.
|
||||
- 같은 날 같은 돌파 신호를 쿨다운 후 반복 사용하는 구조 차단.
|
||||
- SL/TIME 낭비 거래 제거 → R:R 개선.
|
||||
|
||||
---
|
||||
|
||||
## 위험 및 주의사항
|
||||
|
||||
- TP 목표가 계산 로직이 정확해야 필터가 올바르게 동작한다.
|
||||
(`tp_target`이 진입 가능 구간 안에 있을 때만 진입하는 원래 의도와 동일.)
|
||||
- 극히 드문 케이스: 목표가 재계산(장중 업데이트) 여부 확인 필요.
|
||||
현재 구현이 고정 목표가라면 문제없음; 장중 재계산이 있다면 로직 검토 추가 필요.
|
||||
- 샘플: 오늘 2건 관찰. 통계적 근거로는 부족하나, 이는 파라미터 조정이 아니라
|
||||
- 최초 돌파 진입은 기존과 동일하게 허용된다.
|
||||
- TIME/FORCE 뒤에도 가격이 목표가 아래로 내려갔다가 다시 돌파하면 재진입 가능하다.
|
||||
- 샘플: 오늘 1건의 명확한 재진입 사례 관찰. 통계적 근거로는 부족하나, 이는 파라미터 조정이 아니라
|
||||
**논리적 버그 수정**에 해당하므로 소량 샘플로도 충분히 정당화됨.
|
||||
|
||||
---
|
||||
@@ -72,8 +75,8 @@ if current_price >= tp_target:
|
||||
|
||||
## 승인 조건
|
||||
|
||||
- [ ] `volatility_breakout.py` 내 `tp_target` 변수가 진입 시점에 접근 가능한지 확인.
|
||||
- [ ] 장중 목표가 재계산 여부 확인.
|
||||
- [ ] 수동 코드 검토 후 적용.
|
||||
- [x] `volatility_breakout.py` 내 돌파 목표가 변수(`target`) 접근 확인.
|
||||
- [x] `TIME/FORCE` 청산 후 같은 신호 재사용 경로 확인.
|
||||
- [x] 수동 코드 검토 후 적용.
|
||||
|
||||
**FORCE_EXIT = "14:50"** 변경 없음. SL 우선순위 변경 없음.
|
||||
|
||||
@@ -85,7 +85,7 @@ async def main():
|
||||
now = datetime.now()
|
||||
now_str = now.strftime("%H:%M")
|
||||
|
||||
if not ("09:00" <= now_str <= "15:10"):
|
||||
if not ("09:00" <= now_str < "15:10"):
|
||||
print(f"[{now_str}] outside trading window - watchdog skipped")
|
||||
return
|
||||
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
chcp 65001 | Out-Null
|
||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
$env:PYTHONUTF8 = "1"
|
||||
$env:PYTHONIOENCODING = "utf-8"
|
||||
|
||||
$Root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
|
||||
Set-Location $Root
|
||||
. "$Root\scripts\stockbot_env.ps1"
|
||||
|
||||
@@ -27,7 +27,7 @@ if ($LASTEXITCODE -ne 0) {
|
||||
|
||||
$now = Get-Date
|
||||
$start = Get-Date -Hour 9 -Minute 0 -Second 0
|
||||
$end = Get-Date -Hour 15 -Minute 10 -Second 59
|
||||
$end = Get-Date -Hour 15 -Minute 9 -Second 59
|
||||
if ($now -lt $start -or $now -gt $end) {
|
||||
Write-WatchdogLog "outside watchdog window - skipped"
|
||||
exit 0
|
||||
|
||||
@@ -30,6 +30,7 @@ function Register-StockTask {
|
||||
$Settings = New-ScheduledTaskSettingsSet `
|
||||
-ExecutionTimeLimit (New-TimeSpan -Minutes $LimitMinutes) `
|
||||
-StartWhenAvailable `
|
||||
-WakeToRun `
|
||||
-DontStopIfGoingOnBatteries `
|
||||
-RunOnlyIfNetworkAvailable:$false
|
||||
$Settings.DisallowStartIfOnBatteries = $false
|
||||
@@ -47,15 +48,21 @@ function Register-StockTask {
|
||||
}
|
||||
|
||||
function Register-WatchdogTask {
|
||||
$TaskName = "\StockBot\StockBot_Watchdog"
|
||||
$ScriptPath = Join-Path $Project "scripts\run_watchdog.ps1"
|
||||
$Command = 'schtasks /Create /TN "\StockBot\StockBot_Watchdog" /TR "\"powershell.exe\" -NonInteractive -ExecutionPolicy Bypass -File \"' + $ScriptPath + '\"" /SC MINUTE /MO 5 /ST 09:00 /ET 15:10 /F'
|
||||
$Command = 'schtasks /Create /TN "\StockBot\StockBot_Watchdog" /TR "\"powershell.exe\" -NonInteractive -ExecutionPolicy Bypass -File \"' + $ScriptPath + '\"" /SC WEEKLY /D MON,TUE,WED,THU,FRI /ST 09:00 /RI 5 /DU 06:05 /F'
|
||||
cmd.exe /c $Command | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "StockBot_Watchdog registration failed"
|
||||
}
|
||||
|
||||
Write-Host "[OK] StockBot_Watchdog registered at 09:00-15:10 every 5 minutes" -ForegroundColor Green
|
||||
$Task = Get-ScheduledTask -TaskName "StockBot_Watchdog" -TaskPath $TaskPath
|
||||
$Task.Settings.StartWhenAvailable = $true
|
||||
$Task.Settings.WakeToRun = $true
|
||||
$Task.Settings.DisallowStartIfOnBatteries = $false
|
||||
$Task.Settings.StopIfGoingOnBatteries = $false
|
||||
Set-ScheduledTask -TaskName "StockBot_Watchdog" -TaskPath $TaskPath -Settings $Task.Settings | Out-Null
|
||||
|
||||
Write-Host "[OK] StockBot_Watchdog registered weekdays at 09:00-15:05 every 5 minutes" -ForegroundColor Green
|
||||
}
|
||||
|
||||
Register-StockTask "StockBot_Morning" "08:15" "run_morning.ps1" 20
|
||||
|
||||
+8
-6
@@ -155,8 +155,8 @@ AI는 두 종류로 나뉜다.
|
||||
| 08:15 | `StockBot_Morning` | `/morning`, 뉴스/시장 분석, `daily_context.json` 생성 |
|
||||
| 08:30 | 봇 컨텍스트 로드 | AI 컨텍스트 로드, 유니버스 갱신 |
|
||||
| 08:50 | 목표가 계산 | 전일 고저와 당일 시가 기반 |
|
||||
| 09:00 | 매매 루프 시작 | 실제 신규 진입은 `ENTRY_START=09:15` 이후 |
|
||||
| 09:00-15:10 | `StockBot_Watchdog` | 5분마다 봇 생존 감시 |
|
||||
| 09:00 | 매매 루프 시작 | 실제 신규 진입은 `ENTRY_START=09:20` 이후 |
|
||||
| 09:00-15:05 | `StockBot_Watchdog` | 5분마다 봇 생존 감시, 15:10 결산 직후 재시작 금지 |
|
||||
| 11:00 | 점심 컨텍스트 대기 | `midday_context.json` 전까지 신규 진입 중지 |
|
||||
| 11:20 | `StockBot_Midday` | `/midday`, 점심 세션 조건 생성 |
|
||||
| 14:00 | 신규 진입 중단 | 보유 포지션 청산 체크는 계속 |
|
||||
@@ -177,9 +177,10 @@ AI는 두 종류로 나뉜다.
|
||||
| `StockBot_Midday` | 11:20 | `scripts/run_midday.ps1` |
|
||||
| `StockBot_Evening` | 15:30 | `scripts/run_evening.ps1` |
|
||||
| `StockBot_Training` | 16:00 | `scripts/run_training_pipeline.ps1` |
|
||||
| `StockBot_Watchdog` | 09:00-15:10, 5분마다 | `scripts/run_watchdog.ps1` |
|
||||
| `StockBot_Watchdog` | 09:00-15:05, 5분마다 | `scripts/run_watchdog.ps1` |
|
||||
|
||||
모든 실행 스크립트는 프로젝트 내부 `.venv`의 Python을 우선 사용한다.
|
||||
08:50 이후 재시작 시 봇은 목표가를 즉시 재계산하며, KIS 시가가 `0`이면 목표가 계산에서 제외한다.
|
||||
|
||||
---
|
||||
|
||||
@@ -273,7 +274,7 @@ Restore_StockBot.bat
|
||||
2. KIS REST 요청 제한 초과와 타임아웃이 장초반에 발생할 수 있다.
|
||||
3. WebSocket/Redis 기반 실시간 구조는 아직 미완성이다.
|
||||
4. 실거래 전환 전에는 체결, 부분체결, 미체결, 취소/정정, 재시작 복구 로직이 더 필요하다.
|
||||
5. 초반 09:15 이후에도 손실 집중이 반복되는지 추가 검증이 필요하다.
|
||||
5. 초반 09:20 이후에도 손실 집중이 반복되는지 추가 검증이 필요하다.
|
||||
6. `AI_RISK_SL_MAP`의 한글 키 인코딩은 점검이 필요하다. 정상 risk level과 매핑되지 않으면 리스크별 SL 조정이 무력화될 수 있다.
|
||||
7. 기존 로그와 일부 문서는 인코딩 깨짐이 남아 있어 장기적으로 정리해야 한다.
|
||||
|
||||
@@ -286,10 +287,11 @@ Restore_StockBot.bat
|
||||
| 제안 | 상태 |
|
||||
|------|------|
|
||||
| `ENTRY_START` 09:05 -> 09:15 | 승인 및 적용 |
|
||||
| `ENTRY_START` 09:15 -> 09:20 | 승인 및 적용 |
|
||||
| 장초반 포지션 축소 | 보류 |
|
||||
| 시간대별 SL 강화 | 보류 |
|
||||
|
||||
`ENTRY_START=09:15` 변경 후 최소 5거래 이상 관찰한 뒤 다음 조정을 판단한다.
|
||||
`ENTRY_START=09:20` 변경 후 최소 5거래 이상 관찰한 뒤 다음 조정을 판단한다.
|
||||
|
||||
---
|
||||
|
||||
@@ -323,7 +325,7 @@ DRY_RUN=false
|
||||
1. `AI_RISK_SL_MAP` 인코딩/키 매핑 점검
|
||||
2. KIS API rate-limit 완화
|
||||
3. KIS minute-bar 실응답 검증
|
||||
4. 장초반 09:15 이후 손익 데이터 축적
|
||||
4. 장초반 09:20 이후 손익 데이터 축적
|
||||
5. WebSocket 시세 구조 도입
|
||||
6. 실거래용 주문 복구/부분체결/미체결 처리 강화
|
||||
7. NAS Docker 이전
|
||||
|
||||
Reference in New Issue
Block a user