최초 프로젝트 업로드 (Script Auto Commit)

This commit is contained in:
2025-12-03 22:37:46 +09:00
commit ed7084dd8f
30 changed files with 3254 additions and 0 deletions

268
main.py Normal file
View File

@@ -0,0 +1,268 @@
import os
import time
import threading
import argparse
from typing import List, Dict, Tuple
from dotenv import load_dotenv
import logging
# Modular imports
from src.common import logger, setup_logger
from src.config import load_config, read_symbols, get_symbols_file, build_runtime_config, RuntimeConfig
from src.threading_utils import run_sequential, run_with_threads
from src.notifications import send_telegram, report_error, send_startup_test_message
from src.holdings import load_holdings
from src.signals import check_sell_conditions, CheckType
from src.kiwoom_api import get_kiwoom_api
load_dotenv()
def minutes_to_timeframe(minutes: int) -> str:
"""분 단위를 캔들봉 timeframe 문자열로 변환 (예: 60 -> '1h', 240 -> '4h')"""
if minutes < 60:
return f"{minutes}m"
elif minutes % 1440 == 0:
return f"{minutes // 1440}d"
elif minutes % 60 == 0:
return f"{minutes // 60}h"
else:
return f"{minutes}m"
def process_symbols_and_holdings(
cfg: RuntimeConfig,
symbols: List[str],
config: Dict[str, any],
last_buy_check_time: float,
last_sell_check_time: float,
last_profit_taking_check_time: float
) -> Tuple[float, float, float]:
"""매수/매도 조건을 확인하고 실행.
Args:
cfg: 런타임 설정
symbols: 심볼 리스트
config: 설정 딕셔너리
last_buy_check_time: 마지막 매수 체크 시간
last_sell_check_time: 마지막 손절 체크 시간
last_profit_taking_check_time: 마지막 익절 체크 시간
Returns:
(업데이트된 매수 체크 시간, 손절 체크 시간, 익절 체크 시간)
"""
holdings = load_holdings('holdings.json')
held_symbols = set(holdings.keys()) if holdings else set()
buy_candidate_symbols = [s for s in symbols if s not in held_symbols]
current_time = time.time()
buy_signal_count = 0
sell_signal_count = 0
buy_interval_minutes = config.get("buy_check_interval_minutes", 240)
buy_interval = buy_interval_minutes * 60
buy_timeframe = minutes_to_timeframe(buy_interval_minutes)
if current_time - last_buy_check_time >= buy_interval:
logger.info("매수 조건 확인 시작 (주기: %d분, 데이터: %s)", buy_interval_minutes, buy_timeframe)
from src.holdings import get_kiwoom_balances
krw_balance = None
try:
balances = get_kiwoom_balances(cfg.kiwoom_account_number)
krw_balance = balances.get('KRW', 0)
except Exception as e:
logger.warning("Kiwoom 잔고 조회 실패: %s", e)
buy_amount = config.get('auto_trade', {}).get('buy_amount', 1000000)
if krw_balance is not None and krw_balance < buy_amount:
msg = f"[매수 건너뜀] Kiwoom 계좌 잔고 부족: 현재 KRW={krw_balance:.0f}, 필요={buy_amount:.0f}"
logger.warning(msg)
if cfg.telegram_bot_token and cfg.telegram_chat_id:
send_telegram(cfg.telegram_bot_token, cfg.telegram_chat_id, msg, parse_mode=cfg.telegram_parse_mode or "HTML")
else:
from src.config import RuntimeConfig
cfg_with_buy_timeframe = RuntimeConfig(
timeframe=buy_timeframe,
indicator_timeframe=buy_timeframe,
candle_count=cfg.candle_count,
symbol_delay=cfg.symbol_delay,
interval=cfg.interval,
loop=cfg.loop,
dry_run=cfg.dry_run,
max_threads=cfg.max_threads,
telegram_parse_mode=cfg.telegram_parse_mode,
trading_mode=cfg.trading_mode,
telegram_bot_token=cfg.telegram_bot_token,
telegram_chat_id=cfg.telegram_chat_id,
kiwoom_account_number=cfg.kiwoom_account_number,
aggregate_alerts=cfg.aggregate_alerts,
benchmark=cfg.benchmark,
telegram_test=getattr(cfg, 'telegram_test', False),
config=cfg.config
)
if cfg.max_threads > 1:
buy_signal_count = run_with_threads(buy_candidate_symbols, parse_mode=cfg.telegram_parse_mode, aggregate_enabled=cfg.aggregate_alerts, cfg=cfg_with_buy_timeframe)
else:
buy_signal_count = run_sequential(buy_candidate_symbols, parse_mode=cfg.telegram_parse_mode, aggregate_enabled=cfg.aggregate_alerts, cfg=cfg_with_buy_timeframe)
last_buy_check_time = current_time
else:
logger.debug("매수 조건 확인 대기 중 (다음 확인까지 %.1f분 남음)", (buy_interval - (current_time - last_buy_check_time)) / 60)
# 손절/익절 조건 체크 설정
stop_loss_interval_minutes = config.get("stop_loss_check_interval_minutes", 60)
stop_loss_interval = stop_loss_interval_minutes * 60
profit_taking_interval_minutes = config.get("profit_taking_check_interval_minutes", 240)
profit_taking_interval = profit_taking_interval_minutes * 60
sell_signal_count = 0
# 손절/익절 체크 여부 확인
should_check_stop_loss = (current_time - last_sell_check_time >= stop_loss_interval)
should_check_profit_taking = (current_time - last_profit_taking_check_time >= profit_taking_interval)
# Holdings 업데이트는 한 번만 수행 (손절/익절 체크 전)
if should_check_stop_loss or should_check_profit_taking:
account_number = cfg.kiwoom_account_number
# Holdings 한 번만 업데이트
if account_number:
try:
from src.holdings import save_holdings, fetch_holdings_from_kiwoom
updated_holdings = fetch_holdings_from_kiwoom(account_number)
if updated_holdings is not None:
holdings = updated_holdings
save_holdings(holdings, 'holdings.json')
except Exception as e:
logger.exception("Kiwoom holdings 조회 중 오류: %s", e)
report_error(cfg.telegram_bot_token, cfg.telegram_chat_id, f"[오류] Holdings 조회 실패: {e}", cfg.dry_run)
# 손절 체크 (1시간 주기): 조건1, 조건3, 조건4-2, 조건5-2
if should_check_stop_loss:
logger.info("손절 조건 확인 시작 (주기: %d분)", stop_loss_interval_minutes)
try:
if holdings:
sell_results, count = check_sell_conditions(holdings, cfg=cfg, config=config, check_type=CheckType.STOP_LOSS)
sell_signal_count += count
logger.info("보유 종목 손절 조건 확인 완료: %d개 검사, %d개 매도 신호", len(sell_results), count)
else:
logger.debug("보유 종목 없음")
except Exception as e:
logger.exception("보유 종목 손절 조건 확인 중 오류: %s", e)
report_error(cfg.telegram_bot_token, cfg.telegram_chat_id, f"[오류] 손절 조건 확인 실패: {e}", cfg.dry_run)
last_sell_check_time = current_time
# 익절 체크 (4시간 주기): 조건2, 조건4-1, 조건5-1
if should_check_profit_taking:
logger.info("익절 조건 확인 시작 (주기: %d분)", profit_taking_interval_minutes)
try:
if holdings:
sell_results, count = check_sell_conditions(holdings, cfg=cfg, config=config, check_type=CheckType.PROFIT_TAKING)
sell_signal_count += count
logger.info("보유 종목 익절 조건 확인 완료: %d개 검사, %d개 매도 신호", len(sell_results), count)
else:
logger.debug("보유 종목 없음")
except Exception as e:
logger.exception("보유 종목 익절 조건 확인 중 오류: %s", e)
report_error(cfg.telegram_bot_token, cfg.telegram_chat_id, f"[오류] 익절 조건 확인 실패: {e}", cfg.dry_run)
last_profit_taking_check_time = current_time
else:
if not should_check_stop_loss:
logger.debug("손절 조건 확인 대기 중 (다음 확인까지 %.1f분 남음)", (stop_loss_interval - (current_time - last_sell_check_time)) / 60)
if not should_check_profit_taking:
logger.debug("익절 조건 확인 대기 중 (다음 확인까지 %.1f분 남음)", (profit_taking_interval - (current_time - last_profit_taking_check_time)) / 60)
logger.info("[요약] 매수 조건 충족 심볼: %d개, 매도 조건 충족 심볼: %d", buy_signal_count, sell_signal_count)
return last_buy_check_time, last_sell_check_time, last_profit_taking_check_time
def execute_benchmark(cfg, symbols):
"""Execute benchmark to compare single-thread and multi-thread performance."""
logger.info("간단 벤치마크 시작: 심볼=%d", len(symbols))
start = time.time()
run_sequential(symbols, parse_mode=cfg.telegram_parse_mode, aggregate_enabled=False, cfg=cfg)
elapsed_single = time.time() - start
logger.info("순차 처리 소요 시간: %.2f", elapsed_single)
start = time.time()
run_with_threads(symbols, parse_mode=cfg.telegram_parse_mode, aggregate_enabled=False, cfg=cfg)
elapsed_parallel = time.time() - start
logger.info("병렬 처리(%d 스레드) 소요 시간: %.2f", cfg.max_threads, elapsed_parallel)
if elapsed_parallel < elapsed_single:
reduction = (elapsed_single - elapsed_parallel) / elapsed_single * 100
logger.info("병렬로 %.1f%% 빨라졌습니다 (권장 스레드=%d).", reduction, cfg.max_threads)
else:
logger.info("병렬이 순차보다 빠르지 않습니다. 네트워크/IO 패턴을 점검하세요.")
def main():
parser = argparse.ArgumentParser(description="주식 자동매매 프로그램")
parser.add_argument("--benchmark", action="store_true", help="벤치마크 실행")
args = parser.parse_args()
config = load_config()
if not config:
logger.error("설정 로드 실패; 종료합니다")
return
cfg = build_runtime_config(config)
setup_logger(cfg.dry_run)
logger.info("=" * 80)
logger.info("주식 자동매매 프로그램 시작")
logger.info("=" * 80)
# Kiwoom API 로그인
get_kiwoom_api()
symbols = read_symbols(get_symbols_file(config))
if not symbols:
logger.error("심볼 로드 실패; 종료합니다")
return
if cfg.telegram_test:
send_startup_test_message(cfg.telegram_bot_token, cfg.telegram_chat_id, cfg.telegram_parse_mode, cfg.dry_run)
if not cfg.dry_run and (not cfg.telegram_bot_token or not cfg.telegram_chat_id):
logger.error("dry-run이 아닐 때 텔레그램 환경변수 필수: TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID")
return
buy_check_minutes = config.get("buy_check_interval_minutes", 240)
stop_loss_check_minutes = config.get("stop_loss_check_interval_minutes", 60)
profit_taking_check_minutes = config.get("profit_taking_check_interval_minutes", 240)
logger.info("설정: symbols=%d, symbol_delay=%.2f초, candle_count=%d, loop=%s, dry_run=%s, max_threads=%d, trading_mode=%s",
len(symbols), cfg.symbol_delay, cfg.candle_count, cfg.loop, cfg.dry_run, cfg.max_threads, cfg.trading_mode)
logger.info("매수 확인 주기: %d분 (%s봉), 손절 확인 주기: %d분 (%s봉), 익절 확인 주기: %d분 (%s봉)",
buy_check_minutes, minutes_to_timeframe(buy_check_minutes),
stop_loss_check_minutes, minutes_to_timeframe(stop_loss_check_minutes),
profit_taking_check_minutes, minutes_to_timeframe(profit_taking_check_minutes))
if args.benchmark:
execute_benchmark(cfg, symbols)
return
last_buy_check_time = 0
last_sell_check_time = 0
last_profit_taking_check_time = 0
if not cfg.loop:
process_symbols_and_holdings(cfg, symbols, config, last_buy_check_time, last_sell_check_time, last_profit_taking_check_time)
else:
buy_check_minutes = config.get("buy_check_interval_minutes", 240)
stop_loss_check_minutes = config.get("stop_loss_check_interval_minutes", 60)
profit_taking_check_minutes = config.get("profit_taking_check_interval_minutes", 240)
loop_interval_minutes = min(buy_check_minutes, stop_loss_check_minutes, profit_taking_check_minutes)
interval_seconds = max(10, loop_interval_minutes * 60)
logger.info("루프 모드 시작: %d분 간격 (매수확인: %d분마다, 손절확인: %d분마다, 익절확인: %d분마다)",
loop_interval_minutes, buy_check_minutes, stop_loss_check_minutes, profit_taking_check_minutes)
try:
while True:
try:
last_buy_check_time, last_sell_check_time, last_profit_taking_check_time = process_symbols_and_holdings(
cfg, symbols, config, last_buy_check_time, last_sell_check_time, last_profit_taking_check_time)
except Exception as e:
logger.exception("루프 내 작업 중 오류: %s", e)
report_error(cfg.telegram_bot_token, cfg.telegram_chat_id, f"[오류] 루프 내 작업 실패: {e}", cfg.dry_run)
logger.info("다음 실행까지 %d초 대기", interval_seconds)
time.sleep(interval_seconds)
except KeyboardInterrupt:
logger.info("사용자가 루프를 중단함")
if __name__ == "__main__":
main()