Spaces:
Running
Running
login_enabled_only 3menu
Browse files- app.py +115 -931
- bollinger_count_debug.py +145 -0
- final_sell_monitor.py +206 -0
- requirements.txt +2 -3
- vcp_debug.py +163 -0
app.py
CHANGED
|
@@ -1,968 +1,152 @@
|
|
| 1 |
# -*- coding: utf-8 -*-
|
| 2 |
"""
|
| 3 |
-
์ฃผ์ ๋๋ฒ๊ฑฐ (๋ชจ๋ฐ์ผ ์น์ฑ)
|
| 4 |
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
5) ์ ๋๋งค๋ (RSยท์ด๊ฒฉ) (sell_+10_debug.py)
|
| 10 |
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
"""
|
| 13 |
|
| 14 |
import io
|
| 15 |
-
import
|
| 16 |
-
import
|
| 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 |
-
|
| 27 |
-
|
| 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 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
|
| 207 |
-
|
| 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 |
-
|
| 428 |
-
analyze(ticker)
|
| 429 |
-
except SystemExit:
|
| 430 |
-
pass
|
| 431 |
except Exception as e:
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
|
|
|
| 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 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 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 |
-
|
| 704 |
-
|
| 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 |
-
|
| 713 |
-
|
| 714 |
-
print("
|
| 715 |
-
|
| 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 |
-
|
| 786 |
-
|
| 787 |
-
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
|
| 795 |
-
|
| 796 |
-
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
|
| 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
|
| 925 |
ticker = (ticker or "").strip()
|
| 926 |
if not ticker:
|
| 927 |
-
return "
|
| 928 |
-
|
|
|
|
| 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.
|
| 944 |
-
|
| 945 |
-
|
| 946 |
-
|
| 947 |
-
|
| 948 |
-
|
| 949 |
-
with gr.
|
| 950 |
-
|
| 951 |
-
|
| 952 |
-
|
| 953 |
-
|
| 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 |
-
|
| 964 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 965 |
|
| 966 |
|
| 967 |
if __name__ == "__main__":
|
| 968 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 2 |
pandas
|
| 3 |
numpy
|
| 4 |
-
|
| 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)
|