최초 프로젝트 업로드 (Script Auto Commit)
This commit is contained in:
268
main.py
Normal file
268
main.py
Normal 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()
|
||||
Reference in New Issue
Block a user