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