[2026-05-27] 포맷 후 복구 설치 스크립트 추가
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
import sys, os
|
||||
sys.path.insert(0, '.')
|
||||
from app.db.models import get_conn
|
||||
from datetime import datetime
|
||||
|
||||
with get_conn() as c:
|
||||
pos = c.execute('SELECT ticker, name, entry_time, entry_price, quantity, stop_price FROM positions').fetchall()
|
||||
|
||||
print('=== 현재 포지션 ===')
|
||||
if pos:
|
||||
for r in pos:
|
||||
print(f' {r[1]}({r[0]}) {r[4]}주 @ {r[3]:,.0f}원 SL={r[5]:,.0f}원 진입={r[2]}')
|
||||
else:
|
||||
print(' 없음')
|
||||
|
||||
today = datetime.now().strftime('%Y-%m-%d')
|
||||
with get_conn() as c:
|
||||
trades = c.execute(
|
||||
"SELECT name, exit_reason, pnl FROM trades WHERE date=? AND exit_time IS NOT NULL ORDER BY id",
|
||||
(today,)
|
||||
).fetchall()
|
||||
|
||||
print(f'\n=== 오늘 거래 ({len(trades)}건) ===')
|
||||
wins = sum(1 for t in trades if t[2] and t[2] > 0)
|
||||
losses = len(trades) - wins
|
||||
for t in trades:
|
||||
sign = '+' if t[2] and t[2] > 0 else ''
|
||||
print(f' {t[0]:12} [{t[1]:4}] {sign}{t[2]:,.0f}원')
|
||||
print(f' 승:{wins} 패:{losses}')
|
||||
@@ -0,0 +1,9 @@
|
||||
import asyncio, sys, os
|
||||
sys.path.insert(0, r'C:\Users\whdwo\OneDrive\바탕 화면\stockbot_v3')
|
||||
from app.main import load_env; load_env()
|
||||
from app.monitor.notifier import send
|
||||
mode = os.getenv('KIS_MOCK', 'true')
|
||||
dry = os.getenv('DRY_RUN', 'true')
|
||||
label = '[모의투자]' if mode == 'true' else '[실거래]'
|
||||
asyncio.run(send(f'{label} 자동매매 봇 시작 (DRY_RUN={dry})'))
|
||||
print('Discord 알림 전송 완료')
|
||||
@@ -0,0 +1,25 @@
|
||||
import subprocess, os
|
||||
|
||||
pid_file = r'C:\Users\whdwo\OneDrive\바탕 화면\stockbot_v3\logs\bot.pid'
|
||||
|
||||
if os.path.exists(pid_file):
|
||||
try:
|
||||
pid = int(open(pid_file).read().strip())
|
||||
subprocess.run(['taskkill', '/PID', str(pid), '/F'], capture_output=True)
|
||||
print(f'PID 파일 종료: {pid}')
|
||||
except Exception as e:
|
||||
print(f'PID 파일 종료 실패: {e}')
|
||||
os.remove(pid_file)
|
||||
|
||||
r = subprocess.run(
|
||||
['powershell', '-Command',
|
||||
'Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -like "*app/main.py*" -or $_.CommandLine -like "*app\\main.py*" } | Select-Object -ExpandProperty ProcessId'],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
pids = [p.strip() for p in r.stdout.strip().splitlines() if p.strip().isdigit()]
|
||||
for pid in pids:
|
||||
subprocess.run(['taskkill', '/PID', pid, '/F'], capture_output=True)
|
||||
print(f'잔존 프로세스 종료: {pid}')
|
||||
|
||||
if not pids:
|
||||
print('실행 중인 봇 없음 — 새로 시작합니다')
|
||||
@@ -0,0 +1,29 @@
|
||||
import subprocess, os
|
||||
|
||||
pid_file = r'C:\Users\whdwo\OneDrive\바탕 화면\stockbot_v3\logs\bot.pid'
|
||||
|
||||
# 1-a) PID 파일로 종료
|
||||
if os.path.exists(pid_file):
|
||||
try:
|
||||
pid = int(open(pid_file).read().strip())
|
||||
subprocess.run(['taskkill', '/PID', str(pid), '/F'], capture_output=True)
|
||||
print(f'PID 파일 종료: {pid}')
|
||||
except Exception as e:
|
||||
print(f'PID 파일 종료 실패: {e}')
|
||||
os.remove(pid_file)
|
||||
else:
|
||||
print('PID 파일 없음')
|
||||
|
||||
# 1-b) Get-CimInstance로 잔존 프로세스 스캔
|
||||
r = subprocess.run(
|
||||
['powershell', '-Command',
|
||||
'Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -like "*app/main.py*" -or $_.CommandLine -like "*app\\main.py*" } | Select-Object -ExpandProperty ProcessId'],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
pids = [p.strip() for p in r.stdout.strip().splitlines() if p.strip().isdigit()]
|
||||
for pid in pids:
|
||||
subprocess.run(['taskkill', '/PID', pid, '/F'], capture_output=True)
|
||||
print(f'잔존 프로세스 종료: {pid}')
|
||||
|
||||
if not pids:
|
||||
print('실행 중인 봇 없음 — 새로 시작합니다')
|
||||
@@ -0,0 +1,9 @@
|
||||
import asyncio, sys, os
|
||||
sys.path.insert(0, '.')
|
||||
from app.main import load_env; load_env()
|
||||
from app.monitor.notifier import send
|
||||
mode = os.getenv('KIS_MOCK', 'true')
|
||||
dry = os.getenv('DRY_RUN', 'true')
|
||||
label = '[모의투자]' if mode == 'true' else '[실거래]'
|
||||
asyncio.run(send(f'{label} 자동매매 봇 시작 (DRY_RUN={dry})'))
|
||||
print('Discord 알림 전송 완료')
|
||||
@@ -0,0 +1,9 @@
|
||||
import asyncio, sys, os
|
||||
sys.path.insert(0, '.')
|
||||
from app.main import load_env; load_env()
|
||||
from app.monitor.notifier import send
|
||||
mode = os.getenv('KIS_MOCK', 'true')
|
||||
dry = os.getenv('DRY_RUN', 'true')
|
||||
label = '[모의투자]' if mode == 'true' else '[실거래]'
|
||||
asyncio.run(send(f'{label} 자동매매 봇 시작 (DRY_RUN={dry})'))
|
||||
print('Discord 알림 전송 완료')
|
||||
@@ -0,0 +1,113 @@
|
||||
"""
|
||||
워치독 태스크를 XML로 직접 등록
|
||||
관리자 권한 없이 현재 사용자 세션으로 등록
|
||||
"""
|
||||
import subprocess, sys, os, tempfile
|
||||
|
||||
PROJECT = r'C:\Users\whdwo\OneDrive\바탕 화면\stockbot_v3'
|
||||
SCRIPT = fr'{PROJECT}\scripts\run_watchdog.ps1'
|
||||
|
||||
xml = f'''<?xml version="1.0" encoding="UTF-16"?>
|
||||
<Task version="1.3" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
||||
<RegistrationInfo>
|
||||
<URI>\\StockBot\\StockBot_Watchdog</URI>
|
||||
</RegistrationInfo>
|
||||
<Principals>
|
||||
<Principal id="Author">
|
||||
<LogonType>InteractiveToken</LogonType>
|
||||
<RunLevel>LeastPrivilege</RunLevel>
|
||||
</Principal>
|
||||
</Principals>
|
||||
<Settings>
|
||||
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
|
||||
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
|
||||
<ExecutionTimeLimit>PT3M</ExecutionTimeLimit>
|
||||
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
|
||||
<StartWhenAvailable>true</StartWhenAvailable>
|
||||
<IdleSettings>
|
||||
<Duration>PT10M</Duration>
|
||||
<WaitTimeout>PT1H</WaitTimeout>
|
||||
<StopOnIdleEnd>true</StopOnIdleEnd>
|
||||
<RestartOnIdle>false</RestartOnIdle>
|
||||
</IdleSettings>
|
||||
</Settings>
|
||||
<Triggers>
|
||||
<CalendarTrigger>
|
||||
<StartBoundary>2026-05-27T09:00:00+09:00</StartBoundary>
|
||||
<Repetition>
|
||||
<Interval>PT5M</Interval>
|
||||
<Duration>PT6H10M</Duration>
|
||||
<StopAtDurationEnd>true</StopAtDurationEnd>
|
||||
</Repetition>
|
||||
<ScheduleByWeek>
|
||||
<WeeksInterval>1</WeeksInterval>
|
||||
<DaysOfWeek>
|
||||
<Monday />
|
||||
<Tuesday />
|
||||
<Wednesday />
|
||||
<Thursday />
|
||||
<Friday />
|
||||
</DaysOfWeek>
|
||||
</ScheduleByWeek>
|
||||
</CalendarTrigger>
|
||||
</Triggers>
|
||||
<Actions Context="Author">
|
||||
<Exec>
|
||||
<Command>powershell.exe</Command>
|
||||
<Arguments>-NonInteractive -ExecutionPolicy Bypass -File "{SCRIPT}"</Arguments>
|
||||
<WorkingDirectory>{PROJECT}</WorkingDirectory>
|
||||
</Exec>
|
||||
</Actions>
|
||||
</Task>'''
|
||||
|
||||
# UTF-16 LE BOM으로 임시 파일 저장 (Task Scheduler XML 요구사항)
|
||||
with tempfile.NamedTemporaryFile(suffix='.xml', delete=False, mode='wb') as f:
|
||||
f.write(b'\xff\xfe') # UTF-16 LE BOM
|
||||
f.write(xml.encode('utf-16-le'))
|
||||
tmp = f.name
|
||||
|
||||
try:
|
||||
# 1단계: XML로 등록
|
||||
ps_cmd = f'Register-ScheduledTask -TaskName "StockBot_Watchdog" -TaskPath "\\\\StockBot\\\\" -Xml (Get-Content -Path \'{tmp}\' -Raw -Encoding Unicode) -Force | Out-Null; Write-Host "1단계 완료"'
|
||||
r = subprocess.run(
|
||||
['powershell', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', ps_cmd],
|
||||
capture_output=True, text=True, encoding='utf-8', errors='replace'
|
||||
)
|
||||
print(r.stdout.strip())
|
||||
|
||||
# 2단계: 한글 경로 깨짐 교정 (setup_scheduler.ps1과 동일 방식)
|
||||
# 등록된 태스크의 Arguments에서 garbled 문자열 추출 → 정상 한글로 치환 후 재등록
|
||||
fix_cmd = r'''
|
||||
$task = Get-ScheduledTask -TaskName "StockBot_Watchdog" -TaskPath "\StockBot\"
|
||||
$stored = $task.Actions[0].Arguments
|
||||
if ($stored -match [regex]::Escape("바탕 화면")) {
|
||||
Write-Host "경로 정상"
|
||||
} else {
|
||||
$xml = Export-ScheduledTask -TaskName "StockBot_Watchdog" -TaskPath "\StockBot\"
|
||||
if ($stored -match 'OneDrive\\(.+?)\\stockbot') {
|
||||
$garbled = $Matches[1]
|
||||
$fixedXml = $xml.Replace($garbled, "바탕 화면")
|
||||
Register-ScheduledTask -TaskName "StockBot_Watchdog" -TaskPath "\StockBot\" -Xml $fixedXml -Force | Out-Null
|
||||
Write-Host "한글 경로 교정 완료"
|
||||
} else {
|
||||
Write-Host "교정 패턴 미일치 — 수동 확인 필요"
|
||||
}
|
||||
}
|
||||
'''
|
||||
r2 = subprocess.run(
|
||||
['powershell', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', fix_cmd],
|
||||
capture_output=True, text=True, encoding='utf-8', errors='replace'
|
||||
)
|
||||
print(r2.stdout.strip())
|
||||
if r2.returncode != 0:
|
||||
print('STDERR:', r2.stderr.strip())
|
||||
|
||||
# 최종 확인
|
||||
check = subprocess.run(
|
||||
['powershell', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command',
|
||||
'Get-ScheduledTask -TaskPath "\\StockBot\\" | Format-Table TaskName, State -AutoSize'],
|
||||
capture_output=True, text=True, encoding='utf-8', errors='replace'
|
||||
)
|
||||
print(check.stdout)
|
||||
finally:
|
||||
os.unlink(tmp)
|
||||
@@ -0,0 +1,17 @@
|
||||
import asyncio, json, sys
|
||||
sys.path.insert(0, '.')
|
||||
from app.main import load_env; load_env()
|
||||
from app.monitor.notifier import send
|
||||
|
||||
ctx = json.load(open('data/midday_context.json', encoding='utf-8'))
|
||||
hot = ', '.join(ctx.get('hot_sectors', [])) or '없음'
|
||||
avoid = ', '.join(ctx.get('avoid_sectors', [])) or '없음'
|
||||
flag = '✅ 점심진입허용' if ctx.get('lunch_trade_allowed', True) else '🚫 점심진입중단'
|
||||
msg = (
|
||||
f'[장중분석] {ctx["date"]} {ctx.get("generated_at","")}\n'
|
||||
f'{flag} | 포지션배율: x{ctx.get("position_size_multiplier", 1.0)}\n'
|
||||
f'주목: {hot} | 회피: {avoid}\n'
|
||||
f'📝 {ctx.get("reason","")}'
|
||||
)
|
||||
asyncio.run(send(msg))
|
||||
print('Discord 전송 완료')
|
||||
@@ -0,0 +1,54 @@
|
||||
import subprocess, os, sys, asyncio, time
|
||||
|
||||
BASE = r'C:\Users\whdwo\OneDrive\바탕 화면\stockbot_v3'
|
||||
pid_file = os.path.join(BASE, 'logs', 'bot.pid')
|
||||
os.chdir(BASE)
|
||||
sys.path.insert(0, BASE)
|
||||
|
||||
# 토큰 발급 가능한지 테스트 (최대 3분 재시도)
|
||||
from app.main import load_env
|
||||
load_env()
|
||||
|
||||
max_wait = 180
|
||||
interval = 15
|
||||
elapsed = 0
|
||||
while elapsed < max_wait:
|
||||
try:
|
||||
import httpx
|
||||
from app.execution.kis_client import KISClient
|
||||
client = KISClient()
|
||||
asyncio.run(client.get_access_token())
|
||||
print(f"토큰 발급 성공 ({elapsed}초 경과)")
|
||||
break
|
||||
except RuntimeError as e:
|
||||
if "EGW00133" in str(e) or "잠시 후" in str(e):
|
||||
print(f"토큰 대기 중... ({elapsed}초, {interval}초 후 재시도)")
|
||||
time.sleep(interval)
|
||||
elapsed += interval
|
||||
else:
|
||||
print(f"토큰 오류 (재시도 불가): {e}")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print("토큰 발급 타임아웃")
|
||||
sys.exit(1)
|
||||
|
||||
# 봇 시작
|
||||
log_path = os.path.join(BASE, "logs", "bot_stderr.log")
|
||||
proc = subprocess.Popen(
|
||||
[sys.executable, "app/main.py"],
|
||||
creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP,
|
||||
stdout=open(log_path, "a", encoding="utf-8"),
|
||||
stderr=subprocess.STDOUT,
|
||||
close_fds=True,
|
||||
)
|
||||
with open(pid_file, "w") as f:
|
||||
f.write(str(proc.pid))
|
||||
print(f"봇 시작 완료 PID={proc.pid}")
|
||||
|
||||
from app.monitor.notifier import send
|
||||
import os as _os
|
||||
mode = _os.getenv("KIS_MOCK", "true")
|
||||
dry = _os.getenv("DRY_RUN", "true")
|
||||
label = "[모의투자]" if mode == "true" else "[실거래]"
|
||||
asyncio.run(send(f"{label} 자동매매 봇 시작 (DRY_RUN={dry})"))
|
||||
print("Discord 알림 전송 완료")
|
||||
@@ -0,0 +1,52 @@
|
||||
import subprocess, os, sys, asyncio
|
||||
|
||||
pid_file = r'C:\Users\whdwo\OneDrive\바탕 화면\stockbot_v3\logs\bot.pid'
|
||||
project_dir = r'C:\Users\whdwo\OneDrive\바탕 화면\stockbot_v3'
|
||||
|
||||
# 1-a) PID 파일로 종료
|
||||
if os.path.exists(pid_file):
|
||||
try:
|
||||
pid = int(open(pid_file).read().strip())
|
||||
subprocess.run(['taskkill', '/PID', str(pid), '/F'], capture_output=True)
|
||||
print(f'PID 파일 종료: {pid}')
|
||||
except Exception as e:
|
||||
print(f'PID 파일 종료 실패: {e}')
|
||||
os.remove(pid_file)
|
||||
|
||||
# 1-b) Get-CimInstance로 잔존 프로세스 스캔
|
||||
r = subprocess.run(
|
||||
['powershell', '-Command',
|
||||
'Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -like "*app/main.py*" -or $_.CommandLine -like "*app\\main.py*" } | Select-Object -ExpandProperty ProcessId'],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
pids = [p.strip() for p in r.stdout.strip().splitlines() if p.strip().isdigit()]
|
||||
for pid in pids:
|
||||
subprocess.run(['taskkill', '/PID', pid, '/F'], capture_output=True)
|
||||
print(f'잔존 프로세스 종료: {pid}')
|
||||
|
||||
if not pids:
|
||||
print('실행 중인 봇 없음 — 새로 시작합니다')
|
||||
|
||||
# 2. 봇 시작
|
||||
os.chdir(project_dir)
|
||||
proc = subprocess.Popen(
|
||||
[sys.executable, 'app/main.py'],
|
||||
creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP,
|
||||
stdout=open('logs/bot_stderr.log', 'a', encoding='utf-8'),
|
||||
stderr=subprocess.STDOUT,
|
||||
close_fds=True,
|
||||
)
|
||||
with open('logs/bot.pid', 'w') as f:
|
||||
f.write(str(proc.pid))
|
||||
print(f'봇 시작 완료 PID={proc.pid}')
|
||||
|
||||
# 3. Discord 알림
|
||||
sys.path.insert(0, '.')
|
||||
from app.main import load_env
|
||||
load_env()
|
||||
from app.monitor.notifier import send
|
||||
mode = os.getenv('KIS_MOCK', 'true')
|
||||
dry = os.getenv('DRY_RUN', 'true')
|
||||
label = '[모의투자]' if mode == 'true' else '[실거래]'
|
||||
asyncio.run(send(f'{label} 자동매매 봇 시작 (DRY_RUN={dry})'))
|
||||
print('Discord 알림 전송 완료')
|
||||
@@ -0,0 +1,14 @@
|
||||
import subprocess, sys, os
|
||||
|
||||
os.chdir(r'C:\Users\whdwo\OneDrive\바탕 화면\stockbot_v3')
|
||||
proc = subprocess.Popen(
|
||||
[sys.executable, 'app/main.py'],
|
||||
creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP,
|
||||
stdout=open('logs/bot_stderr.log', 'a', encoding='utf-8'),
|
||||
stderr=subprocess.STDOUT,
|
||||
close_fds=True,
|
||||
)
|
||||
# PID 파일 저장 (BOM 없이 저장)
|
||||
with open('logs/bot.pid', 'w', encoding='utf-8', newline='') as f:
|
||||
f.write(str(proc.pid))
|
||||
print(f'봇 시작 완료 PID={proc.pid}')
|
||||
@@ -0,0 +1,58 @@
|
||||
import subprocess, os, sys, asyncio
|
||||
|
||||
BASE = r'C:\Users\whdwo\OneDrive\바탕 화면\stockbot_v3'
|
||||
pid_file = os.path.join(BASE, 'logs', 'bot.pid')
|
||||
|
||||
# 1-a) PID 파일로 종료
|
||||
if os.path.exists(pid_file):
|
||||
try:
|
||||
pid = int(open(pid_file).read().strip())
|
||||
result = subprocess.run(["taskkill", "/PID", str(pid), "/F"], capture_output=True, text=True)
|
||||
print(f"PID 파일 종료: {pid} -> returncode={result.returncode}")
|
||||
except Exception as e:
|
||||
print(f"PID 파일 종료 실패: {e}")
|
||||
os.remove(pid_file)
|
||||
else:
|
||||
print("PID 파일 없음")
|
||||
|
||||
# 1-b) 잔존 프로세스 스캔
|
||||
r = subprocess.run(
|
||||
["powershell", "-Command",
|
||||
"Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -like '*app/main.py*' -or $_.CommandLine -like '*app\\\\main.py*' } | Select-Object -ExpandProperty ProcessId"],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
pids = [p.strip() for p in r.stdout.strip().splitlines() if p.strip().isdigit()]
|
||||
for pid in pids:
|
||||
result = subprocess.run(["taskkill", "/PID", pid, "/F"], capture_output=True, text=True)
|
||||
print(f"잔존 프로세스 종료: {pid} -> returncode={result.returncode}")
|
||||
|
||||
if not pids:
|
||||
print("실행 중인 봇 없음 — 새로 시작합니다")
|
||||
|
||||
# 2. 봇 시작 + PID 저장
|
||||
os.chdir(BASE)
|
||||
log_path = os.path.join(BASE, "logs", "bot_stderr.log")
|
||||
env = os.environ.copy()
|
||||
env["PYTHONUNBUFFERED"] = "1"
|
||||
proc = subprocess.Popen(
|
||||
[sys.executable, "-u", "app/main.py"],
|
||||
creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP,
|
||||
stdout=open(log_path, "a", encoding="utf-8"),
|
||||
stderr=subprocess.STDOUT,
|
||||
close_fds=True,
|
||||
env=env,
|
||||
)
|
||||
with open(pid_file, "w") as f:
|
||||
f.write(str(proc.pid))
|
||||
print(f"봇 시작 완료 PID={proc.pid}")
|
||||
|
||||
# 3. Discord 알림
|
||||
sys.path.insert(0, BASE)
|
||||
from app.main import load_env
|
||||
load_env()
|
||||
from app.monitor.notifier import send
|
||||
mode = os.getenv("KIS_MOCK", "true")
|
||||
dry = os.getenv("DRY_RUN", "true")
|
||||
label = "[모의투자]" if mode == "true" else "[실거래]"
|
||||
asyncio.run(send(f"{label} 자동매매 봇 시작 (DRY_RUN={dry})"))
|
||||
print("Discord 알림 전송 완료")
|
||||
@@ -0,0 +1,15 @@
|
||||
import sys, asyncio
|
||||
sys.path.insert(0, '.')
|
||||
from app.main import load_env, StockBot
|
||||
load_env()
|
||||
|
||||
async def test():
|
||||
bot = StockBot()
|
||||
print("StockBot 생성 완료")
|
||||
await bot.initialize()
|
||||
print("initialize() 완료")
|
||||
print("5초 슬립...")
|
||||
await asyncio.sleep(5)
|
||||
print("5초 완료 - 정상 실행 중")
|
||||
|
||||
asyncio.run(test())
|
||||
@@ -0,0 +1,170 @@
|
||||
"""
|
||||
Build training rows from external minute bars.
|
||||
|
||||
This generates synthetic candidate rows from minute bars, not actual bot trades.
|
||||
Rows are useful for pretraining movement/holding-period models.
|
||||
"""
|
||||
import argparse
|
||||
import csv
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
MINUTE_ROOT = ROOT / "data" / "external" / "minute"
|
||||
DAILY_ROOT = ROOT / "data" / "external" / "daily"
|
||||
DEFAULT_OUT = ROOT / "data" / "external_training_dataset.csv"
|
||||
|
||||
|
||||
CHECKPOINTS = (1, 3, 5, 10)
|
||||
|
||||
|
||||
def _read_csv(path: Path) -> list[dict]:
|
||||
with path.open("r", encoding="utf-8-sig", newline="") as f:
|
||||
return list(csv.DictReader(f))
|
||||
|
||||
|
||||
def _num(value, default=0.0):
|
||||
try:
|
||||
return float(str(value).replace(",", ""))
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
|
||||
def _load_daily_amounts() -> dict[tuple[str, str], dict]:
|
||||
result = {}
|
||||
for file in DAILY_ROOT.glob("*/stocks.csv"):
|
||||
rows = _read_csv(file)
|
||||
for row in rows:
|
||||
date = str(row.get("date") or file.parent.name)
|
||||
ticker = str(row.get("ticker") or row.get("티커") or "")
|
||||
if ticker:
|
||||
result[(date, ticker)] = row
|
||||
return result
|
||||
|
||||
|
||||
def _future_metrics(rows: list[dict], idx: int, entry_price: float):
|
||||
metrics = {}
|
||||
highs = []
|
||||
lows = []
|
||||
for minutes in CHECKPOINTS:
|
||||
j = idx + minutes
|
||||
if j >= len(rows):
|
||||
metrics[f"price_{minutes}m"] = ""
|
||||
metrics[f"ret_{minutes}m"] = ""
|
||||
metrics[f"mfe_{minutes}m"] = ""
|
||||
metrics[f"mae_{minutes}m"] = ""
|
||||
continue
|
||||
window = rows[idx + 1:j + 1]
|
||||
highs.extend(_num(r["high"]) for r in window)
|
||||
lows.extend(_num(r["low"]) for r in window)
|
||||
close = _num(rows[j]["close"])
|
||||
high = max(highs) if highs else entry_price
|
||||
low = min(lows) if lows else entry_price
|
||||
metrics[f"price_{minutes}m"] = close
|
||||
metrics[f"ret_{minutes}m"] = (close - entry_price) / entry_price * 100 if entry_price else 0
|
||||
metrics[f"mfe_{minutes}m"] = (high - entry_price) / entry_price * 100 if entry_price else 0
|
||||
metrics[f"mae_{minutes}m"] = (low - entry_price) / entry_price * 100 if entry_price else 0
|
||||
return metrics
|
||||
|
||||
|
||||
def _rows_for_file(path: Path, daily: dict, k: float, breakout_only: bool):
|
||||
rows = _read_csv(path)
|
||||
rows = [r for r in rows if r.get("time") and _num(r.get("close")) > 0]
|
||||
rows.sort(key=lambda r: (r.get("date", ""), r.get("time", "")))
|
||||
if len(rows) < 20:
|
||||
return []
|
||||
|
||||
by_date = defaultdict(list)
|
||||
for row in rows:
|
||||
by_date[row["date"]].append(row)
|
||||
|
||||
out = []
|
||||
prev_by_date = {}
|
||||
for date in sorted(by_date):
|
||||
day_rows = by_date[date]
|
||||
ticker = day_rows[0]["ticker"]
|
||||
daily_row = daily.get((date, ticker), {})
|
||||
prev_high = _num(daily_row.get("high"))
|
||||
prev_low = _num(daily_row.get("low"))
|
||||
prev_amount = _num(daily_row.get("amount"))
|
||||
|
||||
if not prev_high or not prev_low:
|
||||
prev = prev_by_date.get(ticker)
|
||||
if prev:
|
||||
prev_high = prev["high"]
|
||||
prev_low = prev["low"]
|
||||
prev_amount = prev["amount"]
|
||||
|
||||
today_open = _num(day_rows[0]["open"]) or _num(day_rows[0]["close"])
|
||||
target = today_open + (prev_high - prev_low) * k if prev_high and prev_low else 0
|
||||
crossed = False
|
||||
|
||||
for idx, row in enumerate(day_rows[:-max(CHECKPOINTS)]):
|
||||
tm = row["time"][:4]
|
||||
if tm < "0905" or tm >= "1400":
|
||||
continue
|
||||
close = _num(row["close"])
|
||||
if breakout_only:
|
||||
if not target or close < target or crossed:
|
||||
continue
|
||||
crossed = True
|
||||
|
||||
metrics = _future_metrics(day_rows, idx, close)
|
||||
label_win = 1 if _num(metrics.get("ret_10m")) > 0 else 0
|
||||
label_stop_loss = 1 if _num(metrics.get("mae_10m")) <= -2.0 else 0
|
||||
out.append({
|
||||
"source": "external_minute",
|
||||
"date": date,
|
||||
"ticker": ticker,
|
||||
"entry_time": row["time"],
|
||||
"current_price": close,
|
||||
"entry_price": close,
|
||||
"target_price": target,
|
||||
"today_open": today_open,
|
||||
"prev_high": prev_high,
|
||||
"prev_low": prev_low,
|
||||
"prev_amount": prev_amount,
|
||||
"volume": row.get("volume", ""),
|
||||
**metrics,
|
||||
"label_win": label_win,
|
||||
"label_stop_loss": label_stop_loss,
|
||||
})
|
||||
|
||||
prev_by_date[ticker] = {
|
||||
"high": max(_num(r["high"]) for r in day_rows),
|
||||
"low": min(_num(r["low"]) for r in day_rows),
|
||||
"amount": sum(_num(r["close"]) * _num(r["volume"]) for r in day_rows),
|
||||
}
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--minute-root", default=str(MINUTE_ROOT))
|
||||
parser.add_argument("--out", default=str(DEFAULT_OUT))
|
||||
parser.add_argument("--k", type=float, default=0.5)
|
||||
parser.add_argument("--all-minutes", action="store_true", help="Use every eligible minute, not only first breakout.")
|
||||
args = parser.parse_args()
|
||||
|
||||
daily = _load_daily_amounts()
|
||||
all_rows = []
|
||||
for path in Path(args.minute_root).glob("*/*.csv"):
|
||||
all_rows.extend(_rows_for_file(path, daily, args.k, breakout_only=not args.all_minutes))
|
||||
|
||||
out_path = Path(args.out)
|
||||
if not out_path.is_absolute():
|
||||
out_path = ROOT / out_path
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
fieldnames = sorted({key for row in all_rows for key in row.keys()})
|
||||
with out_path.open("w", encoding="utf-8-sig", newline="") as f:
|
||||
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
||||
writer.writeheader()
|
||||
writer.writerows(all_rows)
|
||||
|
||||
print(f"external dataset rows={len(all_rows)} -> {out_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,84 @@
|
||||
"""
|
||||
Collect daily Korean-market features for model training.
|
||||
|
||||
Outputs:
|
||||
data/external/daily/YYYYMMDD/stocks.csv
|
||||
data/external/daily/YYYYMMDD/indexes.csv
|
||||
"""
|
||||
import argparse
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import pandas as pd
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
OUT_ROOT = ROOT / "data" / "external" / "daily"
|
||||
|
||||
|
||||
def _yyyymmdd(date_text: str | None) -> str:
|
||||
if date_text:
|
||||
return date_text.replace("-", "")
|
||||
return datetime.now().strftime("%Y%m%d")
|
||||
|
||||
|
||||
def _standardize_stock_ohlcv(df: pd.DataFrame, date_yyyymmdd: str) -> pd.DataFrame:
|
||||
df = df.reset_index()
|
||||
columns = list(df.columns)
|
||||
rename = {columns[0]: "ticker"}
|
||||
standard = ["open", "high", "low", "close", "volume", "amount", "change_pct"]
|
||||
for source, target in zip(columns[1:], standard):
|
||||
rename[source] = target
|
||||
df = df.rename(columns=rename)
|
||||
df.insert(0, "date", date_yyyymmdd)
|
||||
return df[[c for c in ["date", "ticker", *standard] if c in df.columns]]
|
||||
|
||||
|
||||
def _standardize_index_row(row: dict, date_yyyymmdd: str, code: str, name: str) -> dict:
|
||||
values = list(row.values())
|
||||
keys = ["open", "high", "low", "close", "volume", "amount", "change_pct"]
|
||||
out = {"date": date_yyyymmdd, "code": code, "name": name}
|
||||
for key, value in zip(keys, values):
|
||||
out[key] = value
|
||||
return out
|
||||
|
||||
|
||||
def collect_with_pykrx(date_yyyymmdd: str, out_dir: Path):
|
||||
try:
|
||||
from pykrx import stock
|
||||
except ImportError as exc:
|
||||
raise RuntimeError("pykrx is not installed. Install requirements first.") from exc
|
||||
|
||||
stocks_raw = stock.get_market_ohlcv_by_ticker(date_yyyymmdd, market="ALL")
|
||||
stocks = _standardize_stock_ohlcv(stocks_raw, date_yyyymmdd)
|
||||
stocks.to_csv(out_dir / "stocks.csv", index=False, encoding="utf-8-sig")
|
||||
|
||||
index_rows = []
|
||||
for code, name in (("1001", "KOSPI"), ("2001", "KOSDAQ")):
|
||||
try:
|
||||
df = stock.get_index_ohlcv_by_date(date_yyyymmdd, date_yyyymmdd, code)
|
||||
if not df.empty:
|
||||
index_rows.append(_standardize_index_row(df.iloc[-1].to_dict(), date_yyyymmdd, code, name))
|
||||
except Exception as exc:
|
||||
print(f"index fetch failed {name}: {exc}", file=sys.stderr)
|
||||
|
||||
pd.DataFrame(index_rows).to_csv(out_dir / "indexes.csv", index=False, encoding="utf-8-sig")
|
||||
return len(stocks), len(index_rows)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--date", help="YYYY-MM-DD or YYYYMMDD. Defaults to today.")
|
||||
args = parser.parse_args()
|
||||
|
||||
date_yyyymmdd = _yyyymmdd(args.date)
|
||||
out_dir = OUT_ROOT / date_yyyymmdd
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
stock_count, index_count = collect_with_pykrx(date_yyyymmdd, out_dir)
|
||||
print(f"saved daily features: stocks={stock_count}, indexes={index_count}, dir={out_dir}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,102 @@
|
||||
"""
|
||||
Collect KIS intraday minute bars for selected tickers.
|
||||
|
||||
Usage:
|
||||
python scripts/collect_minute_data.py --tickers 005930,000660
|
||||
python scripts/collect_minute_data.py --top 30
|
||||
"""
|
||||
import argparse
|
||||
import asyncio
|
||||
import csv
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from app.main import load_env
|
||||
from app.execution.kis_client import KISClient
|
||||
|
||||
|
||||
OUT_ROOT = ROOT / "data" / "external" / "minute"
|
||||
|
||||
|
||||
def _date_dir(date_text: str | None) -> str:
|
||||
if date_text:
|
||||
return date_text.replace("-", "")
|
||||
return datetime.now().strftime("%Y%m%d")
|
||||
|
||||
|
||||
def _load_cached_tickers(limit: int) -> list[str]:
|
||||
cache = ROOT / "data" / "universe_cache.json"
|
||||
if not cache.exists():
|
||||
return []
|
||||
data = json.loads(cache.read_text(encoding="utf-8"))
|
||||
return list(data.get("tickers", []))[:limit]
|
||||
|
||||
|
||||
async def _resolve_tickers(kis: KISClient, args) -> list[str]:
|
||||
if args.tickers:
|
||||
return [t.strip() for t in args.tickers.split(",") if t.strip()]
|
||||
|
||||
cached = _load_cached_tickers(args.top)
|
||||
if cached:
|
||||
return cached
|
||||
|
||||
rank = await kis.get_volume_rank(top_n=args.top)
|
||||
return [r["ticker"] for r in rank]
|
||||
|
||||
|
||||
def _write_csv(path: Path, rows: list[dict]):
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with path.open("w", newline="", encoding="utf-8-sig") as f:
|
||||
writer = csv.DictWriter(f, fieldnames=["date", "time", "ticker", "open", "high", "low", "close", "volume"])
|
||||
writer.writeheader()
|
||||
writer.writerows(rows)
|
||||
|
||||
|
||||
async def main_async(args):
|
||||
load_env()
|
||||
if args.real_quotes:
|
||||
os.environ["KIS_MOCK"] = "false"
|
||||
|
||||
kis = KISClient()
|
||||
await kis.get_access_token()
|
||||
tickers = await _resolve_tickers(kis, args)
|
||||
|
||||
out_dir = OUT_ROOT / _date_dir(args.date)
|
||||
saved = 0
|
||||
for ticker in tickers:
|
||||
try:
|
||||
rows = await kis.get_ohlcv_minute(ticker, hour=args.hour)
|
||||
if rows:
|
||||
_write_csv(out_dir / f"{ticker}.csv", rows)
|
||||
saved += 1
|
||||
print(f"saved {ticker}: {len(rows)} rows")
|
||||
else:
|
||||
print(f"no rows {ticker}")
|
||||
except Exception as exc:
|
||||
print(f"failed {ticker}: {exc}", file=sys.stderr)
|
||||
await asyncio.sleep(args.sleep)
|
||||
|
||||
print(f"minute collection done: saved={saved}/{len(tickers)}, dir={out_dir}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--date", help="YYYY-MM-DD or YYYYMMDD. Defaults to today.")
|
||||
parser.add_argument("--tickers", help="Comma-separated tickers.")
|
||||
parser.add_argument("--top", type=int, default=30)
|
||||
parser.add_argument("--hour", default="153000", help="KIS upper-bound time HHMMSS.")
|
||||
parser.add_argument("--sleep", type=float, default=1.1)
|
||||
parser.add_argument("--real-quotes", action="store_true", help="Use real quote API even if .env is mock.")
|
||||
args = parser.parse_args()
|
||||
asyncio.run(main_async(args))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,13 @@
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$Root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
|
||||
Set-Location $Root
|
||||
|
||||
$Python = "python"
|
||||
$Requirements = Join-Path $Root "requirements.txt"
|
||||
$Wheelhouse = Join-Path $Root "vendor\wheels"
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $Wheelhouse | Out-Null
|
||||
|
||||
Write-Host "[download] saving packages to $Wheelhouse"
|
||||
& $Python -m pip download -r $Requirements -d $Wheelhouse
|
||||
@@ -0,0 +1,128 @@
|
||||
"""
|
||||
Export entry snapshots joined with trade outcomes for later model training.
|
||||
|
||||
Usage:
|
||||
python scripts/export_training_dataset.py
|
||||
python scripts/export_training_dataset.py data/training_dataset.csv
|
||||
"""
|
||||
import csv
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from app.db.models import get_conn, init_db
|
||||
|
||||
|
||||
DEFAULT_OUT = ROOT / "data" / "training_dataset.csv"
|
||||
|
||||
|
||||
def export_csv(out_path: Path):
|
||||
init_db()
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
query = """
|
||||
WITH post AS (
|
||||
SELECT
|
||||
trade_id,
|
||||
MAX(CASE WHEN elapsed_sec = 60 THEN current_price END) AS price_1m,
|
||||
MAX(CASE WHEN elapsed_sec = 60 THEN return_pct END) AS ret_1m,
|
||||
MAX(CASE WHEN elapsed_sec = 60 THEN mfe_pct END) AS mfe_1m,
|
||||
MAX(CASE WHEN elapsed_sec = 60 THEN mae_pct END) AS mae_1m,
|
||||
MAX(CASE WHEN elapsed_sec = 180 THEN current_price END) AS price_3m,
|
||||
MAX(CASE WHEN elapsed_sec = 180 THEN return_pct END) AS ret_3m,
|
||||
MAX(CASE WHEN elapsed_sec = 180 THEN mfe_pct END) AS mfe_3m,
|
||||
MAX(CASE WHEN elapsed_sec = 180 THEN mae_pct END) AS mae_3m,
|
||||
MAX(CASE WHEN elapsed_sec = 300 THEN current_price END) AS price_5m,
|
||||
MAX(CASE WHEN elapsed_sec = 300 THEN return_pct END) AS ret_5m,
|
||||
MAX(CASE WHEN elapsed_sec = 300 THEN mfe_pct END) AS mfe_5m,
|
||||
MAX(CASE WHEN elapsed_sec = 300 THEN mae_pct END) AS mae_5m,
|
||||
MAX(CASE WHEN elapsed_sec = 600 THEN current_price END) AS price_10m,
|
||||
MAX(CASE WHEN elapsed_sec = 600 THEN return_pct END) AS ret_10m,
|
||||
MAX(CASE WHEN elapsed_sec = 600 THEN mfe_pct END) AS mfe_10m,
|
||||
MAX(CASE WHEN elapsed_sec = 600 THEN mae_pct END) AS mae_10m
|
||||
FROM post_entry_snapshots
|
||||
GROUP BY trade_id
|
||||
)
|
||||
SELECT
|
||||
s.trade_id,
|
||||
s.date,
|
||||
s.ticker,
|
||||
s.name,
|
||||
s.entry_time,
|
||||
s.current_price,
|
||||
s.entry_price,
|
||||
s.target_price,
|
||||
s.stop_price,
|
||||
s.today_open,
|
||||
s.prev_high,
|
||||
s.prev_low,
|
||||
s.prev_amount,
|
||||
s.volume,
|
||||
s.change_pct,
|
||||
s.market_sentiment,
|
||||
s.sentiment_score,
|
||||
s.risk_level,
|
||||
s.trade_allowed,
|
||||
s.hot_sectors,
|
||||
s.avoid_sectors,
|
||||
s.boosted_tickers,
|
||||
s.blacklist_tickers,
|
||||
s.ai_boosted,
|
||||
s.ai_win_score,
|
||||
s.ai_stop_loss_score,
|
||||
s.ai_model_version,
|
||||
s.position_size_multiplier,
|
||||
s.combined_multiplier,
|
||||
s.entry_reason,
|
||||
post.price_1m,
|
||||
post.ret_1m,
|
||||
post.mfe_1m,
|
||||
post.mae_1m,
|
||||
post.price_3m,
|
||||
post.ret_3m,
|
||||
post.mfe_3m,
|
||||
post.mae_3m,
|
||||
post.price_5m,
|
||||
post.ret_5m,
|
||||
post.mfe_5m,
|
||||
post.mae_5m,
|
||||
post.price_10m,
|
||||
post.ret_10m,
|
||||
post.mfe_10m,
|
||||
post.mae_10m,
|
||||
t.exit_time,
|
||||
t.exit_price,
|
||||
t.quantity,
|
||||
t.exit_reason,
|
||||
t.pnl,
|
||||
CASE WHEN t.pnl > 0 THEN 1 ELSE 0 END AS label_win,
|
||||
CASE WHEN t.exit_reason = 'SL' THEN 1 ELSE 0 END AS label_stop_loss
|
||||
FROM entry_snapshots s
|
||||
LEFT JOIN trades t ON t.id = s.trade_id
|
||||
LEFT JOIN post ON post.trade_id = s.trade_id
|
||||
ORDER BY s.date, s.entry_time, s.ticker
|
||||
"""
|
||||
|
||||
with get_conn() as conn:
|
||||
conn.row_factory = None
|
||||
cur = conn.execute(query)
|
||||
headers = [d[0] for d in cur.description]
|
||||
rows = cur.fetchall()
|
||||
|
||||
with out_path.open("w", newline="", encoding="utf-8-sig") as f:
|
||||
writer = csv.writer(f)
|
||||
writer.writerow(headers)
|
||||
writer.writerows(rows)
|
||||
|
||||
print(f"exported {len(rows)} rows -> {out_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
target = Path(sys.argv[1]) if len(sys.argv) > 1 else DEFAULT_OUT
|
||||
if not target.is_absolute():
|
||||
target = ROOT / target
|
||||
export_csv(target)
|
||||
@@ -0,0 +1,25 @@
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$Root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
|
||||
Set-Location $Root
|
||||
|
||||
. "$Root\scripts\stockbot_env.ps1"
|
||||
$Python = Resolve-StockBotPython -Project $Root
|
||||
$Requirements = Join-Path $Root "requirements.txt"
|
||||
$Wheelhouse = Join-Path $Root "vendor\wheels"
|
||||
|
||||
if (-not (Test-Path $Requirements)) {
|
||||
throw "requirements.txt not found: $Requirements"
|
||||
}
|
||||
|
||||
if (Test-Path $Wheelhouse) {
|
||||
$WheelFiles = Get-ChildItem -Path $Wheelhouse -Filter "*.whl" -ErrorAction SilentlyContinue
|
||||
if ($WheelFiles.Count -gt 0) {
|
||||
Write-Host "[install] using local wheelhouse: $Wheelhouse"
|
||||
& $Python -m pip install --no-index --find-links $Wheelhouse -r $Requirements
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "[install] using online package index"
|
||||
& $Python -m pip install -r $Requirements
|
||||
@@ -0,0 +1,192 @@
|
||||
param(
|
||||
[switch]$SkipWinget,
|
||||
[switch]$SkipCliInstall,
|
||||
[switch]$SkipScheduler,
|
||||
[switch]$OnlinePip
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
$env:PYTHONUTF8 = "1"
|
||||
$env:PYTHONIOENCODING = "utf-8"
|
||||
|
||||
$Project = Split-Path -Parent $PSScriptRoot
|
||||
$VenvPython = Join-Path $Project ".venv\Scripts\python.exe"
|
||||
$Requirements = Join-Path $Project "requirements.txt"
|
||||
$Wheelhouse = Join-Path $Project "vendor\wheels"
|
||||
|
||||
function Write-Step {
|
||||
param([string]$Message)
|
||||
Write-Host ""
|
||||
Write-Host "== $Message ==" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
function Test-Admin {
|
||||
$Identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$Principal = New-Object Security.Principal.WindowsPrincipal($Identity)
|
||||
return $Principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
}
|
||||
|
||||
function Ensure-Directory {
|
||||
param([string]$Path)
|
||||
New-Item -ItemType Directory -Force -Path $Path | Out-Null
|
||||
}
|
||||
|
||||
function Install-WingetPackage {
|
||||
param(
|
||||
[string]$Id,
|
||||
[string]$CommandName
|
||||
)
|
||||
|
||||
if ($SkipWinget) {
|
||||
Write-Host "[skip] winget install disabled for $Id"
|
||||
return
|
||||
}
|
||||
|
||||
if ($CommandName -and (Get-Command $CommandName -ErrorAction SilentlyContinue)) {
|
||||
Write-Host "[ok] $CommandName already available"
|
||||
return
|
||||
}
|
||||
|
||||
if (-not (Get-Command winget -ErrorAction SilentlyContinue)) {
|
||||
Write-Host "[warn] winget not found. Install $Id manually if this step is needed." -ForegroundColor Yellow
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host "[install] $Id"
|
||||
winget install --id $Id -e --accept-package-agreements --accept-source-agreements
|
||||
Refresh-ProcessPath
|
||||
}
|
||||
|
||||
function Refresh-ProcessPath {
|
||||
$MachinePath = [Environment]::GetEnvironmentVariable("Path", "Machine")
|
||||
$UserPath = [Environment]::GetEnvironmentVariable("Path", "User")
|
||||
$env:Path = "$MachinePath;$UserPath"
|
||||
}
|
||||
|
||||
function Resolve-BasePython {
|
||||
Refresh-ProcessPath
|
||||
|
||||
if (Get-Command py -ErrorAction SilentlyContinue) {
|
||||
$Path = (& py -3.11 -c "import sys; print(sys.executable)" 2>$null)
|
||||
if ($LASTEXITCODE -eq 0 -and $Path) {
|
||||
return $Path.Trim()
|
||||
}
|
||||
}
|
||||
|
||||
if (Get-Command python -ErrorAction SilentlyContinue) {
|
||||
$Version = (& python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" 2>$null)
|
||||
if ($LASTEXITCODE -eq 0 -and $Version.Trim() -eq "3.11") {
|
||||
$Path = (& python -c "import sys; print(sys.executable)")
|
||||
return $Path.Trim()
|
||||
}
|
||||
}
|
||||
|
||||
$KnownPaths = @(
|
||||
(Join-Path $env:LOCALAPPDATA "Programs\Python\Python311\python.exe"),
|
||||
"C:\Program Files\Python311\python.exe",
|
||||
"C:\Program Files (x86)\Python311\python.exe"
|
||||
)
|
||||
foreach ($KnownPath in $KnownPaths) {
|
||||
if (Test-Path $KnownPath) {
|
||||
return $KnownPath
|
||||
}
|
||||
}
|
||||
|
||||
throw "Python 3.11 was not found. Re-run without -SkipWinget, or install Python 3.11 manually."
|
||||
}
|
||||
|
||||
function Install-NodeCli {
|
||||
if ($SkipCliInstall) {
|
||||
Write-Host "[skip] CLI install disabled"
|
||||
return
|
||||
}
|
||||
|
||||
Refresh-ProcessPath
|
||||
|
||||
if (-not (Get-Command npm -ErrorAction SilentlyContinue)) {
|
||||
Write-Host "[warn] npm not found. Claude/Codex CLI install skipped." -ForegroundColor Yellow
|
||||
return
|
||||
}
|
||||
|
||||
$HasClaude = (Get-Command claude.cmd -ErrorAction SilentlyContinue) -or (Test-Path (Join-Path $env:APPDATA "npm\claude.cmd"))
|
||||
$HasCodex = (Get-Command codex.cmd -ErrorAction SilentlyContinue) -or (Test-Path (Join-Path $env:APPDATA "npm\codex.cmd"))
|
||||
|
||||
if (-not $HasClaude) {
|
||||
Write-Host "[install] Claude Code CLI"
|
||||
npm install -g @anthropic-ai/claude-code
|
||||
} else {
|
||||
Write-Host "[ok] Claude CLI already available"
|
||||
}
|
||||
|
||||
if (-not $HasCodex) {
|
||||
Write-Host "[install] Codex CLI"
|
||||
npm install -g @openai/codex
|
||||
} else {
|
||||
Write-Host "[ok] Codex CLI already available"
|
||||
}
|
||||
}
|
||||
|
||||
if (-not (Test-Admin)) {
|
||||
Write-Host "[warn] Not running as Administrator. Scheduler registration may fail." -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
Set-Location $Project
|
||||
|
||||
Write-Step "Project folders"
|
||||
Ensure-Directory (Join-Path $Project "logs")
|
||||
Ensure-Directory (Join-Path $Project "data")
|
||||
Ensure-Directory (Join-Path $Project "models")
|
||||
Ensure-Directory (Join-Path $Project "reports\daily")
|
||||
Ensure-Directory (Join-Path $Project "reports\proposals")
|
||||
|
||||
if (-not (Test-Path (Join-Path $Project ".env"))) {
|
||||
Write-Host "[warn] .env not found. Restore it from your backup before live API use." -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
Write-Step "System tools"
|
||||
Refresh-ProcessPath
|
||||
Install-WingetPackage -Id "Git.Git" -CommandName "git"
|
||||
Install-WingetPackage -Id "OpenJS.NodeJS.LTS" -CommandName "node"
|
||||
Install-WingetPackage -Id "Python.Python.3.11" -CommandName $null
|
||||
|
||||
Write-Step "Python virtual environment"
|
||||
$BasePython = Resolve-BasePython
|
||||
Write-Host "[ok] Python base: $BasePython"
|
||||
|
||||
if (-not (Test-Path $VenvPython)) {
|
||||
& $BasePython -m venv (Join-Path $Project ".venv")
|
||||
}
|
||||
|
||||
& $VenvPython -m ensurepip --upgrade
|
||||
|
||||
Write-Step "Python libraries"
|
||||
if (-not (Test-Path $Requirements)) {
|
||||
throw "requirements.txt not found: $Requirements"
|
||||
}
|
||||
|
||||
if ((Test-Path $Wheelhouse) -and -not $OnlinePip) {
|
||||
Write-Host "[install] using local wheelhouse: $Wheelhouse"
|
||||
& $VenvPython -m pip install --no-index --find-links $Wheelhouse -r $Requirements
|
||||
} else {
|
||||
Write-Host "[install] using online package index"
|
||||
& $VenvPython -m pip install -r $Requirements
|
||||
}
|
||||
|
||||
Write-Step "Claude/Codex CLI"
|
||||
Install-NodeCli
|
||||
|
||||
Write-Step "Sanity checks"
|
||||
& $VenvPython -c "import aiohttp, pandas, sklearn, joblib, holidays; print('python deps ok')"
|
||||
|
||||
if (-not $SkipScheduler) {
|
||||
Write-Step "Windows Task Scheduler"
|
||||
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $Project "scripts\setup_scheduler.ps1")
|
||||
} else {
|
||||
Write-Host "[skip] scheduler registration disabled"
|
||||
}
|
||||
|
||||
Write-Step "Done"
|
||||
Write-Host "Restore complete. Reboot once if newly installed winget packages are not visible in new terminals." -ForegroundColor Green
|
||||
+7
-5
@@ -1,6 +1,8 @@
|
||||
@echo off
|
||||
REM 매매 봇 실행 스크립트
|
||||
REM 작업 스케줄러에서 07:55에 실행 (평일)
|
||||
|
||||
cd /d "C:\Users\whdwo\OneDrive\바탕 화면\stockbot_v3"
|
||||
python app\main.py
|
||||
setlocal
|
||||
cd /d "%~dp0.."
|
||||
if exist ".venv\Scripts\python.exe" (
|
||||
".venv\Scripts\python.exe" app\main.py
|
||||
) else (
|
||||
python app\main.py
|
||||
)
|
||||
|
||||
+3
-2
@@ -7,8 +7,9 @@ $env:PYTHONIOENCODING = "utf-8"
|
||||
|
||||
$PROJECT = Split-Path -Parent $PSScriptRoot
|
||||
$LOG = "$PROJECT\logs\bot_start.log"
|
||||
$CLAUDE = "C:\Users\whdwo\AppData\Roaming\npm\claude.cmd"
|
||||
$PYTHON = "C:\Users\whdwo\.pyenv\pyenv-win\versions\3.11.9\python.exe"
|
||||
. "$PROJECT\scripts\stockbot_env.ps1"
|
||||
$CLAUDE = Resolve-StockBotClaude
|
||||
$PYTHON = Resolve-StockBotPython -Project $PROJECT
|
||||
$utf8 = New-Object System.Text.UTF8Encoding $false
|
||||
|
||||
Set-Location $PROJECT
|
||||
|
||||
@@ -9,8 +9,9 @@ $env:PYTHONIOENCODING = "utf-8"
|
||||
|
||||
$PROJECT = Split-Path -Parent $PSScriptRoot
|
||||
$LOG = "$PROJECT\logs\evening.log"
|
||||
$CLAUDE = "C:\Users\whdwo\AppData\Roaming\npm\claude.cmd"
|
||||
$PYTHON = "C:\Users\whdwo\.pyenv\pyenv-win\versions\3.11.9\python.exe"
|
||||
. "$PROJECT\scripts\stockbot_env.ps1"
|
||||
$CLAUDE = Resolve-StockBotClaude
|
||||
$PYTHON = Resolve-StockBotPython -Project $PROJECT
|
||||
$utf8 = New-Object System.Text.UTF8Encoding $false
|
||||
|
||||
Set-Location $PROJECT
|
||||
|
||||
@@ -9,8 +9,9 @@ $env:PYTHONIOENCODING = "utf-8"
|
||||
|
||||
$PROJECT = Split-Path -Parent $PSScriptRoot
|
||||
$LOG = "$PROJECT\logs\midday.log"
|
||||
$CLAUDE = "C:\Users\whdwo\AppData\Roaming\npm\claude.cmd"
|
||||
$PYTHON = "C:\Users\whdwo\.pyenv\pyenv-win\versions\3.11.9\python.exe"
|
||||
. "$PROJECT\scripts\stockbot_env.ps1"
|
||||
$CLAUDE = Resolve-StockBotClaude
|
||||
$PYTHON = Resolve-StockBotPython -Project $PROJECT
|
||||
$utf8 = New-Object System.Text.UTF8Encoding $false
|
||||
|
||||
Set-Location $PROJECT
|
||||
|
||||
@@ -9,8 +9,9 @@ $env:PYTHONIOENCODING = "utf-8"
|
||||
|
||||
$PROJECT = Split-Path -Parent $PSScriptRoot
|
||||
$LOG = "$PROJECT\logs\morning.log"
|
||||
$CLAUDE = "C:\Users\whdwo\AppData\Roaming\npm\claude.cmd"
|
||||
$PYTHON = "C:\Users\whdwo\.pyenv\pyenv-win\versions\3.11.9\python.exe"
|
||||
. "$PROJECT\scripts\stockbot_env.ps1"
|
||||
$CLAUDE = Resolve-StockBotClaude
|
||||
$PYTHON = Resolve-StockBotPython -Project $PROJECT
|
||||
$utf8 = New-Object System.Text.UTF8Encoding $false
|
||||
|
||||
Set-Location $PROJECT
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$Root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
|
||||
Set-Location $Root
|
||||
. "$Root\scripts\stockbot_env.ps1"
|
||||
$Python = Resolve-StockBotPython -Project $Root
|
||||
|
||||
$LogDir = Join-Path $Root "logs"
|
||||
New-Item -ItemType Directory -Force -Path $LogDir | Out-Null
|
||||
$LogPath = Join-Path $LogDir "training.log"
|
||||
|
||||
function Write-Log {
|
||||
param([string]$Message)
|
||||
$Line = "[{0}] {1}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss"), $Message
|
||||
Add-Content -Path $LogPath -Value $Line -Encoding UTF8
|
||||
Write-Host $Line
|
||||
}
|
||||
|
||||
function Invoke-PythonStep {
|
||||
param(
|
||||
[string]$Name,
|
||||
[string[]]$Args,
|
||||
[bool]$Required = $true
|
||||
)
|
||||
|
||||
Write-Log $Name
|
||||
& $Python @Args *>> $LogPath
|
||||
$Code = $LASTEXITCODE
|
||||
if ($Code -ne 0) {
|
||||
$Message = "$Name failed with exit code $Code"
|
||||
if ($Required) {
|
||||
throw $Message
|
||||
}
|
||||
Write-Log "warning: $Message"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Log "training pipeline started"
|
||||
|
||||
$HolidayCheck = & $Python scripts\_is_trading_day.py 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Log "market closed - skipped ($HolidayCheck)"
|
||||
exit 0
|
||||
}
|
||||
|
||||
Invoke-PythonStep -Name "collecting daily market features" -Args @("scripts\collect_daily_features.py") -Required $false
|
||||
|
||||
Invoke-PythonStep -Name "collecting KIS minute data" -Args @("scripts\collect_minute_data.py", "--top", "30", "--real-quotes") -Required $false
|
||||
|
||||
Invoke-PythonStep -Name "exporting bot training dataset" -Args @("scripts\export_training_dataset.py", "data\training_dataset.csv") -Required $true
|
||||
|
||||
Invoke-PythonStep -Name "building external training dataset" -Args @("scripts\build_external_training_dataset.py", "--out", "data\external_training_dataset.csv") -Required $true
|
||||
|
||||
Invoke-PythonStep -Name "training model" -Args @("scripts\train_ai_model.py") -Required $true
|
||||
|
||||
Write-Log "training pipeline finished"
|
||||
+28
-10
@@ -1,21 +1,39 @@
|
||||
# 워치독 스크립트 — 봇 생존 감시 + 자동 재시작
|
||||
# 작업 스케줄러에서 5분마다 실행 (평일 09:00~15:10)
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
$env:PYTHONUTF8 = "1"
|
||||
$env:PYTHONIOENCODING = "utf-8"
|
||||
|
||||
$PROJECT = Split-Path -Parent $PSScriptRoot
|
||||
$LOG = "$PROJECT\logs\watchdog.log"
|
||||
$utf8 = New-Object System.Text.UTF8Encoding $false
|
||||
$LOG = "$PROJECT\logs\watchdog.log"
|
||||
. "$PROJECT\scripts\stockbot_env.ps1"
|
||||
$PYTHON = Resolve-StockBotPython -Project $PROJECT
|
||||
$utf8 = New-Object System.Text.UTF8Encoding $false
|
||||
|
||||
Set-Location $PROJECT
|
||||
|
||||
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
||||
[System.IO.File]::AppendAllText($LOG, "[$timestamp] 워치독 실행`n", $utf8)
|
||||
function Write-WatchdogLog {
|
||||
param([string]$Message)
|
||||
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
||||
[System.IO.File]::AppendAllText($LOG, "[$timestamp] $Message`n", $utf8)
|
||||
}
|
||||
|
||||
python scripts/_watchdog.py 2>&1 |
|
||||
$holidayResult = & $PYTHON scripts\_is_trading_day.py 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-WatchdogLog "market closed - skipped ($holidayResult)"
|
||||
exit 0
|
||||
}
|
||||
|
||||
$now = Get-Date
|
||||
$start = Get-Date -Hour 9 -Minute 0 -Second 0
|
||||
$end = Get-Date -Hour 15 -Minute 10 -Second 59
|
||||
if ($now -lt $start -or $now -gt $end) {
|
||||
Write-WatchdogLog "outside watchdog window - skipped"
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-WatchdogLog "watchdog started"
|
||||
& $PYTHON scripts\_watchdog.py 2>&1 |
|
||||
ForEach-Object { [System.IO.File]::AppendAllText($LOG, "$_`n", $utf8) }
|
||||
|
||||
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
||||
[System.IO.File]::AppendAllText($LOG, "[$timestamp] 워치독 완료`n", $utf8)
|
||||
Write-WatchdogLog "watchdog finished"
|
||||
|
||||
+53
-70
@@ -1,92 +1,75 @@
|
||||
# StockBot 작업 스케줄러 등록 (전체 재등록용)
|
||||
# 실행: powershell -ExecutionPolicy Bypass -File scripts\setup_scheduler.ps1
|
||||
# 주의: 이 파일은 UTF-8 BOM으로 저장해야 한글 경로가 올바르게 등록됨
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# 콘솔 인코딩 UTF-8 강제 (한글 경로 깨짐 방지)
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
chcp 65001 | Out-Null
|
||||
|
||||
$PROJECT = "C:\Users\whdwo\OneDrive\바탕 화면\stockbot_v3"
|
||||
$weekdays = @(
|
||||
[DayOfWeek]::Monday, [DayOfWeek]::Tuesday, [DayOfWeek]::Wednesday,
|
||||
[DayOfWeek]::Thursday, [DayOfWeek]::Friday
|
||||
$Project = Split-Path -Parent $PSScriptRoot
|
||||
$TaskPath = "\StockBot\"
|
||||
$Weekdays = @(
|
||||
[DayOfWeek]::Monday,
|
||||
[DayOfWeek]::Tuesday,
|
||||
[DayOfWeek]::Wednesday,
|
||||
[DayOfWeek]::Thursday,
|
||||
[DayOfWeek]::Friday
|
||||
)
|
||||
|
||||
function Register-StockTask($name, $time, $script, $limitMin) {
|
||||
$trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek $weekdays -At $time
|
||||
$action = New-ScheduledTaskAction `
|
||||
function Register-StockTask {
|
||||
param(
|
||||
[string]$Name,
|
||||
[string]$Time,
|
||||
[string]$Script,
|
||||
[int]$LimitMinutes
|
||||
)
|
||||
|
||||
$Trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek $Weekdays -At $Time
|
||||
$Action = New-ScheduledTaskAction `
|
||||
-Execute "powershell.exe" `
|
||||
-Argument "-NonInteractive -ExecutionPolicy Bypass -File `"$PROJECT\scripts\$script`"" `
|
||||
-WorkingDirectory $PROJECT
|
||||
$settings = New-ScheduledTaskSettingsSet `
|
||||
-ExecutionTimeLimit (New-TimeSpan -Minutes $limitMin) `
|
||||
-Argument "-NonInteractive -ExecutionPolicy Bypass -File `"$Project\scripts\$Script`"" `
|
||||
-WorkingDirectory $Project
|
||||
$Settings = New-ScheduledTaskSettingsSet `
|
||||
-ExecutionTimeLimit (New-TimeSpan -Minutes $LimitMinutes) `
|
||||
-StartWhenAvailable `
|
||||
-DontStopIfGoingOnBatteries `
|
||||
-RunOnlyIfNetworkAvailable:$false
|
||||
$settings.DisallowStartIfOnBatteries = $false # 배터리 시작 제한 해제
|
||||
Register-ScheduledTask -TaskName $name -TaskPath "\StockBot\" `
|
||||
-Trigger $trigger -Action $action -Settings $settings -RunLevel Limited -Force | Out-Null
|
||||
$Settings.DisallowStartIfOnBatteries = $false
|
||||
|
||||
# PowerShell 5.1 버그: Register-ScheduledTask가 한글 경로를 ANSI로 저장
|
||||
# → XML export → 경로 수정 → 재import로 교정
|
||||
$xml = Export-ScheduledTask -TaskName $name -TaskPath "\StockBot\"
|
||||
$stored = (Get-ScheduledTask -TaskName $name -TaskPath "\StockBot\").Actions[0].Arguments
|
||||
if (-not ($stored -match [regex]::Escape("바탕 화면"))) {
|
||||
$garbled = ($stored -replace '.*OneDrive\\(.+?)\\stockbot.*', '$1')
|
||||
if ($garbled -ne $stored) {
|
||||
$fixedXml = $xml.Replace($garbled, "바탕 화면")
|
||||
Register-ScheduledTask -TaskName $name -TaskPath "\StockBot\" -Xml $fixedXml -Force | Out-Null
|
||||
}
|
||||
}
|
||||
Write-Host "[OK] $name 등록 완료 (평일 $time)" -ForegroundColor Green
|
||||
Register-ScheduledTask `
|
||||
-TaskName $Name `
|
||||
-TaskPath $TaskPath `
|
||||
-Trigger $Trigger `
|
||||
-Action $Action `
|
||||
-Settings $Settings `
|
||||
-RunLevel Limited `
|
||||
-Force | Out-Null
|
||||
|
||||
Write-Host "[OK] $Name registered at $Time" -ForegroundColor Green
|
||||
}
|
||||
|
||||
# ── 태스크 등록 ────────────────────────────────────────────────────────────────
|
||||
# 08:15 claude /morning → 뉴스+KIS 수집 → daily_context.json → /start-bot 호출
|
||||
Register-StockTask "StockBot_Morning" "08:15" "run_morning.ps1" 20
|
||||
|
||||
# 11:20 claude /midday → 장중 스냅샷 → midday_context.json → 점심 세션 시작
|
||||
Register-StockTask "StockBot_Midday" "11:20" "run_midday.ps1" 20
|
||||
|
||||
# 15:30 claude /evening → 결과 분석 → 리포트 → Discord
|
||||
Register-StockTask "StockBot_Evening" "15:30" "run_evening.ps1" 30
|
||||
|
||||
# 워치독: 5분마다 봇 생존 확인 → 죽어있으면 Discord 알림 + 자동 재시작
|
||||
function Register-WatchdogTask {
|
||||
$times = @("09:00","09:05","09:10","09:15","09:20","09:25","09:30","09:35","09:40","09:45","09:50","09:55",
|
||||
"10:00","10:05","10:10","10:15","10:20","10:25","10:30","10:35","10:40","10:45","10:50","10:55",
|
||||
"11:00","11:05","11:10","11:15","11:20","11:25","11:30","11:35","11:40","11:45","11:50","11:55",
|
||||
"12:00","12:05","12:10","12:15","12:20","12:25","12:30","12:35","12:40","12:45","12:50","12:55",
|
||||
"13:00","13:05","13:10","13:15","13:20","13:25","13:30","13:35","13:40","13:45","13:50","13:55",
|
||||
"14:00","14:05","14:10","14:15","14:20","14:25","14:30","14:35","14:40","14:45","14:50","14:55",
|
||||
"15:00","15:05","15:10")
|
||||
$triggers = $times | ForEach-Object {
|
||||
New-ScheduledTaskTrigger -Weekly -DaysOfWeek $weekdays -At $_
|
||||
$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'
|
||||
cmd.exe /c $Command | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "StockBot_Watchdog registration failed"
|
||||
}
|
||||
$action = New-ScheduledTaskAction `
|
||||
-Execute "powershell.exe" `
|
||||
-Argument "-NonInteractive -ExecutionPolicy Bypass -File `"$PROJECT\scripts\run_watchdog.ps1`"" `
|
||||
-WorkingDirectory $PROJECT
|
||||
$settings = New-ScheduledTaskSettingsSet `
|
||||
-ExecutionTimeLimit (New-TimeSpan -Minutes 3) `
|
||||
-StartWhenAvailable `
|
||||
-MultipleInstances IgnoreNew `
|
||||
-DontStopIfGoingOnBatteries
|
||||
$settings.DisallowStartIfOnBatteries = $false
|
||||
Register-ScheduledTask -TaskName "StockBot_Watchdog" -TaskPath "\StockBot\" `
|
||||
-Trigger $triggers -Action $action -Settings $settings -RunLevel Limited -Force | Out-Null
|
||||
Write-Host "[OK] StockBot_Watchdog 등록 완료 (평일 09:00~15:10, 5분 간격)" -ForegroundColor Green
|
||||
|
||||
Write-Host "[OK] StockBot_Watchdog registered at 09:00-15:10 every 5 minutes" -ForegroundColor Green
|
||||
}
|
||||
|
||||
Register-StockTask "StockBot_Morning" "08:15" "run_morning.ps1" 20
|
||||
Register-StockTask "StockBot_Midday" "11:20" "run_midday.ps1" 20
|
||||
Register-StockTask "StockBot_Evening" "15:30" "run_evening.ps1" 30
|
||||
Register-StockTask "StockBot_Training" "16:00" "run_training_pipeline.ps1" 60
|
||||
Register-WatchdogTask
|
||||
|
||||
# StockBot_Bot 비활성화 유지 (이미 존재할 경우)
|
||||
$botTask = Get-ScheduledTask -TaskName "StockBot_Bot" -TaskPath "\StockBot\" -ErrorAction SilentlyContinue
|
||||
if ($botTask) {
|
||||
Disable-ScheduledTask -TaskName "StockBot_Bot" -TaskPath "\StockBot\" | Out-Null
|
||||
Write-Host "[OK] StockBot_Bot 비활성화 유지" -ForegroundColor Yellow
|
||||
$BotTask = Get-ScheduledTask -TaskName "StockBot_Bot" -TaskPath $TaskPath -ErrorAction SilentlyContinue
|
||||
if ($BotTask) {
|
||||
Disable-ScheduledTask -TaskName "StockBot_Bot" -TaskPath $TaskPath | Out-Null
|
||||
Write-Host "[OK] StockBot_Bot disabled" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "등록된 작업:" -ForegroundColor Cyan
|
||||
Get-ScheduledTask -TaskPath "\StockBot\" | Format-Table TaskName, State
|
||||
Write-Host "Registered StockBot tasks:" -ForegroundColor Cyan
|
||||
Get-ScheduledTask -TaskPath $TaskPath | Format-Table TaskName, State
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Get-StockBotProjectRoot {
|
||||
return (Split-Path -Parent $PSScriptRoot)
|
||||
}
|
||||
|
||||
function Resolve-StockBotPython {
|
||||
param([string]$Project)
|
||||
|
||||
$VenvPython = Join-Path $Project ".venv\Scripts\python.exe"
|
||||
if (Test-Path $VenvPython) {
|
||||
return $VenvPython
|
||||
}
|
||||
|
||||
$PyenvPython = Join-Path $env:USERPROFILE ".pyenv\pyenv-win\versions\3.11.9\python.exe"
|
||||
if (Test-Path $PyenvPython) {
|
||||
return $PyenvPython
|
||||
}
|
||||
|
||||
$Command = Get-Command python -ErrorAction SilentlyContinue
|
||||
if ($Command) {
|
||||
return $Command.Source
|
||||
}
|
||||
|
||||
throw "Python not found. Run Restore_StockBot.bat first."
|
||||
}
|
||||
|
||||
function Resolve-StockBotClaude {
|
||||
$Candidates = @()
|
||||
if ($env:STOCKBOT_CLAUDE) {
|
||||
$Candidates += $env:STOCKBOT_CLAUDE
|
||||
}
|
||||
if ($env:APPDATA) {
|
||||
$Candidates += (Join-Path $env:APPDATA "npm\claude.cmd")
|
||||
$Candidates += (Join-Path $env:APPDATA "npm\codex.cmd")
|
||||
}
|
||||
|
||||
foreach ($Candidate in $Candidates) {
|
||||
if ($Candidate -and (Test-Path $Candidate)) {
|
||||
return $Candidate
|
||||
}
|
||||
}
|
||||
|
||||
$Claude = Get-Command claude.cmd -ErrorAction SilentlyContinue
|
||||
if ($Claude) {
|
||||
return $Claude.Source
|
||||
}
|
||||
|
||||
$Codex = Get-Command codex.cmd -ErrorAction SilentlyContinue
|
||||
if ($Codex) {
|
||||
return $Codex.Source
|
||||
}
|
||||
|
||||
throw "Claude/Codex CLI not found. Run Restore_StockBot.bat or install the CLI, then retry."
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import joblib
|
||||
import pandas as pd
|
||||
from sklearn.ensemble import RandomForestClassifier
|
||||
from sklearn.metrics import accuracy_score, precision_score, roc_auc_score
|
||||
from sklearn.model_selection import train_test_split
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from app.ml.features import LABEL_COLUMNS, build_feature_matrix, select_feature_columns
|
||||
|
||||
|
||||
DEFAULT_INPUTS = [
|
||||
Path("data/training_dataset.csv"),
|
||||
Path("data/external_training_dataset.csv"),
|
||||
]
|
||||
|
||||
|
||||
def _read_csv(path: Path) -> pd.DataFrame:
|
||||
if not path.exists() or path.stat().st_size == 0:
|
||||
return pd.DataFrame()
|
||||
return pd.read_csv(path)
|
||||
|
||||
|
||||
def _load_inputs(paths: list[Path]) -> pd.DataFrame:
|
||||
frames = []
|
||||
for path in paths:
|
||||
frame = _read_csv(path)
|
||||
if not frame.empty:
|
||||
frame["source_file"] = str(path)
|
||||
frames.append(frame)
|
||||
if not frames:
|
||||
return pd.DataFrame()
|
||||
return pd.concat(frames, ignore_index=True, sort=False)
|
||||
|
||||
|
||||
def _train_one_target(
|
||||
x: pd.DataFrame,
|
||||
y: pd.Series,
|
||||
random_state: int,
|
||||
) -> tuple[RandomForestClassifier, dict[str, float | int]]:
|
||||
stratify = y if y.nunique() == 2 and y.value_counts().min() >= 2 else None
|
||||
x_train, x_test, y_train, y_test = train_test_split(
|
||||
x,
|
||||
y,
|
||||
test_size=0.25,
|
||||
random_state=random_state,
|
||||
stratify=stratify,
|
||||
)
|
||||
|
||||
model = RandomForestClassifier(
|
||||
n_estimators=300,
|
||||
max_depth=7,
|
||||
min_samples_leaf=10,
|
||||
class_weight="balanced_subsample",
|
||||
random_state=random_state,
|
||||
n_jobs=-1,
|
||||
)
|
||||
model.fit(x_train, y_train)
|
||||
|
||||
prediction = model.predict(x_test)
|
||||
metrics: dict[str, float | int] = {
|
||||
"rows": int(len(y)),
|
||||
"train_rows": int(len(y_train)),
|
||||
"test_rows": int(len(y_test)),
|
||||
"positive_rows": int(y.sum()),
|
||||
"accuracy": float(accuracy_score(y_test, prediction)),
|
||||
"precision": float(precision_score(y_test, prediction, zero_division=0)),
|
||||
}
|
||||
|
||||
if y_test.nunique() == 2 and hasattr(model, "predict_proba"):
|
||||
metrics["roc_auc"] = float(roc_auc_score(y_test, model.predict_proba(x_test)[:, 1]))
|
||||
|
||||
return model, metrics
|
||||
|
||||
|
||||
def train(args: argparse.Namespace) -> int:
|
||||
df = _load_inputs(args.inputs)
|
||||
if df.empty:
|
||||
print("No training rows found. Export datasets first.")
|
||||
return 0
|
||||
|
||||
targets = [target for target in args.targets if target in df.columns]
|
||||
if not targets:
|
||||
print("No supported label columns found.")
|
||||
return 0
|
||||
|
||||
if len(df) < args.min_rows:
|
||||
print(f"Only {len(df)} rows found. Need at least {args.min_rows} rows before training.")
|
||||
return 0
|
||||
|
||||
feature_columns = select_feature_columns(df, targets)
|
||||
if not feature_columns:
|
||||
print("No numeric feature columns found.")
|
||||
return 0
|
||||
|
||||
x, medians = build_feature_matrix(df, feature_columns)
|
||||
|
||||
models = {}
|
||||
metrics = {
|
||||
"created_at": datetime.now().isoformat(timespec="seconds"),
|
||||
"input_files": [str(path) for path in args.inputs],
|
||||
"feature_count": len(feature_columns),
|
||||
"features": feature_columns,
|
||||
"targets": {},
|
||||
}
|
||||
|
||||
for target in targets:
|
||||
labeled = df[target].dropna()
|
||||
target_index = labeled.index
|
||||
y = pd.to_numeric(labeled, errors="coerce").dropna().astype(int)
|
||||
target_x = x.loc[y.index]
|
||||
|
||||
if len(y) < args.min_rows:
|
||||
print(f"Skipping {target}: only {len(y)} labeled rows.")
|
||||
continue
|
||||
if y.nunique() < 2:
|
||||
print(f"Skipping {target}: target has only one class.")
|
||||
continue
|
||||
|
||||
model, target_metrics = _train_one_target(target_x, y, args.random_state)
|
||||
models[target] = model
|
||||
metrics["targets"][target] = target_metrics
|
||||
print(f"Trained {target}: {target_metrics}")
|
||||
|
||||
if not models:
|
||||
print("No model was trained.")
|
||||
return 0
|
||||
|
||||
args.out.parent.mkdir(parents=True, exist_ok=True)
|
||||
bundle = {
|
||||
"created_at": metrics["created_at"],
|
||||
"feature_columns": feature_columns,
|
||||
"medians": medians,
|
||||
"models": models,
|
||||
"metrics": metrics,
|
||||
}
|
||||
joblib.dump(bundle, args.out)
|
||||
|
||||
metrics_path = args.metrics_out or args.out.with_suffix(".metrics.json")
|
||||
metrics_path.write_text(json.dumps(metrics, indent=2), encoding="utf-8")
|
||||
|
||||
print(f"Saved model: {args.out}")
|
||||
print(f"Saved metrics: {metrics_path}")
|
||||
return 0
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Train scalping AI models from exported datasets.")
|
||||
parser.add_argument(
|
||||
"--inputs",
|
||||
nargs="+",
|
||||
type=Path,
|
||||
default=DEFAULT_INPUTS,
|
||||
help="CSV datasets to combine.",
|
||||
)
|
||||
parser.add_argument("--out", type=Path, default=Path("models/scalping_model.joblib"))
|
||||
parser.add_argument("--metrics-out", type=Path, default=None)
|
||||
parser.add_argument("--min-rows", type=int, default=200)
|
||||
parser.add_argument("--random-state", type=int, default=42)
|
||||
parser.add_argument(
|
||||
"--targets",
|
||||
nargs="+",
|
||||
default=sorted(LABEL_COLUMNS),
|
||||
choices=sorted(LABEL_COLUMNS),
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(train(parse_args()))
|
||||
Reference in New Issue
Block a user