|
6 | 6 | # 💡 [V24.16 팩트 동기화] 하락장 방어 매수 Buy2 타점 (0.9725) 교정 |
7 | 7 | # 💡 [V24.16 팩트 동기화] 1층 전량 익절 타점 고유 매수가 기반(layer_price * 1.006) 원복 |
8 | 8 | # 🚨 [V25.13 디커플링 스왑 패치] UI와 동일하게 Buy1과 Buy2의 타점을 고가->저가 순으로 스왑 연동 |
| 9 | +# 🚨 [V25.14 팩트 동기화] 1층 물귀신 덤핑 차단 및 지층별 평단가 완벽 분리 개별 탈출(Decoupling) 이식 |
9 | 10 | # ========================================================== |
10 | 11 | import math |
11 | 12 |
|
@@ -92,34 +93,36 @@ def get_dynamic_plan(self, ticker, curr_p, prev_c, current_weight, vwap_status, |
92 | 93 | if curr_p >= jackpot_trigger: |
93 | 94 | target_sell_qty = rem_qty |
94 | 95 | target_p = round(jackpot_trigger, 2) |
| 96 | + orders.append({"side": "SELL", "qty": target_sell_qty, "price": target_p}) |
95 | 97 | else: |
96 | | - target_sell_qty = 0 |
97 | | - target_p = 0.0 |
98 | | - |
| 98 | + # MODIFIED: [V25.14 팩트 동기화] 1층과 상위 층(2~N층) 평단가 및 익절 타점 완벽 분리 |
99 | 99 | dates_in_queue = sorted(list(set(item.get('date') for item in q_data if item.get('date'))), reverse=True) |
100 | 100 |
|
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 |
117 | 109 |
|
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}) |
123 | 126 |
|
124 | 127 | return {"orders": orders, "trigger_loc": True} |
125 | 128 |
|
@@ -153,50 +156,71 @@ def get_dynamic_plan(self, ticker, curr_p, prev_c, current_weight, vwap_status, |
153 | 156 |
|
154 | 157 | else: # SELL |
155 | 158 | if total_q > 0: |
156 | | - target_sell_qty = 0 |
157 | 159 | jackpot_trigger = avg_price * 1.010 |
158 | | - sell_price_target = round(prev_c * 1.006, 2) |
159 | 160 |
|
160 | 161 | if curr_p >= jackpot_trigger: |
161 | 162 | target_sell_qty = total_q |
162 | 163 | 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) |
165 | 164 |
|
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) |
169 | 170 |
|
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 |
173 | 173 |
|
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 |
185 | 188 |
|
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 |
194 | 193 |
|
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 |
197 | 196 |
|
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}) |
200 | 224 |
|
201 | 225 | return {"orders": orders, "trigger_loc": False} |
202 | 226 |
|
|
0 commit comments