glorifiedjx commited on
Commit
26ae68c
ยท
verified ยท
1 Parent(s): e5fd9c2

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +960 -0
app.py ADDED
@@ -0,0 +1,960 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ ์ฃผ์‹ ๋””๋ฒ„๊ฑฐ (๋ชจ๋ฐ”์ผ ์›น์•ฑ) - 5๊ฐœ ๋ถ„์„ ํ†ตํ•ฉ
4
+
5
+ 1) VCP ๋ŒํŒŒ ๊ฐ์ง€ (past_vcp_debug.py) * ์ง„๋‹จ ๋‚ ์งœ ์„ ํƒ ์ž…๋ ฅ ๊ฐ€๋Šฅ
6
+ 2) VCP ์‹คํŒจ ๋ถ€๋ถ„๋งค๋„ (vcp_sell_debug.py)
7
+ 3) ๋ณผ๋ฆฐ์ € ์นด์šดํŠธ (bollinger_count_debug.py) * ์ตœ๊ทผ 1๋…„ ์ž๋™
8
+ 4) 240์„  ์ถ”์„ธ์‚ฌ๋ง ์ปท (MA240_cut_debug.py)
9
+ 5) ์ „๋Ÿ‰๋งค๋„ (RSยท์ด๊ฒฉ) (sell_+10_debug.py)
10
+
11
+ ๊ฐ ๋ถ„์„์€ ๋ฐ์Šคํฌํƒ‘ ์Šคํฌ๋ฆฝํŠธ์˜ ๋กœ์ง/์ถœ๋ ฅ์„ ๊ทธ๋Œ€๋กœ ๋ณด์กด(print ์ถœ๋ ฅ์„ ์บก์ฒ˜).
12
+ """
13
+
14
+ import io
15
+ import sys
16
+ import ssl
17
+ import contextlib
18
+ import warnings
19
+ from datetime import datetime, timedelta
20
+
21
+ import pandas as pd
22
+ import numpy as np
23
+ import FinanceDataReader as fdr
24
+ import gradio as gr
25
+
26
+ ssl._create_default_https_context = ssl._create_unverified_context
27
+ warnings.filterwarnings('ignore')
28
+
29
+
30
+ # ============================================================
31
+ # 1) ๋ณผ๋ฆฐ์ € ์นด์šดํŠธ (๋‚ ์งœ๋งŒ ์ตœ๊ทผ 1๋…„ ์ž๋™, ๋‚˜๋จธ์ง€ ๋™์ผ)
32
+ # ============================================================
33
+ def run_bollinger(ticker):
34
+ buf = io.StringIO()
35
+ try:
36
+ with contextlib.redirect_stdout(buf):
37
+ TICKER = ticker
38
+ BB_WINDOW = 20
39
+ BB_STD = 2
40
+ MA_240 = 240
41
+ MA_60 = 60
42
+ BB_THRESHOLD = 1.030
43
+
44
+ today = datetime.today()
45
+ DEBUG_TO = today.strftime('%Y-%m-%d')
46
+ DEBUG_FROM = (today - timedelta(days=365)).strftime('%Y-%m-%d')
47
+ DATA_START = (today - timedelta(days=1100)).strftime('%Y-%m-%d')
48
+
49
+ print(f"({TICKER}) ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์ค‘...")
50
+ df = fdr.DataReader(TICKER, DATA_START, DEBUG_TO)
51
+ print(f"๋ฐ์ดํ„ฐ: {len(df)}์ผ์น˜\n")
52
+
53
+ df['ma20'] = df['Close'].rolling(BB_WINDOW).mean()
54
+ df['std20'] = df['Close'].rolling(BB_WINDOW).std()
55
+ df['bb_upper'] = df['ma20'] + BB_STD * df['std20']
56
+ df['bb_lower'] = df['ma20'] - BB_STD * df['std20']
57
+ df['ma60'] = df['Close'].rolling(MA_60).mean()
58
+ df['ma240'] = df['Close'].rolling(MA_240).mean()
59
+
60
+ count = 0
61
+ peak_close = 0
62
+ was_below_60 = False
63
+ counts = []
64
+ peak_list = []
65
+ flags = []
66
+
67
+ for i in range(len(df)):
68
+ c = df['Close'].iloc[i]
69
+ o = df['Open'].iloc[i]
70
+ u = df['bb_upper'].iloc[i]
71
+ m60 = df['ma60'].iloc[i]
72
+ m240 = df['ma240'].iloc[i]
73
+
74
+ if pd.isna(m240) or pd.isna(u) or pd.isna(m60):
75
+ counts.append(count)
76
+ peak_list.append(peak_close)
77
+ flags.append('๋ฐ์ดํ„ฐ๋ถ€์กฑ')
78
+ continue
79
+
80
+ if c < m240:
81
+ count = 0
82
+ peak_close = 0
83
+ was_below_60 = False
84
+ flag = '240์„ ์ดํƒˆโ†’๋ฆฌ์…‹'
85
+
86
+ elif c < m60:
87
+ was_below_60 = True
88
+ flag = f'60์„ ์ดํƒˆโ†’์œ ์ง€(count={count})'
89
+
90
+ else:
91
+ open_above_240 = (o > m240)
92
+ clearly_above = (c > u * BB_THRESHOLD)
93
+
94
+ if not open_above_240:
95
+ flag = f'์‹œ์ดˆ๊ฐ€({o:,.0f})<240์„ ({m240:,.0f})โ†’๋ฌดํšจ'
96
+ elif clearly_above:
97
+ if was_below_60:
98
+ if o > peak_close:
99
+ count += 1
100
+ peak_close = c
101
+ was_below_60 = False
102
+ flag = f'+{count} (60์„ ๋ณต๊ท€,์‹œ์ดˆ๊ฐ€>{peak_close:,.0f})'
103
+ else:
104
+ flag = f'BB๋ŒํŒŒbut์‹œ์ดˆ๊ฐ€({o:,.0f})โ‰คํ”ผํฌ({peak_close:,.0f})โ†’์Šคํ‚ต'
105
+ else:
106
+ count += 1
107
+ peak_close = max(peak_close, c)
108
+ flag = f'+{count}'
109
+ else:
110
+ pct = (c / u - 1) * 100
111
+ flag = f'์ข…๊ฐ€๋ฏธ๋‹ฌ(BB๋Œ€๋น„{pct:+.1f}%)'
112
+
113
+ counts.append(count)
114
+ peak_list.append(peak_close)
115
+ flags.append(flag)
116
+
117
+ df['bb_count'] = counts
118
+ df['peak_close'] = peak_list
119
+ df['flag'] = flags
120
+
121
+ mask = (df.index >= DEBUG_FROM) & (df.index <= DEBUG_TO)
122
+ debug_df = df[mask].copy()
123
+
124
+ print("=" * 115)
125
+ print(f"๋‚ ์งœ๋ณ„ ์นด์šดํŒ… ({DEBUG_FROM} ~ {DEBUG_TO})")
126
+ print("=" * 115)
127
+ print(f"{'๋‚ ์งœ':<12} {'์ข…๊ฐ€':>7} {'์‹œ๊ฐ€':>7} {'BB์ƒ๋‹จ':>8} {'BBร—1.03':>9} "
128
+ f"{'60MA':>7} {'240MA':>7} {'ํ”ผํฌ':>8} {'์นด์šดํŠธ':>6} ํŒ์ •")
129
+ print("-" * 115)
130
+
131
+ prev_count = 0
132
+ for date, row in debug_df.iterrows():
133
+ cnt = int(row['bb_count'])
134
+ flag = row['flag']
135
+ is_notable = (cnt != prev_count or
136
+ '๋ฆฌ์…‹' in flag or '์ดํƒˆ' in flag or
137
+ '๋ณต๊ท€' in flag or '์Šคํ‚ต' in flag or
138
+ '๋ฌดํšจ' in flag)
139
+ if is_notable:
140
+ mark = ' โ—€' if cnt > prev_count else ''
141
+ print(f"{str(date)[:10]:<12} "
142
+ f"{row['Close']:>7,.0f} "
143
+ f"{row['Open']:>7,.0f} "
144
+ f"{row['bb_upper']:>8,.0f} "
145
+ f"{row['bb_upper']*BB_THRESHOLD:>9,.0f} "
146
+ f"{row['ma60']:>7,.0f} "
147
+ f"{row['ma240']:>7,.0f} "
148
+ f"{row['peak_close']:>8,.0f} "
149
+ f"{cnt:>6} {flag}{mark}")
150
+ prev_count = cnt
151
+
152
+ print("\n" + "=" * 60)
153
+ print("์นด์šดํŠธ ์ฆ๊ฐ€ํ•œ ๋‚ ๋“ค ์š”์•ฝ")
154
+ print("=" * 60)
155
+ prev = 0
156
+ for date, row in debug_df.iterrows():
157
+ cnt = int(row['bb_count'])
158
+ if cnt > prev:
159
+ print(f" {str(date)[:10]}: {row['flag']}"
160
+ f" (์ข…๊ฐ€:{row['Close']:,.0f}, ์‹œ๊ฐ€:{row['Open']:,.0f}, "
161
+ f"BBร—1.03:{row['bb_upper']*BB_THRESHOLD:,.0f}, "
162
+ f"240MA:{row['ma240']:,.0f})")
163
+ prev = cnt
164
+
165
+ print(f"\n์ตœ๋Œ€ ์นด์šดํŠธ: {debug_df['bb_count'].max()}")
166
+ except SystemExit:
167
+ pass
168
+ except Exception as e:
169
+ buf.write(f"\n[์˜ค๋ฅ˜] {type(e).__name__}: {e}\n"
170
+ f"ํ‹ฐ์ปค ๋ฒˆํ˜ธ(6์ž๋ฆฌ)๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”. ์˜ˆ: 005930")
171
+ return buf.getvalue()
172
+
173
+
174
+ # ============================================================
175
+ # 2) 240์„  ์ถ”์„ธ์‚ฌ๋ง ์ปท (MA240_cut_debug.py ๋กœ์ง ๊ทธ๋Œ€๋กœ)
176
+ # ============================================================
177
+ def run_ma240(ticker):
178
+ buf = io.StringIO()
179
+
180
+ BREAK_PCT = 0.05
181
+ CONFIRM_DAYS = 3
182
+ USE_SLOPE = False
183
+ SLOPE_LOOKBACK = 20
184
+ ARM_PCT = 0.25
185
+ DATA_BACK = 800
186
+ SHOW_FROM = None
187
+
188
+ def compute_240ma_cut(df, break_pct=BREAK_PCT, confirm_days=CONFIRM_DAYS,
189
+ use_slope=USE_SLOPE, slope_lookback=SLOPE_LOOKBACK,
190
+ arm_pct=ARM_PCT):
191
+ df = df.copy()
192
+ df['ma240'] = df['Close'].rolling(240).mean()
193
+ closes = df['Close'].values.astype(float)
194
+ ma240 = df['ma240'].values.astype(float)
195
+ dates = df.index
196
+
197
+ rows = []
198
+ consec = 0
199
+ is_sold = False
200
+ armed = False
201
+ sell_info = None
202
+
203
+ for i in range(len(df)):
204
+ c = closes[i]
205
+ m = ma240[i]
206
+
207
+ rec = {
208
+ 'date': dates[i], 'close': c, 'ma240': m,
209
+ 'cut_line': np.nan, 'gap_pct': np.nan, 'ma_dir': '-',
210
+ 'consec': 0, 'below_cut': False, 'armed': armed,
211
+ 'category': 'nodata', 'verdict': '๋ฐ์ดํ„ฐ๋ถ€์กฑ',
212
+ 'sold_today': False, 'is_sold': is_sold,
213
+ }
214
+
215
+ if np.isnan(m):
216
+ rows.append(rec)
217
+ continue
218
+
219
+ cut_line = m * (1 - break_pct)
220
+ gap_pct = (c / m - 1) * 100
221
+
222
+ j = i - slope_lookback
223
+ if j >= 0 and not np.isnan(ma240[j]):
224
+ ma_dir = 'down' if m < ma240[j] else 'up'
225
+ else:
226
+ ma_dir = 'na'
227
+
228
+ just_armed = False
229
+ if not armed and gap_pct >= arm_pct * 100:
230
+ armed = True
231
+ just_armed = True
232
+
233
+ below_cut = c < cut_line
234
+ rec.update({'cut_line': cut_line, 'gap_pct': gap_pct,
235
+ 'ma_dir': ma_dir, 'below_cut': below_cut, 'armed': armed})
236
+
237
+ if is_sold:
238
+ consec = 0
239
+ rec['category'] = 'sold'
240
+ if sell_info and sell_info['price'] > 0:
241
+ chg = (c / sell_info['price'] - 1) * 100
242
+ rec['verdict'] = f"(์ด๋ฏธ ์ „๋Ÿ‰๋งค๋„) ๋งค๋„๊ฐ€ ๋Œ€๋น„ {chg:+.1f}%"
243
+ else:
244
+ rec['verdict'] = "(์ด๋ฏธ ์ „๋Ÿ‰๋งค๋„)"
245
+ rec['consec'] = 0
246
+ rec['is_sold'] = True
247
+ rows.append(rec)
248
+ continue
249
+
250
+ if not below_cut:
251
+ consec = 0
252
+ if c >= m:
253
+ rec['category'] = 'safe_above'
254
+ rec['verdict'] = "240์„  ์œ„ - ๋ณด์œ "
255
+ if just_armed:
256
+ rec['verdict'] += f" โ˜…์ปท ๋ฌด์žฅ(240์„  +{arm_pct*100:.0f}% ๋ŒํŒŒ)"
257
+ else:
258
+ rec['category'] = 'buffer'
259
+ rec['verdict'] = (f"240์„  ์•„๋ž˜์ง€๋งŒ {gap_pct:+.1f}% "
260
+ f"(์ปท๋ผ์ธ ์œ„, ์‚ด์ง๋งŒ ๊นธ) - ๋ณด์œ ")
261
+ else:
262
+ consec += 1
263
+ if not armed:
264
+ rec['category'] = 'below_unarmed'
265
+ rec['verdict'] = (f"์ปท๋ผ์ธ {gap_pct:+.1f}% ์•„๋ž˜์ง€๋งŒ 240์„  "
266
+ f"+{arm_pct*100:.0f}% ๋ŒํŒŒ ์ด๋ ฅ ์—†์Œ(๋™์‚ฐ ๊ตฌ๊ฐ„) "
267
+ f"-> ์ปท ๋Œ€์ƒ ์•„๋‹˜ - ๋ณด์œ ")
268
+ else:
269
+ slope_ok = (not use_slope) or (ma_dir == 'down')
270
+ rec['category'] = 'below_cut'
271
+ if consec < confirm_days:
272
+ rec['verdict'] = (f"์ปท๋ผ์ธ {gap_pct:+.1f}% ์•„๋ž˜ - "
273
+ f"{consec}/{confirm_days}์ผ์งธ (ํ™•์ • ์ „) - ๋ณด์œ ")
274
+ elif not slope_ok:
275
+ rec['verdict'] = (f"์ปท๋ผ์ธ ์•„๋ž˜ {consec}์ผ์งธ BUT 240MA ์•„์ง ์ƒ์Šน์ค‘ "
276
+ f"-> ๊ธฐ์šธ๊ธฐ์กฐ๊ฑด ๋ฏธ์ถฉ์กฑ - ๋ณด์œ ")
277
+ else:
278
+ tail = ", 240MA ํ•˜๋ฝ ์ „ํ™˜" if use_slope else ""
279
+ rec['verdict'] = (f">>> ์ „๋Ÿ‰ ๋งค๋„! "
280
+ f"(๋ฌด์žฅ+240์„  {break_pct*100:.0f}% ์•„๋ž˜๋กœ "
281
+ f"{confirm_days}์ผ ํ™•์ •{tail})")
282
+ rec['category'] = 'sell'
283
+ rec['sold_today'] = True
284
+ is_sold = True
285
+ sell_info = {'date': dates[i], 'price': c, 'ma240': m,
286
+ 'gap_pct': gap_pct, 'index_i': i}
287
+
288
+ rec['consec'] = consec
289
+ rec['is_sold'] = is_sold
290
+ rows.append(rec)
291
+
292
+ return rows, sell_info
293
+
294
+ def analyze(ticker):
295
+ print("=" * 96)
296
+ print(f"240์ผ์„  ์ถ”์„ธ์‚ฌ๋ง ์ปท ๋””๋ฒ„๊ทธ : {ticker}")
297
+ print(f"๊ทœ์น™ : ์ข…๊ฐ€ < 240MAร—{1-BREAK_PCT:.2f} ({BREAK_PCT*100:.0f}% ์•„๋ž˜) "
298
+ f"๊ฐ€ {CONFIRM_DAYS}์ผ ์—ฐ์† -> ์ „๋Ÿ‰ ๋งค๋„"
299
+ + (f" | 240MA ํ•˜๋ฝ์ „ํ™˜ ํ•„์ˆ˜(USE_SLOPE)" if USE_SLOPE else " | 240MA๋ฐฉํ–ฅ์€ ์ •๋ณด๋กœ๋งŒ"))
300
+ print("=" * 96)
301
+
302
+ name, market = ticker, '-'
303
+ try:
304
+ kospi = fdr.StockListing('KOSPI')
305
+ kosdaq = fdr.StockListing('KOSDAQ')
306
+ if ticker in kospi['Code'].values:
307
+ market = 'KOSPI'; name = kospi[kospi['Code'] == ticker]['Name'].values[0]
308
+ elif ticker in kosdaq['Code'].values:
309
+ market = 'KOSDAQ'; name = kosdaq[kosdaq['Code'] == ticker]['Name'].values[0]
310
+ except Exception:
311
+ pass
312
+
313
+ data_start = (datetime.today() - timedelta(days=DATA_BACK)).strftime('%Y-%m-%d')
314
+ analyze_to = datetime.today().strftime('%Y-%m-%d')
315
+ df = fdr.DataReader(ticker, data_start, analyze_to)
316
+ if df is None or len(df) == 0:
317
+ print("๋ฐ์ดํ„ฐ ์—†์Œ"); return
318
+ print(f"์ข…๋ชฉ : {name} ({ticker}) / {market} | ๋ฐ์ดํ„ฐ {len(df)}์ผ\n")
319
+
320
+ rows, sell_info = compute_240ma_cut(df)
321
+
322
+ if np.isnan(rows[-1]['ma240']):
323
+ print("!! 240์ผ์น˜ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ถ€์กฑํ•ด์„œ 240MA๋ฅผ ๋ชป ๊ตฌํ•จ -> ๋ถ„์„ ๋ถˆ๊ฐ€ "
324
+ "(์ƒ์žฅ 1๋…„ ๋ฏธ๋งŒ ์ข…๋ชฉ)")
325
+ return
326
+
327
+ valid_rows = [r for r in rows if r['category'] != 'nodata']
328
+ if not valid_rows:
329
+ print("240MA ์œ ํšจ ๊ตฌ๊ฐ„ ์—†์Œ"); return
330
+
331
+ if SHOW_FROM:
332
+ win = [r for r in valid_rows if str(r['date'])[:10] >= SHOW_FROM]
333
+ if not win:
334
+ print(f"!! SHOW_FROM({SHOW_FROM}) ์ดํ›„ ๋ฐ์ดํ„ฐ ์—†์Œ. 240MA ์œ ํšจ ๋ฒ”์œ„: "
335
+ f"{str(valid_rows[0]['date'])[:10]} ~ {str(valid_rows[-1]['date'])[:10]}")
336
+ return
337
+ else:
338
+ win = valid_rows
339
+
340
+ print(f"240MA ์œ ํšจ ์‹œ์ž‘: {str(valid_rows[0]['date'])[:10]} | "
341
+ f"ํ‘œ ํ‘œ์‹œ: {str(win[0]['date'])[:10]} ~ {str(win[-1]['date'])[:10]}")
342
+
343
+ print("-" * 96)
344
+ print(f"{'๋‚ ์งœ':<11}{'์ข…๊ฐ€':>9}{'240MA':>9}{'์ปท๋ผ์ธ':>9}"
345
+ f"{'vs240':>8}{'๋ฐฉํ–ฅ':>5}{'์—ฐ์†':>5} ํŒ์ •")
346
+ print("-" * 96)
347
+
348
+ dir_map = {'up': 'UP', 'down': 'DN', 'na': '-', '-': '-'}
349
+ prev_cat = None
350
+ for k, r in enumerate(win):
351
+ cat = r['category']
352
+ is_last = (k == len(win) - 1)
353
+ notable = (cat != prev_cat) or (cat in ('below_cut', 'sell')) or is_last
354
+ prev_cat = cat
355
+ if not notable:
356
+ continue
357
+ consec_s = str(r['consec']) if r['category'] == 'below_cut' else ''
358
+ mark = ' <<<<<' if r['sold_today'] else ''
359
+ print(f"{str(r['date'])[:10]:<11}"
360
+ f"{r['close']:>9,.0f}"
361
+ f"{r['ma240']:>9,.0f}"
362
+ f"{r['cut_line']:>9,.0f}"
363
+ f"{r['gap_pct']:>+7.1f}%"
364
+ f"{dir_map.get(r['ma_dir'],'-'):>5}"
365
+ f"{consec_s:>5} {r['verdict']}{mark}")
366
+
367
+ print("\n" + "=" * 96)
368
+ print("์š”์•ฝ")
369
+ print("=" * 96)
370
+ print(f"์ข…๋ชฉ : {name} ({ticker}) / {market}")
371
+ print(f"์ปท ๊ทœ์น™ : ๋ฌด์žฅ(240์„  +{ARM_PCT*100:.0f}% ์ด๋ ฅ) + 240MAร—{1-BREAK_PCT:.2f} "
372
+ f"์•„๋ž˜ {CONFIRM_DAYS}์ผ ์—ฐ์†" + (" + 240MA ํ•˜๋ฝ์ „ํ™˜" if USE_SLOPE else ""))
373
+
374
+ valid_gaps = [r['gap_pct'] for r in valid_rows if not np.isnan(r['gap_pct'])]
375
+ max_gap = max(valid_gaps) if valid_gaps else float('nan')
376
+ ever_armed = any(r['armed'] for r in valid_rows)
377
+ print(f"๋ฌด์žฅ ์ƒํƒœ: {'๋ฌด์žฅ๋จ' if ever_armed else '๋ฌด์žฅ ์•ˆ ๋จ(๋™์‚ฐ ๊ตฌ๊ฐ„ -> 240์„  ์ปท ๋Œ€์ƒ ์•„๋‹˜)'}"
378
+ f" | 240์„  ๋Œ€๋น„ ์ตœ๊ณ  {max_gap:+.1f}%")
379
+
380
+ if sell_info is not None:
381
+ si = sell_info
382
+ in_win = "" if si['date'] >= win[0]['date'] else " (ํ‘œ ์‹œ์ž‘ ์ด์ „ ๊ตฌ๊ฐ„)"
383
+ print(f"๋งค๋„ ์‹ ํ˜ธ: >>> {str(si['date'])[:10]}{in_win} | "
384
+ f"๋งค๋„๊ฐ€ {si['price']:,.0f} | ๊ทธ๋•Œ 240MA {si['ma240']:,.0f} "
385
+ f"({si['gap_pct']:+.1f}%)")
386
+
387
+ after = [r for r in rows[si['index_i'] + 1:] if not np.isnan(r['close'])]
388
+ if after:
389
+ ac = np.array([r['close'] for r in after])
390
+ mn_i = int(np.argmin(ac)); mx_i = int(np.argmax(ac))
391
+ mn, mx, lp = ac[mn_i], ac[mx_i], ac[-1]
392
+ mn_chg = (mn / si['price'] - 1) * 100
393
+ mx_chg = (mx / si['price'] - 1) * 100
394
+ lp_chg = (lp / si['price'] - 1) * 100
395
+ print(f"\n[๋งค๋„ ํ›„ ๊ฒ€์ฆ] ๋งค๋„๊ฐ€ {si['price']:,.0f} ๊ธฐ์ค€")
396
+ print(f" ์ดํ›„ ์ตœ๊ณ  : {mx:,.0f} ({str(after[mx_i]['date'])[:10]}) -> {mx_chg:+.1f}%")
397
+ print(f" ์ดํ›„ ์ตœ์ € : {mn:,.0f} ({str(after[mn_i]['date'])[:10]}) -> {mn_chg:+.1f}%")
398
+ print(f" ํ˜„์žฌ๊ฐ€ : {lp:,.0f} -> {lp_chg:+.1f}%")
399
+ if mx_chg >= 10 and mx_i < mn_i:
400
+ print(f" => ๋งค๋„ ํ›„ {mx_chg:+.0f}%๊นŒ์ง€ ๋ฐ˜๋“ฑํ–ˆ๋‹ค๊ฐ€ ๋น ์ง "
401
+ f"-> ํœฉ์˜(์ €์  ๊ทผ์ฒ˜์—์„œ ๋„ˆ๋ฌด ์ผ์ฐ ์ปท) ๊ฐ€๋Šฅ์„ฑ ํผ.")
402
+ elif mn_chg <= -5:
403
+ print(f" => ๋งค๋„ ํ›„ ๋” ๋น ์ง -> ์ปท์ด ์†์‹ค์„ ๋ง‰์•„์ค€ ์ผ€์ด์Šค.")
404
+ elif lp_chg > 8:
405
+ print(f" => ๋งค๋„ ํ›„ ๋ฐ˜๋“ฑ ์œ ์ง€ -> ํœฉ์˜(๋„ˆ๋ฌด ์ผ์ฐ ์ปท) ๊ฐ€๋Šฅ์„ฑ.")
406
+ else:
407
+ print(f" => ๋งค๋„ ํ›„ ํฐ ๋ณ€๋™ ์—†์Œ.")
408
+ else:
409
+ last = win[-1]
410
+ cat = last['category']
411
+ print(f"๋งค๋„ ์‹ ํ˜ธ: ์—†์Œ (240์„  ์ปท ๋ฏธ๋ฐœ์ƒ, ๊ณ„์† ๋ณด์œ )")
412
+ if cat == 'safe_above':
413
+ tail = "240์„  ์œ„, ๋ณด์œ "
414
+ elif cat == 'buffer':
415
+ tail = "240์„  ์•„๋ž˜์ง€๋งŒ ์ปท๋ผ์ธ ์œ„(์‚ด์ง ๊นธ), ๋ณด์œ "
416
+ elif cat == 'below_cut':
417
+ tail = (f"์ปท๋ผ์ธ ์•„๋ž˜ {last['consec']}์ผ์งธ "
418
+ f"(ํ™•์ • ์ „, {CONFIRM_DAYS}์ผ ๋˜๋ฉด ๋งค๋„), ๋ณด์œ ")
419
+ elif cat == 'below_unarmed':
420
+ tail = (f"์ปท๋ผ์ธ ์•„๋ž˜์ง€๋งŒ ๋™์‚ฐ ๊ตฌ๊ฐ„(๋ฌด์žฅ ์•ˆ ๋จ) -> ์ปท ๋Œ€์ƒ ์•„๋‹˜, ๋ณด์œ ")
421
+ else:
422
+ tail = "๋ณด์œ "
423
+ print(f"ํ˜„์žฌ ์ƒํƒœ: ์ข…๊ฐ€ {last['close']:,.0f} / 240MA {last['ma240']:,.0f} "
424
+ f"({last['gap_pct']:+.1f}%) -> {tail}")
425
+
426
+ try:
427
+ with contextlib.redirect_stdout(buf):
428
+ analyze(ticker)
429
+ except SystemExit:
430
+ pass
431
+ except Exception as e:
432
+ buf.write(f"\n[์˜ค๋ฅ˜] {type(e).__name__}: {e}\n"
433
+ f"ํ‹ฐ์ปค ๋ฒˆํ˜ธ(6์ž๋ฆฌ)๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”. ์˜ˆ: 005930")
434
+ return buf.getvalue()
435
+
436
+
437
+ # ============================================================
438
+ # 3) ์ „๋Ÿ‰๋งค๋„ (RSยท์ด๊ฒฉ) (sell_+10_debug.py ๋กœ์ง ๊ทธ๋Œ€๋กœ)
439
+ # ============================================================
440
+ def run_sell(ticker):
441
+ buf = io.StringIO()
442
+ try:
443
+ with contextlib.redirect_stdout(buf):
444
+ TICKER = ticker
445
+
446
+ DIV_THRESHOLD = -0.10
447
+ RS_MA_PERIOD = 20
448
+ INDEX_PEAK_LOOKBACK = 180
449
+ DATA_START = (datetime.today() - timedelta(days=600)).strftime('%Y-%m-%d')
450
+ ANALYZE_TO = datetime.today().strftime('%Y-%m-%d')
451
+
452
+ print("=" * 70)
453
+ print(f"๋ถ„์„ ์ข…๋ชฉ: {TICKER}")
454
+ print("=" * 70)
455
+ print("\n[1/3] ์‹œ์žฅ ํŒ๋ณ„ ์ค‘...")
456
+ market = 'KOSDAQ'; name = TICKER
457
+ try:
458
+ kospi = fdr.StockListing('KOSPI')
459
+ kosdaq = fdr.StockListing('KOSDAQ')
460
+ if TICKER in kospi['Code'].values:
461
+ market = 'KOSPI'
462
+ name = kospi[kospi['Code'] == TICKER]['Name'].values[0]
463
+ elif TICKER in kosdaq['Code'].values:
464
+ market = 'KOSDAQ'
465
+ name = kosdaq[kosdaq['Code'] == TICKER]['Name'].values[0]
466
+ print(f" {name} ({TICKER}) / {market}")
467
+ except:
468
+ print(f" ํŒ๋ณ„ ์‹คํŒจ -> {market} ๊ฐ€์ •")
469
+
470
+ print("\n[2/3] ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์ค‘...")
471
+ stock_df = fdr.DataReader(TICKER, DATA_START, ANALYZE_TO)
472
+ if stock_df is None or len(stock_df) == 0:
473
+ print("์ข…๋ชฉ ๋ฐ์ดํ„ฐ ์—†์Œ"); sys.exit()
474
+
475
+ candidates = ['KQ11', '^KQ11'] if market == 'KOSDAQ' else ['KS11', '^KS11']
476
+ index_df = None
477
+ for c in candidates:
478
+ try:
479
+ t = fdr.DataReader(c, DATA_START, ANALYZE_TO)
480
+ if t is not None and len(t) > 0:
481
+ index_df = t; print(f" ์ง€์ˆ˜: {c}"); break
482
+ except:
483
+ continue
484
+ if index_df is None:
485
+ print("์ง€์ˆ˜ ๋กœ๋“œ ์‹คํŒจ"); sys.exit()
486
+
487
+ merged = pd.concat([
488
+ stock_df['Close'].rename('stock'),
489
+ index_df['Close'].rename('index')
490
+ ], axis=1).dropna()
491
+ print(f" ์ข…๋ชฉ {len(stock_df)}์ผ | ์ง€์ˆ˜ {len(index_df)}์ผ")
492
+
493
+ print("\n[3/3] ์ง€์ˆ˜ ๊ณ ์  ํƒ์ƒ‰ ์ค‘...")
494
+ cutoff = (datetime.today() - timedelta(days=INDEX_PEAK_LOOKBACK)).strftime('%Y-%m-%d')
495
+ recent_idx = merged[merged.index >= cutoff]['index']
496
+ index_peak_date = recent_idx.idxmax()
497
+ index_peak_value = float(recent_idx.max())
498
+ stock_at_peak = float(merged.loc[index_peak_date, 'stock']) if index_peak_date in merged.index else None
499
+
500
+ latest_date = merged.index[-1]
501
+ is_ath = (index_peak_date == latest_date)
502
+
503
+ days_from_peak = (latest_date.to_pydatetime().replace(tzinfo=None) -
504
+ index_peak_date.to_pydatetime().replace(tzinfo=None)).days
505
+
506
+ print(f" ์ง€์ˆ˜ ๊ณ ์ : {index_peak_value:,.2f} ({str(index_peak_date)[:10]})")
507
+ print(f" ์ •์ ๋‚  ์ข…๋ชฉ: {stock_at_peak:,.0f}" if stock_at_peak else " ์ •์ ๋‚  ์ข…๋ชฉ: ์—†์Œ")
508
+ print(f" ์˜ค๋Š˜ ์ง€์ˆ˜: {float(merged['index'].iloc[-1]):,.2f} ({str(latest_date)[:10]})")
509
+ print(f" ATH ์—ฌ๋ถ€: {'์˜ˆ โ†’ RS Line ๋ถ„์„' if is_ath else f'์•„๋‹ˆ์˜ค (๊ณ ์  {days_from_peak}์ผ ๊ฒฝ๊ณผ) โ†’ ์ด๊ฒฉ ๋ถ„์„'}")
510
+
511
+ FLAG_START = str(index_peak_date)[:10]
512
+
513
+ if not is_ath:
514
+ flag_data = merged[merged.index >= FLAG_START].copy()
515
+ if len(flag_data) == 0:
516
+ print("๋ฐ์ดํ„ฐ ์—†์Œ"); sys.exit()
517
+
518
+ print("\n" + "=" * 70)
519
+ print("[์ด๊ฒฉ ๋ถ„์„] ์ง€์ˆ˜ ๊ณ ์  ๊ธฐ์ค€")
520
+ print(f"์ง€์ˆ˜ ์ •์ : {FLAG_START} ({index_peak_value:,.2f})")
521
+ print("=" * 70)
522
+ print(f"\n{'๋‚ ์งœ':<12} {'์ข…๋ชฉ๊ฐ€':>8} {'์ง€์ˆ˜':>8} {'์ง€์ˆ˜์ˆ˜์ต':>9} {'์ข…๋ชฉ์ˆ˜์ต':>9} {'์ด๊ฒฉ':>8} {'flag':>5} ํŒ์ •")
523
+ print("-" * 78)
524
+
525
+ div_flag = False; div_flag_value = None
526
+ sell_signal = False; sell_date = None; sell_reason = None
527
+
528
+ for date, row in flag_data.iterrows():
529
+ s = float(row['stock']); idx = float(row['index'])
530
+ if stock_at_peak is None:
531
+ continue
532
+ ir = (idx / index_peak_value) - 1
533
+ sr = (s / stock_at_peak) - 1
534
+ div = sr - ir
535
+ verdict = ''
536
+ if div <= DIV_THRESHOLD:
537
+ if not div_flag:
538
+ div_flag = True; div_flag_value = div
539
+ verdict = '*** FLAG ON (์˜ค๋Š˜ ๋งค๋„X)'
540
+ else:
541
+ if div < div_flag_value:
542
+ verdict = '!!! ์ „๋Ÿ‰๋งค๋„!'
543
+ if not sell_signal:
544
+ sell_signal = True; sell_date = str(date)[:10]
545
+ sell_reason = f'์ด๊ฒฉ {div*100:.1f}%'
546
+ div_flag_value = div
547
+ else:
548
+ verdict = 'Watch'; div_flag_value = div
549
+ else:
550
+ if div_flag:
551
+ div_flag = False; div_flag_value = None; verdict = 'FLAG OFF'
552
+
553
+ print(f"{str(date)[:10]:<12} {s:>8,.0f} {idx:>8,.0f} "
554
+ f"{ir*100:>+8.1f}% {sr*100:>+8.1f}% {div*100:>+7.1f}% "
555
+ f"{'ON' if div_flag else '':>5} {verdict}")
556
+
557
+ print()
558
+ if sell_signal:
559
+ print(f">>> ์ „๋Ÿ‰ ๋งค๋„ ์‹ ํ˜ธ: {sell_date} | {sell_reason}")
560
+ else:
561
+ print(">>> ์•„์ง ์ „๋Ÿ‰ ๋งค๋„ ์‹ ํ˜ธ ์—†์Œ")
562
+
563
+ if stock_at_peak:
564
+ last = flag_data.iloc[-1]
565
+ dv_now = ((float(last['stock'])/stock_at_peak) - 1) - ((float(last['index'])/index_peak_value) - 1)
566
+ print(f"\nํ˜„์žฌ ์ด๊ฒฉ ({str(flag_data.index[-1])[:10]}):")
567
+ print(f" ์ง€์ˆ˜ ์ •์  : {index_peak_value:,.2f} ({FLAG_START})")
568
+ print(f" ์ •์ ๋‚  ์ข…๋ชฉ : {stock_at_peak:,.0f}")
569
+ print(f" ํ˜„์žฌ ์ข…๋ชฉ : {float(last['stock']):,.0f}")
570
+ print(f" ์ด๊ฒฉ : {dv_now*100:+.1f}% (๊ธฐ์ค€ {DIV_THRESHOLD*100:.0f}%)")
571
+ print(f" div_flag : {'ON' if div_flag else 'off'}")
572
+
573
+ print("\n" + "=" * 70)
574
+ print("์ตœ์ข… ์š”์•ฝ")
575
+ print("=" * 70)
576
+ print(f"์ข…๋ชฉ : {name} ({TICKER}) / {market}")
577
+ print(f"์ง€์ˆ˜ ์ •์  : {FLAG_START} ({index_peak_value:,.2f}) | {days_from_peak}์ผ ๊ฒฝ๊ณผ")
578
+ print(f"์ด๊ฒฉ ์‹ ํ˜ธ : {('>>> ' + sell_date) if sell_signal else 'None'}")
579
+ print()
580
+ if sell_signal:
581
+ print(f"==> ๋งค๋„ ์‹ ํ˜ธ: {sell_date}")
582
+ else:
583
+ print("==> ์•„์ง ๋งค๋„ ์‹ ํ˜ธ ์—†์Œ. ๊ณ„์† ๋ณด์œ .")
584
+
585
+ else:
586
+ print("\n" + "=" * 70)
587
+ print(f"[RS Line ๋ถ„์„] MA{RS_MA_PERIOD} - ์˜ค๋Š˜์ด ์ง€์ˆ˜ ๊ณ ์  (ATH)")
588
+ print("=" * 70)
589
+
590
+ rs_all = merged.copy()
591
+ rs_all['rs'] = rs_all['stock'] / rs_all['index']
592
+ rs_all['rs_ma'] = rs_all['rs'].rolling(RS_MA_PERIOD).mean()
593
+ rs_flag = rs_all[rs_all.index >= FLAG_START].dropna(subset=['rs_ma'])
594
+
595
+ rs_sell_signal = False; rs_sell_date = None
596
+
597
+ if len(rs_flag) == 0:
598
+ print("RS MA ๊ณ„์‚ฐ ๋ถˆ๊ฐ€ (๋ฐ์ดํ„ฐ ๋ถ€์กฑ)")
599
+ else:
600
+ print(f"\n{'๋‚ ์งœ':<12} {'์ข…๋ชฉ๊ฐ€':>8} {'RS':>10} {'RS MA20':>10} {'vs MA':>8} ํŒ์ •")
601
+ print("-" * 58)
602
+ for date, row in rs_flag.iterrows():
603
+ rs = float(row['rs']); rma = float(row['rs_ma'])
604
+ pct = (rs / rma - 1) * 100
605
+ v = '>>> ๋ฐ๋“œํฌ๋กœ์Šค!' if rs < rma else 'Golden'
606
+ if rs < rma and not rs_sell_signal:
607
+ rs_sell_signal = True; rs_sell_date = str(date)[:10]
608
+ print(f"{str(date)[:10]:<12} {float(row['stock']):>8,.0f} "
609
+ f"{rs:>10.4f} {rma:>10.4f} {pct:>+7.1f}% {v}")
610
+
611
+ print("\n" + "=" * 70)
612
+ print("์ตœ์ข… ์š”์•ฝ")
613
+ print("=" * 70)
614
+ print(f"์ข…๋ชฉ : {name} ({TICKER}) / {market}")
615
+ print(f"์ง€์ˆ˜ ์ •์  : {FLAG_START} ({index_peak_value:,.2f}) | ATH")
616
+ print(f"RS ์‹ ํ˜ธ : {('>>> ' + rs_sell_date) if rs_sell_signal else 'None'}")
617
+ print()
618
+ if rs_sell_signal:
619
+ print(f"==> RS Line ๋งค๋„ ์‹ ํ˜ธ: {rs_sell_date}")
620
+ else:
621
+ print("==> RS Line ๋ฐ๋“œํฌ๋กœ์Šค ์—†์Œ. ๊ณ„์† ๋ณด์œ .")
622
+ except SystemExit:
623
+ pass
624
+ except Exception as e:
625
+ buf.write(f"\n[์˜ค๋ฅ˜] {type(e).__name__}: {e}\n"
626
+ f"ํ‹ฐ์ปค ๋ฒˆํ˜ธ(6์ž๋ฆฌ)๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”. ์˜ˆ: 005930")
627
+ return buf.getvalue()
628
+
629
+
630
+ # ============================================================
631
+ # 4) VCP ์‹คํŒจ ๋ถ€๋ถ„๋งค๋„ (vcp_sell_debug.py ๋กœ์ง ๊ทธ๋Œ€๋กœ)
632
+ # ============================================================
633
+ def run_vcp_sell(ticker):
634
+ buf = io.StringIO()
635
+
636
+ VCP_BUFFER = 0.05
637
+ VCP_BUY_KRW = None
638
+ STOP_REF = 'breakout_low'
639
+
640
+ CONTRACT_DAYS = 2
641
+ VOL_DRYUP_SURGE = 2.0
642
+ MIN_PRICE_GAIN = 0.10
643
+ BASE_LOOKBACK = 10
644
+
645
+ ANALYZE_DAYS = 365
646
+ WARMUP_DAYS = 40
647
+ DATA_START = (datetime.today() - timedelta(days=ANALYZE_DAYS + WARMUP_DAYS)).strftime('%Y-%m-%d')
648
+ ANALYZE_FROM = (datetime.today() - timedelta(days=ANALYZE_DAYS)).strftime('%Y-%m-%d')
649
+
650
+ def find_vcp_breakouts(df):
651
+ O = df['Open'].values.astype(float); H = df['High'].values.astype(float)
652
+ L = df['Low'].values.astype(float); C = df['Close'].values.astype(float)
653
+ V = df['Volume'].values.astype(float)
654
+ out = []
655
+ for t in range(BASE_LOOKBACK, len(df)):
656
+ quiet_max = V[t - CONTRACT_DAYS:t].max()
657
+ if quiet_max <= 0 or C[t - 1] <= 0:
658
+ continue
659
+ surge = V[t] / quiet_max
660
+ base_high = H[t - BASE_LOOKBACK:t].max()
661
+ gain = C[t] / C[t - 1] - 1
662
+ if (surge >= VOL_DRYUP_SURGE and C[t] > base_high
663
+ and gain >= MIN_PRICE_GAIN and C[t] > O[t]):
664
+ out.append({
665
+ 'i': t, 'date': str(df.index[t])[:10],
666
+ 'close': C[t], 'open': O[t], 'low': L[t],
667
+ 'gain': round(gain * 100, 1), 'surge': round(surge, 2),
668
+ 'base_high': base_high, 'base_low': float(L[t - BASE_LOOKBACK:t].min()),
669
+ })
670
+ return [b for b in out if b['date'] >= ANALYZE_FROM]
671
+
672
+ def track_vcp_stop(closes, dates, breakout_i, ref_low, entry_price, buffer=VCP_BUFFER):
673
+ stop = ref_low * (1 - buffer)
674
+ daily, failed, fail = [], False, None
675
+ for k in range(breakout_i + 1, len(closes)):
676
+ ck = float(closes[k])
677
+ if ck < stop:
678
+ failed = True
679
+ fail = {'date': dates[k], 'price': ck, 'k': k,
680
+ 'loss_pct': (ck / entry_price - 1) * 100}
681
+ daily.append((dates[k], ck, 'fail')); break
682
+ elif ck < stop * 1.05:
683
+ daily.append((dates[k], ck, 'warn'))
684
+ else:
685
+ daily.append((dates[k], ck, 'hold'))
686
+ return stop, failed, fail, daily
687
+
688
+ def analyze(ticker):
689
+ ref_name = "๋ŒํŒŒ์ผ ์ €์ " if STOP_REF == 'breakout_low' else "๋ฒ ์ด์Šค ์ €์ "
690
+ print("=" * 80)
691
+ print(f"VCP ์‹คํŒจ ๋ถ€๋ถ„๋งค๋„ ๋””๋ฒ„๊ทธ : {ticker}")
692
+ print(f"๊ทœ์น™ : ๋ŒํŒŒ ๋งค์ˆ˜ ํ›„ ์ข…๊ฐ€ < {ref_name}ร—{1-VCP_BUFFER:.2f} ({VCP_BUFFER*100:.0f}% ๋ฒ„ํผ) "
693
+ f"-> VCP ์‹คํŒจ -> VCP๋กœ ์‚ฐ ๋งŒํผ๋งŒ ๋ถ€๋ถ„ ๋งค๋„")
694
+ print(f" (VCP ์‹คํŒจ โ‰  ์ข…๋ชฉ ์•…ํ™”. ๋‚˜๋จธ์ง€ ํฌ์ง€์…˜์€ ๊ทธ๋Œ€๋กœ ์œ ์ง€)")
695
+ print("=" * 80)
696
+
697
+ name, market = ticker, '-'
698
+ try:
699
+ kospi = fdr.StockListing('KOSPI')
700
+ kosdaq = fdr.StockListing('KOSDAQ')
701
+ if ticker in kospi['Code'].values:
702
+ market = 'KOSPI'; name = kospi[kospi['Code'] == ticker]['Name'].values[0]
703
+ elif ticker in kosdaq['Code'].values:
704
+ market = 'KOSDAQ'; name = kosdaq[kosdaq['Code'] == ticker]['Name'].values[0]
705
+ except Exception:
706
+ pass
707
+
708
+ df = fdr.DataReader(ticker, DATA_START, datetime.today().strftime('%Y-%m-%d'))
709
+ if df is None or len(df) == 0:
710
+ print("๋ฐ์ดํ„ฐ ์—†์Œ"); return
711
+ print(f"์ข…๋ชฉ : {name} ({ticker}) / {market} | ๋ฐ์ดํ„ฐ {len(df)}์ผ\n")
712
+
713
+ bos = find_vcp_breakouts(df)
714
+ closes = df['Close'].values.astype(float)
715
+ dates = [str(d)[:10] for d in df.index]
716
+ print(f"VCP ๋ŒํŒŒ(๋งค์ˆ˜): {len(bos)}๊ฐœ\n")
717
+ if not bos:
718
+ print("VCP ๋ŒํŒŒ ์—†์Œ -> ์ถ”์  ๋Œ€์ƒ ์—†์Œ"); return
719
+
720
+ results = []
721
+ for n, b in enumerate(bos, 1):
722
+ entry = b['close']
723
+ ref_low = b['low'] if STOP_REF == 'breakout_low' else b['base_low']
724
+ stop, failed, fail, daily = track_vcp_stop(closes, dates, b['i'], ref_low, entry, VCP_BUFFER)
725
+
726
+ print("-" * 80)
727
+ print(f"VCP #{n} ๋ŒํŒŒ {b['date']} @ {entry:,.0f}์› "
728
+ f"(์ƒ์Šน +{b['gain']}%, ๊ฑฐ๋ž˜๋Ÿ‰ {b['surge']}๋ฐฐ)")
729
+ print(f" {ref_name}: {ref_low:,.0f} | ์†์ ˆ์„  = {ref_low:,.0f}ร—{1-VCP_BUFFER:.2f} = {stop:,.0f}์› "
730
+ f"(์ง„์ž…๊ฐ€ ๋Œ€๋น„ {(stop/entry-1)*100:+.1f}%)")
731
+ print(f" ---- ๋ŒํŒŒ ํ›„ ์ถ”์  ----")
732
+ prev = None
733
+ for j, (d, c, st) in enumerate(daily):
734
+ is_last = (j == len(daily) - 1)
735
+ if not ((st != prev) or st in ('warn', 'fail') or is_last):
736
+ prev = st; continue
737
+ prev = st
738
+ if st == 'fail':
739
+ msg = (f">>> VCP ์‹คํŒจ! ์ข…๊ฐ€ {c:,.0f} < ์†์ ˆ์„  {stop:,.0f} "
740
+ f"-> VCP๋ถ„๋งŒ ๋งค๋„ (์ง„์ž…๊ฐ€ ๋Œ€๋น„ {fail['loss_pct']:+.1f}%) <<<<<")
741
+ elif st == 'warn':
742
+ msg = f"๊ฒฝ๊ณ  (์ข…๊ฐ€ {c:,.0f}, ์†์ ˆ์„  {stop:,.0f} ๊ทผ์ ‘) - ๋ณด์œ "
743
+ else:
744
+ msg = f"๋ณด์œ  (์ข…๊ฐ€ {c:,.0f} > ์†์ ˆ์„  {stop:,.0f})"
745
+ print(f" {d:<11} {msg}")
746
+ if not failed:
747
+ cur = closes[-1]
748
+ print(f" => VCP ์œ ์ง€ ์ค‘ (ํ˜„์žฌ {cur:,.0f}, ์ง„์ž…๊ฐ€ ๋Œ€๋น„ {(cur/entry-1)*100:+.1f}%)")
749
+ results.append({'n': n, 'entry': entry, 'failed': failed, 'fail': fail})
750
+ print()
751
+
752
+ fails = [r for r in results if r['failed']]
753
+ holds = [r for r in results if not r['failed']]
754
+ print("=" * 80)
755
+ print("์š”์•ฝ")
756
+ print("=" * 80)
757
+ print(f"์ข…๋ชฉ : {name} ({ticker}) / {market}")
758
+ print(f"VCP ๋ŒํŒŒ : {len(bos)}๊ฐœ | ์‹คํŒจ(๋ถ€๋ถ„๋งค๋„): {len(fails)}๊ฐœ | ์œ ์ง€: {len(holds)}๊ฐœ")
759
+ if fails:
760
+ avg = np.mean([r['fail']['loss_pct'] for r in fails])
761
+ print(f"์‹คํŒจ๋ถ„ ์†์‹ค: ํ‰๊ท  {avg:+.1f}% (VCP ํŠธ๋žœ์น˜ ๊ธฐ์ค€, ๋‚˜๋จธ์ง€ ํฌ์ง€์…˜์€ ์œ ์ง€)")
762
+ for r in fails:
763
+ line = f" VCP #{r['n']}: ์ง„์ž… {r['entry']:,.0f} -> {r['fail']['date']} ๋งค๋„ ({r['fail']['loss_pct']:+.1f}%)"
764
+ if VCP_BUY_KRW:
765
+ loss = VCP_BUY_KRW * r['fail']['loss_pct'] / 100
766
+ line += f" | ๋งค๋„๊ธˆ์•ก ~{VCP_BUY_KRW+loss:,.0f}์› (์†์ต {loss:+,.0f}์›)"
767
+ print(line)
768
+
769
+ try:
770
+ with contextlib.redirect_stdout(buf):
771
+ analyze(ticker)
772
+ except SystemExit:
773
+ pass
774
+ except Exception as e:
775
+ buf.write(f"\n[์˜ค๋ฅ˜] {type(e).__name__}: {e}\n"
776
+ f"ํ‹ฐ์ปค ๋ฒˆํ˜ธ(6์ž๋ฆฌ)๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”. ์˜ˆ: 005930")
777
+ return buf.getvalue()
778
+
779
+
780
+ # ============================================================
781
+ # 5) VCP ๋ŒํŒŒ ๊ฐ์ง€ (past_vcp_debug.py ๋กœ์ง ๊ทธ๋Œ€๋กœ + ์ง„๋‹จ ๋‚ ์งœ ์„ ํƒ)
782
+ # ============================================================
783
+ def run_vcp_detect(ticker, diagnose_date=None):
784
+ buf = io.StringIO()
785
+
786
+ CONTRACT_DAYS = 2
787
+ VOL_DRYUP_SURGE = 6.0
788
+ MIN_PRICE_GAIN = 0.13
789
+ BASE_LOOKBACK = 10
790
+
791
+ DIAGNOSE_DATE = diagnose_date # ๋น„์šฐ๋ฉด ์ง„๋‹จ ์ƒ๋žต
792
+
793
+ ANALYZE_DAYS = 365
794
+ WARMUP_DAYS = 40
795
+ DATA_START = (datetime.today() - timedelta(days=ANALYZE_DAYS + WARMUP_DAYS)).strftime('%Y-%m-%d')
796
+ ANALYZE_FROM = (datetime.today() - timedelta(days=ANALYZE_DAYS)).strftime('%Y-%m-%d')
797
+ ANALYZE_TO = datetime.today().strftime('%Y-%m-%d')
798
+
799
+ def find_vcp_breakouts(df):
800
+ O = df['Open'].values.astype(float); H = df['High'].values.astype(float)
801
+ L = df['Low'].values.astype(float); C = df['Close'].values.astype(float)
802
+ V = df['Volume'].values.astype(float)
803
+ out = []
804
+ for t in range(BASE_LOOKBACK, len(df)):
805
+ quiet_max = V[t - CONTRACT_DAYS:t].max()
806
+ if quiet_max <= 0 or C[t - 1] <= 0:
807
+ continue
808
+ surge = V[t] / quiet_max
809
+ base_high = H[t - BASE_LOOKBACK:t].max()
810
+ gain = C[t] / C[t - 1] - 1
811
+ if (surge >= VOL_DRYUP_SURGE and C[t] > base_high
812
+ and gain >= MIN_PRICE_GAIN and C[t] > O[t]):
813
+ out.append({
814
+ 'i': t, 'date': str(df.index[t])[:10],
815
+ 'close': C[t], 'open': O[t], 'low': L[t],
816
+ 'gain': round(gain * 100, 1), 'surge': round(surge, 2),
817
+ 'base_high': base_high, 'base_low': float(L[t - BASE_LOOKBACK:t].min()),
818
+ })
819
+ return [b for b in out if b['date'] >= ANALYZE_FROM]
820
+
821
+ def run_diagnose(df, date_str):
822
+ O = df['Open'].values.astype(float); H = df['High'].values.astype(float)
823
+ C = df['Close'].values.astype(float); V = df['Volume'].values.astype(float)
824
+ idx = [str(d)[:10] for d in df.index]
825
+ pos = next((k for k, d in enumerate(idx) if d >= date_str), len(df) - 1)
826
+ lo = max(BASE_LOOKBACK, pos - 12); hi = min(len(df), pos + 4)
827
+ bh_label = f"{BASE_LOOKBACK}์ผ๊ณ ๊ฐ€"
828
+
829
+ print("\n" + "=" * 92)
830
+ print(f"[์ง„๋‹จ] {date_str} ์ฃผ๋ณ€ ์ผ๋ณ„ (๋ฐฐ์ˆ˜ = ๊ทธ๋‚  ๊ฑฐ๋ž˜๋Ÿ‰ / ์ง์ „ {CONTRACT_DAYS}์ผ '์ตœ๋Œ€' ๊ฑฐ๋ž˜๋Ÿ‰)")
831
+ print("=" * 92)
832
+ print(f"{'๋‚ ์งœ':<11}{'์ข…๊ฐ€':>8}{'์‹œ๊ฐ€':>8}{'๊ฑฐ๋ž˜๋Ÿ‰':>12}{'์ง์ „์ตœ๋Œ€':>12}{'๋ฐฐ์ˆ˜':>7}"
833
+ f"{'๋Œ€๋น„%':>7}{'์–‘๋ด‰':>5}{bh_label:>9}{'๊ณ ๊ฐ€๋ŒํŒŒ':>7}")
834
+ print("-" * 92)
835
+ for k in range(lo, hi):
836
+ qa = V[k - CONTRACT_DAYS:k].max()
837
+ sg = (V[k] / qa) if qa > 0 else 0
838
+ bh = H[k - BASE_LOOKBACK:k].max()
839
+ gn = (C[k] / C[k - 1] - 1) * 100 if C[k - 1] > 0 else 0
840
+ bull = 'O' if C[k] > O[k] else ''
841
+ pbk = 'O' if C[k] > bh else ''
842
+ mark = ' <==' if idx[k] == date_str else ''
843
+ print(f"{idx[k]:<11}{C[k]:>8,.0f}{O[k]:>8,.0f}{V[k]:>12,.0f}{qa:>12,.0f}{sg:>6.1f}x"
844
+ f"{gn:>+6.1f}%{bull:>5}{bh:>9,.0f}{pbk:>7}{mark}")
845
+
846
+ print("\n [ํŒ์ •]")
847
+ target = idx[pos]
848
+ if target != date_str:
849
+ print(f" (์ฐธ๊ณ : {date_str}์€ ๊ฑฐ๋ž˜์ผ ์•„๋‹˜ -> ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด {target} ๊ธฐ์ค€)")
850
+ qa = V[pos - CONTRACT_DAYS:pos].max()
851
+ sg = (V[pos] / qa) if qa > 0 else 0
852
+ bh = H[pos - BASE_LOOKBACK:pos].max()
853
+ gn = (C[pos] / C[pos - 1] - 1) * 100 if C[pos - 1] > 0 else 0
854
+ c1 = sg >= VOL_DRYUP_SURGE; c2 = C[pos] > bh
855
+ c3 = gn >= MIN_PRICE_GAIN * 100; c4 = C[pos] > O[pos]
856
+ print(f" ๊ฑฐ๋ž˜๋Ÿ‰ ๋ฐฐ์ˆ˜ {sg:.1f}x >= {VOL_DRYUP_SURGE}? {'O' if c1 else 'X'}")
857
+ print(f" ์ข…๊ฐ€ {C[pos]:,.0f} > {BASE_LOOKBACK}์ผ๊ณ ๊ฐ€ {bh:,.0f}? {'O' if c2 else 'X'}")
858
+ print(f" ์ƒ์Šน๋ฅ  {gn:+.1f}% >= {MIN_PRICE_GAIN*100:.0f}%? {'O' if c3 else 'X'}")
859
+ print(f" ์–‘๋ด‰ (์ข…๊ฐ€ {C[pos]:,.0f} > ์‹œ๊ฐ€ {O[pos]:,.0f})? {'O' if c4 else 'X'}")
860
+ if c1 and c2 and c3 and c4:
861
+ print(f" => 4์กฐ๊ฑด ํ†ต๊ณผ -> ์žกํ˜€์•ผ ํ•จ.")
862
+ else:
863
+ fails = [n for n, ok in [(f'๊ฑฐ๋ž˜๋Ÿ‰ ๋ฐฐ์ˆ˜ ๋ถ€์กฑ({sg:.1f}x)', c1), ('๊ฐ€๊ฒฉ์ด ๊ณ ๊ฐ€ ๋ชป ๋„˜์Œ', c2),
864
+ (f'์ƒ์Šน๋ฅ  ๋ถ€์กฑ({gn:+.1f}%)', c3), ('์Œ๋ด‰', c4)] if not ok]
865
+ print(f" => ์•ˆ ์žกํž˜. ์ด์œ : {', '.join(fails)}")
866
+ if not c1:
867
+ print(f" (VOL_DRYUP_SURGE๋ฅผ {sg:.1f} ๋ฐ‘์œผ๋กœ ๋‚ฎ์ถ”๋ฉด ์žกํ˜€)")
868
+
869
+ try:
870
+ with contextlib.redirect_stdout(buf):
871
+ TICKER = ticker
872
+ print("=" * 70)
873
+ print(f"VCP ๋ŒํŒŒ ๊ฐ์ง€: {TICKER} | ๊ธฐ๊ฐ„: {ANALYZE_FROM} ~ {ANALYZE_TO}")
874
+ print(f"์กฐ๊ฑด: ์ง์ „{CONTRACT_DAYS}์ผ ๋Œ€๋น„ ๊ฑฐ๋ž˜๋Ÿ‰ {VOL_DRYUP_SURGE}๋ฐฐ+ & {BASE_LOOKBACK}์ผ๊ณ ๊ฐ€ ๋ŒํŒŒ "
875
+ f"& ์ƒ์Šน +{MIN_PRICE_GAIN*100:.0f}%+ & ์–‘๋ด‰")
876
+ print("=" * 70)
877
+
878
+ df = fdr.DataReader(TICKER, DATA_START, ANALYZE_TO)
879
+ if df is None or len(df) == 0:
880
+ print("๋ฐ์ดํ„ฐ ์—†์Œ"); sys.exit()
881
+ print(f"๋ฐ์ดํ„ฐ: {len(df)}์ผ ๋กœ๋“œ ์™„๋ฃŒ\n")
882
+
883
+ bos = find_vcp_breakouts(df)
884
+ print(f"๊ฐ์ง€๋œ VCP ๋ŒํŒŒ: {len(bos)}๊ฐœ\n")
885
+
886
+ for n, b in enumerate(bos, 1):
887
+ print("=" * 70)
888
+ print(f"VCP #{n} ๋ŒํŒŒ {b['date']} @ {b['close']:,.0f}์›")
889
+ print(f" ์ƒ์Šน๋ฅ  +{b['gain']}% | ๊ฑฐ๋ž˜๋Ÿ‰ {b['surge']}๋ฐฐ(์ง์ „{CONTRACT_DAYS}์ผ์˜ ์ตœ๋Œ€ ๊ธฐ์ค€) | "
890
+ f"{BASE_LOOKBACK}์ผ๊ณ ๊ฐ€({b['base_high']:,.0f}) ๋ŒํŒŒ | ์–‘๋ด‰")
891
+ print()
892
+
893
+ print("=" * 70)
894
+ print(f"์š”์•ฝ: ์ด {len(bos)}๊ฐœ ๋ŒํŒŒ")
895
+ for b in bos:
896
+ print(f" {b['date']} | {b['close']:,.0f}์› | +{b['gain']}% | ๊ฑฐ๋ž˜๋Ÿ‰ {b['surge']}๋ฐฐ")
897
+
898
+ if DIAGNOSE_DATE:
899
+ run_diagnose(df, DIAGNOSE_DATE)
900
+ except SystemExit:
901
+ pass
902
+ except Exception as e:
903
+ buf.write(f"\n[์˜ค๋ฅ˜] {type(e).__name__}: {e}\n"
904
+ f"ํ‹ฐ์ปค ๋ฒˆํ˜ธ(6์ž๋ฆฌ)๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”. ์˜ˆ: 005930")
905
+ return buf.getvalue()
906
+
907
+
908
+ # ============================================================
909
+ # Gradio UI
910
+ # ============================================================
911
+ VCP_DETECT = "VCP ๋ŒํŒŒ ๊ฐ์ง€"
912
+ VCP_SELL = "VCP ์‹คํŒจ ๋ถ€๋ถ„๋งค๋„"
913
+ BOLL = "๋ณผ๋ฆฐ์ € ์นด์šดํŠธ"
914
+ MA240 = "240์„  ์ถ”์„ธ์‚ฌ๋ง ์ปท"
915
+ SELL = "์ „๋Ÿ‰๋งค๋„ (RSยท์ด๊ฒฉ)"
916
+
917
+ CHOICES = [VCP_DETECT, VCP_SELL, BOLL, MA240, SELL]
918
+
919
+
920
+ def analyze(choice, ticker, diag_date):
921
+ ticker = (ticker or "").strip()
922
+ if not ticker:
923
+ return "ํ‹ฐ์ปค ๋ฒˆํ˜ธ(6์ž๋ฆฌ)๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”.\n์˜ˆ: 005930(์‚ผ์„ฑ์ „์ž), 009150(์‚ผ์„ฑ์ „๊ธฐ)"
924
+ diag = (diag_date or "").strip() or None
925
+
926
+ if choice == VCP_DETECT:
927
+ return run_vcp_detect(ticker, diag)
928
+ if choice == VCP_SELL:
929
+ return run_vcp_sell(ticker)
930
+ if choice == BOLL:
931
+ return run_bollinger(ticker)
932
+ if choice == MA240:
933
+ return run_ma240(ticker)
934
+ if choice == SELL:
935
+ return run_sell(ticker)
936
+ return "๋ถ„์„ ์ข…๋ฅ˜๋ฅผ ์„ ํƒํ•˜์„ธ์š”."
937
+
938
+
939
+ with gr.Blocks(title="์ฃผ์‹ ๋””๋ฒ„๊ฑฐ", theme=gr.themes.Soft()) as demo:
940
+ gr.Markdown(
941
+ "## ๐Ÿ“ˆ ์ฃผ์‹ ๋””๋ฒ„๊ฑฐ\n"
942
+ "๋ถ„์„ ์ข…๋ฅ˜๋ฅผ ๊ณ ๋ฅด๊ณ  ํ‹ฐ์ปค(6์ž๋ฆฌ)๋ฅผ ์ž…๋ ฅํ•œ ๋’ค **๋ถ„์„ ์‹คํ–‰**์„ ๋ˆ„๋ฅด์„ธ์š”.\n"
943
+ "ํ‘œ๊ฐ€ ๋„“์œผ๋ฉด ์ขŒ์šฐ๋กœ ์Šคํฌ๋กคํ•˜๊ฑฐ๋‚˜ ๊ฐ€๋กœํ™”๋ฉด์œผ๋กœ ๋ณด์„ธ์š”."
944
+ )
945
+ with gr.Row():
946
+ choice = gr.Dropdown(choices=CHOICES, value=VCP_DETECT, label="๋ถ„์„ ์ข…๋ฅ˜")
947
+ ticker = gr.Textbox(label="ํ‹ฐ์ปค (6์ž๋ฆฌ)", placeholder="์˜ˆ: 005930")
948
+ diag_date = gr.Textbox(
949
+ label="VCP ์ง„๋‹จ ๋‚ ์งœ (์„ ํƒ ยท 'VCP ๋ŒํŒŒ ๊ฐ์ง€'์—์„œ๋งŒ ์‚ฌ์šฉ)",
950
+ placeholder="์˜ˆ: 2025-12-16 (๋น„์›Œ๋‘๋ฉด ์ง„๋‹จ ์ƒ๋žต)",
951
+ )
952
+ btn = gr.Button("๋ถ„์„ ์‹คํ–‰", variant="primary")
953
+ out = gr.Code(label="๊ฒฐ๊ณผ", lines=30)
954
+
955
+ btn.click(analyze, inputs=[choice, ticker, diag_date], outputs=out)
956
+ ticker.submit(analyze, inputs=[choice, ticker, diag_date], outputs=out)
957
+
958
+
959
+ if __name__ == "__main__":
960
+ demo.launch()