Spaces:
Running
Running
johnkim commited on
Commit ·
29b99a7
1
Parent(s): 6536241
app.py 정상 복구 + RS 탭 추가
Browse files- app.py +368 -107
- final_sell_monitor_rs.py +1 -1
app.py
CHANGED
|
@@ -1,150 +1,411 @@
|
|
| 1 |
# -*- coding: utf-8 -*-
|
| 2 |
"""
|
| 3 |
-
주식 디버거 (모바일 웹앱) —
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
|
|
|
| 9 |
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
모든 출력은 contextlib.redirect_stdout 로 그대로 캡처해 화면에 표시.
|
| 14 |
-
|
| 15 |
-
KRX(StockListing)는 해외 서버 IP를 막으므로 자동 시장판별이 실패할 수 있음.
|
| 16 |
-
주가/지수 데이터(Naver)는 정상 로드됨. 전량매도는 시장(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 |
-
|
| 28 |
-
import
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
| 32 |
BOLL_PATH = os.path.join(HERE, "bollinger_count_debug.py")
|
| 33 |
VCP_PATH = os.path.join(HERE, "vcp_debug.py")
|
| 34 |
|
|
|
|
|
|
|
| 35 |
_TICKER_RE = re.compile(r"TICKER\s*=\s*['\"][0-9A-Za-z]+['\"]")
|
| 36 |
|
| 37 |
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
|
|
|
| 56 |
|
|
|
|
| 57 |
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
|
| 70 |
|
| 71 |
-
#
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
|
| 86 |
def run_vcp(ticker):
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
|
| 92 |
|
| 93 |
-
def
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
return _call_func(final_sell_monitor.analyze_one, h)
|
| 99 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
return "종목코드를 입력하세요 (예: 005930)"
|
| 105 |
-
h = {"ticker": ticker, "market": market, "name": (name or "").strip() or None}
|
| 106 |
-
return _call_func(final_sell_monitor_rs.analyze_one, h)
|
| 107 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
|
| 109 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
with gr.Blocks(title="주식 디버거") as demo:
|
| 111 |
-
gr.Markdown("#
|
| 112 |
|
| 113 |
-
# 탭 순서: 1)최후보루 2)볼린저 3)삼성·ETF 매도(RS) 4)VCP
|
| 114 |
with gr.Tab("전량매도 최후보루"):
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
f_btn.click(run_final, inputs=[f_tk, f_mkt, f_nm], outputs=f_out)
|
| 122 |
|
| 123 |
with gr.Tab("볼린저 카운트"):
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
with gr.Tab("삼성·ETF 매도(RS)"):
|
| 130 |
-
with gr.Row():
|
| 131 |
-
rs_tk = gr.Textbox(label="종목코드", placeholder="예: 005930", scale=2)
|
| 132 |
-
rs_mkt = gr.Radio(["KOSPI", "KOSDAQ"], value="KOSPI", label="시장", scale=2)
|
| 133 |
-
rs_nm = gr.Textbox(label="종목명(선택)", placeholder="예: 삼성전자")
|
| 134 |
-
rs_btn = gr.Button("실행", variant="primary")
|
| 135 |
-
rs_out = gr.Textbox(label="결과", lines=16)
|
| 136 |
-
rs_btn.click(run_final_rs, inputs=[rs_tk, rs_mkt, rs_nm], outputs=rs_out)
|
| 137 |
|
| 138 |
-
with gr.Tab("
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
|
|
|
|
|
|
| 143 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
|
| 145 |
if __name__ == "__main__":
|
| 146 |
-
|
| 147 |
-
demo.launch(
|
| 148 |
-
server_name="0.0.0.0",
|
| 149 |
-
server_port=int(os.environ.get("PORT", 7860)),
|
| 150 |
-
)
|
|
|
|
| 1 |
# -*- coding: utf-8 -*-
|
| 2 |
"""
|
| 3 |
+
주식 디버거 (모바일 웹앱) — Hugging Face Spaces / Gradio
|
| 4 |
+
=========================================================
|
| 5 |
+
탭 순서:
|
| 6 |
+
1) 전량매도 최후보루(다종목) — confirm_sell_backtest.py 로직 그대로
|
| 7 |
+
2) 볼린저 카운트 (bollinger_count_debug.py) — 최근 1년 자동
|
| 8 |
+
3) 삼성·ETF 매도 (RS) — final_sell_monitor_rs.py (RS_A4, 위아래 억제)
|
| 9 |
+
4) VCP 돌파 감지 (vcp_debug.py) — 최근 1년 자동
|
| 10 |
|
| 11 |
+
원칙:
|
| 12 |
+
- 전량매도: confirm_sell_backtest.py 를 import (백테스터와 100% 동일 로직)
|
| 13 |
+
- VCP·볼린저: 데스크탑 스크립트 소스를 읽어 TICKER만 바꿔 exec, 출력 그대로 캡처
|
| 14 |
+
- RS 매도: final_sell_monitor_rs.analyze_one 호출 (삼성·ETF처럼 이격으로 안 잡히는 종목)
|
| 15 |
+
- 시장(KOSPI/KOSDAQ): market_map.csv 로 자동판별 (KRX StockListing 안 부름)
|
| 16 |
|
| 17 |
+
같이 올릴 파일: app.py, confirm_sell_backtest.py, vcp_debug.py, bollinger_count_debug.py,
|
| 18 |
+
market_map.csv, requirements.txt,
|
| 19 |
+
final_sell_monitor_rs.py, confirm_sell_backtest_rs.py, buy_engine_dipfix.py
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
"""
|
| 21 |
|
| 22 |
import io
|
| 23 |
import os
|
| 24 |
import re
|
| 25 |
import contextlib
|
| 26 |
+
from datetime import datetime, timezone, timedelta
|
| 27 |
|
| 28 |
import gradio as gr
|
| 29 |
|
| 30 |
+
import confirm_sell_backtest as BT # 전량매도 핵심 로직/설정 그대로
|
| 31 |
+
import final_sell_monitor_rs # RS 매도(삼성·ETF용 보조). analyze_one 호출
|
| 32 |
+
|
| 33 |
+
KST = timezone(timedelta(hours=9))
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def asof_label(last_date):
|
| 37 |
+
"""마지막 데이터가 '오늘'이고 장중이면 '장중 현재가(HH:MM)', 아니면 '종가'. 한국 장 09:00~15:30 KST."""
|
| 38 |
+
now = datetime.now(KST)
|
| 39 |
+
today = now.strftime("%Y-%m-%d")
|
| 40 |
+
intraday = (last_date == today
|
| 41 |
+
and now.weekday() < 5
|
| 42 |
+
and (9 * 60) <= (now.hour * 60 + now.minute) <= (15 * 60 + 30))
|
| 43 |
+
if intraday:
|
| 44 |
+
return f"{last_date} 장중 현재가 ({now:%H:%M} KST 기준, 종가 아님)"
|
| 45 |
+
return f"{last_date} 종가 기준"
|
| 46 |
|
| 47 |
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 48 |
+
MAP_PATH = os.path.join(HERE, "market_map.csv")
|
| 49 |
BOLL_PATH = os.path.join(HERE, "bollinger_count_debug.py")
|
| 50 |
VCP_PATH = os.path.join(HERE, "vcp_debug.py")
|
| 51 |
|
| 52 |
+
ALERT_DAYS = 5 # 전량매도: 최근 이 거래일 안의 매도신호를 [매도]로 본다
|
| 53 |
+
|
| 54 |
_TICKER_RE = re.compile(r"TICKER\s*=\s*['\"][0-9A-Za-z]+['\"]")
|
| 55 |
|
| 56 |
|
| 57 |
+
# ====================================================================
|
| 58 |
+
# 공통: market_map.csv (code -> (market, name)) 3컬럼: code,market,name
|
| 59 |
+
# ====================================================================
|
| 60 |
+
def load_market_map():
|
| 61 |
+
m = {}
|
| 62 |
+
try:
|
| 63 |
+
with open(MAP_PATH, encoding="utf-8") as f:
|
| 64 |
+
for line in f:
|
| 65 |
+
parts = [p.strip() for p in line.strip().split(",")]
|
| 66 |
+
if len(parts) < 2:
|
| 67 |
+
continue
|
| 68 |
+
code, mkt = parts[0], parts[1]
|
| 69 |
+
if code.lower() == "code":
|
| 70 |
+
continue
|
| 71 |
+
name = parts[2] if len(parts) >= 3 else ""
|
| 72 |
+
m[code.zfill(6)] = (mkt.upper(), name)
|
| 73 |
+
except Exception as e:
|
| 74 |
+
print(f"[경고] market_map.csv 로드 실패: {e}")
|
| 75 |
+
return m
|
| 76 |
|
| 77 |
+
MARKET_MAP = load_market_map()
|
| 78 |
|
| 79 |
+
import difflib
|
| 80 |
+
|
| 81 |
+
def _norm(s):
|
| 82 |
+
return str(s).replace(" ", "").lower()
|
| 83 |
+
|
| 84 |
+
# 종목명(정규화) -> 코드
|
| 85 |
+
NAME_TO_CODE = {}
|
| 86 |
+
_NORM_NAMES = []
|
| 87 |
+
for _c, (_m, _n) in MARKET_MAP.items():
|
| 88 |
+
if _n:
|
| 89 |
+
NAME_TO_CODE[_norm(_n)] = _c
|
| 90 |
+
_NORM_NAMES.append(_norm(_n))
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def resolve_to_code(q):
|
| 94 |
+
"""6자리 코드면 그대로. 종목명이면 코드로 (대소문자 무시, 부분일치, 오타허용).
|
| 95 |
+
반환 (code, 메시지). 못 찾거나 후보 여럿이면 (None, 안내)."""
|
| 96 |
+
q = str(q).strip()
|
| 97 |
+
z = q.zfill(6)
|
| 98 |
+
if z.isdigit() and len(z) == 6:
|
| 99 |
+
return z, "" # 숫자 코드
|
| 100 |
+
zu = q.upper().zfill(6) # ETF/ETN 영숫자 코드 (예: 0091P0)
|
| 101 |
+
if len(zu) == 6 and zu.isalnum() and zu in MARKET_MAP:
|
| 102 |
+
return zu, ""
|
| 103 |
+
key = _norm(q)
|
| 104 |
+
if not key:
|
| 105 |
+
return None, "입력이 비었어"
|
| 106 |
+
if key in NAME_TO_CODE: # 정확 일치(대소문자 무시)
|
| 107 |
+
return NAME_TO_CODE[key], ""
|
| 108 |
+
hits = [(nm, c) for nm, c in NAME_TO_CODE.items() if key in nm] # 부분 일치
|
| 109 |
+
if len(hits) == 1:
|
| 110 |
+
return hits[0][1], f"('{q}' → {MARKET_MAP[hits[0][1]][1]})"
|
| 111 |
+
if len(hits) > 1:
|
| 112 |
+
hits.sort(key=lambda h: difflib.SequenceMatcher(None, key, h[0]).ratio(), reverse=True)
|
| 113 |
+
cand = ", ".join(f"{MARKET_MAP[c][1]}({c})" for _, c in hits[:8])
|
| 114 |
+
return None, f"'{q}' 후보 여럿 → {cand}"
|
| 115 |
+
close = difflib.get_close_matches(key, _NORM_NAMES, n=5, cutoff=0.6) # 오타 허용
|
| 116 |
+
if len(close) == 1:
|
| 117 |
+
c = NAME_TO_CODE[close[0]]
|
| 118 |
+
return c, f"('{q}' → {MARKET_MAP[c][1]} 로 추정)"
|
| 119 |
+
if len(close) > 1:
|
| 120 |
+
cand = ", ".join(f"{MARKET_MAP[NAME_TO_CODE[nm]][1]}({NAME_TO_CODE[nm]})" for nm in close)
|
| 121 |
+
return None, f"'{q}' 혹시 이거? → {cand}"
|
| 122 |
+
return None, f"'{q}' 종목명/코드 못 찾음"
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def market_of(code):
|
| 126 |
+
return MARKET_MAP.get(code.zfill(6), ("KOSDAQ", ""))[0]
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def name_of(code):
|
| 130 |
+
nm = MARKET_MAP.get(code.zfill(6), ("", ""))[1]
|
| 131 |
+
return nm if nm else code.zfill(6)
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def _label(code):
|
| 135 |
+
"""결과 표시용: '레인보우로보틱스(277810)' 형태. 이름 없으면 코드만."""
|
| 136 |
+
nm = MARKET_MAP.get(code.zfill(6), ("", ""))[1]
|
| 137 |
+
return f"{nm}({code.zfill(6)})" if nm else code.zfill(6)
|
| 138 |
|
| 139 |
|
| 140 |
+
# ====================================================================
|
| 141 |
+
# 탭: VCP / 볼린저 — 스크립트 소스를 읽어 TICKER 바꿔 exec
|
| 142 |
+
# ====================================================================
|
| 143 |
+
def run_script(path, ticker):
|
| 144 |
+
code, msg = resolve_to_code(ticker)
|
| 145 |
+
if code is None:
|
| 146 |
+
return msg
|
| 147 |
+
ticker = code
|
| 148 |
+
try:
|
| 149 |
+
src = open(path, encoding="utf-8").read()
|
| 150 |
+
except Exception as e:
|
| 151 |
+
return f"스크립트 로드 실패: {e}"
|
| 152 |
+
src, n = _TICKER_RE.subn(f"TICKER = '{ticker}'", src, count=1)
|
| 153 |
+
if n == 0:
|
| 154 |
+
return "스크립트에서 TICKER 라인을 못 찾음."
|
| 155 |
+
buf = io.StringIO()
|
| 156 |
+
g = {"__name__": "__main__"}
|
| 157 |
+
try:
|
| 158 |
+
with contextlib.redirect_stdout(buf):
|
| 159 |
+
exec(compile(src, path, "exec"), g)
|
| 160 |
+
except SystemExit:
|
| 161 |
+
pass
|
| 162 |
+
except Exception as e:
|
| 163 |
+
return f"{buf.getvalue()}\n\n[오류] {e}"
|
| 164 |
+
out = buf.getvalue().strip()
|
| 165 |
+
head = f"[{_label(code)}]{(' ' + msg) if msg else ''}\n\n"
|
| 166 |
+
return head + (out if out else "출력 없음 (데이터가 없거나 조건 미충족)")
|
| 167 |
|
| 168 |
|
| 169 |
def run_vcp(ticker):
|
| 170 |
+
return run_script(VCP_PATH, ticker)
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
def run_boll(ticker):
|
| 174 |
+
return run_script(BOLL_PATH, ticker)
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
# ====================================================================
|
| 178 |
+
# 탭: 전량매도 최후보루 (다종목) — 현재 단계 status 포함
|
| 179 |
+
# ====================================================================
|
| 180 |
+
def _real_sells(stock, low, ma240, sells):
|
| 181 |
+
"""백테스터 main()과 동일한 4종 필터로 '진짜 매도'만 (240터치/240없음/너무일찍/추세유지 SKIP)."""
|
| 182 |
+
out = []
|
| 183 |
+
for s in sells:
|
| 184 |
+
i = s["i"]; price = s["price"]
|
| 185 |
+
cat, days = BT.classify_240(stock, low, ma240, i, price)
|
| 186 |
+
if getattr(BT, "SKIP_IF_NO_240", False) and cat == "UNKNOWN":
|
| 187 |
+
continue
|
| 188 |
+
if getattr(BT, "SKIP_IF_BELOW_240", False) and cat == "ALREADY_DOWN":
|
| 189 |
+
continue
|
| 190 |
+
med = getattr(BT, "MAX_EARLY_DAYS", None)
|
| 191 |
+
if med is not None and cat == "EARLY" and days is not None and days > med:
|
| 192 |
+
continue
|
| 193 |
+
if getattr(BT, "SKIP_IF_STILL_UP", False) and cat == "STILL_UP":
|
| 194 |
+
continue
|
| 195 |
+
out.append(s)
|
| 196 |
+
return out
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
def _skip_reason(stock, low, ma240, s):
|
| 200 |
+
cat, days = BT.classify_240(stock, low, ma240, s["i"], s["price"])
|
| 201 |
+
if getattr(BT, "SKIP_IF_NO_240", False) and cat == "UNKNOWN":
|
| 202 |
+
return "240일선이 아직 없는 종목(상장 얼마 안 됨)이라 신호를 믿을 수 없어서"
|
| 203 |
+
if getattr(BT, "SKIP_IF_BELOW_240", False) and cat == "ALREADY_DOWN":
|
| 204 |
+
return "신호 당일 주가가 이미 240일선을 건드린 상태 → 이미 늦은 신호라서"
|
| 205 |
+
med = getattr(BT, "MAX_EARLY_DAYS", None)
|
| 206 |
+
if med is not None and cat == "EARLY" and days is not None and days > med:
|
| 207 |
+
return f"신호 뒤 {days}거래일이나 지나서야 240일선이 깨짐 → 너무 성급한 신호라서"
|
| 208 |
+
if getattr(BT, "SKIP_IF_STILL_UP", False) and cat == "STILL_UP":
|
| 209 |
+
return "신호 뒤 200거래일 동안 240일선을 안 깨고 추세가 살아있었음 → 헛매도라서"
|
| 210 |
+
return None
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
def check_one(code):
|
| 214 |
+
code = code.zfill(6)
|
| 215 |
+
market = market_of(code)
|
| 216 |
+
r = BT.load_one(code, market)
|
| 217 |
+
if r is None:
|
| 218 |
+
return {"code": code, "market": market, "ok": False, "msg": "데이터 없음"}
|
| 219 |
+
stock = r["stock"]; index = r["index"]; low = r["low"]; ma240 = r["ma240"]
|
| 220 |
+
raw_sells, jumps, state = BT.find_confirmed_sells(
|
| 221 |
+
stock, index, r["dates"], r["start_pos"], return_state=True)
|
| 222 |
+
sells = _real_sells(stock, low, ma240, raw_sells) # 4종 필터
|
| 223 |
+
last_i = len(stock) - 1
|
| 224 |
+
recent = [s for s in sells if s["i"] >= last_i - ALERT_DAYS]
|
| 225 |
+
trigger = None; skipped = None
|
| 226 |
+
if state.get("sold") and state.get("sold_i") is not None:
|
| 227 |
+
si = state["sold_i"]
|
| 228 |
+
trigger = next((s for s in sells if s["i"] == si), None)
|
| 229 |
+
if trigger is None:
|
| 230 |
+
raw = next((s for s in raw_sells if s["i"] == si), None)
|
| 231 |
+
if raw is not None:
|
| 232 |
+
skipped = {"sell": raw,
|
| 233 |
+
"reason": _skip_reason(stock, low, ma240, raw) or "조건 미달"}
|
| 234 |
+
state = dict(state, sold=False)
|
| 235 |
+
import numpy as _np
|
| 236 |
+
_m = ma240[last_i]
|
| 237 |
+
return {
|
| 238 |
+
"code": code, "market": market, "ok": True,
|
| 239 |
+
"price_now": float(stock[last_i]),
|
| 240 |
+
"ma240_now": (None if _np.isnan(_m) else float(_m)),
|
| 241 |
+
"last_date": r["dates"][last_i],
|
| 242 |
+
"recent": recent,
|
| 243 |
+
"last_sell": sells[-1] if sells else None,
|
| 244 |
+
"trigger": trigger,
|
| 245 |
+
"skipped": skipped,
|
| 246 |
+
"state": state,
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
def _status_line(res):
|
| 251 |
+
st = res.get("state")
|
| 252 |
+
if not st:
|
| 253 |
+
return "관망 (신호 대기)"
|
| 254 |
+
if st.get("sold"):
|
| 255 |
+
return (f"과거 매도신호 유지중 (지금 팔라는 신호 아님) · "
|
| 256 |
+
f"지금 이격 {st['div']*100:.1f}%")
|
| 257 |
+
nm = {"없음": "관망", "flagON": "flag ON (대기)", "1차": "1차 (진행중)",
|
| 258 |
+
"2차flagON": "2차 flag ON (전량매도 임박!)",
|
| 259 |
+
"2차매도": "관망 (매도신호 있었으나 필터로 제외됨)"}.get(st["stage_name"], st["stage_name"])
|
| 260 |
+
return f"{nm} · 지금 이격 {st['div']*100:.1f}%"
|
| 261 |
|
| 262 |
|
| 263 |
+
def run_monitor(text):
|
| 264 |
+
raw = [c.strip() for c in text.replace(",", " ").replace("\n", " ").split()]
|
| 265 |
+
raw = [c for c in raw if c]
|
| 266 |
+
if not raw:
|
| 267 |
+
return "종목코드 또는 종목명을 입력해줘 (예: 277810 레인보우로보틱스)"
|
|
|
|
| 268 |
|
| 269 |
+
head = (f"**전량매도 최후보루** · 최근 {ALERT_DAYS}거래일 기준\n\n"
|
| 270 |
+
f"기준: 이격 {BT.DIV_THRESHOLD*100:.0f}% 단계누적({BT.MIN_DEEPEN*100:.1f}%p+) "
|
| 271 |
+
f"+ 하루급변 {BT.DAILY_JUMP*100:.0f}%p 점프. "
|
| 272 |
+
f"필터: 240터치·240없음·헛매도 제외\n\n---\n")
|
| 273 |
+
out = [head]
|
| 274 |
+
alerts = []
|
| 275 |
+
asof = None
|
| 276 |
+
for _q in raw:
|
| 277 |
+
code, msg = resolve_to_code(_q)
|
| 278 |
+
if code is None:
|
| 279 |
+
out.append(f"- {msg}")
|
| 280 |
+
continue
|
| 281 |
+
res = check_one(code)
|
| 282 |
+
lbl = _label(code) + (f" {msg}" if msg else "")
|
| 283 |
+
if not res["ok"]:
|
| 284 |
+
out.append(f"- {lbl} ({res['market']}) — {res['msg']}")
|
| 285 |
+
continue
|
| 286 |
+
asof = res.get("last_date")
|
| 287 |
+
st = res.get("state") or {}
|
| 288 |
+
steps = st.get("steps") or []
|
| 289 |
+
trig = res.get("trigger")
|
| 290 |
+
skp = res.get("skipped")
|
| 291 |
|
| 292 |
+
m240 = res.get("ma240_now")
|
| 293 |
+
m240s = (f"240선 {m240:,.0f} ({(res['price_now']/m240-1)*100:+.1f}%)"
|
| 294 |
+
if m240 else "240선 없음")
|
|
|
|
|
|
|
|
|
|
| 295 |
|
| 296 |
+
if res["recent"]:
|
| 297 |
+
sl = res["recent"][-1]
|
| 298 |
+
alerts.append(_label(code))
|
| 299 |
+
ago = st.get("last_i", 0) - sl["i"]
|
| 300 |
+
verdict = (f"### 🔴 [★지금 매도] {_label(code)} ({res['market']})\n"
|
| 301 |
+
f"- **전량매도 신호: {sl['date']} @ {sl['price']:,.0f}** "
|
| 302 |
+
f"(신호시점 이격 {sl['div']*100:.0f}%) · {ago}거래일 전\n")
|
| 303 |
+
elif trig and st.get("sold"):
|
| 304 |
+
ago = st.get("last_i", 0) - trig["i"]
|
| 305 |
+
verdict = (f"### 🟡 [과거 신호] {_label(code)} ({res['market']})\n"
|
| 306 |
+
f"- **※ 지금 팔라는 신호 아님**\n"
|
| 307 |
+
f"- 전량매도 신호: {trig['date']} @ {trig['price']:,.0f} "
|
| 308 |
+
f"(그때 이격 {trig['div']*100:.0f}%) · {ago}거래일 전\n"
|
| 309 |
+
f"- 이격 {BT.DIV_THRESHOLD*100:.0f}% 위로 회복하면 리셋\n")
|
| 310 |
+
elif skp:
|
| 311 |
+
sl = skp["sell"]
|
| 312 |
+
ago = st.get("last_i", 0) - sl["i"]
|
| 313 |
+
verdict = (f"### 🟢 [보유] {_label(code)} ({res['market']})\n"
|
| 314 |
+
f"- 무시된 신호: {sl['date']} @ {sl['price']:,.0f} "
|
| 315 |
+
f"(그때 이격 {sl['div']*100:.0f}%) · {ago}거래일 전\n"
|
| 316 |
+
f"- 무시한 이유 — {skp['reason']}\n")
|
| 317 |
+
else:
|
| 318 |
+
verdict = (f"### 🟢 [보유] {_label(code)} ({res['market']})\n"
|
| 319 |
+
f"- 매도 신호 없음. 보유 유지.\n"
|
| 320 |
+
f"- 현재 단계: {_status_line(res)}\n")
|
| 321 |
+
out.append(verdict)
|
| 322 |
|
| 323 |
+
out.append(f"**[지금]** {res['last_date']} 종가 {res['price_now']:,.0f} · {m240s}\n")
|
| 324 |
+
if "peak_price" in st:
|
| 325 |
+
out.append(
|
| 326 |
+
f"- 고점 {st['peak_price']:,.0f} ({st['peak_date']})\n"
|
| 327 |
+
f"- 지금 이격 **{st['div']*100:.1f}%** "
|
| 328 |
+
f"= 종목 고점대비 {st['stock_from_peak']*100:.1f}% "
|
| 329 |
+
f"− 지수 같은기간 {st['index_from_peak']*100:+.1f}%\n")
|
| 330 |
+
|
| 331 |
+
if steps:
|
| 332 |
+
out.append(f"**[단계 {len(steps)}개]**\n")
|
| 333 |
+
for stp in steps:
|
| 334 |
+
mark = " ←★" if "전량매도" in stp["name"] else ""
|
| 335 |
+
out.append(f"- `{stp['name']}` {stp['date']} @ {stp['price']:,.0f} "
|
| 336 |
+
f"(이격 {stp['div']*100:.1f}%){mark}\n")
|
| 337 |
+
out.append("\n")
|
| 338 |
+
|
| 339 |
+
out.append("\n---\n")
|
| 340 |
+
if asof:
|
| 341 |
+
out.append(f"*상태 기준: {asof_label(asof)}*\n")
|
| 342 |
+
if alerts:
|
| 343 |
+
out.append(f"**오늘 확인 필요: {len(alerts)}종목 → {', '.join(alerts)}**")
|
| 344 |
+
else:
|
| 345 |
+
out.append("**최근 매도신호 없음. 전부 보유 유지.**")
|
| 346 |
+
return "\n".join(out)
|
| 347 |
+
|
| 348 |
+
|
| 349 |
+
# ====================================================================
|
| 350 |
+
# 탭: 삼성·ETF 매도 (RS) — final_sell_monitor_rs.analyze_one 호출
|
| 351 |
+
# ====================================================================
|
| 352 |
+
def run_rs(text):
|
| 353 |
+
raw = [c.strip() for c in text.replace(",", " ").replace("\n", " ").split()]
|
| 354 |
+
raw = [c for c in raw if c]
|
| 355 |
+
if not raw:
|
| 356 |
+
return "종목코드 또는 종목명을 입력해줘 (예: 005930 삼성전자)"
|
| 357 |
+
out = []
|
| 358 |
+
for _q in raw:
|
| 359 |
+
code, msg = resolve_to_code(_q)
|
| 360 |
+
if code is None:
|
| 361 |
+
out.append(msg)
|
| 362 |
+
continue
|
| 363 |
+
market = market_of(code)
|
| 364 |
+
try:
|
| 365 |
+
res = final_sell_monitor_rs.analyze_one(
|
| 366 |
+
{"ticker": code, "market": market, "name": name_of(code)})
|
| 367 |
+
except Exception as e:
|
| 368 |
+
res = f"[오류] {_label(code)} — {e}"
|
| 369 |
+
out.append(res)
|
| 370 |
+
out.append("")
|
| 371 |
+
return "\n".join(out)
|
| 372 |
+
|
| 373 |
+
|
| 374 |
+
# ====================================================================
|
| 375 |
+
# UI (탭 순서: 최후보루 → 볼린저 → 삼성·ETF(RS) → VCP)
|
| 376 |
+
# ====================================================================
|
| 377 |
with gr.Blocks(title="주식 디버거") as demo:
|
| 378 |
+
gr.Markdown("# 📊 주식 디버거")
|
| 379 |
|
|
|
|
| 380 |
with gr.Tab("전량매도 최후보루"):
|
| 381 |
+
gr.Markdown("보유종목 **코드 또는 종목명**을 넣으면 매도신호/현재단계를 알려줘. (시장 자동판별, 여러 종목 가능)")
|
| 382 |
+
m_in = gr.Textbox(label="종목코드 또는 종목명 (여러 개는 공백/줄바꿈/쉼표로)",
|
| 383 |
+
value="277810 레인보우로보틱스", lines=3)
|
| 384 |
+
m_btn = gr.Button("매도신호 확인", variant="primary")
|
| 385 |
+
m_out = gr.Markdown()
|
| 386 |
+
m_btn.click(run_monitor, inputs=m_in, outputs=m_out)
|
|
|
|
| 387 |
|
| 388 |
with gr.Tab("볼린저 카운트"):
|
| 389 |
+
gr.Markdown("볼린저밴드 상단 돌파 카운팅 (최근 1년 자동)")
|
| 390 |
+
b_in = gr.Textbox(label="종목코드 또는 종목명", value="420770")
|
| 391 |
+
b_btn = gr.Button("분석", variant="primary")
|
| 392 |
+
b_out = gr.Code(label="결과")
|
| 393 |
+
b_btn.click(run_boll, inputs=b_in, outputs=b_out)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 394 |
|
| 395 |
+
with gr.Tab("삼성·ETF 매도 (RS)"):
|
| 396 |
+
gr.Markdown("삼성전자·ETF처럼 이격으로 안 잡히는 종목의 RS 매도신호. (위아래 매수 구간이면 억제됨)")
|
| 397 |
+
rs_in = gr.Textbox(label="종목코드 또는 종목명 (여러 개는 공백/줄바꿈/쉼표로)",
|
| 398 |
+
value="005930 삼성전자", lines=3)
|
| 399 |
+
rs_btn = gr.Button("RS 매도신호 확인", variant="primary")
|
| 400 |
+
rs_out = gr.Code(label="결과")
|
| 401 |
+
rs_btn.click(run_rs, inputs=rs_in, outputs=rs_out)
|
| 402 |
|
| 403 |
+
with gr.Tab("VCP 돌파"):
|
| 404 |
+
gr.Markdown("미너비니 VCP 돌파 감지 (최근 1년 자동)")
|
| 405 |
+
v_in = gr.Textbox(label="종목코드 또는 종목명", value="222080")
|
| 406 |
+
v_btn = gr.Button("분석", variant="primary")
|
| 407 |
+
v_out = gr.Code(label="결과")
|
| 408 |
+
v_btn.click(run_vcp, inputs=v_in, outputs=v_out)
|
| 409 |
|
| 410 |
if __name__ == "__main__":
|
| 411 |
+
demo.launch()
|
|
|
|
|
|
|
|
|
|
|
|
final_sell_monitor_rs.py
CHANGED
|
@@ -197,4 +197,4 @@ def analyze_one(h):
|
|
| 197 |
if __name__ == '__main__':
|
| 198 |
tk = sys.argv[1] if len(sys.argv) > 1 else '005930'
|
| 199 |
mkt = sys.argv[2] if len(sys.argv) > 2 else 'KOSPI'
|
| 200 |
-
print(analyze_one({'ticker': tk, 'market': mkt, 'name': ''}))
|
|
|
|
| 197 |
if __name__ == '__main__':
|
| 198 |
tk = sys.argv[1] if len(sys.argv) > 1 else '005930'
|
| 199 |
mkt = sys.argv[2] if len(sys.argv) > 2 else 'KOSPI'
|
| 200 |
+
print(analyze_one({'ticker': tk, 'market': mkt, 'name': ''}))
|