[2026-06-01] Fix duplicate bot startup guards
This commit is contained in:
@@ -1,71 +1,17 @@
|
||||
# start-bot — 매매 봇 백그라운드 시작
|
||||
# start-bot - deterministic bot startup
|
||||
|
||||
`app/main.py`를 독립 백그라운드 프로세스로 실행한다.
|
||||
Claude Code가 종료된 뒤에도 봇은 계속 실행된다.
|
||||
Start the trading bot with the project-owned startup script. Do not inline or rewrite
|
||||
process-management code in this command.
|
||||
|
||||
## 실행 순서
|
||||
## Steps
|
||||
|
||||
### 1. 기존 봇 전부 종료 (PID 파일 + 프로세스 스캔 병행)
|
||||
```python
|
||||
import subprocess, os
|
||||
1. Run:
|
||||
```powershell
|
||||
python scripts\start_bot.py
|
||||
```
|
||||
|
||||
pid_file = r'C:\Users\whdwo\OneDrive\바탕 화면\stockbot_v3\logs\bot.pid'
|
||||
2. Report the script output. A successful run prints the new PID and sends the
|
||||
Discord start notification.
|
||||
|
||||
# 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. 봇 시작 + PID 저장
|
||||
```python
|
||||
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 파일 저장 (다음 재시작 때 확실히 종료하기 위해)
|
||||
with open('logs/bot.pid', 'w') as f:
|
||||
f.write(str(proc.pid))
|
||||
print(f'봇 시작 완료 PID={proc.pid}')
|
||||
```
|
||||
|
||||
### 3. Discord 시작 알림
|
||||
```python
|
||||
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 알림 전송 완료')
|
||||
```
|
||||
|
||||
### 4. 완료
|
||||
"봇 시작 완료" 메시지를 출력하고 종료한다.
|
||||
The script stops existing `app/main.py` processes, starts exactly one new bot,
|
||||
waits briefly to verify it is still alive, then writes `logs/bot.pid`.
|
||||
|
||||
+188
-4
@@ -85,6 +85,67 @@ from app.config import (
|
||||
)
|
||||
|
||||
|
||||
class SingleInstanceLock:
|
||||
"""Process-wide lock so only one StockBot can run per workspace."""
|
||||
|
||||
def __init__(self, path: str | Path):
|
||||
self.path = Path(path)
|
||||
self._fh = None
|
||||
self._mode = None
|
||||
|
||||
def acquire(self) -> bool:
|
||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self._fh = open(self.path, "a+", encoding="utf-8")
|
||||
if os.name == "nt":
|
||||
import msvcrt
|
||||
|
||||
self._fh.seek(0, os.SEEK_END)
|
||||
if self._fh.tell() == 0:
|
||||
self._fh.write("0")
|
||||
self._fh.flush()
|
||||
self._fh.seek(0)
|
||||
try:
|
||||
msvcrt.locking(self._fh.fileno(), msvcrt.LK_NBLCK, 1)
|
||||
except OSError:
|
||||
self._fh.close()
|
||||
self._fh = None
|
||||
return False
|
||||
self._mode = "msvcrt"
|
||||
else:
|
||||
import fcntl
|
||||
|
||||
try:
|
||||
fcntl.flock(self._fh.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except OSError:
|
||||
self._fh.close()
|
||||
self._fh = None
|
||||
return False
|
||||
self._mode = "fcntl"
|
||||
|
||||
self._fh.seek(0)
|
||||
self._fh.truncate()
|
||||
self._fh.write(str(os.getpid()))
|
||||
self._fh.flush()
|
||||
return True
|
||||
|
||||
def release(self):
|
||||
if self._fh is None:
|
||||
return
|
||||
try:
|
||||
if self._mode == "msvcrt":
|
||||
import msvcrt
|
||||
|
||||
self._fh.seek(0)
|
||||
msvcrt.locking(self._fh.fileno(), msvcrt.LK_UNLCK, 1)
|
||||
elif self._mode == "fcntl":
|
||||
import fcntl
|
||||
|
||||
fcntl.flock(self._fh.fileno(), fcntl.LOCK_UN)
|
||||
finally:
|
||||
self._fh.close()
|
||||
self._fh = None
|
||||
|
||||
|
||||
class StockBot:
|
||||
def __init__(self):
|
||||
self.kis = KISClient()
|
||||
@@ -550,6 +611,99 @@ class StockBot:
|
||||
with get_conn() as conn:
|
||||
conn.execute("DELETE FROM positions WHERE ticker=?", (ticker,))
|
||||
|
||||
def _db_has_open_position(self, ticker: str) -> bool:
|
||||
with get_conn() as conn:
|
||||
row = conn.execute(
|
||||
"SELECT 1 FROM positions WHERE ticker=?",
|
||||
(ticker,),
|
||||
).fetchone()
|
||||
return row is not None
|
||||
|
||||
def _db_reserve_position(
|
||||
self,
|
||||
ticker: str,
|
||||
name: str,
|
||||
entry_price: float,
|
||||
qty: int,
|
||||
target_price: float,
|
||||
stop_price: float,
|
||||
ai_boosted: bool,
|
||||
) -> bool:
|
||||
"""Atomically reserve a ticker before sending a buy order."""
|
||||
with get_conn() as conn:
|
||||
cur = conn.execute("""
|
||||
INSERT OR IGNORE INTO positions
|
||||
(ticker, name, entry_time, entry_price, quantity,
|
||||
tp1_done, target_price, stop_price, ai_boosted)
|
||||
VALUES (?,?,?,?,?,?,?,?,?)
|
||||
""", (
|
||||
ticker,
|
||||
name,
|
||||
datetime.now().strftime("%H:%M:%S"),
|
||||
entry_price,
|
||||
qty,
|
||||
0,
|
||||
target_price,
|
||||
stop_price,
|
||||
1 if ai_boosted else 0,
|
||||
))
|
||||
return cur.rowcount == 1
|
||||
|
||||
@staticmethod
|
||||
def _is_rate_limit_error(err) -> bool:
|
||||
msg = str(err)
|
||||
return (
|
||||
"초당" in msg
|
||||
or "거래건수" in msg
|
||||
or "rate limit" in msg.lower()
|
||||
or "too many" in msg.lower()
|
||||
)
|
||||
|
||||
async def _get_price_with_retry(self, ticker: str, purpose: str, attempts: int = 3):
|
||||
delays = (1.2, 2.5, 5.0)
|
||||
for attempt in range(1, attempts + 1):
|
||||
try:
|
||||
return await self.kis.get_price(ticker)
|
||||
except Exception as e:
|
||||
if attempt >= attempts or not self._is_rate_limit_error(e):
|
||||
raise
|
||||
wait = delays[min(attempt - 1, len(delays) - 1)]
|
||||
logger.warning(
|
||||
"%s price retry %s/%s for %s after rate limit: %s",
|
||||
purpose,
|
||||
attempt,
|
||||
attempts,
|
||||
ticker,
|
||||
e,
|
||||
)
|
||||
await asyncio.sleep(wait)
|
||||
|
||||
async def _sell_with_retry(
|
||||
self,
|
||||
ticker: str,
|
||||
name: str,
|
||||
qty: int,
|
||||
reason: str,
|
||||
attempts: int = 3,
|
||||
) -> dict:
|
||||
delays = (1.2, 2.5, 5.0)
|
||||
for attempt in range(1, attempts + 1):
|
||||
result = await self.executor.sell(ticker, name, qty, reason)
|
||||
if result.get("success"):
|
||||
return result
|
||||
error = result.get("error", "")
|
||||
if attempt >= attempts or not self._is_rate_limit_error(error):
|
||||
return result
|
||||
wait = delays[min(attempt - 1, len(delays) - 1)]
|
||||
logger.warning(
|
||||
"SELL retry %s/%s for %s after rate limit: %s",
|
||||
attempt,
|
||||
attempts,
|
||||
ticker,
|
||||
error,
|
||||
)
|
||||
await asyncio.sleep(wait)
|
||||
|
||||
# ─────────────────────────────────────────
|
||||
# 유니버스 갱신 (08:30)
|
||||
# ─────────────────────────────────────────
|
||||
@@ -792,6 +946,10 @@ class StockBot:
|
||||
if _do_diag:
|
||||
_diag.append(f"{ticker}:보유중")
|
||||
continue
|
||||
if self._db_has_open_position(ticker):
|
||||
if _do_diag:
|
||||
_diag.append(f"{ticker}:DB보유중")
|
||||
continue
|
||||
if ticker in self.sl_tickers:
|
||||
if _do_diag:
|
||||
_diag.append(f"{ticker}:SL차단")
|
||||
@@ -806,7 +964,8 @@ class StockBot:
|
||||
continue
|
||||
|
||||
try:
|
||||
price_info = await self.kis.get_price(ticker) # rate limiter가 자동 throttle
|
||||
reserved = False
|
||||
price_info = await self._get_price_with_retry(ticker, "ENTRY")
|
||||
current = price_info["current"]
|
||||
name = self.ticker_names.get(ticker, ticker)
|
||||
sector = (
|
||||
@@ -839,6 +998,19 @@ class StockBot:
|
||||
)
|
||||
invest = self.risk.get_pos_size(cash, combined_mult)
|
||||
qty = max(1, int(invest // current))
|
||||
reserve_stop = current * (1 - self.risk.get_sl_pct())
|
||||
if not self._db_reserve_position(
|
||||
ticker=ticker,
|
||||
name=name,
|
||||
entry_price=current,
|
||||
qty=qty,
|
||||
target_price=target,
|
||||
stop_price=reserve_stop,
|
||||
ai_boosted=signal.get("boosted", False),
|
||||
):
|
||||
logger.info("ENTRY blocked: DB active position exists for %s", ticker)
|
||||
continue
|
||||
reserved = True
|
||||
self._log_entry_acceptance(
|
||||
ticker=ticker,
|
||||
name=name,
|
||||
@@ -856,6 +1028,7 @@ class StockBot:
|
||||
)
|
||||
|
||||
if result["success"]:
|
||||
reserved = False
|
||||
entry_price = result["price"] or current
|
||||
sl_price = entry_price * (1 - self.risk.get_sl_pct())
|
||||
model_scores = self._score_entry_candidate(
|
||||
@@ -913,7 +1086,14 @@ class StockBot:
|
||||
boosted=signal.get("boosted", False),
|
||||
)
|
||||
|
||||
else:
|
||||
if reserved:
|
||||
self._db_delete_position(ticker)
|
||||
reserved = False
|
||||
|
||||
except Exception as e:
|
||||
if "reserved" in locals() and reserved:
|
||||
self._db_delete_position(ticker)
|
||||
logger.error(f"진입 체크 오류 {ticker}: {type(e).__name__}: {e}")
|
||||
|
||||
if _do_diag:
|
||||
@@ -931,7 +1111,7 @@ class StockBot:
|
||||
"""보유 포지션 청산 신호 확인"""
|
||||
for ticker, pos in list(self.positions.items()):
|
||||
try:
|
||||
price_info = await self.kis.get_price(ticker)
|
||||
price_info = await self._get_price_with_retry(ticker, "EXIT")
|
||||
current = price_info["current"]
|
||||
name = pos["name"]
|
||||
|
||||
@@ -968,7 +1148,7 @@ class StockBot:
|
||||
current: float, qty: int, reason: str):
|
||||
"""실제 청산 실행"""
|
||||
name = pos["name"]
|
||||
result = await self.executor.sell(ticker, name, qty, reason)
|
||||
result = await self._sell_with_retry(ticker, name, qty, reason)
|
||||
|
||||
if not result["success"]:
|
||||
return
|
||||
@@ -1026,7 +1206,7 @@ class StockBot:
|
||||
logger.info("14:50 강제 청산 시작")
|
||||
for ticker, pos in list(self.positions.items()):
|
||||
try:
|
||||
price_info = await self.kis.get_price(ticker)
|
||||
price_info = await self._get_price_with_retry(ticker, "FORCE_EXIT")
|
||||
current = price_info["current"]
|
||||
await self._do_exit(
|
||||
ticker, pos, current,
|
||||
@@ -1162,6 +1342,10 @@ async def run():
|
||||
if __name__ == "__main__":
|
||||
os.makedirs("logs", exist_ok=True)
|
||||
os.makedirs("data", exist_ok=True)
|
||||
instance_lock = SingleInstanceLock("logs/stockbot.lock")
|
||||
if not instance_lock.acquire():
|
||||
logger.error("StockBot already running; duplicate process exiting")
|
||||
sys.exit(2)
|
||||
logger.info("=" * 50)
|
||||
logger.info("단타 자동매매 시스템 시작")
|
||||
logger.info(f"모드: {'모의투자' if os.getenv('KIS_MOCK','true')=='true' else '실거래'}")
|
||||
|
||||
+74
-37
@@ -1,44 +1,83 @@
|
||||
"""
|
||||
watchdog.py — 봇 생존 감시 + 자동 재시작
|
||||
5분마다 실행 (작업 스케줄러) / 장중(09:00~15:10)에만 작동
|
||||
"""
|
||||
import asyncio, os, subprocess, sys
|
||||
import asyncio
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT = r'C:\Users\whdwo\OneDrive\바탕 화면\stockbot_v3'
|
||||
PID_FILE = os.path.join(PROJECT, 'logs', 'bot.pid')
|
||||
|
||||
PROJECT = Path(__file__).resolve().parents[1]
|
||||
PID_FILE = PROJECT / "logs" / "bot.pid"
|
||||
|
||||
os.chdir(PROJECT)
|
||||
sys.path.insert(0, '.')
|
||||
sys.path.insert(0, str(PROJECT))
|
||||
|
||||
from app.main import load_env
|
||||
|
||||
load_env()
|
||||
|
||||
|
||||
def is_process_alive(pid: int) -> bool:
|
||||
r = subprocess.run(
|
||||
['tasklist', '/FI', f'PID eq {pid}', '/NH'],
|
||||
capture_output=True, text=True,
|
||||
def _is_process_alive(pid: int) -> bool:
|
||||
result = subprocess.run(
|
||||
["tasklist", "/FI", f"PID eq {pid}", "/NH"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return str(pid) in r.stdout
|
||||
return str(pid) in result.stdout
|
||||
|
||||
|
||||
def get_pid() -> int | None:
|
||||
def _find_bot_pids() -> list[int]:
|
||||
command = (
|
||||
"Get-CimInstance Win32_Process | "
|
||||
"Where-Object { $_.Name -like 'python*' -and "
|
||||
"($_.CommandLine -like '*app/main.py*' "
|
||||
"-or $_.CommandLine -like '*app\\\\main.py*') } | "
|
||||
"Select-Object -ExpandProperty ProcessId"
|
||||
)
|
||||
result = subprocess.run(
|
||||
["powershell", "-NoProfile", "-Command", command],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
pids: list[int] = []
|
||||
for line in result.stdout.splitlines():
|
||||
line = line.strip()
|
||||
if line.isdigit():
|
||||
pids.append(int(line))
|
||||
return pids
|
||||
|
||||
|
||||
def _get_pid() -> int | None:
|
||||
try:
|
||||
return int(open(PID_FILE).read().strip())
|
||||
return int(PID_FILE.read_text(encoding="utf-8").strip())
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def restart_bot() -> int:
|
||||
def _write_pid(pid: int) -> None:
|
||||
PID_FILE.parent.mkdir(exist_ok=True)
|
||||
PID_FILE.write_text(str(pid), encoding="utf-8")
|
||||
|
||||
|
||||
def _restart_bot() -> int:
|
||||
env = os.environ.copy()
|
||||
env["PYTHONUNBUFFERED"] = "1"
|
||||
creationflags = 0
|
||||
if hasattr(subprocess, "DETACHED_PROCESS"):
|
||||
creationflags = subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP
|
||||
|
||||
log_path = PROJECT / "logs" / "bot_stderr.log"
|
||||
with open(log_path, "a", encoding="utf-8") as log:
|
||||
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'),
|
||||
[sys.executable, "-u", "app/main.py"],
|
||||
cwd=PROJECT,
|
||||
creationflags=creationflags,
|
||||
stdout=log,
|
||||
stderr=subprocess.STDOUT,
|
||||
close_fds=True,
|
||||
env=env,
|
||||
)
|
||||
with open(PID_FILE, 'w') as f:
|
||||
f.write(str(proc.pid))
|
||||
_write_pid(proc.pid)
|
||||
return proc.pid
|
||||
|
||||
|
||||
@@ -46,35 +85,33 @@ async def main():
|
||||
now = datetime.now()
|
||||
now_str = now.strftime("%H:%M")
|
||||
|
||||
# 장 외 시간은 체크 안 함
|
||||
if not ("09:00" <= now_str <= "15:10"):
|
||||
print(f"[{now_str}] 장 외 시간 — 워치독 종료")
|
||||
print(f"[{now_str}] outside trading window - watchdog skipped")
|
||||
return
|
||||
|
||||
from app.monitor.notifier import send
|
||||
|
||||
pid = get_pid()
|
||||
pid = _get_pid()
|
||||
if pid is not None and _is_process_alive(pid):
|
||||
print(f"[{now_str}] bot running PID={pid}")
|
||||
return
|
||||
|
||||
if pid is None:
|
||||
msg = f"[경고] 봇 PID 파일 없음 — 봇이 실행되지 않은 상태입니다 ({now_str})"
|
||||
live_pids = _find_bot_pids()
|
||||
if live_pids:
|
||||
recovered_pid = live_pids[0]
|
||||
_write_pid(recovered_pid)
|
||||
msg = f"[복구] bot.pid corrected to running bot PID={recovered_pid} ({now_str})"
|
||||
print(msg)
|
||||
await send(msg)
|
||||
new_pid = restart_bot()
|
||||
await send(f"[복구] 봇 자동 재시작 완료 PID={new_pid}")
|
||||
return
|
||||
|
||||
if is_process_alive(pid):
|
||||
print(f"[{now_str}] 봇 정상 실행 중 PID={pid}")
|
||||
return
|
||||
|
||||
# 봇이 죽어있음
|
||||
msg = f"[긴급] 봇 프로세스 종료 감지 (PID={pid}) — 자동 재시작 시도"
|
||||
msg = f"[긴급] bot process not found (pid={pid}) - restarting"
|
||||
print(msg)
|
||||
await send(msg)
|
||||
|
||||
new_pid = restart_bot()
|
||||
await send(f"[복구] 봇 자동 재시작 완료 PID={new_pid} ({now_str})")
|
||||
print(f"봇 재시작 완료 PID={new_pid}")
|
||||
new_pid = _restart_bot()
|
||||
await send(f"[복구] bot restarted PID={new_pid} ({now_str})")
|
||||
print(f"bot restarted PID={new_pid}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+1
-2
@@ -8,7 +8,6 @@ $env:PYTHONIOENCODING = "utf-8"
|
||||
$PROJECT = Split-Path -Parent $PSScriptRoot
|
||||
$LOG = "$PROJECT\logs\bot_start.log"
|
||||
. "$PROJECT\scripts\stockbot_env.ps1"
|
||||
$CLAUDE = Resolve-StockBotClaude
|
||||
$PYTHON = Resolve-StockBotPython -Project $PROJECT
|
||||
$utf8 = New-Object System.Text.UTF8Encoding $false
|
||||
|
||||
@@ -25,7 +24,7 @@ if ($LASTEXITCODE -ne 0) {
|
||||
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
||||
[System.IO.File]::AppendAllText($LOG, "[$timestamp] /start-bot 실행`n", $utf8)
|
||||
|
||||
& $CLAUDE -p "/start-bot" --dangerously-skip-permissions 2>&1 |
|
||||
& $PYTHON "scripts\start_bot.py" 2>&1 |
|
||||
ForEach-Object { [System.IO.File]::AppendAllText($LOG, "$_`n", $utf8) }
|
||||
|
||||
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
||||
|
||||
@@ -37,7 +37,7 @@ $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
||||
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
||||
[System.IO.File]::AppendAllText($LOG, "[$timestamp] /start-bot 시작`n", $utf8)
|
||||
|
||||
& $CLAUDE -p "/start-bot" --dangerously-skip-permissions 2>&1 |
|
||||
& $PYTHON "scripts\start_bot.py" 2>&1 |
|
||||
ForEach-Object { [System.IO.File]::AppendAllText($LOG, "$_`n", $utf8) }
|
||||
|
||||
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import asyncio
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
PROJECT = Path(__file__).resolve().parents[1]
|
||||
PID_FILE = PROJECT / "logs" / "bot.pid"
|
||||
LOG_FILE = PROJECT / "logs" / "bot_stderr.log"
|
||||
|
||||
|
||||
def _taskkill(pid: int) -> None:
|
||||
subprocess.run(["taskkill", "/PID", str(pid), "/F"], capture_output=True, text=True)
|
||||
|
||||
|
||||
def _find_bot_pids() -> list[int]:
|
||||
command = (
|
||||
"Get-CimInstance Win32_Process | "
|
||||
"Where-Object { $_.Name -like 'python*' -and "
|
||||
"($_.CommandLine -like '*app/main.py*' "
|
||||
"-or $_.CommandLine -like '*app\\\\main.py*') } | "
|
||||
"Select-Object -ExpandProperty ProcessId"
|
||||
)
|
||||
result = subprocess.run(
|
||||
["powershell", "-NoProfile", "-Command", command],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
pids: list[int] = []
|
||||
for line in result.stdout.splitlines():
|
||||
line = line.strip()
|
||||
if line.isdigit():
|
||||
pids.append(int(line))
|
||||
return pids
|
||||
|
||||
|
||||
def _kill_existing_bots() -> None:
|
||||
killed: set[int] = set()
|
||||
if PID_FILE.exists():
|
||||
try:
|
||||
pid = int(PID_FILE.read_text(encoding="utf-8").strip())
|
||||
_taskkill(pid)
|
||||
killed.add(pid)
|
||||
print(f"PID file process stopped: {pid}")
|
||||
except Exception as exc:
|
||||
print(f"PID file stop skipped: {exc}")
|
||||
PID_FILE.unlink(missing_ok=True)
|
||||
|
||||
for pid in _find_bot_pids():
|
||||
if pid in killed:
|
||||
continue
|
||||
_taskkill(pid)
|
||||
killed.add(pid)
|
||||
print(f"Existing bot process stopped: {pid}")
|
||||
|
||||
if not killed:
|
||||
print("No existing bot process found")
|
||||
|
||||
|
||||
def _start_bot() -> int:
|
||||
PROJECT.joinpath("logs").mkdir(exist_ok=True)
|
||||
env = os.environ.copy()
|
||||
env["PYTHONUNBUFFERED"] = "1"
|
||||
creationflags = 0
|
||||
if hasattr(subprocess, "DETACHED_PROCESS"):
|
||||
creationflags = subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP
|
||||
|
||||
with open(LOG_FILE, "a", encoding="utf-8") as log:
|
||||
proc = subprocess.Popen(
|
||||
[sys.executable, "-u", "app/main.py"],
|
||||
cwd=PROJECT,
|
||||
creationflags=creationflags,
|
||||
stdout=log,
|
||||
stderr=subprocess.STDOUT,
|
||||
close_fds=True,
|
||||
env=env,
|
||||
)
|
||||
|
||||
time.sleep(2)
|
||||
if proc.poll() is not None:
|
||||
raise RuntimeError(f"bot process exited during startup: returncode={proc.returncode}")
|
||||
|
||||
PID_FILE.write_text(str(proc.pid), encoding="utf-8")
|
||||
return proc.pid
|
||||
|
||||
|
||||
async def _notify_start() -> None:
|
||||
sys.path.insert(0, str(PROJECT))
|
||||
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 "[실거래]"
|
||||
await send(f"{label} 자동매매 봇 시작 (DRY_RUN={dry})")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
os.chdir(PROJECT)
|
||||
_kill_existing_bots()
|
||||
pid = _start_bot()
|
||||
print(f"Bot started PID={pid}")
|
||||
asyncio.run(_notify_start())
|
||||
print("Discord start notification sent")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user