Skip to content

Commit 63bb5b4

Browse files
aaron-jangclaude
andcommitted
merge: upstream/main 동기화 및 trading-system-spec V25.14 업데이트
upstream의 프롬프트.txt 변경사항을 docs/trading-system-spec.md에 통합. V-REV 하이브리드 전술, AVWAP 듀얼 레퍼런싱, TrueSync 로직 A~F 등 반영. 프롬프트.txt는 기존 docs 전환 정책에 따라 삭제 유지. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2 parents 6d6679b + 6209a6f commit 63bb5b4

7 files changed

Lines changed: 578 additions & 472 deletions

File tree

docs/trading-system-spec.md

Lines changed: 83 additions & 64 deletions
Large diffs are not rendered by default.

trading_bot/strategy/reversion.py

Lines changed: 81 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
# 💡 [V24.16 팩트 동기화] 하락장 방어 매수 Buy2 타점 (0.9725) 교정
77
# 💡 [V24.16 팩트 동기화] 1층 전량 익절 타점 고유 매수가 기반(layer_price * 1.006) 원복
88
# 🚨 [V25.13 디커플링 스왑 패치] UI와 동일하게 Buy1과 Buy2의 타점을 고가->저가 순으로 스왑 연동
9+
# 🚨 [V25.14 팩트 동기화] 1층 물귀신 덤핑 차단 및 지층별 평단가 완벽 분리 개별 탈출(Decoupling) 이식
910
# ==========================================================
1011
import math
1112

@@ -92,34 +93,36 @@ def get_dynamic_plan(self, ticker, curr_p, prev_c, current_weight, vwap_status,
9293
if curr_p >= jackpot_trigger:
9394
target_sell_qty = rem_qty
9495
target_p = round(jackpot_trigger, 2)
96+
orders.append({"side": "SELL", "qty": target_sell_qty, "price": target_p})
9597
else:
96-
target_sell_qty = 0
97-
target_p = 0.0
98-
98+
# MODIFIED: [V25.14 팩트 동기화] 1층과 상위 층(2~N층) 평단가 및 익절 타점 완벽 분리
9999
dates_in_queue = sorted(list(set(item.get('date') for item in q_data if item.get('date'))), reverse=True)
100100

101-
for i, d in enumerate(dates_in_queue):
102-
if i >= 3: break
103-
104-
lots_for_date = [item for item in q_data if item.get('date') == d]
105-
grp_qty = sum(item.get('qty', 0) for item in lots_for_date)
106-
if grp_qty == 0: continue
107-
108-
# 💡 [핵심 수술] 1층(i==0)은 고유 매수가(layer_price) 앵커 적용
109-
if i == 0:
110-
layer_price = sum(item.get('qty', 0) * item.get('price', 0.0) for item in lots_for_date) / grp_qty if grp_qty > 0 else prev_c
111-
trigger = round(layer_price * 1.006, 2)
112-
else:
113-
trigger = round(avg_price * 1.005, 2)
114-
115-
if target_p == 0.0 or trigger < target_p:
116-
target_p = trigger
101+
layer_1_qty = 0
102+
layer_1_price = 0.0
103+
104+
if dates_in_queue:
105+
lots_1 = [item for item in q_data if item.get('date') == dates_in_queue[0]]
106+
layer_1_qty = sum(item.get('qty', 0) for item in lots_1)
107+
if layer_1_qty > 0:
108+
layer_1_price = sum(item.get('qty', 0) * item.get('price', 0.0) for item in lots_1) / layer_1_qty
117109

118-
target_sell_qty += grp_qty
119-
120-
safe_sell_qty = min(target_sell_qty, rem_qty)
121-
if safe_sell_qty > 0 and target_p > 0:
122-
orders.append({"side": "SELL", "qty": safe_sell_qty, "price": target_p})
110+
upper_qty = total_q - layer_1_qty
111+
total_inv = sum(item.get('qty', 0) * item.get('price', 0.0) for item in q_data)
112+
upper_inv = total_inv - (layer_1_qty * layer_1_price)
113+
upper_avg = upper_inv / upper_qty if upper_qty > 0 else 0.0
114+
115+
trigger_1 = round(layer_1_price * 1.006, 2)
116+
trigger_upper = round(upper_avg * 1.005, 2) if upper_qty > 0 else 0.0
117+
118+
available_l1 = min(layer_1_qty, rem_qty)
119+
available_upper = min(upper_qty, rem_qty - available_l1)
120+
121+
if available_l1 > 0 and curr_p >= trigger_1:
122+
orders.append({"side": "SELL", "qty": available_l1, "price": trigger_1})
123+
124+
if available_upper > 0 and trigger_upper > 0 and curr_p >= trigger_upper:
125+
orders.append({"side": "SELL", "qty": available_upper, "price": trigger_upper})
123126

124127
return {"orders": orders, "trigger_loc": True}
125128

@@ -153,50 +156,71 @@ def get_dynamic_plan(self, ticker, curr_p, prev_c, current_weight, vwap_status,
153156

154157
else: # SELL
155158
if total_q > 0:
156-
target_sell_qty = 0
157159
jackpot_trigger = avg_price * 1.010
158-
sell_price_target = round(prev_c * 1.006, 2)
159160

160161
if curr_p >= jackpot_trigger:
161162
target_sell_qty = total_q
162163
sell_price_target = round(jackpot_trigger, 2)
163-
else:
164-
dates_in_queue = sorted(list(set(item.get('date') for item in q_data if item.get('date'))), reverse=True)
165164

166-
target_p = 0.0
167-
for i, d in enumerate(dates_in_queue):
168-
if i >= 3: break
165+
rem_qty_to_sell = max(0, target_sell_qty - self.executed["SELL_QTY"].get(ticker, 0))
166+
167+
if rem_qty_to_sell > 0:
168+
exact_qs = (target_sell_qty * slice_ratio_sell) + self.residual["SELL"].get(ticker, 0.0)
169+
alloc_qs = math.floor(exact_qs)
169170

170-
lots_for_date = [item for item in q_data if item.get('date') == d]
171-
grp_qty = sum(item.get('qty', 0) for item in lots_for_date)
172-
if grp_qty == 0: continue
171+
alloc_qs = min(alloc_qs, rem_qty_to_sell)
172+
self.residual["SELL"][ticker] = exact_qs - alloc_qs
173173

174-
# 💡 [핵심 수술] 1층(i==0)은 고유 매수가(layer_price) 앵커 적용
175-
if i == 0:
176-
layer_price = sum(item.get('qty', 0) * item.get('price', 0.0) for item in lots_for_date) / grp_qty if grp_qty > 0 else prev_c
177-
trigger = round(layer_price * 1.006, 2)
178-
else:
179-
trigger = round(avg_price * 1.005, 2)
180-
181-
target_sell_qty += grp_qty
182-
183-
if target_p == 0.0 or trigger < target_p:
184-
target_p = trigger
174+
if alloc_qs > 0:
175+
orders.append({"side": "SELL", "qty": alloc_qs, "price": sell_price_target})
176+
else:
177+
# MODIFIED: [V25.14 팩트 동기화] 1층과 상위 층(2~N층) 평단가 및 익절 타점 완벽 분리
178+
dates_in_queue = sorted(list(set(item.get('date') for item in q_data if item.get('date'))), reverse=True)
179+
180+
layer_1_qty = 0
181+
layer_1_price = 0.0
182+
183+
if dates_in_queue:
184+
lots_1 = [item for item in q_data if item.get('date') == dates_in_queue[0]]
185+
layer_1_qty = sum(item.get('qty', 0) for item in lots_1)
186+
if layer_1_qty > 0:
187+
layer_1_price = sum(item.get('qty', 0) * item.get('price', 0.0) for item in lots_1) / layer_1_qty
185188

186-
if target_p > 0.0:
187-
sell_price_target = target_p
188-
189-
rem_qty_to_sell = max(0, target_sell_qty - self.executed["SELL_QTY"].get(ticker, 0))
190-
191-
if rem_qty_to_sell > 0:
192-
exact_qs = (target_sell_qty * slice_ratio_sell) + self.residual["SELL"].get(ticker, 0.0)
193-
alloc_qs = math.floor(exact_qs)
189+
upper_qty = total_q - layer_1_qty
190+
total_inv = sum(item.get('qty', 0) * item.get('price', 0.0) for item in q_data)
191+
upper_inv = total_inv - (layer_1_qty * layer_1_price)
192+
upper_avg = upper_inv / upper_qty if upper_qty > 0 else 0.0
194193

195-
alloc_qs = min(alloc_qs, rem_qty_to_sell)
196-
self.residual["SELL"][ticker] = exact_qs - alloc_qs
194+
trigger_1 = round(layer_1_price * 1.006, 2)
195+
trigger_upper = round(upper_avg * 1.005, 2) if upper_qty > 0 else 0.0
197196

198-
if alloc_qs > 0:
199-
orders.append({"side": "SELL", "qty": alloc_qs, "price": sell_price_target})
197+
target_sell_qty = 0
198+
sell_price_target = 0.0
199+
200+
is_l1_hit = (layer_1_qty > 0 and curr_p >= trigger_1)
201+
is_upper_hit = (upper_qty > 0 and trigger_upper > 0 and curr_p >= trigger_upper)
202+
203+
if is_l1_hit and is_upper_hit:
204+
target_sell_qty = layer_1_qty + upper_qty
205+
sell_price_target = min(trigger_1, trigger_upper)
206+
elif is_l1_hit:
207+
target_sell_qty = layer_1_qty
208+
sell_price_target = trigger_1
209+
elif is_upper_hit:
210+
target_sell_qty = upper_qty
211+
sell_price_target = trigger_upper
212+
213+
rem_qty_to_sell = max(0, target_sell_qty - self.executed["SELL_QTY"].get(ticker, 0))
214+
215+
if rem_qty_to_sell > 0:
216+
exact_qs = (target_sell_qty * slice_ratio_sell) + self.residual["SELL"].get(ticker, 0.0)
217+
alloc_qs = math.floor(exact_qs)
218+
219+
alloc_qs = min(alloc_qs, rem_qty_to_sell)
220+
self.residual["SELL"][ticker] = exact_qs - alloc_qs
221+
222+
if alloc_qs > 0:
223+
orders.append({"side": "SELL", "qty": alloc_qs, "price": sell_price_target})
200224

201225
return {"orders": orders, "trigger_loc": False}
202226

trading_bot/strategy/v_avwap.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
# ==========================================================
2-
# [strategy_v_avwap.py]
2+
# [strategy_v_avwap.py] - Part 1/2 부 (상반부)
33
# 💡 V-REV 하이브리드 전용 차세대 AVWAP 스나이퍼 플러그인 (Dual-Referencing)
44
# ⚠️ 초공격형 당일 청산 암살자 (V-REV 잉여 현금 100% 몰빵 & -3% 하드스탑)
55
# ⚠️ 옵션 B 아키텍처: 기초자산(SOXX) 시그널 스캔 + 파생상품(SOXL) 미시구조 타격
6+
# 🚨 [PEP 8 포맷팅 패치] 미사용 변수(time_0930) 소각 (Ruff F841 교정 완료)
67
# ==========================================================
78
import logging
89
import datetime
@@ -25,7 +26,6 @@ def fetch_macro_context(self, base_ticker):
2526
[Pre-Fetch] 야후 파이낸스를 통해 기초자산(SOXX)의 과거 20일 20MA 및 30분봉(09:30) 평균 거래량 추출
2627
매일 장 초반 단 1회만 호출되어 메모리에 캐싱됩니다.
2728
"""
28-
# MODIFIED: ticker 변수명을 기초자산 명확성을 위해 base_ticker로 변경
2929
try:
3030
tkr = yf.Ticker(base_ticker)
3131
# 1. 20MA 추출용 일봉 데이터
@@ -68,6 +68,13 @@ def fetch_macro_context(self, base_ticker):
6868
except Exception as e:
6969
logging.error(f"🚨 [V_AVWAP] YF 기초자산 매크로 컨텍스트 추출 실패 ({base_ticker}): {e}")
7070
return None
71+
# ==========================================================
72+
# [strategy_v_avwap.py] - Part 2/2 부 (하반부)
73+
# 💡 V-REV 하이브리드 전용 차세대 AVWAP 스나이퍼 플러그인 (Dual-Referencing)
74+
# ⚠️ 초공격형 당일 청산 암살자 (V-REV 잉여 현금 100% 몰빵 & -3% 하드스탑)
75+
# ⚠️ 옵션 B 아키텍처: 기초자산(SOXX) 시그널 스캔 + 파생상품(SOXL) 미시구조 타격
76+
# 🚨 [PEP 8 포맷팅 패치] 미사용 변수(time_0930) 소각 (Ruff F841 교정 완료)
77+
# ==========================================================
7178

7279
# MODIFIED: 듀얼 레퍼런싱을 위해 base(SOXX)와 exec(SOXL) 파라미터로 이원화
7380
def get_decision(self, base_ticker, exec_ticker, base_curr_p, exec_curr_p, base_day_open, avwap_avg_price, avwap_qty, avwap_alloc_cash, context_data, df_1min_base, now_est):
@@ -78,7 +85,7 @@ def get_decision(self, base_ticker, exec_ticker, base_curr_p, exec_curr_p, base_
7885
curr_time = now_est.time()
7986

8087
# 기본 시간 통제선
81-
time_0930 = datetime.time(9, 30)
88+
# MODIFIED: [PEP 8 교정] 미사용 변수 time_0930 영구 소각 완료
8289
time_1000 = datetime.time(10, 0)
8390
time_1400 = datetime.time(14, 0)
8491
time_1430 = datetime.time(14, 30)
@@ -169,4 +176,3 @@ def get_decision(self, base_ticker, exec_ticker, base_curr_p, exec_curr_p, base_
169176
return {'action': 'WAIT', 'reason': '예산_부족_관망', 'vwap': base_vwap}
170177

171178
return {'action': 'WAIT', 'reason': '타점_대기중', 'vwap': base_vwap}
172-

trading_bot/strategy/volatility.py

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
# ==========================================================
2-
# [volatility_engine.py] - Part 1/2 부 (상반부)
2+
# [volatility_engine.py] - 🌟 100% 통합 완성본 🌟
33
# ⚠️ V3.2 패치: 기초지수 1년 ATR 절대 진폭 고정 및 공포지수 방향타 스위치 엔진 탑재
44
# 💡 [V24.09 패치] 야후 파이낸스 교착(Deadlock) 방어용 timeout=5 전면 이식 완료
55
# 💡 [V24.11 패치] 클래스 래퍼(VolatilityEngine) 구조 도입 및 calculate_weight 공통 인터페이스 신설
6+
# 🚨 [PEP 8 포맷팅 패치] 미사용 변수(weight) 100% 소각 (Ruff F841 교정 완료)
67
# ==========================================================
78
import yfinance as yf
89
import pandas as pd
@@ -89,11 +90,6 @@ def _calculate_1y_atr(ticker, cache_key, default_atr):
8990
except Exception as e:
9091
print(f"⚠️ [Engine] {ticker} ATR 연산 오류: {e}")
9192
return _load_cache(cache_key, default_atr)
92-
# ==========================================================
93-
# [volatility_engine.py] - Part 2/2 부 (하반부)
94-
# ⚠️ V3.2 패치: 기초지수 1년 ATR 절대 진폭 고정 및 공포지수 방향타 스위치 엔진 탑재
95-
# 💡 [V24.11 패치] 클래스 래퍼(VolatilityEngine) 구조 도입 및 calculate_weight 인터페이스 신설
96-
# ==========================================================
9793

9894
def get_tqqq_target_drop():
9995
""" [ TQQQ 스나이퍼 ] 실시간 VXN과 QQQ 1년 ATR을 결합하여 타격선 계산 """
@@ -112,17 +108,15 @@ def get_tqqq_target_drop():
112108
if valid_closes_1y.empty:
113109
return round(-(1.65 * 3), 2)
114110

115-
current_vxn = float(valid_closes_1y.iloc[-1])
116-
111+
# 미사용 변수(current_vxn)는 Ruff F841 교정을 위해 삭제 또는 필요 시점까지 보류
112+
# 본 로직에서는 가중치를 사용하지 않으므로 모두 제거
117113
try:
118114
mean_vxn = float(valid_closes_1y.mean())
119115
if pd.isna(mean_vxn) or mean_vxn <= 0:
120116
raise ValueError("Invalid Mean")
121117
_save_cache("VXN_MEAN", mean_vxn)
122118
except Exception:
123-
mean_vxn = _load_cache("VXN_MEAN", 20.0)
124-
125-
weight = current_vxn / mean_vxn
119+
_load_cache("VXN_MEAN", 20.0)
126120

127121
# 💡 [V3.2 패치] 1배수 기초지수 QQQ의 1년 ATR * 3배 동적 스케일링 (가중치 배제 절대 진폭 고정)
128122
qqq_1y_atr = _calculate_1y_atr("QQQ", "QQQ_ATR_1Y", 1.65)
@@ -156,17 +150,13 @@ def get_soxl_target_drop():
156150
if valid_hvs_1y.empty:
157151
return round(-(2.93 * 3), 2)
158152

159-
latest_hv = float(valid_hvs_1y.iloc[-1])
160-
161153
try:
162154
mean_hv = float(valid_hvs_1y.mean())
163155
if pd.isna(mean_hv) or mean_hv <= 0:
164156
raise ValueError("Invalid Mean")
165157
_save_cache("SOXX_HV_MEAN", mean_hv)
166158
except Exception:
167-
mean_hv = _load_cache("SOXX_HV_MEAN", 25.0)
168-
169-
weight = latest_hv / mean_hv
159+
_load_cache("SOXX_HV_MEAN", 25.0)
170160

171161
# 💡 [V3.2 패치] 1배수 기초지수 SOXX의 1년 ATR * 3배 동적 스케일링 (가중치 배제 절대 진폭 고정)
172162
soxx_1y_atr = _calculate_1y_atr("SOXX", "SOXX_ATR_1Y", 2.93)
@@ -270,9 +260,6 @@ def get_soxl_target_drop_full():
270260
fallback_amp = round(-(2.93 * 3), 2)
271261
return 0.0, 1.0, fallback_amp, fallback_amp
272262

273-
# ==========================================================
274-
# 💡 [V24.11 핵심 수술] main.py와 완벽한 통신을 위한 클래스 래퍼 신설
275-
# ==========================================================
276263
class VolatilityEngine:
277264
def __init__(self):
278265
pass
@@ -294,3 +281,4 @@ def calculate_weight(self, ticker):
294281
except Exception as e:
295282
print(f"⚠️ [VolatilityEngine] {ticker} 가중치 산출 래퍼 오류: {e}")
296283
return {'weight': 1.0}
284+

0 commit comments

Comments
 (0)