glorifiedjx commited on
Commit
169cb82
ยท
verified ยท
1 Parent(s): cd11f92

login_enabled_only 3menu

Browse files
Files changed (5) hide show
  1. app.py +115 -931
  2. bollinger_count_debug.py +145 -0
  3. final_sell_monitor.py +206 -0
  4. requirements.txt +2 -3
  5. vcp_debug.py +163 -0
app.py CHANGED
@@ -1,968 +1,152 @@
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
- # ============================================================
439
- # 3) ์ „๋Ÿ‰๋งค๋„ (์ตœํ›„๋ณด๋ฃจ) (final_sell_monitor.py ๋กœ์ง ์ด์‹ ยท ๋‹จ์ผ ํ‹ฐ์ปค)
440
- # ์ฃผ ์‹ ํ˜ธ : ์ž๊ธฐ๊ณ ์  ์ด๊ฒฉ -10% -> 22์ผ๋‚ด lower-low ์žฌํ™•์ธ (240์˜ฌ ๊ตฌ๊ฐ„)
441
- # ๋ฐฑ์—… : ์ข…๊ฐ€๊ฐ€ 240์„  BREAK_CONFIRM์ผ ์—ฐ์† ์ดํƒˆ
442
- # ============================================================
443
- def run_sell(ticker, market_override="์ž๋™"):
444
  buf = io.StringIO()
445
-
446
- DIV_THRESHOLD = -0.10 # ์ด๊ฒฉ ๊ธฐ์ค€
447
- CONFIRM_WINDOW = 22 # lower-low ์žฌํ™•์ธ ์œˆ๋„์šฐ(๊ฑฐ๋ž˜์ผ)
448
- TREND_MIN_DAYS = 60 # '240์˜ฌ': ์ตœ๊ทผ N๊ฑฐ๋ž˜์ผ ์—ฐ์† 240์„  ์œ„
449
- TREND_MIN_GAP = 0.05 # '240์˜ฌ': 240์„  +5% ์ด์ƒ
450
- BREAK_PCT = 0.00 # ๋ฐฑ์—… 240์„  ์ดํƒˆ ๊ธฐ์ค€
451
- BREAK_CONFIRM = 2 # ๋ฐฑ์—…: ์—ฐ์† N์ผ ์ดํƒˆ
452
- DATA_BACK_DAYS = 900 # 240MA ์›Œ๋ฐ์—… ํฌํ•จ ๋กœ๋“œ(โ‰ˆ2.5๋…„)
453
-
454
- def in_240_run(stock, ma240, i):
455
- if i - TREND_MIN_DAYS + 1 < 0:
456
- return False
457
- m = ma240[i]
458
- if np.isnan(m) or stock[i] <= m * (1 + TREND_MIN_GAP):
459
- return False
460
- for k in range(i - TREND_MIN_DAYS + 1, i + 1):
461
- if np.isnan(ma240[k]) or stock[k] <= ma240[k]:
462
- return False
463
- return True
464
-
465
- def load(tk, market):
466
- end = datetime.today().strftime('%Y-%m-%d')
467
- start = (datetime.today() - timedelta(days=DATA_BACK_DAYS)).strftime('%Y-%m-%d')
468
- sdf = fdr.DataReader(tk, start, end)
469
- if sdf is None or len(sdf) == 0:
470
- return None
471
- idx_code = 'KS11' if market == 'KOSPI' else 'KQ11'
472
- idf = None
473
- for c in (idx_code, '^' + idx_code):
474
- try:
475
- t = fdr.DataReader(c, start, end)
476
- if t is not None and len(t) > 0:
477
- idf = t; break
478
- except Exception:
479
- continue
480
- if idf is None:
481
- return None
482
- merged = pd.concat([sdf['Close'].rename('stock'), idf['Close'].rename('index')],
483
- axis=1, sort=True)
484
- merged['index'] = merged['index'].ffill()
485
- merged = merged.dropna()
486
- merged['ma240'] = merged['stock'].rolling(240).mean()
487
- return merged
488
-
489
- try:
490
- with contextlib.redirect_stdout(buf):
491
- TICKER = ticker
492
- name = TICKER
493
-
494
- print("=" * 72)
495
- print(f"์ „๋Ÿ‰๋งค๋„ ์ตœํ›„๋ณด๋ฃจ ๋ชจ๋‹ˆํ„ฐ | {datetime.today().strftime('%Y-%m-%d %H:%M')}")
496
- print(f"์ฃผ ์‹ ํ˜ธ: 240์˜ฌ + ์ด๊ฒฉ {DIV_THRESHOLD*100:.0f}% lower-low ์žฌํ™•์ธ({CONFIRM_WINDOW}์ผ)"
497
- f" | ๋ฐฑ์—…: 240์„  {BREAK_CONFIRM}์ผ ์—ฐ์† ์ดํƒˆ")
498
- print("=" * 72)
499
-
500
- # --- ์‹œ์žฅ ๊ฒฐ์ • (์ˆ˜๋™ ์šฐ์„ , ์ž๋™์€ KRX ๊ฐ€๋Šฅํ•  ๋•Œ๋งŒ) ---
501
- print("\n[์‹œ์žฅ ํŒ๋ณ„]")
502
- mo = (market_override or "์ž๋™").strip()
503
- market = None
504
- if mo in ('KOSPI', 'KOSDAQ'):
505
- market = mo
506
- print(f" (์ˆ˜๋™ ์ง€์ •) {market}")
507
- else:
508
- try:
509
- for mk in ('KOSPI', 'KOSDAQ'):
510
- lst = fdr.StockListing(mk)
511
- code_col = next((c for c in lst.columns
512
- if str(c).lower() in ('code', 'symbol', '์ข…๋ชฉ์ฝ”๋“œ')), None)
513
- name_col = next((c for c in lst.columns
514
- if str(c).lower() in ('name', '์ข…๋ชฉ๋ช…', 'korname')), None)
515
- codes = (lst[code_col] if code_col is not None else lst.index).astype(str)
516
- hit = (codes == TICKER)
517
- if bool(hit.any()):
518
- market = mk
519
- if name_col is not None:
520
- name = str(lst.loc[hit, name_col].values[0])
521
- break
522
- if market is None:
523
- market = 'KOSDAQ'
524
- print(f" ๋ชฉ๋ก์—์„œ ๋ชป ์ฐพ์Œ -> KOSDAQ ๊ฐ€์ • (์œ„ '์‹œ์žฅ' ๋ฉ”๋‰ด์—์„œ ์ง์ ‘ ์„ ํƒ)")
525
- else:
526
- print(f" {name} ({TICKER}) / {market}")
527
- except Exception as e:
528
- market = 'KOSDAQ'
529
- print(f" ์ž๋™ํŒ๋ณ„ ์‹คํŒจ({type(e).__name__}) -> KOSDAQ ๊ฐ€์ •. "
530
- f"์œ„ '์‹œ์žฅ' ๋ฉ”๋‰ด์—์„œ KOSPI/KOSDAQ ์ง์ ‘ ์„ ํƒ ๊ถŒ์žฅ.")
531
-
532
- # --- ๋ฐ์ดํ„ฐ ๋กœ๋“œ ---
533
- df = load(TICKER, market)
534
- if df is None:
535
- print(f"\n[{TICKER} {name}] ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์‹คํŒจ (ํ‹ฐ์ปค/์‹œ์žฅ ํ™•์ธ)"); sys.exit()
536
-
537
- dates = [str(d)[:10] for d in df.index]
538
- stock = df['stock'].values.astype(float)
539
- index = df['index'].values.astype(float)
540
- ma240 = df['ma240'].values.astype(float)
541
- n = len(stock)
542
-
543
- # --- ์›Œํฌํฌ์›Œ๋“œ: ์ž๊ธฐ๊ณ ์  + ์ด๊ฒฉ ์ด๋ฒคํŠธ + lower-low ์žฌํ™•์ธ (๊ฐ€์žฅ ์ตœ๊ทผ ๊ฒƒ ์ถ”์ ) ---
544
- running_peak = -1.0; index_at_peak = None; peak_date = None
545
- div_flag = False; div_flag_value = None; div_sold = False
546
- last_event = None
547
- confirmed = None
548
-
549
- for t in range(n):
550
- st = stock[t]; it = index[t]
551
- if running_peak < 0 or st >= running_peak:
552
- running_peak = st; index_at_peak = it; peak_date = dates[t]
553
- div_flag = False; div_flag_value = None; div_sold = False
554
- last_event = None
555
- confirmed = None
556
- else:
557
- sr = st / running_peak - 1
558
- ir = (it / index_at_peak - 1) if index_at_peak else 0.0
559
- div = sr - ir
560
- if div <= DIV_THRESHOLD:
561
- if not div_flag:
562
- div_flag = True; div_flag_value = div; div_sold = False
563
- elif div < div_flag_value:
564
- if not div_sold:
565
- if (last_event is not None
566
- and t - last_event['i'] <= CONFIRM_WINDOW
567
- and st < last_event['price']):
568
- confirmed = {'i': t, 'date': dates[t], 'price': st,
569
- 'prev_date': last_event['date'],
570
- 'run': in_240_run(stock, ma240, t)}
571
- last_event = {'i': t, 'price': st, 'date': dates[t]}
572
- div_sold = True
573
- div_flag_value = div
574
- else:
575
- div_flag_value = div
576
- else:
577
- div_flag = False; div_flag_value = None; div_sold = False
578
-
579
- # --- ๋ฐฑ์—…: 240์„  ์ดํƒˆ (์—ฐ์† BREAK_CONFIRM์ผ) ---
580
- break_240 = None
581
- streak = 0
582
- for t in range(n):
583
- if np.isnan(ma240[t]):
584
- continue
585
- if stock[t] < ma240[t] * (1 - BREAK_PCT):
586
- streak += 1
587
- if streak >= BREAK_CONFIRM and break_240 is None:
588
- break_240 = dates[t]
589
- else:
590
- streak = 0
591
- break_240 = None
592
-
593
- # --- ์˜ค๋Š˜ ์ƒํƒœ ---
594
- t = n - 1
595
- today = dates[t]
596
- m = ma240[t]
597
- run_now = in_240_run(stock, ma240, t)
598
- sr_now = stock[t] / running_peak - 1 if running_peak > 0 else 0.0
599
- ir_now = (index[t] / index_at_peak - 1) if index_at_peak else 0.0
600
- div_now = sr_now - ir_now
601
-
602
- print("\n" + "=" * 72)
603
- print(f"[{TICKER} {name}] {market} | {today} ์ข…๊ฐ€ {stock[t]:,.0f}"
604
- + (f" | 240์„  {m:,.0f} ({(stock[t]/m-1)*100:+.1f}%)" if not np.isnan(m) else " | 240์„  ๋ฐ์ดํ„ฐ๋ถ€์กฑ"))
605
- print(f" 240์˜ฌ ์—ฌ๋ถ€ : {'์˜ˆ (60์ผ ์—ฐ์† 240์œ„ + 5%โ†‘)' if run_now else '์•„๋‹ˆ์˜ค'}")
606
- print(f" ์ข…๋ชฉ ๊ณ ์  : {running_peak:,.0f} ({peak_date}) | ํ˜„์žฌ ์ด๊ฒฉ {div_now*100:+.1f}%"
607
- f" (๊ธฐ์ค€ {DIV_THRESHOLD*100:.0f}%) | flag {'ON' if div_flag else 'off'}")
608
- if last_event and not confirmed:
609
- remain = CONFIRM_WINDOW - (t - last_event['i'])
610
- if remain > 0:
611
- print(f" 1์ฐจ ์ด๋ฒคํŠธ : {last_event['date']} @ {last_event['price']:,.0f}"
612
- f" -> ์žฌํ™•์ธ ๋Œ€๊ธฐ {remain}๊ฑฐ๋ž˜์ผ ๋‚จ์Œ (์ด๋ณด๋‹ค ๋‚ฎ์€ 2์ฐจ ์ด๋ฒคํŠธ๋ฉด ์ „๋Ÿ‰๋งค๋„)")
613
-
614
- # --- ํŒ์ • ---
615
- if confirmed:
616
- tag = '240์˜ฌ' if confirmed['run'] else '๋น„240์˜ฌ(์ฐธ๊ณ )'
617
- ago = (n - 1) - confirmed['i']
618
- print(f"\n >>> [์ฃผ ์‹ ํ˜ธ] ์ „๋Ÿ‰๋งค๋„: {confirmed['date']} @ {confirmed['price']:,.0f}"
619
- f" (1์ฐจ {confirmed['prev_date']} -> lower-low ์žฌํ™•์ธ, {tag}) | {ago}๊ฑฐ๋ž˜์ผ ์ „")
620
- if not confirmed['run']:
621
- print(" (240์˜ฌ ๊ตฌ๊ฐ„์ด ์•„๋‹ˆ์–ด์„œ ์‹ ๋ขฐ๋„๋Š” ํ•œ ๋‹จ๊ณ„ ๋‚ฎ์Œ โ€” ์ฐจํŠธ๋กœ ํ™•์ธ)")
622
- if break_240:
623
- print(f"\n >>> [๋ฐฑ์—…] 240์„  ์ดํƒˆ ๋งค๋„: {break_240} (์—ฐ์† {BREAK_CONFIRM}์ผ ์ดํƒˆ, ํ˜„์žฌ๋„ ์ดํƒˆ ์ค‘)")
624
- if not confirmed and not break_240:
625
- print(f"\n ==> ๋งค๋„ ์‹ ํ˜ธ ์—†์Œ. ๋ณด์œ  ์œ ์ง€.")
626
- except SystemExit:
627
- pass
628
- except Exception as e:
629
- buf.write(f"\n[์˜ค๋ฅ˜] {type(e).__name__}: {e}\n"
630
- f"ํ‹ฐ์ปค ๋ฒˆํ˜ธ(6์ž๋ฆฌ)๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”. ์˜ˆ: 005930")
631
- return buf.getvalue()
632
-
633
-
634
- # ============================================================
635
- # 4) VCP ์‹คํŒจ ๋ถ€๋ถ„๋งค๋„ (vcp_sell_debug.py ๋กœ์ง ๊ทธ๋Œ€๋กœ)
636
- # ============================================================
637
- def run_vcp_sell(ticker):
638
  buf = io.StringIO()
639
-
640
- VCP_BUFFER = 0.05
641
- VCP_BUY_KRW = None
642
- STOP_REF = 'breakout_low'
643
-
644
- CONTRACT_DAYS = 2
645
- VOL_DRYUP_SURGE = 2.0
646
- MIN_PRICE_GAIN = 0.10
647
- BASE_LOOKBACK = 10
648
-
649
- ANALYZE_DAYS = 365
650
- WARMUP_DAYS = 40
651
- DATA_START = (datetime.today() - timedelta(days=ANALYZE_DAYS + WARMUP_DAYS)).strftime('%Y-%m-%d')
652
- ANALYZE_FROM = (datetime.today() - timedelta(days=ANALYZE_DAYS)).strftime('%Y-%m-%d')
653
-
654
- def find_vcp_breakouts(df):
655
- O = df['Open'].values.astype(float); H = df['High'].values.astype(float)
656
- L = df['Low'].values.astype(float); C = df['Close'].values.astype(float)
657
- V = df['Volume'].values.astype(float)
658
- out = []
659
- for t in range(BASE_LOOKBACK, len(df)):
660
- quiet_max = V[t - CONTRACT_DAYS:t].max()
661
- if quiet_max <= 0 or C[t - 1] <= 0:
662
- continue
663
- surge = V[t] / quiet_max
664
- base_high = H[t - BASE_LOOKBACK:t].max()
665
- gain = C[t] / C[t - 1] - 1
666
- if (surge >= VOL_DRYUP_SURGE and C[t] > base_high
667
- and gain >= MIN_PRICE_GAIN and C[t] > O[t]):
668
- out.append({
669
- 'i': t, 'date': str(df.index[t])[:10],
670
- 'close': C[t], 'open': O[t], 'low': L[t],
671
- 'gain': round(gain * 100, 1), 'surge': round(surge, 2),
672
- 'base_high': base_high, 'base_low': float(L[t - BASE_LOOKBACK:t].min()),
673
- })
674
- return [b for b in out if b['date'] >= ANALYZE_FROM]
675
-
676
- def track_vcp_stop(closes, dates, breakout_i, ref_low, entry_price, buffer=VCP_BUFFER):
677
- stop = ref_low * (1 - buffer)
678
- daily, failed, fail = [], False, None
679
- for k in range(breakout_i + 1, len(closes)):
680
- ck = float(closes[k])
681
- if ck < stop:
682
- failed = True
683
- fail = {'date': dates[k], 'price': ck, 'k': k,
684
- 'loss_pct': (ck / entry_price - 1) * 100}
685
- daily.append((dates[k], ck, 'fail')); break
686
- elif ck < stop * 1.05:
687
- daily.append((dates[k], ck, 'warn'))
688
- else:
689
- daily.append((dates[k], ck, 'hold'))
690
- return stop, failed, fail, daily
691
-
692
- def analyze(ticker):
693
- ref_name = "๋ŒํŒŒ์ผ ์ €์ " if STOP_REF == 'breakout_low' else "๋ฒ ์ด์Šค ์ €์ "
694
- print("=" * 80)
695
- print(f"VCP ์‹คํŒจ ๋ถ€๋ถ„๋งค๋„ ๋””๋ฒ„๊ทธ : {ticker}")
696
- print(f"๊ทœ์น™ : ๋ŒํŒŒ ๋งค์ˆ˜ ํ›„ ์ข…๊ฐ€ < {ref_name}ร—{1-VCP_BUFFER:.2f} ({VCP_BUFFER*100:.0f}% ๋ฒ„ํผ) "
697
- f"-> VCP ์‹คํŒจ -> VCP๋กœ ์‚ฐ ๋งŒํผ๋งŒ ๋ถ€๋ถ„ ๋งค๋„")
698
- print(f" (VCP ์‹คํŒจ โ‰  ์ข…๋ชฉ ์•…ํ™”. ๋‚˜๋จธ์ง€ ํฌ์ง€์…˜์€ ๊ทธ๋Œ€๋กœ ์œ ์ง€)")
699
- print("=" * 80)
700
-
701
- name, market = ticker, '-'
702
  try:
703
- kospi = fdr.StockListing('KOSPI')
704
- kosdaq = fdr.StockListing('KOSDAQ')
705
- if ticker in kospi['Code'].values:
706
- market = 'KOSPI'; name = kospi[kospi['Code'] == ticker]['Name'].values[0]
707
- elif ticker in kosdaq['Code'].values:
708
- market = 'KOSDAQ'; name = kosdaq[kosdaq['Code'] == ticker]['Name'].values[0]
709
- except Exception:
710
  pass
711
-
712
- df = fdr.DataReader(ticker, DATA_START, datetime.today().strftime('%Y-%m-%d'))
713
- if df is None or len(df) == 0:
714
- print("๋ฐ์ดํ„ฐ ์—†์Œ"); return
715
- print(f"์ข…๋ชฉ : {name} ({ticker}) / {market} | ๋ฐ์ดํ„ฐ {len(df)}์ผ\n")
716
-
717
- bos = find_vcp_breakouts(df)
718
- closes = df['Close'].values.astype(float)
719
- dates = [str(d)[:10] for d in df.index]
720
- print(f"VCP ๋ŒํŒŒ(๋งค์ˆ˜): {len(bos)}๊ฐœ\n")
721
- if not bos:
722
- print("VCP ๋ŒํŒŒ ์—†์Œ -> ์ถ”์  ๋Œ€์ƒ ์—†์Œ"); return
723
-
724
- results = []
725
- for n, b in enumerate(bos, 1):
726
- entry = b['close']
727
- ref_low = b['low'] if STOP_REF == 'breakout_low' else b['base_low']
728
- stop, failed, fail, daily = track_vcp_stop(closes, dates, b['i'], ref_low, entry, VCP_BUFFER)
729
-
730
- print("-" * 80)
731
- print(f"VCP #{n} ๋ŒํŒŒ {b['date']} @ {entry:,.0f}์› "
732
- f"(์ƒ์Šน +{b['gain']}%, ๊ฑฐ๋ž˜๋Ÿ‰ {b['surge']}๋ฐฐ)")
733
- print(f" {ref_name}: {ref_low:,.0f} | ์†์ ˆ์„  = {ref_low:,.0f}ร—{1-VCP_BUFFER:.2f} = {stop:,.0f}์› "
734
- f"(์ง„์ž…๊ฐ€ ๋Œ€๋น„ {(stop/entry-1)*100:+.1f}%)")
735
- print(f" ---- ๋ŒํŒŒ ํ›„ ์ถ”์  ----")
736
- prev = None
737
- for j, (d, c, st) in enumerate(daily):
738
- is_last = (j == len(daily) - 1)
739
- if not ((st != prev) or st in ('warn', 'fail') or is_last):
740
- prev = st; continue
741
- prev = st
742
- if st == 'fail':
743
- msg = (f">>> VCP ์‹คํŒจ! ์ข…๊ฐ€ {c:,.0f} < ์†์ ˆ์„  {stop:,.0f} "
744
- f"-> VCP๋ถ„๋งŒ ๋งค๋„ (์ง„์ž…๊ฐ€ ๋Œ€๋น„ {fail['loss_pct']:+.1f}%) <<<<<")
745
- elif st == 'warn':
746
- msg = f"๊ฒฝ๊ณ  (์ข…๊ฐ€ {c:,.0f}, ์†์ ˆ์„  {stop:,.0f} ๊ทผ์ ‘) - ๋ณด์œ "
747
- else:
748
- msg = f"๋ณด์œ  (์ข…๊ฐ€ {c:,.0f} > ์†์ ˆ์„  {stop:,.0f})"
749
- print(f" {d:<11} {msg}")
750
- if not failed:
751
- cur = closes[-1]
752
- print(f" => VCP ์œ ์ง€ ์ค‘ (ํ˜„์žฌ {cur:,.0f}, ์ง„์ž…๊ฐ€ ๋Œ€๋น„ {(cur/entry-1)*100:+.1f}%)")
753
- results.append({'n': n, 'entry': entry, 'failed': failed, 'fail': fail})
754
- print()
755
-
756
- fails = [r for r in results if r['failed']]
757
- holds = [r for r in results if not r['failed']]
758
- print("=" * 80)
759
- print("์š”์•ฝ")
760
- print("=" * 80)
761
- print(f"์ข…๋ชฉ : {name} ({ticker}) / {market}")
762
- print(f"VCP ๋ŒํŒŒ : {len(bos)}๊ฐœ | ์‹คํŒจ(๋ถ€๋ถ„๋งค๋„): {len(fails)}๊ฐœ | ์œ ์ง€: {len(holds)}๊ฐœ")
763
- if fails:
764
- avg = np.mean([r['fail']['loss_pct'] for r in fails])
765
- print(f"์‹คํŒจ๋ถ„ ์†์‹ค: ํ‰๊ท  {avg:+.1f}% (VCP ํŠธ๋žœ์น˜ ๊ธฐ์ค€, ๋‚˜๋จธ์ง€ ํฌ์ง€์…˜์€ ์œ ์ง€)")
766
- for r in fails:
767
- line = f" VCP #{r['n']}: ์ง„์ž… {r['entry']:,.0f} -> {r['fail']['date']} ๋งค๋„ ({r['fail']['loss_pct']:+.1f}%)"
768
- if VCP_BUY_KRW:
769
- loss = VCP_BUY_KRW * r['fail']['loss_pct'] / 100
770
- line += f" | ๋งค๋„๊ธˆ์•ก ~{VCP_BUY_KRW+loss:,.0f}์› (์†์ต {loss:+,.0f}์›)"
771
- print(line)
772
-
773
- try:
774
- with contextlib.redirect_stdout(buf):
775
- analyze(ticker)
776
- except SystemExit:
777
- pass
778
- except Exception as e:
779
- buf.write(f"\n[์˜ค๋ฅ˜] {type(e).__name__}: {e}\n"
780
- f"ํ‹ฐ์ปค ๋ฒˆํ˜ธ(6์ž๋ฆฌ)๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”. ์˜ˆ: 005930")
781
- return buf.getvalue()
782
 
783
 
784
- # ============================================================
785
- # 5) VCP ๋ŒํŒŒ ๊ฐ์ง€ (past_vcp_debug.py ๋กœ์ง ๊ทธ๋Œ€๋กœ + ์ง„๋‹จ ๋‚ ์งœ ์„ ํƒ)
786
- # ============================================================
787
- def run_vcp_detect(ticker, diagnose_date=None):
788
- buf = io.StringIO()
789
-
790
- CONTRACT_DAYS = 2
791
- VOL_DRYUP_SURGE = 6.0
792
- MIN_PRICE_GAIN = 0.13
793
- BASE_LOOKBACK = 10
794
-
795
- DIAGNOSE_DATE = diagnose_date # ๋น„์šฐ๋ฉด ์ง„๋‹จ ์ƒ๋žต
796
-
797
- ANALYZE_DAYS = 365
798
- WARMUP_DAYS = 40
799
- DATA_START = (datetime.today() - timedelta(days=ANALYZE_DAYS + WARMUP_DAYS)).strftime('%Y-%m-%d')
800
- ANALYZE_FROM = (datetime.today() - timedelta(days=ANALYZE_DAYS)).strftime('%Y-%m-%d')
801
- ANALYZE_TO = datetime.today().strftime('%Y-%m-%d')
802
-
803
- def find_vcp_breakouts(df):
804
- O = df['Open'].values.astype(float); H = df['High'].values.astype(float)
805
- L = df['Low'].values.astype(float); C = df['Close'].values.astype(float)
806
- V = df['Volume'].values.astype(float)
807
- out = []
808
- for t in range(BASE_LOOKBACK, len(df)):
809
- quiet_max = V[t - CONTRACT_DAYS:t].max()
810
- if quiet_max <= 0 or C[t - 1] <= 0:
811
- continue
812
- surge = V[t] / quiet_max
813
- base_high = H[t - BASE_LOOKBACK:t].max()
814
- gain = C[t] / C[t - 1] - 1
815
- if (surge >= VOL_DRYUP_SURGE and C[t] > base_high
816
- and gain >= MIN_PRICE_GAIN and C[t] > O[t]):
817
- out.append({
818
- 'i': t, 'date': str(df.index[t])[:10],
819
- 'close': C[t], 'open': O[t], 'low': L[t],
820
- 'gain': round(gain * 100, 1), 'surge': round(surge, 2),
821
- 'base_high': base_high, 'base_low': float(L[t - BASE_LOOKBACK:t].min()),
822
- })
823
- return [b for b in out if b['date'] >= ANALYZE_FROM]
824
-
825
- def run_diagnose(df, date_str):
826
- O = df['Open'].values.astype(float); H = df['High'].values.astype(float)
827
- C = df['Close'].values.astype(float); V = df['Volume'].values.astype(float)
828
- idx = [str(d)[:10] for d in df.index]
829
- pos = next((k for k, d in enumerate(idx) if d >= date_str), len(df) - 1)
830
- lo = max(BASE_LOOKBACK, pos - 12); hi = min(len(df), pos + 4)
831
- bh_label = f"{BASE_LOOKBACK}์ผ๊ณ ๊ฐ€"
832
-
833
- print("\n" + "=" * 92)
834
- print(f"[์ง„๋‹จ] {date_str} ์ฃผ๋ณ€ ์ผ๋ณ„ (๋ฐฐ์ˆ˜ = ๊ทธ๋‚  ๊ฑฐ๋ž˜๋Ÿ‰ / ์ง์ „ {CONTRACT_DAYS}์ผ '์ตœ๋Œ€' ๊ฑฐ๋ž˜๋Ÿ‰)")
835
- print("=" * 92)
836
- print(f"{'๋‚ ์งœ':<11}{'์ข…๊ฐ€':>8}{'์‹œ๊ฐ€':>8}{'๊ฑฐ๋ž˜๋Ÿ‰':>12}{'์ง์ „์ตœ๋Œ€':>12}{'๋ฐฐ์ˆ˜':>7}"
837
- f"{'๋Œ€๋น„%':>7}{'์–‘๋ด‰':>5}{bh_label:>9}{'๊ณ ๊ฐ€๋ŒํŒŒ':>7}")
838
- print("-" * 92)
839
- for k in range(lo, hi):
840
- qa = V[k - CONTRACT_DAYS:k].max()
841
- sg = (V[k] / qa) if qa > 0 else 0
842
- bh = H[k - BASE_LOOKBACK:k].max()
843
- gn = (C[k] / C[k - 1] - 1) * 100 if C[k - 1] > 0 else 0
844
- bull = 'O' if C[k] > O[k] else ''
845
- pbk = 'O' if C[k] > bh else ''
846
- mark = ' <==' if idx[k] == date_str else ''
847
- print(f"{idx[k]:<11}{C[k]:>8,.0f}{O[k]:>8,.0f}{V[k]:>12,.0f}{qa:>12,.0f}{sg:>6.1f}x"
848
- f"{gn:>+6.1f}%{bull:>5}{bh:>9,.0f}{pbk:>7}{mark}")
849
-
850
- print("\n [ํŒ์ •]")
851
- target = idx[pos]
852
- if target != date_str:
853
- print(f" (์ฐธ๊ณ : {date_str}์€ ๊ฑฐ๋ž˜์ผ ์•„๋‹˜ -> ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด {target} ๊ธฐ์ค€)")
854
- qa = V[pos - CONTRACT_DAYS:pos].max()
855
- sg = (V[pos] / qa) if qa > 0 else 0
856
- bh = H[pos - BASE_LOOKBACK:pos].max()
857
- gn = (C[pos] / C[pos - 1] - 1) * 100 if C[pos - 1] > 0 else 0
858
- c1 = sg >= VOL_DRYUP_SURGE; c2 = C[pos] > bh
859
- c3 = gn >= MIN_PRICE_GAIN * 100; c4 = C[pos] > O[pos]
860
- print(f" ๊ฑฐ๋ž˜๋Ÿ‰ ๋ฐฐ์ˆ˜ {sg:.1f}x >= {VOL_DRYUP_SURGE}? {'O' if c1 else 'X'}")
861
- print(f" ์ข…๊ฐ€ {C[pos]:,.0f} > {BASE_LOOKBACK}์ผ๊ณ ๊ฐ€ {bh:,.0f}? {'O' if c2 else 'X'}")
862
- print(f" ์ƒ์Šน๋ฅ  {gn:+.1f}% >= {MIN_PRICE_GAIN*100:.0f}%? {'O' if c3 else 'X'}")
863
- print(f" ์–‘๋ด‰ (์ข…๊ฐ€ {C[pos]:,.0f} > ์‹œ๊ฐ€ {O[pos]:,.0f})? {'O' if c4 else 'X'}")
864
- if c1 and c2 and c3 and c4:
865
- print(f" => 4์กฐ๊ฑด ํ†ต๊ณผ -> ์žกํ˜€์•ผ ํ•จ.")
866
- else:
867
- fails = [n for n, ok in [(f'๊ฑฐ๋ž˜๋Ÿ‰ ๋ฐฐ์ˆ˜ ๋ถ€์กฑ({sg:.1f}x)', c1), ('๊ฐ€๊ฒฉ์ด ๊ณ ๊ฐ€ ๋ชป ๋„˜์Œ', c2),
868
- (f'์ƒ์Šน๋ฅ  ๋ถ€์กฑ({gn:+.1f}%)', c3), ('์Œ๋ด‰', c4)] if not ok]
869
- print(f" => ์•ˆ ์žกํž˜. ์ด์œ : {', '.join(fails)}")
870
- if not c1:
871
- print(f" (VOL_DRYUP_SURGE๋ฅผ {sg:.1f} ๋ฐ‘์œผ๋กœ ๋‚ฎ์ถ”๋ฉด ์žกํ˜€)")
872
-
873
- try:
874
- with contextlib.redirect_stdout(buf):
875
- TICKER = ticker
876
- print("=" * 70)
877
- print(f"VCP ๋ŒํŒŒ ๊ฐ์ง€: {TICKER} | ๊ธฐ๊ฐ„: {ANALYZE_FROM} ~ {ANALYZE_TO}")
878
- print(f"์กฐ๊ฑด: ์ง์ „{CONTRACT_DAYS}์ผ ๋Œ€๋น„ ๊ฑฐ๋ž˜๋Ÿ‰ {VOL_DRYUP_SURGE}๋ฐฐ+ & {BASE_LOOKBACK}์ผ๊ณ ๊ฐ€ ๋ŒํŒŒ "
879
- f"& ์ƒ์Šน +{MIN_PRICE_GAIN*100:.0f}%+ & ์–‘๋ด‰")
880
- print("=" * 70)
881
-
882
- df = fdr.DataReader(TICKER, DATA_START, ANALYZE_TO)
883
- if df is None or len(df) == 0:
884
- print("๋ฐ์ดํ„ฐ ์—†์Œ"); sys.exit()
885
- print(f"๋ฐ์ดํ„ฐ: {len(df)}์ผ ๋กœ๋“œ ์™„๋ฃŒ\n")
886
-
887
- bos = find_vcp_breakouts(df)
888
- print(f"๊ฐ์ง€๋œ VCP ๋ŒํŒŒ: {len(bos)}๊ฐœ\n")
889
-
890
- for n, b in enumerate(bos, 1):
891
- print("=" * 70)
892
- print(f"VCP #{n} ๋ŒํŒŒ {b['date']} @ {b['close']:,.0f}์›")
893
- print(f" ์ƒ์Šน๋ฅ  +{b['gain']}% | ๊ฑฐ๋ž˜๋Ÿ‰ {b['surge']}๋ฐฐ(์ง์ „{CONTRACT_DAYS}์ผ์˜ ์ตœ๋Œ€ ๊ธฐ์ค€) | "
894
- f"{BASE_LOOKBACK}์ผ๊ณ ๊ฐ€({b['base_high']:,.0f}) ๋ŒํŒŒ | ์–‘๋ด‰")
895
- print()
896
-
897
- print("=" * 70)
898
- print(f"์š”์•ฝ: ์ด {len(bos)}๊ฐœ ๋ŒํŒŒ")
899
- for b in bos:
900
- print(f" {b['date']} | {b['close']:,.0f}์› | +{b['gain']}% | ๊ฑฐ๋ž˜๋Ÿ‰ {b['surge']}๋ฐฐ")
901
-
902
- if DIAGNOSE_DATE:
903
- run_diagnose(df, DIAGNOSE_DATE)
904
- except SystemExit:
905
- pass
906
- except Exception as e:
907
- buf.write(f"\n[์˜ค๋ฅ˜] {type(e).__name__}: {e}\n"
908
- f"ํ‹ฐ์ปค ๋ฒˆํ˜ธ(6์ž๋ฆฌ)๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”. ์˜ˆ: 005930")
909
- return buf.getvalue()
910
-
911
-
912
- # ============================================================
913
- # Gradio UI
914
- # ============================================================
915
- VCP_DETECT = "VCP ๋ŒํŒŒ ๊ฐ์ง€"
916
- VCP_SELL = "VCP ์‹คํŒจ ๋ถ€๋ถ„๋งค๋„"
917
- BOLL = "๋ณผ๋ฆฐ์ € ์นด์šดํŠธ"
918
- MA240 = "240์„  ์ถ”์„ธ์‚ฌ๋ง ์ปท"
919
- SELL = "์ „๏ฟฝ๏ฟฝ๏ฟฝ๋งค๋„ (์ตœํ›„๋ณด๋ฃจ)"
920
-
921
- CHOICES = [VCP_DETECT, VCP_SELL, BOLL, MA240, SELL]
922
 
923
 
924
- def analyze(choice, ticker, diag_date, market_sel):
925
  ticker = (ticker or "").strip()
926
  if not ticker:
927
- return "ํ‹ฐ์ปค ๋ฒˆํ˜ธ(6์ž๋ฆฌ)๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”.\n์˜ˆ: 005930(์‚ผ์„ฑ์ „์ž), 009150(์‚ผ์„ฑ์ „๊ธฐ)"
928
- diag = (diag_date or "").strip() or None
 
929
 
930
- if choice == VCP_DETECT:
931
- return run_vcp_detect(ticker, diag)
932
- if choice == VCP_SELL:
933
- return run_vcp_sell(ticker)
934
- if choice == BOLL:
935
- return run_bollinger(ticker)
936
- if choice == MA240:
937
- return run_ma240(ticker)
938
- if choice == SELL:
939
- return run_sell(ticker, market_sel)
940
- return "๋ถ„์„ ์ข…๋ฅ˜๋ฅผ ์„ ํƒํ•˜์„ธ์š”."
941
 
 
 
 
942
 
943
- with gr.Blocks(title="์ฃผ์‹ ๋””๋ฒ„๊ฑฐ", theme=gr.themes.Soft()) as demo:
944
- gr.Markdown(
945
- "## ๐Ÿ“ˆ ์ฃผ์‹ ๋””๋ฒ„๊ฑฐ\n"
946
- "๋ถ„์„ ์ข…๋ฅ˜๋ฅผ ๊ณ ๋ฅด๊ณ  ํ‹ฐ์ปค(6์ž๋ฆฌ)๋ฅผ ์ž…๋ ฅํ•œ ๋’ค **๋ถ„์„ ์‹คํ–‰**์„ ๋ˆ„๋ฅด์„ธ์š”.\n"
947
- "ํ‘œ๊ฐ€ ๋„“์œผ๋ฉด ์ขŒ์šฐ๋กœ ์Šคํฌ๋กคํ•˜๊ฑฐ๋‚˜ ๊ฐ€๋กœํ™”๋ฉด์œผ๋กœ ๋ณด์„ธ์š”."
948
- )
949
- with gr.Row():
950
- choice = gr.Dropdown(choices=CHOICES, value=VCP_DETECT, label="๋ถ„์„ ์ข…๋ฅ˜")
951
- ticker = gr.Textbox(label="ํ‹ฐ์ปค (6์ž๋ฆฌ)", placeholder="์˜ˆ: 005930")
952
- diag_date = gr.Textbox(
953
- label="VCP ์ง„๋‹จ ๋‚ ์งœ (์„ ํƒ ยท 'VCP ๋ŒํŒŒ ๊ฐ์ง€'์—์„œ๋งŒ ์‚ฌ์šฉ)",
954
- placeholder="์˜ˆ: 2025-12-16 (๋น„์›Œ๋‘๋ฉด ์ง„๋‹จ ์ƒ๋žต)",
955
- )
956
- market_sel = gr.Dropdown(
957
- choices=["์ž๋™", "KOSPI", "KOSDAQ"], value="์ž๋™",
958
- label="์‹œ์žฅ (์ „๋Ÿ‰๋งค๋„์šฉ ยท ์ž๋™ ํŒ๋ณ„ ์‹คํŒจ ์‹œ ์ง์ ‘ ์„ ํƒ)",
959
- )
960
- btn = gr.Button("๋ถ„์„ ์‹คํ–‰", variant="primary")
961
- out = gr.Code(label="๊ฒฐ๊ณผ", lines=30)
962
 
963
- btn.click(analyze, inputs=[choice, ticker, diag_date, market_sel], outputs=out)
964
- ticker.submit(analyze, inputs=[choice, ticker, diag_date, market_sel], outputs=out)
 
 
 
 
 
 
965
 
966
 
967
  if __name__ == "__main__":
968
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
1
  # -*- coding: utf-8 -*-
2
  """
3
+ ์ฃผ์‹ ๋””๋ฒ„๊ฑฐ (๋ชจ๋ฐ”์ผ ์›น์•ฑ) โ€” 3๊ฐœ ๋ถ„์„ / Hugging Face Spaces ๋ฐฐํฌ์šฉ
4
 
5
+ ํƒญ(๋ถ„์„):
6
+ 1) VCP ๋ŒํŒŒ ๊ฐ์ง€ (vcp_debug.py) โ† ๋‚ ์งœ ์ž๋™: ์ตœ๊ทผ 1๋…„
7
+ 2) ๋ณผ๋ฆฐ์ € ์นด์šดํŠธ (bollinger_count_debug.py) โ† ๋‚ ์งœ ์ž๋™: ์ตœ๊ทผ 1๋…„
8
+ 3) ์ „๋Ÿ‰๋งค๋„ ์ตœํ›„๋ณด๋ฃจ (final_sell_monitor.py) โ† ์ตœ์ข… ํ™•์ • ๋ฒ„์ „
 
9
 
10
+ ์›์น™: ๋ฐ์Šคํฌํƒ‘ ์Šคํฌ๋ฆฝํŠธ์˜ ๋กœ์ง/์ถœ๋ ฅ์„ ๊ทธ๋Œ€๋กœ ์žฌํ˜„.
11
+ - ํ•จ์ˆ˜ํ˜•(analyze_one) ์Šคํฌ๋ฆฝํŠธ: import ํ›„ ํ•จ์ˆ˜ ํ˜ธ์ถœ
12
+ - ์ตœ์ƒ์œ„ ์‹คํ–‰ํ˜•(VCPยท๋ณผ๋ฆฐ์ €): ์†Œ์Šค๋ฅผ ์ฝ์–ด TICKER๋งŒ ๋ฐ”๊ฟ” exec (sys.exit ๊ฐ€๋“œ)
13
+ ๋ชจ๋“  ์ถœ๋ ฅ์€ contextlib.redirect_stdout ๋กœ ๊ทธ๋Œ€๋กœ ์บก์ฒ˜ํ•ด ํ™”๋ฉด์— ํ‘œ์‹œ.
14
+
15
+ HF Spaces(ํ•œ๊ตญ ์™ธ IP)์—์„œ๋„ ๋„ค์ด๋ฒ„ ์ฃผ๊ฐ€/์ง€์ˆ˜ ๋ฐ์ดํ„ฐ๋Š” ์ •์ƒ ๋กœ๋“œ๋จ.
16
+ KRX(StockListing)๋งŒ ๋ง‰ํžˆ๋ฏ€๋กœ ์‹œ์žฅ(KOSPI/KOSDAQ)์€ ์ง์ ‘ ์„ ํƒ.
17
  """
18
 
19
  import io
20
+ import os
21
+ import re
22
  import contextlib
 
23
  from datetime import datetime, timedelta
24
 
 
 
 
25
  import gradio as gr
26
 
27
+ # ํ•จ์ˆ˜ํ˜• ์Šคํฌ๋ฆฝํŠธ๋Š” import ํ•ด์„œ ํ˜ธ์ถœ (์ตœ์ƒ์œ„์—์„œ ๋ถ„์„์ด ์‹คํ–‰๋˜์ง€ ์•Š์Œ)
28
+ import final_sell_monitor
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
+ HERE = os.path.dirname(os.path.abspath(__file__))
31
+ BOLL_PATH = os.path.join(HERE, "bollinger_count_debug.py")
32
+ VCP_PATH = os.path.join(HERE, "vcp_debug.py")
33
 
34
+ _TICKER_RE = re.compile(r"TICKER\s*=\s*['\"][0-9A-Za-z]+['\"]")
 
 
 
 
 
 
35
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
+ def _safe(fn, *args):
38
+ """์–ด๋–ค ๋ถ„์„์ด๋“  ์˜ˆ์™ธ๊ฐ€ ๋‚˜๋„ ๋นจ๊ฐ„ '์˜ค๋ฅ˜' ๋Œ€์‹  ์ด์œ ๋ฅผ ๊ธ€๋กœ ๋ฐ˜ํ™˜."""
39
  try:
40
+ return fn(*args)
 
 
 
41
  except Exception as e:
42
+ msg = str(e).strip().splitlines()[0] if str(e).strip() else type(e).__name__
43
+ return (f"[์˜ค๋ฅ˜] {msg[:200]}\n"
44
+ f"์ข…๋ชฉ์ฝ”๋“œ๊ฐ€ ๋งž๋Š”์ง€, ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด ๋ณด์„ธ์š”.\n"
45
+ f"(๋ฐ์ดํ„ฐ ์ œ๊ณต์ฒ˜(๋„ค์ด๋ฒ„) ์ผ์‹œ ์˜ค๋ฅ˜์ด๊ฑฐ๋‚˜, ์‹ ๊ทœ/์ •์ง€/ํ์ง€ ์ข…๋ชฉ์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.)")
46
 
47
 
48
+ def _exec_script(path, ticker, extra_subs=None):
49
+ """์ตœ์ƒ์œ„ ์‹คํ–‰ํ˜• ์Šคํฌ๋ฆฝํŠธ: TICKER(๋ฐ ์ถ”๊ฐ€ ์น˜ํ™˜)๋งŒ ๋ฐ”๊ฟ” exec, ์ถœ๋ ฅ ์บก์ฒ˜."""
 
 
 
 
 
50
  buf = io.StringIO()
51
+ ns = {"__name__": "__sandbox__"} # __main__ ๊ฐ€๋“œ ๋น„ํ™œ์„ฑ
52
+ with contextlib.redirect_stdout(buf):
53
+ try:
54
+ with open(path, encoding="utf-8") as f:
55
+ src = f.read()
56
+ src = _TICKER_RE.sub(f"TICKER = '{ticker}'", src, count=1)
57
+ if extra_subs:
58
+ for pat, repl in extra_subs:
59
+ src = re.sub(pat, repl, src, count=1)
60
+ exec(compile(src, path, "exec"), ns)
61
+ except SystemExit:
62
+ pass # ์Šคํฌ๋ฆฝํŠธ์˜ sys.exit() ๊ฐ€ ์•ฑ์„ ์ฃฝ์ด์ง€ ์•Š๊ฒŒ
63
+ except Exception as e:
64
+ msg = str(e).strip().splitlines()[0] if str(e).strip() else type(e).__name__
65
+ print(f"\n[์˜ค๋ฅ˜] {msg[:200]}")
66
+ return buf.getvalue() or "์ถœ๋ ฅ ์—†์Œ"
67
+
68
+
69
+ def _call_func(fn, *args):
70
+ """ํ•จ์ˆ˜ํ˜• ์Šคํฌ๋ฆฝํŠธ: ํ•จ์ˆ˜ ํ˜ธ์ถœํ•˜๋ฉฐ ์ถœ๋ ฅ ์บก์ฒ˜."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  buf = io.StringIO()
72
+ with contextlib.redirect_stdout(buf):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  try:
74
+ fn(*args)
75
+ except SystemExit:
 
 
 
 
 
76
  pass
77
+ except Exception as e:
78
+ msg = str(e).strip().splitlines()[0] if str(e).strip() else type(e).__name__
79
+ print(f"\n[์˜ค๋ฅ˜] {msg[:200]}")
80
+ print("์ข…๋ชฉ์ฝ”๋“œ์™€ ์‹œ์žฅ(KOSDAQ/KOSPI)์ด ๋งž๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”.")
81
+ return buf.getvalue() or "์ถœ๋ ฅ ์—†์Œ"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
 
83
 
84
+ # ---------- ๋ถ„์„๋ณ„ ์‹คํ–‰ ----------
85
+ def run_bollinger(ticker):
86
+ ticker = (ticker or "").strip()
87
+ if not ticker:
88
+ return "์ข…๋ชฉ์ฝ”๋“œ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š” (์˜ˆ: 420770)"
89
+ today = datetime.today()
90
+ frm = (today - timedelta(days=365)).strftime("%Y-%m-%d")
91
+ to = today.strftime("%Y-%m-%d")
92
+ subs = [
93
+ (r"DEBUG_FROM\s*=\s*['\"][0-9-]+['\"]", f"DEBUG_FROM = '{frm}'"),
94
+ (r"DEBUG_TO\s*=\s*['\"][0-9-]+['\"]", f"DEBUG_TO = '{to}'"),
95
+ ]
96
+ return _exec_script(BOLL_PATH, ticker, subs)
97
+
98
+
99
+ def run_vcp(ticker):
100
+ ticker = (ticker or "").strip()
101
+ if not ticker:
102
+ return "์ข…๋ชฉ์ฝ”๋“œ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š” (์˜ˆ: 007340)"
103
+ return _exec_script(VCP_PATH, ticker)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
 
105
 
106
+ def run_final(ticker, market, name):
107
  ticker = (ticker or "").strip()
108
  if not ticker:
109
+ return "์ข…๋ชฉ์ฝ”๋“œ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š” (์˜ˆ: 195940)"
110
+ h = {"ticker": ticker, "market": market, "name": (name or "").strip() or None}
111
+ return _call_func(final_sell_monitor.analyze_one, h)
112
 
 
 
 
 
 
 
 
 
 
 
 
113
 
114
+ # ---------- UI ----------
115
+ with gr.Blocks(title="์ฃผ์‹ ๋””๋ฒ„๊ฑฐ") as demo:
116
+ gr.Markdown("## ์ฃผ์‹ ๋””๋ฒ„๊ฑฐ (๋ชจ๋ฐ”์ผ)\n๋ถ„์„์„ ๊ณ ๋ฅด๊ณ  ์ข…๋ชฉ์ฝ”๋“œ๋งŒ ๋„ฃ์œผ๋ฉด ๋ฐ์Šคํฌํƒ‘๊ณผ ๋™์ผํ•œ ์ถœ๋ ฅ์ด ๋‚˜์˜ต๋‹ˆ๋‹ค.")
117
 
118
+ with gr.Tab("๋ณผ๋ฆฐ์ € ์นด์šดํŠธ"):
119
+ b_tk = gr.Textbox(label="์ข…๋ชฉ์ฝ”๋“œ", placeholder="์˜ˆ: 420770")
120
+ b_btn = gr.Button("์‹คํ–‰", variant="primary")
121
+ b_out = gr.Textbox(label="๊ฒฐ๊ณผ (์ตœ๊ทผ 1๋…„ ์ž๋™)", lines=20)
122
+ b_btn.click(lambda tk: _safe(run_bollinger, tk), inputs=b_tk, outputs=b_out)
123
+
124
+ with gr.Tab("VCP ๋ŒํŒŒ"):
125
+ v_tk = gr.Textbox(label="์ข…๋ชฉ์ฝ”๋“œ", placeholder="์˜ˆ: 007340")
126
+ v_btn = gr.Button("์‹คํ–‰", variant="primary")
127
+ v_out = gr.Textbox(label="๊ฒฐ๊ณผ (์ตœ๊ทผ 1๋…„ ์ž๋™)", lines=20)
128
+ v_btn.click(lambda tk: _safe(run_vcp, tk), inputs=v_tk, outputs=v_out)
 
 
 
 
 
 
 
 
129
 
130
+ with gr.Tab("์ „๋Ÿ‰๋งค๋„ ์ตœํ›„๋ณด๋ฃจ"):
131
+ with gr.Row():
132
+ f_tk = gr.Textbox(label="์ข…๋ชฉ์ฝ”๋“œ", placeholder="์˜ˆ: 195940", scale=2)
133
+ f_mkt = gr.Radio(["KOSDAQ", "KOSPI"], value="KOSDAQ", label="์‹œ์žฅ", scale=2)
134
+ f_nm = gr.Textbox(label="์ข…๋ชฉ๋ช…(์„ ํƒ)", placeholder="์˜ˆ: HK์ด๋…ธ์—”")
135
+ f_btn = gr.Button("์‹คํ–‰", variant="primary")
136
+ f_out = gr.Textbox(label="๊ฒฐ๊ณผ", lines=16)
137
+ f_btn.click(lambda a,b,c: _safe(run_final, a, b, c), inputs=[f_tk, f_mkt, f_nm], outputs=f_out)
138
 
139
 
140
  if __name__ == "__main__":
141
+ # ---- ๋กœ๊ทธ์ธ ๋ฒฝ: ์•„์ด๋””/๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์•„๋Š” ์‚ฌ๋žŒ๋งŒ ----
142
+ # ์•„๋ž˜ ๋‘ ๊ฐ’์„ ๋ณธ์ธ์ด ์›ํ•˜๋Š” ๊ฑธ๋กœ ๋ฐ”๊ฟ”์„œ ์“ฐ์„ธ์š”. (๊ณต์œ ํ•  ์‚ฌ๋žŒ์—๊ฒŒ๋งŒ ์•Œ๋ ค์คŒ)
143
+ APP_USER = "johnflower"
144
+ APP_PASS = "johnflower"
145
+
146
+ # HF Spaces ํ‘œ์ค€ ์‹คํ–‰ (ํฌํŠธ 7860 ๊ณ ์ •) + ๋กœ๊ทธ์ธ
147
+ demo.launch(
148
+ server_name="0.0.0.0",
149
+ server_port=7860,
150
+ auth=(APP_USER, APP_PASS),
151
+ auth_message="๊ณต์œ ๋ฐ›์€ ์•„์ด๋””/๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”.",
152
+ )
bollinger_count_debug.py ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ์‹ค๋ฆฌ์ฝ˜ํˆฌ ๋ณผ๋ฆฐ์ €๋ฐด๋“œ ์นด์šดํŒ… ๋””๋ฒ„๊น… v5
3
+
4
+ ํ™•์ • ๊ทœ์น™:
5
+ 1. ์‹œ์ดˆ๊ฐ€ > 240์ผ์„  โ† ์‹ ๊ทœ ์ถ”๊ฐ€
6
+ 2. ์ข…๊ฐ€ > BB ์ƒ๋‹จ ร— 1.03
7
+ 3. 60์ผ์„  ์•„๋ž˜ ๋‹ค๋…€์˜จ ํ›„: ์‹œ์ดˆ๊ฐ€ > ์ด์ „ ํ”ผํฌ ์ข…๊ฐ€ (์ถ”๊ฐ€ ์กฐ๊ฑด)
8
+ 4. 240์ผ์„  ์•„๋ž˜ โ†’ ์™„์ „ ๋ฆฌ์…‹
9
+ 5. 60์ผ์„  ์•„๋ž˜ โ†’ ์นด์šดํŠธ ์œ ์ง€
10
+ """
11
+
12
+ import pandas as pd
13
+ import numpy as np
14
+ import FinanceDataReader as fdr
15
+ import warnings
16
+ warnings.filterwarnings('ignore')
17
+
18
+ TICKER = '000660'
19
+ DEBUG_FROM = '2025-05-02'
20
+ DEBUG_TO = '2026-06-19'
21
+ BB_WINDOW = 20
22
+ BB_STD = 2
23
+ MA_240 = 240
24
+ MA_60 = 60
25
+ BB_THRESHOLD = 1.030
26
+
27
+ print(f"({TICKER}) ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์ค‘...")
28
+ df = fdr.DataReader(TICKER, '2022-01-01', DEBUG_TO)
29
+ print(f"๋ฐ์ดํ„ฐ: {len(df)}์ผ์น˜\n")
30
+
31
+ df['ma20'] = df['Close'].rolling(BB_WINDOW).mean()
32
+ df['std20'] = df['Close'].rolling(BB_WINDOW).std()
33
+ df['bb_upper'] = df['ma20'] + BB_STD * df['std20']
34
+ df['bb_lower'] = df['ma20'] - BB_STD * df['std20']
35
+ df['ma60'] = df['Close'].rolling(MA_60).mean()
36
+ df['ma240'] = df['Close'].rolling(MA_240).mean()
37
+
38
+ count = 0
39
+ peak_close = 0
40
+ was_below_60 = False
41
+ counts = []
42
+ peak_list = []
43
+ flags = []
44
+
45
+ for i in range(len(df)):
46
+ c = df['Close'].iloc[i]
47
+ o = df['Open'].iloc[i]
48
+ u = df['bb_upper'].iloc[i]
49
+ m60 = df['ma60'].iloc[i]
50
+ m240 = df['ma240'].iloc[i]
51
+
52
+ if pd.isna(m240) or pd.isna(u) or pd.isna(m60):
53
+ counts.append(count)
54
+ peak_list.append(peak_close)
55
+ flags.append('๋ฐ์ดํ„ฐ๋ถ€์กฑ')
56
+ continue
57
+
58
+ if c < m240:
59
+ count = 0
60
+ peak_close = 0
61
+ was_below_60 = False
62
+ flag = '240์„ ์ดํƒˆโ†’๋ฆฌ์…‹'
63
+
64
+ elif c < m60:
65
+ was_below_60 = True
66
+ flag = f'60์„ ์ดํƒˆโ†’์œ ์ง€(count={count})'
67
+
68
+ else:
69
+ # ํ•ต์‹ฌ ์กฐ๊ฑด๋“ค
70
+ open_above_240 = (o > m240) # โ† ์‹ ๊ทœ: ์‹œ์ดˆ๊ฐ€ > 240์ผ์„ 
71
+ clearly_above = (c > u * BB_THRESHOLD) # ์ข…๊ฐ€ > BB์ƒ๋‹จ ร— 1.020
72
+
73
+ if not open_above_240:
74
+ flag = f'์‹œ์ดˆ๊ฐ€({o:,.0f})<240์„ ({m240:,.0f})โ†’๋ฌดํšจ'
75
+ elif clearly_above:
76
+ if was_below_60:
77
+ if o > peak_close:
78
+ count += 1
79
+ peak_close = c
80
+ was_below_60 = False
81
+ flag = f'+{count} (60์„ ๋ณต๊ท€,์‹œ์ดˆ๊ฐ€>{peak_close:,.0f})'
82
+ else:
83
+ flag = f'BB๋ŒํŒŒbut์‹œ์ดˆ๊ฐ€({o:,.0f})โ‰คํ”ผํฌ({peak_close:,.0f})โ†’์Šคํ‚ต'
84
+ else:
85
+ count += 1
86
+ peak_close = max(peak_close, c)
87
+ flag = f'+{count}'
88
+ else:
89
+ pct = (c / u - 1) * 100
90
+ flag = f'์ข…๊ฐ€๋ฏธ๋‹ฌ(BB๋Œ€๋น„{pct:+.1f}%)'
91
+
92
+ counts.append(count)
93
+ peak_list.append(peak_close)
94
+ flags.append(flag)
95
+
96
+ df['bb_count'] = counts
97
+ df['peak_close'] = peak_list
98
+ df['flag'] = flags
99
+
100
+ mask = (df.index >= DEBUG_FROM) & (df.index <= DEBUG_TO)
101
+ debug_df = df[mask].copy()
102
+
103
+ print("=" * 115)
104
+ print(f"๋‚ ์งœ๋ณ„ ์นด์šดํŒ… ({DEBUG_FROM} ~ {DEBUG_TO})")
105
+ print("=" * 115)
106
+ print(f"{'๋‚ ์งœ':<12} {'์ข…๊ฐ€':>7} {'์‹œ๊ฐ€':>7} {'BB์ƒ๋‹จ':>8} {'BBร—1.03':>9} "
107
+ f"{'60MA':>7} {'240MA':>7} {'ํ”ผํฌ':>8} {'์นด์šดํŠธ':>6} ํŒ์ •")
108
+ print("-" * 115)
109
+
110
+ prev_count = 0
111
+ for date, row in debug_df.iterrows():
112
+ cnt = int(row['bb_count'])
113
+ flag = row['flag']
114
+ is_notable = (cnt != prev_count or
115
+ '๋ฆฌ์…‹' in flag or '์ดํƒˆ' in flag or
116
+ '๋ณต๊ท€' in flag or '์Šคํ‚ต' in flag or
117
+ '๋ฌดํšจ' in flag)
118
+ if is_notable:
119
+ mark = ' โ—€' if cnt > prev_count else ''
120
+ print(f"{str(date)[:10]:<12} "
121
+ f"{row['Close']:>7,.0f} "
122
+ f"{row['Open']:>7,.0f} "
123
+ f"{row['bb_upper']:>8,.0f} "
124
+ f"{row['bb_upper']*BB_THRESHOLD:>9,.0f} "
125
+ f"{row['ma60']:>7,.0f} "
126
+ f"{row['ma240']:>7,.0f} "
127
+ f"{row['peak_close']:>8,.0f} "
128
+ f"{cnt:>6} {flag}{mark}")
129
+ prev_count = cnt
130
+
131
+ print("\n" + "=" * 60)
132
+ print("์นด์šดํŠธ ์ฆ๊ฐ€ํ•œ ๋‚ ๋“ค ์š”์•ฝ")
133
+ print("=" * 60)
134
+ prev = 0
135
+ for date, row in debug_df.iterrows():
136
+ cnt = int(row['bb_count'])
137
+ if cnt > prev:
138
+ print(f" {str(date)[:10]}: {row['flag']}"
139
+ f" (์ข…๊ฐ€:{row['Close']:,.0f}, ์‹œ๊ฐ€:{row['Open']:,.0f}, "
140
+ f"BBร—1.03:{row['bb_upper']*BB_THRESHOLD:,.0f}, "
141
+ f"240MA:{row['ma240']:,.0f})")
142
+ prev = cnt
143
+
144
+ print(f"\n์ตœ๋Œ€ ์นด์šดํŠธ: {debug_df['bb_count'].max()}")
145
+
final_sell_monitor.py ADDED
@@ -0,0 +1,206 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ ์ „๋Ÿ‰๋งค๋„ '์ตœํ›„ ๋ณด๋ฃจ' ๋ชจ๋‹ˆํ„ฐ - ๋ณด์œ  ์ข…๋ชฉ ์ผ๊ด„ ์ ๊ฒ€ (๋งค์ผ ๋Œ๋ฆฌ๋Š” ์šฉ) [์ตœ์†Œ ๋ฒ„์ „]
4
+
5
+ [ ๊ทœ์น™ ]
6
+ ์ฃผ ์‹ ํ˜ธ : ์ข…๋ชฉ ์ž๊ธฐ๊ณ ์  ๊ธฐ์ค€ ์ด๊ฒฉ -10% '1์ฐจ ์ด๋ฒคํŠธ' ํ›„,
7
+ 22๊ฑฐ๋ž˜์ผ ๋‚ด์— '2์ฐจ ์ด๋ฒคํŠธ'(1์ฐจ๋ณด๋‹ค ๋‚ฎ์€ ์ข…๊ฐ€)๊ฐ€ ๋‚˜์˜ค๋ฉด -> ์ „๋Ÿ‰๋งค๋„.
8
+ * 1ยท2์ฐจ๋Š” 'ํ˜„์žฌ ๊ณ ์ ' ์ดํ›„๋กœ๋งŒ ์นด์šดํŠธ(์‹ ๊ณ ๊ฐ€ ๋‚˜๋ฉด ๋ฆฌ์…‹).
9
+ * 2์ฐจ๊ฐ€ 1์ฐจ๋ณด๋‹ค ์•ˆ ๋‚ฎ๊ฑฐ๋‚˜ 22์ผ ๋ฐ–์ด๋ฉด -> ์ด ๊ณ ์ ์—์„  ์‹ ํ˜ธ ์—†์Œ.
10
+ ๋ฐฑ์—… : ์ฃผ ์‹ ํ˜ธ๊ฐ€ ์•ˆ ๋–ด์–ด๋„ ์ข…๊ฐ€๊ฐ€ 240์„ ์„ ์—ฐ์† 2์ผ ์ดํƒˆํ•˜๋ฉด -> ์ „๋Ÿ‰๋งค๋„.
11
+
12
+ [ ์“ฐ๋Š” ๋ฒ• ]
13
+ HOLDINGS ์— ๋ณด์œ  ์ข…๋ชฉ(ticker/market/name)์„ ๋„ฃ๊ณ  ๋งค์ผ ์‹คํ–‰.
14
+ """
15
+
16
+ import sys, ssl
17
+ import pandas as pd
18
+ import numpy as np
19
+ import FinanceDataReader as fdr
20
+ from datetime import datetime, timedelta
21
+
22
+ ssl._create_default_https_context = ssl._create_unverified_context
23
+ if sys.platform == 'win32':
24
+ sys.stdout.reconfigure(encoding='utf-8')
25
+
26
+ # =============================================
27
+ # [๋ณด์œ  ์ข…๋ชฉ โ€” ์—ฌ๊ธฐ๋งŒ ๋ฐ”๊พธ๋ฉด ๋จ]
28
+ # =============================================
29
+ HOLDINGS = [
30
+ {'ticker': '195940', 'market': 'KOSDAQ', 'name': 'HK์ด๋…ธ์—”'},
31
+ {'ticker': '005250', 'market': 'KOSPI', 'name': '๋…น์‹ญ์žํ™€๋”ฉ์Šค'},
32
+ ]
33
+
34
+ # =============================================
35
+ # ์„ค์ •
36
+ # =============================================
37
+ DIV_THRESHOLD = -0.10 # ์ด๊ฒฉ ๊ธฐ์ค€
38
+ CONFIRM_WINDOW = 22 # 2์ฐจ ์žฌํ™•์ธ ์œˆ๋„์šฐ (๊ฑฐ๋ž˜์ผ)
39
+ BREAK_PCT = 0.00 # ๋ฐฑ์—… 240์„  ์ดํƒˆ ๊ธฐ์ค€ (0.00=์ข…๊ฐ€<240์„ )
40
+ BREAK_CONFIRM = 2 # ๋ฐฑ์—…: ์—ฐ์† N์ผ ์ดํƒˆ
41
+ DATA_BACK_DAYS = 900 # 240MA ์›Œ๋ฐ์—… ํฌํ•จ ๋กœ๋“œ
42
+
43
+
44
+ def load(ticker, market):
45
+ end = datetime.today().strftime('%Y-%m-%d')
46
+ start = (datetime.today() - timedelta(days=DATA_BACK_DAYS)).strftime('%Y-%m-%d')
47
+ try:
48
+ sdf = fdr.DataReader(ticker, start, end)
49
+ except Exception:
50
+ return None
51
+ if not isinstance(sdf, pd.DataFrame) or 'Close' not in sdf.columns or len(sdf) == 0:
52
+ return None
53
+ idx_code = 'KS11' if market == 'KOSPI' else 'KQ11'
54
+ idf = None
55
+ for c in (idx_code, '^' + idx_code):
56
+ try:
57
+ t = fdr.DataReader(c, start, end)
58
+ if isinstance(t, pd.DataFrame) and 'Close' in t.columns and len(t) > 0:
59
+ idf = t; break
60
+ except Exception:
61
+ continue
62
+ if idf is None:
63
+ return None
64
+ merged = pd.concat([sdf['Close'].rename('stock'), idf['Close'].rename('index')],
65
+ axis=1, sort=True)
66
+ merged['index'] = merged['index'].ffill()
67
+ merged = merged.dropna()
68
+ if len(merged) == 0:
69
+ return None
70
+ merged['ma240'] = merged['stock'].rolling(240).mean()
71
+ return merged
72
+
73
+
74
+ def resolve_name(ticker, market, given):
75
+ if given:
76
+ return given
77
+ try:
78
+ lst = fdr.StockListing(market)
79
+ for cc in ('Code', 'Symbol', 'code'):
80
+ if cc in lst.columns and ticker in lst[cc].values:
81
+ for nc in ('Name', 'name'):
82
+ if nc in lst.columns:
83
+ return str(lst[lst[cc] == ticker][nc].values[0])
84
+ except Exception:
85
+ pass
86
+ return ticker
87
+
88
+
89
+ def current_peak_events(stock, index, dates):
90
+ """ํ˜„์žฌ ๊ณ ์ (=์‹ ๊ณ ๊ฐ€ ์ดํ›„) ๊ตฌ๊ฐ„์˜ ์ด๊ฒฉ ์ด๋ฒคํŠธ๋ฅผ 1์ฐจ,2์ฐจ... ์ˆœ์„œ๋กœ ๋ฐ˜ํ™˜.
91
+ ์ด๋ฒคํŠธ = flag ON(์ด๊ฒฉ<=-10%) ํ›„ '๋” ๊นŠ์–ด์ง„ ์ฒซ ๋‚ '(์—ํ”ผ์†Œ๋“œ๋‹น 1๊ฐœ)."""
92
+ n = len(stock)
93
+ running_peak = -1.0; index_at_peak = None; peak_date = None; peak_i = 0
94
+ div_flag = False; div_flag_value = None; div_sold = False
95
+ events = []
96
+ for t in range(n):
97
+ st = float(stock[t]); it = float(index[t])
98
+ if running_peak < 0 or st >= running_peak:
99
+ running_peak = st; index_at_peak = it; peak_date = dates[t]; peak_i = t
100
+ div_flag = False; div_flag_value = None; div_sold = False
101
+ events = [] # ์‹ ๊ณ ๊ฐ€ -> ๋ฆฌ์…‹
102
+ else:
103
+ sr = st / running_peak - 1
104
+ ir = (it / index_at_peak - 1) if index_at_peak else 0.0
105
+ div = sr - ir
106
+ if div <= DIV_THRESHOLD:
107
+ if not div_flag:
108
+ div_flag = True; div_flag_value = div; div_sold = False
109
+ elif div < div_flag_value:
110
+ if not div_sold:
111
+ events.append({'i': t, 'date': dates[t], 'price': st, 'div': div})
112
+ div_sold = True
113
+ div_flag_value = div
114
+ else:
115
+ div_flag_value = div
116
+ else:
117
+ div_flag = False; div_flag_value = None; div_sold = False
118
+ return running_peak, peak_date, peak_i, index_at_peak, events
119
+
120
+
121
+ def confirm(events):
122
+ """์ฃผ ์‹ ํ˜ธ: 1์ฐจ ๋’ค '2์ฐจ'๊ฐ€ 1์ฐจ๋กœ๋ถ€ํ„ฐ 22์ผ ๋‚ด + 1์ฐจ๋ณด๋‹ค ๋‚ฎ์œผ๋ฉด ํ™•์ •."""
123
+ if len(events) < 2:
124
+ return None
125
+ e1, e2 = events[0], events[1]
126
+ if (e2['i'] - e1['i'] <= CONFIRM_WINDOW) and (e2['price'] < e1['price']):
127
+ return {'date': e2['date'], 'price': e2['price'], 'i': e2['i'],
128
+ 'first_date': e1['date'], 'first_price': e1['price']}
129
+ return None
130
+
131
+
132
+ def backup_240(stock, ma240, dates):
133
+ """ํ˜„์žฌ๊นŒ์ง€ ์—ฐ์† BREAK_CONFIRM์ผ 240์„  ์ดํƒˆ ์ค‘์ด๋ฉด ๊ทธ ์‹œ์ž‘์ผ ๋ฐ˜ํ™˜."""
134
+ n = len(stock); streak = 0; start = None
135
+ for t in range(n):
136
+ if np.isnan(ma240[t]):
137
+ continue
138
+ if stock[t] < ma240[t] * (1 - BREAK_PCT):
139
+ streak += 1
140
+ if streak >= BREAK_CONFIRM and start is None:
141
+ start = dates[t]
142
+ else:
143
+ streak = 0; start = None
144
+ return start
145
+
146
+
147
+ def analyze_one(h):
148
+ tk = h['ticker']; market = h.get('market', 'KOSDAQ')
149
+ name = resolve_name(tk, market, h.get('name'))
150
+ df = load(tk, market)
151
+ if df is None:
152
+ print(f"\n[{tk} {name}] ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.")
153
+ print(f" - ์ข…๋ชฉ์ฝ”๋“œ({tk})๊ฐ€ ๋งž๋Š”์ง€, ์‹œ์žฅ(KOSDAQ/KOSPI)์ด ๋งž๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”.")
154
+ print(f" - ์‹ ๊ทœ์ƒ์žฅ/๊ฑฐ๋ž˜์ •์ง€/์ƒ์žฅํ์ง€ ์ข…๋ชฉ์€ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.")
155
+ return
156
+
157
+ dates = [str(d)[:10] for d in df.index]
158
+ stock = df['stock'].values.astype(float)
159
+ index = df['index'].values.astype(float)
160
+ ma240 = df['ma240'].values.astype(float)
161
+ n = len(stock); t = n - 1
162
+
163
+ peak, peak_date, peak_i, iap, events = current_peak_events(stock, index, dates)
164
+ confirmed = confirm(events)
165
+ break_day = backup_240(stock, ma240, dates)
166
+
167
+ m = ma240[t]
168
+ div_now = (stock[t] / peak - 1) - ((index[t] / iap - 1) if iap else 0.0)
169
+
170
+ print("\n" + "=" * 64)
171
+ print(f"[{tk} {name}] {market} | {dates[t]} ์ข…๊ฐ€ {stock[t]:,.0f}"
172
+ + (f" | 240์„  {m:,.0f} ({(stock[t]/m-1)*100:+.1f}%)" if not np.isnan(m) else ""))
173
+ print(f" ๊ณ ์  {peak:,.0f}({peak_date}) | ์ด๊ฒฉ {div_now*100:+.1f}% | ์ด๋ฒคํŠธ {len(events)}๊ฐœ")
174
+ for e in events:
175
+ if confirmed and e['date'] == confirmed['date']:
176
+ mk = ' <<< ์ „๋Ÿ‰๋งค๋„(2์ฐจ<1์ฐจ)'
177
+ elif e is events[0]:
178
+ mk = ' (1์ฐจ)'
179
+ else:
180
+ mk = ' (์ฐธ๊ณ )'
181
+ print(f" {e['date']} @ {e['price']:,.0f} (์ด๊ฒฉ {e['div']*100:+.1f}%){mk}")
182
+
183
+ if confirmed:
184
+ ago = t - confirmed['i']
185
+ print(f" >>> [์ฃผ ์‹ ํ˜ธ] ์ „๋Ÿ‰๋งค๋„: {confirmed['date']} @ {confirmed['price']:,.0f} "
186
+ f"(1์ฐจ {confirmed['first_date']} @ {confirmed['first_price']:,.0f} ๋Œ€๋น„ ๋‚ฎ์Œ) | {ago}์ผ ์ „")
187
+ if break_day:
188
+ print(f" >>> [๋ฐฑ์—…] 240์„  ์ดํƒˆ: {break_day} (์—ฐ์† {BREAK_CONFIRM}์ผ, ํ˜„์žฌ๋„ ์ดํƒˆ ์ค‘)")
189
+ if not confirmed and not break_day:
190
+ print(f" ==> ๋งค๋„ ์‹ ํ˜ธ ์—†์Œ. ๋ณด์œ  ์œ ์ง€.")
191
+
192
+
193
+ def main():
194
+ print("=" * 64)
195
+ print(f"์ „๋Ÿ‰๋งค๋„ ์ตœํ›„๋ณด๋ฃจ ๋ชจ๋‹ˆํ„ฐ | {datetime.today().strftime('%Y-%m-%d %H:%M')}")
196
+ print(f"์ฃผ์‹ ํ˜ธ: ์ด๊ฒฉ {DIV_THRESHOLD*100:.0f}% 1์ฐจ->2์ฐจ(22์ผ๋‚ด,๋”๋‚ฎ์Œ) | ๋ฐฑ์—…: 240์„  {BREAK_CONFIRM}์ผ ์ดํƒˆ")
197
+ print("=" * 64)
198
+ for h in HOLDINGS:
199
+ try:
200
+ analyze_one(h)
201
+ except Exception as e:
202
+ print(f"\n[{h.get('ticker')}] ์˜ค๋ฅ˜: {e}")
203
+
204
+
205
+ if __name__ == '__main__':
206
+ main()
requirements.txt CHANGED
@@ -1,5 +1,4 @@
1
- finance-datareader
2
  pandas
3
  numpy
4
- lxml
5
- requests
 
1
+ gradio
2
  pandas
3
  numpy
4
+ finance-datareader
 
vcp_debug.py ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ VCP ๋ŒํŒŒ ๊ฐ์ง€ - ๋„ค ์ •์˜ ๋ฒ„์ „ ('์กฐ์šฉํ•œ N์ผ -> ๊ฑฐ๋ž˜๋Ÿ‰+๊ฐ€๊ฒฉ ํญ๋ฐœ')
4
+ TICKER ํ•˜๋‚˜๋งŒ ๋ฐ”๊พธ๋ฉด ๋จ!
5
+
6
+ [ ๋ŒํŒŒ ์กฐ๊ฑด (๋ชจ๋‘ ์ถฉ์กฑ) ]
7
+ 1. ํญ๋ฐœ์ผ ๊ฑฐ๋ž˜๋Ÿ‰ >= ์ง์ „ CONTRACT_DAYS ์ผ ๊ฑฐ๋ž˜๋Ÿ‰ ์ค‘ ์ตœ๋Œ€ ร— VOL_DRYUP_SURGE
8
+ (= ์กฐ์šฉํ–ˆ๋‹ค๊ฐ€ ๊ฑฐ๋ž˜๋Ÿ‰์ด ํ™• ํ„ฐ์ง. '๊ฑฐ๋ž˜๋Ÿ‰ 20์ผ์„ '์ด ์•„๋‹ˆ๋ผ '์ง์ „ ์กฐ์šฉํ•œ ๋‚  ๋Œ€๋น„'๋ผ์„œ
9
+ ํ‰์†Œ ๊ฑฐ๋ž˜๋Ÿ‰์ด ์ ์€ ์ข…๋ชฉ/๊ตฌ๊ฐ„์—์„œ๋„ ์žกํžˆ๊ณ , ํญ๋ฐœ ํ›„ ํ‰๊ท ์ด ํŠ€๋Š” ๋ฌธ์ œ๋„ ์—†์Œ.)
10
+ 2. ํญ๋ฐœ์ผ ์ข…๊ฐ€ > ์ง์ „ BASE_LOOKBACK์ผ ๊ณ ๊ฐ€ (๊ฐ€๊ฒฉ ๋ŒํŒŒ)
11
+ 3. ํญ๋ฐœ์ผ ์ƒ์Šน๋ฅ (์ „์ผ ์ข…๊ฐ€ ๋Œ€๋น„) >= MIN_PRICE_GAIN
12
+ 4. ํญ๋ฐœ์ผ ์–‘๋ด‰ (์ข…๊ฐ€ > ์‹œ๊ฐ€)
13
+
14
+ โ˜… ์ด ํŒŒ์ผ์˜ '๊ฐ์ง€ ์„ค์ •' ๋ธ”๋ก์€ vcp_sell_debug.py ์™€ ๋˜‘๊ฐ™์ด ์œ ์ง€ํ•  ๊ฒƒ!
15
+ """
16
+
17
+ import sys, ssl
18
+ import pandas as pd
19
+ import numpy as np
20
+ import FinanceDataReader as fdr
21
+ from datetime import datetime, timedelta
22
+
23
+ ssl._create_default_https_context = ssl._create_unverified_context
24
+ if sys.platform == 'win32':
25
+ sys.stdout.reconfigure(encoding='utf-8')
26
+
27
+ # =============================================
28
+ # [์—ฌ๊ธฐ๋งŒ ๋ฐ”๊พธ๋ฉด ๋จ]
29
+ # =============================================
30
+ TICKER = '007340'
31
+
32
+ # =============================================
33
+ # ๊ฐ์ง€ ์„ค์ • (โ˜… vcp_sell_debug.py ์™€ ๋˜‘๊ฐ™์ด!)
34
+ # =============================================
35
+ CONTRACT_DAYS = 2 # ํญ๋ฐœ ์ง์ „ '์กฐ์šฉํ•œ ๋‚ ' ์ˆ˜
36
+ # VOL_DRYUP_SURGE = 6.0 # ํญ๋ฐœ์ผ ๊ฑฐ๋ž˜๋Ÿ‰ รท ์ง์ „ ์กฐ์šฉํ•œ ๋‚  ํ‰๊ท  (์กฐ์šฉํ–ˆ๋‹ค๊ฐ€ ๋ช‡ ๋ฐฐ๋กœ ํ„ฐ์กŒ๋‚˜)
37
+ VOL_DRYUP_SURGE = 5.0 # ํญ๋ฐœ์ผ ๊ฑฐ๋ž˜๋Ÿ‰ รท ์ง์ „ ์กฐ์šฉํ•œ ๋‚  ํ‰๊ท  (์กฐ์šฉํ–ˆ๋‹ค๊ฐ€ ๋ช‡ ๋ฐฐ๋กœ ํ„ฐ์กŒ๋‚˜)
38
+ MIN_PRICE_GAIN = 0.13 # ํญ๋ฐœ์ผ ์ƒ์Šน๋ฅ  (์ „์ผ ์ข…๊ฐ€ ๋Œ€๋น„)
39
+ BASE_LOOKBACK = 10 # ํญ๋ฐœ์ผ ์ข…๊ฐ€๊ฐ€ '์ง์ „ N์ผ ๊ณ ๊ฐ€'๋ฅผ ๋„˜์–ด์•ผ (๊ฐ€๊ฒฉ ๋ŒํŒŒ ๊ธฐ์ค€)
40
+
41
+ # DIAGNOSE_DATE = '2025-12-16' # ์˜ˆ: '2025-07-08' -> ๊ทธ ๋‚ ์ด ์™œ ์žกํ˜”๋‚˜/์•ˆ ์žกํ˜”๋‚˜ ์ง์ ‘ ํŒ์ •
42
+ DIAGNOSE_DATE = None
43
+
44
+ # =============================================
45
+ # ๋ถ„์„ ๊ธฐ๊ฐ„ (๋” ๊ณผ๊ฑฐ๊นŒ์ง€ ๋ณด๋ ค๋ฉด ANALYZE_DAYS๋งŒ)
46
+ # =============================================
47
+ ANALYZE_DAYS = 365 # ๋ฉฐ์น  ์ „๊นŒ์ง€์˜ ๋ŒํŒŒ๋ฅผ ๋ณผ์ง€ (730=2๋…„, 1095=3๋…„)
48
+ WARMUP_DAYS = 40 # ๊ณ ๊ฐ€ lookback ์›Œ๋ฐ์—…์šฉ ์ถ”๊ฐ€ ๋กœ๋“œ (๋ณดํ†ต ๊ทธ๋Œ€๋กœ)
49
+ DATA_START = (datetime.today() - timedelta(days=ANALYZE_DAYS + WARMUP_DAYS)).strftime('%Y-%m-%d')
50
+ ANALYZE_FROM = (datetime.today() - timedelta(days=ANALYZE_DAYS)).strftime('%Y-%m-%d')
51
+ ANALYZE_TO = datetime.today().strftime('%Y-%m-%d')
52
+
53
+
54
+ # =============================================
55
+ # ๋ŒํŒŒ ๊ฐ์ง€ (โ˜… vcp_sell_debug.py ์™€ ๋™์ผ ๋กœ์ง)
56
+ # =============================================
57
+ def find_vcp_breakouts(df):
58
+ O = df['Open'].values.astype(float); H = df['High'].values.astype(float)
59
+ L = df['Low'].values.astype(float); C = df['Close'].values.astype(float)
60
+ V = df['Volume'].values.astype(float)
61
+ out = []
62
+ for t in range(BASE_LOOKBACK, len(df)):
63
+ quiet_max = V[t - CONTRACT_DAYS:t].max() # ์ง์ „ ์กฐ์šฉํ•œ ๋‚ ๋“ค ์ค‘ '์ตœ๋Œ€' ๊ฑฐ๋ž˜๋Ÿ‰
64
+ if quiet_max <= 0 or C[t - 1] <= 0:
65
+ continue
66
+ surge = V[t] / quiet_max # ์ง์ „ ๋ฉฐ์น  '๊ฐ๊ฐ'์˜ N๋ฐฐ (ํ•˜๋‚˜๋ผ๋„ ํฌ๋ฉด ํƒˆ๋ฝ = ํญ๋ฐœ ๋‹ค์Œ๋‚  ๋ฐฉ์ง€)
67
+ base_high = H[t - BASE_LOOKBACK:t].max()
68
+ gain = C[t] / C[t - 1] - 1
69
+ if (surge >= VOL_DRYUP_SURGE and C[t] > base_high
70
+ and gain >= MIN_PRICE_GAIN and C[t] > O[t]):
71
+ out.append({
72
+ 'i': t, 'date': str(df.index[t])[:10],
73
+ 'close': C[t], 'open': O[t], 'low': L[t],
74
+ 'gain': round(gain * 100, 1), 'surge': round(surge, 2),
75
+ 'base_high': base_high, 'base_low': float(L[t - BASE_LOOKBACK:t].min()),
76
+ })
77
+ return [b for b in out if b['date'] >= ANALYZE_FROM]
78
+
79
+
80
+ # =============================================
81
+ # ์ง„๋‹จ: ํŠน์ • ๋‚ ์งœ๊ฐ€ ์™œ ์žกํ˜”๋‚˜/์•ˆ ์žกํ˜”๋‚˜
82
+ # =============================================
83
+ def run_diagnose(df, date_str):
84
+ O = df['Open'].values.astype(float); H = df['High'].values.astype(float)
85
+ C = df['Close'].values.astype(float); V = df['Volume'].values.astype(float)
86
+ idx = [str(d)[:10] for d in df.index]
87
+ pos = next((k for k, d in enumerate(idx) if d >= date_str), len(df) - 1)
88
+ lo = max(BASE_LOOKBACK, pos - 12); hi = min(len(df), pos + 4)
89
+ bh_label = f"{BASE_LOOKBACK}์ผ๊ณ ๊ฐ€"
90
+
91
+ print("\n" + "=" * 92)
92
+ print(f"[์ง„๋‹จ] {date_str} ์ฃผ๋ณ€ ์ผ๋ณ„ (๋ฐฐ์ˆ˜ = ๊ทธ๋‚  ๊ฑฐ๋ž˜๋Ÿ‰ / ์ง์ „ {CONTRACT_DAYS}์ผ '์ตœ๋Œ€' ๊ฑฐ๋ž˜๋Ÿ‰)")
93
+ print("=" * 92)
94
+ print(f"{'๋‚ ์งœ':<11}{'์ข…๊ฐ€':>8}{'์‹œ๊ฐ€':>8}{'๊ฑฐ๋ž˜๋Ÿ‰':>12}{'์ง์ „์ตœ๋Œ€':>12}{'๋ฐฐ์ˆ˜':>7}"
95
+ f"{'๋Œ€๋น„%':>7}{'์–‘๋ด‰':>5}{bh_label:>9}{'๊ณ ๊ฐ€๋ŒํŒŒ':>7}")
96
+ print("-" * 92)
97
+ for k in range(lo, hi):
98
+ qa = V[k - CONTRACT_DAYS:k].max()
99
+ sg = (V[k] / qa) if qa > 0 else 0
100
+ bh = H[k - BASE_LOOKBACK:k].max()
101
+ gn = (C[k] / C[k - 1] - 1) * 100 if C[k - 1] > 0 else 0
102
+ bull = 'O' if C[k] > O[k] else ''
103
+ pbk = 'O' if C[k] > bh else ''
104
+ mark = ' <==' if idx[k] == date_str else ''
105
+ print(f"{idx[k]:<11}{C[k]:>8,.0f}{O[k]:>8,.0f}{V[k]:>12,.0f}{qa:>12,.0f}{sg:>6.1f}x"
106
+ f"{gn:>+6.1f}%{bull:>5}{bh:>9,.0f}{pbk:>7}{mark}")
107
+
108
+ # ํŒ์ •
109
+ print("\n [ํŒ์ •]")
110
+ target = idx[pos]
111
+ if target != date_str:
112
+ print(f" (์ฐธ๊ณ : {date_str}์€ ๊ฑฐ๋ž˜์ผ ์•„๋‹˜ -> ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด {target} ๊ธฐ์ค€)")
113
+ qa = V[pos - CONTRACT_DAYS:pos].max()
114
+ sg = (V[pos] / qa) if qa > 0 else 0
115
+ bh = H[pos - BASE_LOOKBACK:pos].max()
116
+ gn = (C[pos] / C[pos - 1] - 1) * 100 if C[pos - 1] > 0 else 0
117
+ c1 = sg >= VOL_DRYUP_SURGE; c2 = C[pos] > bh
118
+ c3 = gn >= MIN_PRICE_GAIN * 100; c4 = C[pos] > O[pos]
119
+ print(f" ๊ฑฐ๋ž˜๋Ÿ‰ ๋ฐฐ์ˆ˜ {sg:.1f}x >= {VOL_DRYUP_SURGE}? {'O' if c1 else 'X'}")
120
+ print(f" ์ข…๊ฐ€ {C[pos]:,.0f} > {BASE_LOOKBACK}์ผ๊ณ ๊ฐ€ {bh:,.0f}? {'O' if c2 else 'X'}")
121
+ print(f" ์ƒ์Šน๋ฅ  {gn:+.1f}% >= {MIN_PRICE_GAIN*100:.0f}%? {'O' if c3 else 'X'}")
122
+ print(f" ์–‘๋ด‰ (์ข…๊ฐ€ {C[pos]:,.0f} > ์‹œ๊ฐ€ {O[pos]:,.0f})? {'O' if c4 else 'X'}")
123
+ if c1 and c2 and c3 and c4:
124
+ print(f" => 4์กฐ๊ฑด ํ†ต๊ณผ -> ์žกํ˜€์•ผ ํ•จ.")
125
+ else:
126
+ fails = [n for n, ok in [(f'๊ฑฐ๋ž˜๋Ÿ‰ ๋ฐฐ์ˆ˜ ๋ถ€์กฑ({sg:.1f}x)', c1), ('๊ฐ€๊ฒฉ์ด ๊ณ ๊ฐ€ ๋ชป ๋„˜์Œ', c2),
127
+ (f'์ƒ์Šน๋ฅ  ๋ถ€์กฑ({gn:+.1f}%)', c3), ('์Œ๋ด‰', c4)] if not ok]
128
+ print(f" => ์•ˆ ์žกํž˜. ์ด์œ : {', '.join(fails)}")
129
+ if not c1:
130
+ print(f" (VOL_DRYUP_SURGE๋ฅผ {sg:.1f} ๋ฐ‘์œผ๋กœ ๋‚ฎ์ถ”๋ฉด ์žกํ˜€)")
131
+
132
+
133
+ # =============================================
134
+ # ๋ฐ์ดํ„ฐ ๋กœ๋“œ + ์ถœ๋ ฅ
135
+ # =============================================
136
+ print("=" * 70)
137
+ print(f"VCP ๋ŒํŒŒ ๊ฐ์ง€: {TICKER} | ๊ธฐ๊ฐ„: {ANALYZE_FROM} ~ {ANALYZE_TO}")
138
+ print(f"์กฐ๊ฑด: ์ง์ „{CONTRACT_DAYS}์ผ ๋Œ€๋น„ ๊ฑฐ๋ž˜๋Ÿ‰ {VOL_DRYUP_SURGE}๋ฐฐ+ & {BASE_LOOKBACK}์ผ๊ณ ๊ฐ€ ๋ŒํŒŒ "
139
+ f"& ์ƒ์Šน +{MIN_PRICE_GAIN*100:.0f}%+ & ์–‘๋ด‰")
140
+ print("=" * 70)
141
+
142
+ df = fdr.DataReader(TICKER, DATA_START, ANALYZE_TO)
143
+ if df is None or len(df) == 0:
144
+ print("๋ฐ์ดํ„ฐ ์—†์Œ"); sys.exit()
145
+ print(f"๋ฐ์ดํ„ฐ: {len(df)}์ผ ๋กœ๋“œ ์™„๋ฃŒ\n")
146
+
147
+ bos = find_vcp_breakouts(df)
148
+ print(f"๊ฐ์ง€๋œ VCP ๋ŒํŒŒ: {len(bos)}๊ฐœ\n")
149
+
150
+ for n, b in enumerate(bos, 1):
151
+ print("=" * 70)
152
+ print(f"VCP #{n} ๋ŒํŒŒ {b['date']} @ {b['close']:,.0f}์›")
153
+ print(f" ์ƒ์Šน๋ฅ  +{b['gain']}% | ๊ฑฐ๋ž˜๋Ÿ‰ {b['surge']}๋ฐฐ(์ง์ „{CONTRACT_DAYS}์ผ์˜ ์ตœ๋Œ€ ๊ธฐ์ค€) | "
154
+ f"{BASE_LOOKBACK}์ผ๊ณ ๊ฐ€({b['base_high']:,.0f}) ๋ŒํŒŒ | ์–‘๋ด‰")
155
+ print()
156
+
157
+ print("=" * 70)
158
+ print(f"์š”์•ฝ: ์ด {len(bos)}๊ฐœ ๋ŒํŒŒ")
159
+ for b in bos:
160
+ print(f" {b['date']} | {b['close']:,.0f}์› | +{b['gain']}% | ๊ฑฐ๋ž˜๋Ÿ‰ {b['surge']}๋ฐฐ")
161
+
162
+ if DIAGNOSE_DATE:
163
+ run_diagnose(df, DIAGNOSE_DATE)