-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
241 lines (196 loc) · 11.4 KB
/
main.py
File metadata and controls
241 lines (196 loc) · 11.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
# ==========================================================
# [main.py] - 🌟 100% 통합 완성본 🌟
# ⚠️ 이 주석 및 파일명 표기는 절대 지우지 마세요.
# 💡 [V24.10] 텔레그램 API 통신 타임아웃(TimedOut) 방어 및 커넥션 풀 최적화 이식 완료
# 💡 [V24.11 수술] VolatilityEngine 동적 연결 및 TelegramController 의존성 주입
# 💡 [V24.15 대수술] V_VWAP 플러그인 의존성 100% 영구 적출 및 2대 코어 체제 확립
# 💡 [V24.20 패치] 듀얼 레퍼런싱(SOXX/SOXL) 인프라 및 스냅샷 파이프라인 증설
# 🚨 [V25.19 핫픽스] EST/KST 타임존 혼용에 따른 스케줄링 오작동 방어 (명시적 타임존 주입)
# 🚨 [V25.19 핫픽스] 듀얼 레퍼런싱(TICKER_BASE_MAP) 전역 공유 파이프라인 완벽 확립
# 🚀 [V27.00 자가 업데이트 라우터 이식] 텔레그램 핸들러 루프에 'update' 명령어 공식 등록 완료
# 🚨 [V27.11 그랜드 수술] 코파일럿 합작 - asyncio.Lock 런타임 붕괴 방어, NaN 오판 및 역방향 폴백 차단,
# 콜드 스타트 폭풍 제어(first=30) 및 통합 타임존(America/New_York) 파이프라인 구축
# 🚨 [V27.12 핫픽스] 사계절 타임존 이중 타격 원천 차단, 이중 동기화 붕괴 방지 및 FD 최적화 이식
# MODIFIED: [V28.27 그랜드 수술] 로그 파일명 생성 시 KST(서버 시간) 의존성 전면 소각 및 EST(미국 동부) 타임존 락온으로 디버깅 파편화 영구 차단
# ==========================================================
import os
import logging
import datetime
import pytz
import asyncio
import math # 🚨 [수술 완료] NaN 검증용 math 모듈 추가
from telegram.ext import Application, CommandHandler, CallbackQueryHandler, MessageHandler, filters
from dotenv import load_dotenv
from trading_bot.config import ConfigManager
from trading_bot.broker.kis_api import KoreaInvestmentBroker
from trading_bot.strategy.infinite import InfiniteStrategy
from trading_bot.telegram.commands import TelegramController
from trading_bot.strategy.queue_ledger import QueueLedger
from trading_bot.strategy.reversion import ReversionStrategy
from trading_bot.strategy.volatility import VolatilityEngine
from trading_bot.scheduler.core_jobs import (
scheduled_token_check,
scheduled_auto_sync_summer,
scheduled_auto_sync_winter,
scheduled_force_reset,
scheduled_self_cleaning,
get_target_hour,
perform_self_cleaning
)
from trading_bot.scheduler.trade_jobs import (
scheduled_regular_trade,
scheduled_sniper_monitor,
scheduled_vwap_trade,
scheduled_vwap_init_and_cancel,
scheduled_after_market_lottery
)
TICKER_BASE_MAP = {
"SOXL": "SOXX",
"TQQQ": "QQQ",
"TSLL": "TSLA",
"FNGU": "FNGS",
"BULZ": "FNGS"
}
if not os.path.exists('data'): os.makedirs('data')
if not os.path.exists('logs'): os.makedirs('logs')
load_dotenv()
TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN")
try:
ADMIN_CHAT_ID = int(os.getenv("ADMIN_CHAT_ID")) if os.getenv("ADMIN_CHAT_ID") else None
except ValueError:
ADMIN_CHAT_ID = None
APP_KEY = os.getenv("APP_KEY")
APP_SECRET = os.getenv("APP_SECRET")
CANO = os.getenv("CANO")
ACNT_PRDT_CD = os.getenv("ACNT_PRDT_CD", "01")
# 🚨 [수술 완료] ADMIN_CHAT_ID 누락 시 묵언수행(Silent Zombie) 봇 구동 원천 차단
if not all([TELEGRAM_TOKEN, APP_KEY, APP_SECRET, CANO, ADMIN_CHAT_ID]):
print("❌ [치명적 오류] .env 파일에 봇 구동 필수 키(TELEGRAM_TOKEN, APP_KEY, APP_SECRET, CANO, ADMIN_CHAT_ID)가 누락되었습니다. 봇을 종료합니다.")
exit(1)
# 🚨 [V28.27 수술 완료] 로그 파일명 타임존 EST 락온
est_tz_log = pytz.timezone('US/Eastern')
log_filename = f"logs/bot_app_{datetime.datetime.now(est_tz_log).strftime('%Y%m%d')}.log"
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO,
handlers=[
logging.FileHandler(log_filename, encoding='utf-8'),
logging.StreamHandler()
]
)
async def scheduled_volatility_scan(context):
"""
10:20 EST (정규장 개장 50분 후) 격발.
대상 종목들의 HV와 당일 VXN을 연산하여 터미널 메인 화면에 1-Tier 브리핑 덤프
"""
app_data = context.job.data
cfg = app_data['cfg']
base_map = app_data.get('base_map', TICKER_BASE_MAP)
print("\n" + "=" * 60)
print("📈 [자율주행 변동성 스캔 완료] (10:20 EST 스냅샷)")
active_tickers = cfg.get_active_tickers()
if not active_tickers:
print("📊 현재 운용 중인 종목이 없습니다.")
else:
briefing_lines = []
vol_engine = VolatilityEngine()
for ticker in active_tickers:
target_base = base_map.get(ticker, ticker)
try:
weight_data = await asyncio.to_thread(vol_engine.calculate_weight, target_base)
raw_weight = weight_data.get('weight', 1.0) if isinstance(weight_data, dict) else weight_data
real_weight = float(raw_weight)
# 🚨 [수술 완료] NaN/Inf 결측치 침투 시 무조건 중립(1.0) 폴백 적용하여 공격 오판 원천 차단
if not math.isfinite(real_weight):
raise ValueError(f"비정상 수학 수치 산출: {real_weight}")
except Exception as e:
# 🚨 [수술 완료] 에러 시 역방향 배팅(0.85/1.15) 금지. 무조건 중립(1.0) 안전마진 강제 적용
logging.warning(f"[{ticker}] 변동성 지표 산출 실패. 중립 안전마진(1.0) 강제 적용: {e}")
real_weight = 1.0
status_text = "OFF 권장" if real_weight <= 1.0 else "ON 권장"
if ticker != target_base: briefing_lines.append(f"{ticker}({target_base}): {real_weight:.2f} ({status_text})")
else: briefing_lines.append(f"{ticker}: {real_weight:.2f} ({status_text})")
print(f"📊 [자율주행 지표] {' | '.join(briefing_lines)} (상세 게이지: /mode)")
print("=" * 60 + "\n")
# 🚨 [수술 완료] 파이썬 3.10+ 호환성을 위해 이벤트 루프 내부에서 asyncio.Lock()을 안전하게 생성하는 콜백
async def post_init(application: Application):
tx_lock = asyncio.Lock()
application.bot_data['app_data']['tx_lock'] = tx_lock
application.bot_data['bot_controller'].tx_lock = tx_lock
def main():
TARGET_HOUR, season_msg = get_target_hour()
cfg = ConfigManager()
latest_version = cfg.get_latest_version()
print("=" * 60)
print(f"🚀 앱솔루트 스노우볼 퀀트 엔진 {latest_version} (초경량 2대 코어 아키텍처 탑재)")
print(f"📅 날짜 정보: {season_msg}")
print(f"⏰ 자동 동기화: 08:30(여름) / 09:30(겨울) 자동 변경")
print(f"🛡️ 1-Tier 자율주행 지표 스캔 대기 중... (매일 10:20 EST 격발)")
print("=" * 60)
perform_self_cleaning()
# 상단에서 ADMIN_CHAT_ID 유효성 검사를 마쳤으므로 무조건 세팅
cfg.set_chat_id(ADMIN_CHAT_ID)
broker = KoreaInvestmentBroker(APP_KEY, APP_SECRET, CANO, ACNT_PRDT_CD)
strategy = InfiniteStrategy(cfg)
queue_ledger = QueueLedger()
strategy_rev = ReversionStrategy()
# 🚨 [수술 완료] tx_lock은 동기 함수인 main()이 아닌 비동기 post_init에서 생성됩니다.
bot = TelegramController(
cfg, broker, strategy, tx_lock=None,
queue_ledger=queue_ledger, strategy_rev=strategy_rev
)
# 🚨 [수술 완료] IANA 표준 타임존 파이프라인 확립
kst = pytz.timezone('Asia/Seoul')
est = pytz.timezone('America/New_York')
app_data = {
'cfg': cfg, 'broker': broker, 'strategy': strategy,
'queue_ledger': queue_ledger, 'strategy_rev': strategy_rev,
'bot': bot, 'tx_lock': None, 'base_map': TICKER_BASE_MAP,
'tz_kst': kst, 'tz_est': est # 타임존 전역 공유
}
app = (
Application.builder()
.token(TELEGRAM_TOKEN)
.read_timeout(30.0)
.write_timeout(30.0)
.connect_timeout(30.0)
.pool_timeout(30.0)
# MODIFIED: [단일 관리자 봇 환경에 맞춰 FD(파일 디스크립터) 누수 및 OS 자원 고갈 방지를 위해 커넥션 풀을 512에서 8로 최적화]
.connection_pool_size(8)
.post_init(post_init) # 🚨 Lock 생성을 위한 훅 연결
.build()
)
app.bot_data['app_data'] = app_data
app.bot_data['bot_controller'] = bot
for cmd, handler in [
("start", bot.cmd_start), ("record", bot.cmd_record), ("history", bot.cmd_history),
("sync", bot.cmd_sync), ("settlement", bot.cmd_settlement), ("seed", bot.cmd_seed),
("ticker", bot.cmd_ticker), ("mode", bot.cmd_mode), ("reset", bot.cmd_reset),
("version", bot.cmd_version), ("update", bot.cmd_update)
]:
app.add_handler(CommandHandler(cmd, handler))
app.add_handler(CallbackQueryHandler(bot.handle_callback))
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, bot.handle_message))
jq = app.job_queue
# 1. 시스템 관리 스케줄러 (core)
for tt in [datetime.time(7,0,tzinfo=kst), datetime.time(11,0,tzinfo=kst), datetime.time(16,30,tzinfo=kst), datetime.time(22,0,tzinfo=kst)]:
jq.run_daily(scheduled_token_check, time=tt, days=tuple(range(7)), chat_id=ADMIN_CHAT_ID, data=app_data)
# MODIFIED: [이중 잔고 동기화 방어] 사계절(TARGET_HOUR) 기준에 맞춰 여름/겨울 동기화 스케줄을 단 하나만 등록
SYNC_HOUR = 8 if TARGET_HOUR == 17 else 9
SYNC_FUNC = scheduled_auto_sync_summer if TARGET_HOUR == 17 else scheduled_auto_sync_winter
jq.run_daily(SYNC_FUNC, time=datetime.time(SYNC_HOUR, 30, tzinfo=kst), days=tuple(range(7)), chat_id=ADMIN_CHAT_ID, data=app_data)
# MODIFIED: [이중 타격 방어] 17시/18시가 무조건 모두 등록되는 버그를 제거하고 TARGET_HOUR 단일 슬롯에만 락 초기화 등록
jq.run_daily(scheduled_force_reset, time=datetime.time(TARGET_HOUR, 0, tzinfo=kst), days=(0,1,2,3,4), chat_id=ADMIN_CHAT_ID, data=app_data)
jq.run_daily(scheduled_volatility_scan, time=datetime.time(10, 20, tzinfo=est), days=(0,1,2,3,4), chat_id=ADMIN_CHAT_ID, data=app_data)
# 2. 실전 전투 매매 스케줄러 (trade)
# MODIFIED: [이중 타격 방어] 17:05/18:05 동시 발사(Double-buying) 버그를 원천 차단하고 TARGET_HOUR에만 정규장 타격 스케줄 등록
jq.run_daily(scheduled_regular_trade, time=datetime.time(TARGET_HOUR, 5, tzinfo=kst), days=(0,1,2,3,4), chat_id=ADMIN_CHAT_ID, data=app_data)
jq.run_daily(scheduled_vwap_init_and_cancel, time=datetime.time(15, 30, tzinfo=est), days=(0,1,2,3,4), chat_id=ADMIN_CHAT_ID, data=app_data)
# 🚨 [수술 완료] 콜드 스타트 폭풍 방어: 봇 구동 후 30초 뒤 첫 실행(first=30)
jq.run_repeating(scheduled_sniper_monitor, interval=60, first=30, chat_id=ADMIN_CHAT_ID, data=app_data)
jq.run_repeating(scheduled_vwap_trade, interval=60, first=30, chat_id=ADMIN_CHAT_ID, data=app_data)
jq.run_daily(scheduled_after_market_lottery, time=datetime.time(16, 5, tzinfo=est), days=(0,1,2,3,4), chat_id=ADMIN_CHAT_ID, data=app_data)
jq.run_daily(scheduled_self_cleaning, time=datetime.time(6, 0, tzinfo=kst), days=tuple(range(7)), chat_id=ADMIN_CHAT_ID, data=app_data)
app.run_polling()
if __name__ == "__main__":
main()