fix: restore positions on restart and fix notify env loading

- restore_positions(): read Upbit balances on startup to prevent
  double-buying after restart
- notify.py: read TOKEN/CHAT_ID inside _send() to avoid empty values
  when module is imported before load_dotenv()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
joungmin
2026-02-28 10:36:17 +09:00
parent a799fbebbd
commit a287e48522
3 changed files with 41 additions and 5 deletions

View File

@@ -9,19 +9,19 @@ import requests
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_TOKEN = os.getenv("TELEGRAM_TRADE_TOKEN", "")
_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "")
_API = "https://api.telegram.org/bot{token}/sendMessage" _API = "https://api.telegram.org/bot{token}/sendMessage"
def _send(text: str) -> None: def _send(text: str) -> None:
if not _TOKEN or not _CHAT_ID: token = os.getenv("TELEGRAM_TRADE_TOKEN", "")
chat_id = os.getenv("TELEGRAM_CHAT_ID", "")
if not token or not chat_id:
logger.warning("Telegram 설정 없음, 알림 스킵") logger.warning("Telegram 설정 없음, 알림 스킵")
return return
try: try:
requests.post( requests.post(
_API.format(token=_TOKEN), _API.format(token=token),
json={"chat_id": _CHAT_ID, "text": text, "parse_mode": "HTML"}, json={"chat_id": chat_id, "text": text, "parse_mode": "HTML"},
timeout=5, timeout=5,
) )
except Exception as e: except Exception as e:

View File

@@ -39,6 +39,38 @@ def get_positions() -> dict:
return _positions return _positions
def restore_positions() -> None:
"""시작 시 Upbit 실제 잔고를 읽어 포지션 복원 (재시작 이중 매수 방지)."""
upbit = _get_upbit()
balances = upbit.get_balances()
for b in balances:
currency = b["currency"]
if currency == "KRW":
continue
amount = float(b["balance"]) + float(b["locked"])
if amount <= 0:
continue
ticker = f"KRW-{currency}"
current = pyupbit.get_current_price(ticker)
if not current:
continue
invested_krw = int(amount * current)
if invested_krw < 1_000: # 소액 잔고 무시
continue
with _lock:
_positions[ticker] = {
"buy_price": current, # 정확한 매수가 불명 → 현재가로 초기화
"peak_price": current,
"amount": amount,
"invested_krw": min(invested_krw, PER_POSITION),
"entry_time": datetime.now(),
}
logger.info(
f"[복원] {ticker} 수량={amount} | 현재가={current:,.0f}"
f"(재시작 시 복원, 매수가 불명으로 현재가 기준)"
)
def buy(ticker: str) -> bool: def buy(ticker: str) -> bool:
"""시장가 매수. 예산·포지션 수 확인 후 진입.""" """시장가 매수. 예산·포지션 수 확인 후 진입."""
with _lock: with _lock:

View File

@@ -17,10 +17,14 @@ logging.basicConfig(
) )
from core.monitor import run_monitor from core.monitor import run_monitor
from core.trader import restore_positions
from daemon.runner import run_scanner from daemon.runner import run_scanner
def main() -> None: def main() -> None:
# 재시작 시 기존 잔고 복원 (이중 매수 방지)
restore_positions()
# 트레일링 스탑 감시 스레드 (10초 주기) # 트레일링 스탑 감시 스레드 (10초 주기)
monitor_thread = threading.Thread( monitor_thread = threading.Thread(
target=run_monitor, args=(10,), daemon=True, name="monitor" target=run_monitor, args=(10,), daemon=True, name="monitor"