Spaces:
Running
Running
| # -*- coding: utf-8 -*- | |
| """ | |
| ์ฃผ์ ๋๋ฒ๊ฑฐ (๋ชจ๋ฐ์ผ ์น์ฑ) โ Hugging Face Spaces / Gradio | |
| ========================================================= | |
| ํญ ์์: | |
| 1) ์ ๋๋งค๋ ์ตํ๋ณด๋ฃจ(๋ค์ข ๋ชฉ) โ confirm_sell_backtest.py ๋ก์ง ๊ทธ๋๋ก | |
| 2) ๋ณผ๋ฆฐ์ ์นด์ดํธ (bollinger_count_debug.py) โ ์ต๊ทผ 1๋ ์๋ | |
| 3) ์ผ๋ด ์ํ โ buy_engine_dipfix.run ํ be.LAST_STATUS (์ผ๋ด ํ ๊ตญ๋ฉด ํ ์ค) | |
| 4) ์ผ์ฑยทETF ๋งค๋ (RS) โ final_sell_monitor_rs.py (RS_A4, ์์๋ ์ต์ ) | |
| ์์น: | |
| - ์ ๋๋งค๋: confirm_sell_backtest.py ๋ฅผ import (๋ฐฑํ ์คํฐ์ 100% ๋์ผ ๋ก์ง) | |
| - ๋ณผ๋ฆฐ์ : ๋ฐ์คํฌํ ์คํฌ๋ฆฝํธ ์์ค๋ฅผ ์ฝ์ด TICKER๋ง ๋ฐ๊ฟ exec, ์ถ๋ ฅ ๊ทธ๋๋ก ์บก์ฒ | |
| - RS ๋งค๋: final_sell_monitor_rs.analyze_one ํธ์ถ (์ผ์ฑยทETF์ฒ๋ผ ์ด๊ฒฉ์ผ๋ก ์ ์กํ๋ ์ข ๋ชฉ) | |
| - ์ผ๋ด ์ํ: buy_engine_dipfix.run(df,'1day') ์คํ ํ be.LAST_STATUS ํ์ (์ผ๋ด๋ง) | |
| - ์์ฅ(KOSPI/KOSDAQ): market_map.csv ๋ก ์๋ํ๋ณ (KRX StockListing ์ ๋ถ๋ฆ) | |
| ๊ฐ์ด ์ฌ๋ฆด ํ์ผ: app.py, confirm_sell_backtest.py, bollinger_count_debug.py, | |
| market_map.csv, requirements.txt, | |
| final_sell_monitor_rs.py, confirm_sell_backtest_rs.py, buy_engine_dipfix.py | |
| """ | |
| import io | |
| import os | |
| import re | |
| import contextlib | |
| from datetime import datetime, timezone, timedelta | |
| import gradio as gr | |
| import confirm_sell_backtest as BT # ์ ๋๋งค๋ ํต์ฌ ๋ก์ง/์ค์ ๊ทธ๋๋ก | |
| import final_sell_monitor_rs # RS ๋งค๋(์ผ์ฑยทETF์ฉ ๋ณด์กฐ). analyze_one ํธ์ถ | |
| KST = timezone(timedelta(hours=9)) | |
| def asof_label(last_date): | |
| """๋ง์ง๋ง ๋ฐ์ดํฐ๊ฐ '์ค๋'์ด๊ณ ์ฅ์ค์ด๋ฉด '์ฅ์ค ํ์ฌ๊ฐ(HH:MM)', ์๋๋ฉด '์ข ๊ฐ'. ํ๊ตญ ์ฅ 09:00~15:30 KST.""" | |
| now = datetime.now(KST) | |
| today = now.strftime("%Y-%m-%d") | |
| intraday = (last_date == today | |
| and now.weekday() < 5 | |
| and (9 * 60) <= (now.hour * 60 + now.minute) <= (15 * 60 + 30)) | |
| if intraday: | |
| return f"{last_date} ์ฅ์ค ํ์ฌ๊ฐ ({now:%H:%M} KST ๊ธฐ์ค, ์ข ๊ฐ ์๋)" | |
| return f"{last_date} ์ข ๊ฐ ๊ธฐ์ค" | |
| HERE = os.path.dirname(os.path.abspath(__file__)) | |
| MAP_PATH = os.path.join(HERE, "market_map.csv") | |
| BOLL_PATH = os.path.join(HERE, "bollinger_count_debug.py") | |
| # โโ ์คํ ์ market_map.csv ๋ฐฑ๊ทธ๋ผ์ด๋ ์๋ ๊ฐฑ์ (์ค๋๋์ ๋๋ง) โโโโโโโโโโโโโโโโ | |
| # build_market_map.py ๋ฅผ ์ฐพ์ --if-stale N --out <๋ด market_map.csv> ๋ก ๋ฐฑ๊ทธ๋ผ์ด๋ ์คํ. | |
| # fdr/ํ๊ตญIP ์๋ ๋ฐฐํฌ ์๋ฒ์์ ์กฐ์ฉํ ์คํจ โ ์ปค๋ฐ๋ market_map.csv ๊ทธ๋๋ก ์ฌ์ฉ(์ฑ์ ์ ์). | |
| # ๋งฅ ๋ก์ปฌ(fdr ๋จ)์์ ์ฑ ์ผค ๋๋ง๋ค N์ผ ๋์์ผ๋ฉด ์์์ ์ต์ ํ. ๋งฅ ์ค์ผ์ค๋ฌ์ ์ด์ค ์์ ๋ง. | |
| MARKET_MAP_STALE_DAYS = 7 # ์ด ์ผ์ ๋์ผ๋ฉด ๊ฐฑ์ ์๋. 0์ด๋ฉด ์๋๊ฐฑ์ ๋. | |
| def _refresh_market_map_bg(): | |
| import threading, subprocess, sys as _sys | |
| def _run(): | |
| try: | |
| cands = [ | |
| os.path.join(HERE, "build_market_map.py"), | |
| os.path.join(HERE, "..", "quant", "sell_debug", "build_market_map.py"), | |
| os.path.expanduser("~/Projects/quant/sell_debug/build_market_map.py"), | |
| ] | |
| script = next((p for p in cands if os.path.exists(p)), None) | |
| if not script: | |
| return # ๋ฐฐํฌ ์๋ฒ ๋ฑ ์์ฑ๊ธฐ ์์ โ ์กฐ์ฉํ ์คํต | |
| subprocess.run( | |
| [_sys.executable, script, "--if-stale", str(MARKET_MAP_STALE_DAYS), | |
| "--out", MAP_PATH], | |
| timeout=180, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) | |
| except Exception: | |
| pass # fdr ์คํจ/ํด์ธIP/ํ์์์ ๋ฑ ์ ๋ถ ๋ฌด์ โ ์ฑ ๋์์ ์ํฅ ์์ | |
| if MARKET_MAP_STALE_DAYS: | |
| threading.Thread(target=_run, daemon=True).start() | |
| _refresh_market_map_bg() | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| ALERT_DAYS = 5 # ์ ๋๋งค๋: ์ต๊ทผ ์ด ๊ฑฐ๋์ผ ์์ ๋งค๋์ ํธ๋ฅผ [๋งค๋]๋ก ๋ณธ๋ค | |
| _TICKER_RE = re.compile(r"TICKER\s*=\s*['\"][0-9A-Za-z]+['\"]") | |
| # ==================================================================== | |
| # ๊ณตํต: market_map.csv (code -> (market, name)) 3์ปฌ๋ผ: code,market,name | |
| # ==================================================================== | |
| def load_market_map(): | |
| m = {} | |
| try: | |
| with open(MAP_PATH, encoding="utf-8") as f: | |
| for line in f: | |
| parts = [p.strip() for p in line.strip().split(",")] | |
| if len(parts) < 2: | |
| continue | |
| code, mkt = parts[0], parts[1] | |
| if code.lower() == "code": | |
| continue | |
| name = parts[2] if len(parts) >= 3 else "" | |
| m[code.zfill(6)] = (mkt.upper(), name) | |
| except Exception as e: | |
| print(f"[๊ฒฝ๊ณ ] market_map.csv ๋ก๋ ์คํจ: {e}") | |
| return m | |
| MARKET_MAP = load_market_map() | |
| import difflib | |
| def _norm(s): | |
| return str(s).replace(" ", "").lower() | |
| # ์ข ๋ชฉ๋ช (์ ๊ทํ) -> ์ฝ๋ | |
| NAME_TO_CODE = {} | |
| _NORM_NAMES = [] | |
| for _c, (_m, _n) in MARKET_MAP.items(): | |
| if _n: | |
| NAME_TO_CODE[_norm(_n)] = _c | |
| _NORM_NAMES.append(_norm(_n)) | |
| def resolve_to_code(q): | |
| """6์๋ฆฌ ์ฝ๋๋ฉด ๊ทธ๋๋ก. ์ข ๋ชฉ๋ช ์ด๋ฉด ์ฝ๋๋ก (๋์๋ฌธ์ ๋ฌด์, ๋ถ๋ถ์ผ์น, ์คํํ์ฉ). | |
| ๋ฐํ (code, ๋ฉ์์ง). ๋ชป ์ฐพ๊ฑฐ๋ ํ๋ณด ์ฌ๋ฟ์ด๋ฉด (None, ์๋ด).""" | |
| q = str(q).strip() | |
| z = q.zfill(6) | |
| if z.isdigit() and len(z) == 6: | |
| return z, "" # ์ซ์ ์ฝ๋ | |
| zu = q.upper().zfill(6) # ETF/ETN ์์ซ์ ์ฝ๋ (์: 0091P0) | |
| if len(zu) == 6 and zu.isalnum() and zu in MARKET_MAP: | |
| return zu, "" | |
| key = _norm(q) | |
| if not key: | |
| return None, "์ ๋ ฅ์ด ๋น์์ด" | |
| if key in NAME_TO_CODE: # ์ ํ ์ผ์น(๋์๋ฌธ์ ๋ฌด์) | |
| return NAME_TO_CODE[key], "" | |
| hits = [(nm, c) for nm, c in NAME_TO_CODE.items() if key in nm] # ๋ถ๋ถ ์ผ์น | |
| if len(hits) == 1: | |
| return hits[0][1], f"('{q}' โ {MARKET_MAP[hits[0][1]][1]})" | |
| if len(hits) > 1: | |
| hits.sort(key=lambda h: difflib.SequenceMatcher(None, key, h[0]).ratio(), reverse=True) | |
| cand = ", ".join(f"{MARKET_MAP[c][1]}({c})" for _, c in hits[:8]) | |
| return None, f"'{q}' ํ๋ณด ์ฌ๋ฟ โ {cand}" | |
| close = difflib.get_close_matches(key, _NORM_NAMES, n=5, cutoff=0.6) # ์คํ ํ์ฉ | |
| if len(close) == 1: | |
| c = NAME_TO_CODE[close[0]] | |
| return c, f"('{q}' โ {MARKET_MAP[c][1]} ๋ก ์ถ์ )" | |
| if len(close) > 1: | |
| cand = ", ".join(f"{MARKET_MAP[NAME_TO_CODE[nm]][1]}({NAME_TO_CODE[nm]})" for nm in close) | |
| return None, f"'{q}' ํน์ ์ด๊ฑฐ? โ {cand}" | |
| return None, f"'{q}' ์ข ๋ชฉ๋ช /์ฝ๋ ๋ชป ์ฐพ์" | |
| def market_of(code): | |
| return MARKET_MAP.get(code.zfill(6), ("KOSDAQ", ""))[0] | |
| def name_of(code): | |
| nm = MARKET_MAP.get(code.zfill(6), ("", ""))[1] | |
| return nm if nm else code.zfill(6) | |
| def _label(code): | |
| """๊ฒฐ๊ณผ ํ์์ฉ: '๋ ์ธ๋ณด์ฐ๋ก๋ณดํฑ์ค(277810)' ํํ. ์ด๋ฆ ์์ผ๋ฉด ์ฝ๋๋ง.""" | |
| nm = MARKET_MAP.get(code.zfill(6), ("", ""))[1] | |
| return f"{nm}({code.zfill(6)})" if nm else code.zfill(6) | |
| # ==================================================================== | |
| # ํญ: ๋ณผ๋ฆฐ์ โ ์คํฌ๋ฆฝํธ ์์ค๋ฅผ ์ฝ์ด TICKER ๋ฐ๊ฟ exec | |
| # ==================================================================== | |
| def run_script(path, ticker): | |
| code, msg = resolve_to_code(ticker) | |
| if code is None: | |
| return msg | |
| ticker = code | |
| try: | |
| src = open(path, encoding="utf-8").read() | |
| except Exception as e: | |
| return f"์คํฌ๋ฆฝํธ ๋ก๋ ์คํจ: {e}" | |
| src, n = _TICKER_RE.subn(f"TICKER = '{ticker}'", src, count=1) | |
| if n == 0: | |
| return "์คํฌ๋ฆฝํธ์์ TICKER ๋ผ์ธ์ ๋ชป ์ฐพ์." | |
| buf = io.StringIO() | |
| g = {"__name__": "__main__"} | |
| try: | |
| with contextlib.redirect_stdout(buf): | |
| exec(compile(src, path, "exec"), g) | |
| except SystemExit: | |
| pass | |
| except Exception as e: | |
| return f"{buf.getvalue()}\n\n[์ค๋ฅ] {e}" | |
| out = buf.getvalue().strip() | |
| head = f"[{_label(code)}]{(' ' + msg) if msg else ''}\n\n" | |
| return head + (out if out else "์ถ๋ ฅ ์์ (๋ฐ์ดํฐ๊ฐ ์๊ฑฐ๋ ์กฐ๊ฑด ๋ฏธ์ถฉ์กฑ)") | |
| def run_boll(ticker): | |
| return run_script(BOLL_PATH, ticker) | |
| # ==================================================================== | |
| # ํญ: ์ ๋๋งค๋ ์ตํ๋ณด๋ฃจ (๋ค์ข ๋ชฉ) โ ํ์ฌ ๋จ๊ณ status ํฌํจ | |
| # ==================================================================== | |
| def _real_sells(stock, low, ma240, sells): | |
| """๋ฐฑํ ์คํฐ main()๊ณผ ๋์ผํ 4์ข ํํฐ๋ก '์ง์ง ๋งค๋'๋ง (240ํฐ์น/240์์/๋๋ฌด์ผ์ฐ/์ถ์ธ์ ์ง SKIP).""" | |
| out = [] | |
| for s in sells: | |
| i = s["i"]; price = s["price"] | |
| cat, days = BT.classify_240(stock, low, ma240, i, price) | |
| if getattr(BT, "SKIP_IF_NO_240", False) and cat == "UNKNOWN": | |
| continue | |
| if getattr(BT, "SKIP_IF_BELOW_240", False) and cat == "ALREADY_DOWN": | |
| continue | |
| med = getattr(BT, "MAX_EARLY_DAYS", None) | |
| if med is not None and cat == "EARLY" and days is not None and days > med: | |
| continue | |
| if getattr(BT, "SKIP_IF_STILL_UP", False) and cat == "STILL_UP": | |
| continue | |
| out.append(s) | |
| return out | |
| def _skip_reason(stock, low, ma240, s): | |
| cat, days = BT.classify_240(stock, low, ma240, s["i"], s["price"]) | |
| if getattr(BT, "SKIP_IF_NO_240", False) and cat == "UNKNOWN": | |
| return "240์ผ์ ์ด ์์ง ์๋ ์ข ๋ชฉ(์์ฅ ์ผ๋ง ์ ๋จ)์ด๋ผ ์ ํธ๋ฅผ ๋ฏฟ์ ์ ์์ด์" | |
| if getattr(BT, "SKIP_IF_BELOW_240", False) and cat == "ALREADY_DOWN": | |
| return "์ ํธ ๋น์ผ ์ฃผ๊ฐ๊ฐ ์ด๋ฏธ 240์ผ์ ์ ๊ฑด๋๋ฆฐ ์ํ โ ์ด๋ฏธ ๋ฆ์ ์ ํธ๋ผ์" | |
| med = getattr(BT, "MAX_EARLY_DAYS", None) | |
| if med is not None and cat == "EARLY" and days is not None and days > med: | |
| return f"์ ํธ ๋ค {days}๊ฑฐ๋์ผ์ด๋ ์ง๋์์ผ 240์ผ์ ์ด ๊นจ์ง โ ๋๋ฌด ์ฑ๊ธํ ์ ํธ๋ผ์" | |
| if getattr(BT, "SKIP_IF_STILL_UP", False) and cat == "STILL_UP": | |
| return "์ ํธ ๋ค 200๊ฑฐ๋์ผ ๋์ 240์ผ์ ์ ์ ๊นจ๊ณ ์ถ์ธ๊ฐ ์ด์์์์ โ ํ๋งค๋๋ผ์" | |
| return None | |
| def check_one(code): | |
| code = code.zfill(6) | |
| market = market_of(code) | |
| r = BT.load_one(code, market) | |
| if r is None: | |
| return {"code": code, "market": market, "ok": False, "msg": "๋ฐ์ดํฐ ์์"} | |
| stock = r["stock"]; index = r["index"]; low = r["low"]; ma240 = r["ma240"] | |
| raw_sells, jumps, state = BT.find_confirmed_sells( | |
| stock, index, r["dates"], r["start_pos"], return_state=True) | |
| sells = _real_sells(stock, low, ma240, raw_sells) # 4์ข ํํฐ | |
| last_i = len(stock) - 1 | |
| recent = [s for s in sells if s["i"] >= last_i - ALERT_DAYS] | |
| trigger = None; skipped = None | |
| if state.get("sold") and state.get("sold_i") is not None: | |
| si = state["sold_i"] | |
| trigger = next((s for s in sells if s["i"] == si), None) | |
| if trigger is None: | |
| raw = next((s for s in raw_sells if s["i"] == si), None) | |
| if raw is not None: | |
| skipped = {"sell": raw, | |
| "reason": _skip_reason(stock, low, ma240, raw) or "์กฐ๊ฑด ๋ฏธ๋ฌ"} | |
| state = dict(state, sold=False) | |
| import numpy as _np | |
| _m = ma240[last_i] | |
| return { | |
| "code": code, "market": market, "ok": True, | |
| "price_now": float(stock[last_i]), | |
| "ma240_now": (None if _np.isnan(_m) else float(_m)), | |
| "last_date": r["dates"][last_i], | |
| "recent": recent, | |
| "last_sell": sells[-1] if sells else None, | |
| "trigger": trigger, | |
| "skipped": skipped, | |
| "state": state, | |
| } | |
| def _status_line(res): | |
| st = res.get("state") | |
| if not st: | |
| return "๊ด๋ง (์ ํธ ๋๊ธฐ)" | |
| if st.get("sold"): | |
| return (f"๊ณผ๊ฑฐ ๋งค๋์ ํธ ์ ์ง์ค (์ง๊ธ ํ๋ผ๋ ์ ํธ ์๋) ยท " | |
| f"์ง๊ธ ์ด๊ฒฉ {st['div']*100:.1f}%") | |
| nm = {"์์": "๊ด๋ง", "flagON": "flag ON (๋๊ธฐ)", "1์ฐจ": "1์ฐจ (์งํ์ค)", | |
| "2์ฐจflagON": "2์ฐจ flag ON (์ ๋๋งค๋ ์๋ฐ!)", | |
| "2์ฐจ๋งค๋": "๊ด๋ง (๋งค๋์ ํธ ์์์ผ๋ ํํฐ๋ก ์ ์ธ๋จ)"}.get(st["stage_name"], st["stage_name"]) | |
| return f"{nm} ยท ์ง๊ธ ์ด๊ฒฉ {st['div']*100:.1f}%" | |
| def run_monitor(text): | |
| raw = [c.strip() for c in text.replace(",", " ").replace("\n", " ").split()] | |
| raw = [c for c in raw if c] | |
| if not raw: | |
| return "์ข ๋ชฉ์ฝ๋ ๋๋ ์ข ๋ชฉ๋ช ์ ์ ๋ ฅํด์ค (์: 277810 ๋ ์ธ๋ณด์ฐ๋ก๋ณดํฑ์ค)" | |
| head = (f"**์ ๋๋งค๋ ์ตํ๋ณด๋ฃจ** ยท ์ต๊ทผ {ALERT_DAYS}๊ฑฐ๋์ผ ๊ธฐ์ค\n\n" | |
| f"๊ธฐ์ค: ์ด๊ฒฉ {BT.DIV_THRESHOLD*100:.0f}% ๋จ๊ณ๋์ ({BT.MIN_DEEPEN*100:.1f}%p+) " | |
| f"+ ํ๋ฃจ๊ธ๋ณ {BT.DAILY_JUMP*100:.0f}%p ์ ํ. " | |
| f"ํํฐ: 240ํฐ์นยท240์์ยทํ๋งค๋ ์ ์ธ\n\n---\n") | |
| out = [head] | |
| alerts = [] | |
| asof = None | |
| for _q in raw: | |
| code, msg = resolve_to_code(_q) | |
| if code is None: | |
| out.append(f"- {msg}") | |
| continue | |
| res = check_one(code) | |
| lbl = _label(code) + (f" {msg}" if msg else "") | |
| if not res["ok"]: | |
| out.append(f"- {lbl} ({res['market']}) โ {res['msg']}") | |
| continue | |
| asof = res.get("last_date") | |
| st = res.get("state") or {} | |
| steps = st.get("steps") or [] | |
| trig = res.get("trigger") | |
| skp = res.get("skipped") | |
| m240 = res.get("ma240_now") | |
| m240s = (f"240์ {m240:,.0f} ({(res['price_now']/m240-1)*100:+.1f}%)" | |
| if m240 else "240์ ์์") | |
| if res["recent"]: | |
| sl = res["recent"][-1] | |
| alerts.append(_label(code)) | |
| ago = st.get("last_i", 0) - sl["i"] | |
| verdict = (f"### ๐ด [โ ์ง๊ธ ๋งค๋] {_label(code)} ({res['market']})\n" | |
| f"- **์ ๋๋งค๋ ์ ํธ: {sl['date']} @ {sl['price']:,.0f}** " | |
| f"(์ ํธ์์ ์ด๊ฒฉ {sl['div']*100:.0f}%) ยท {ago}๊ฑฐ๋์ผ ์ \n") | |
| elif trig and st.get("sold"): | |
| ago = st.get("last_i", 0) - trig["i"] | |
| verdict = (f"### ๐ก [๊ณผ๊ฑฐ ์ ํธ] {_label(code)} ({res['market']})\n" | |
| f"- **โป ์ง๊ธ ํ๋ผ๋ ์ ํธ ์๋**\n" | |
| f"- ์ ๋๋งค๋ ์ ํธ: {trig['date']} @ {trig['price']:,.0f} " | |
| f"(๊ทธ๋ ์ด๊ฒฉ {trig['div']*100:.0f}%) ยท {ago}๊ฑฐ๋์ผ ์ \n" | |
| f"- ์ด๊ฒฉ {BT.DIV_THRESHOLD*100:.0f}% ์๋ก ํ๋ณตํ๋ฉด ๋ฆฌ์ \n") | |
| elif skp: | |
| sl = skp["sell"] | |
| ago = st.get("last_i", 0) - sl["i"] | |
| verdict = (f"### ๐ข [๋ณด์ ] {_label(code)} ({res['market']})\n" | |
| f"- ๋ฌด์๋ ์ ํธ: {sl['date']} @ {sl['price']:,.0f} " | |
| f"(๊ทธ๋ ์ด๊ฒฉ {sl['div']*100:.0f}%) ยท {ago}๊ฑฐ๋์ผ ์ \n" | |
| f"- ๋ฌด์ํ ์ด์ โ {skp['reason']}\n") | |
| else: | |
| verdict = (f"### ๐ข [๋ณด์ ] {_label(code)} ({res['market']})\n" | |
| f"- ๋งค๋ ์ ํธ ์์. ๋ณด์ ์ ์ง.\n" | |
| f"- ํ์ฌ ๋จ๊ณ: {_status_line(res)}\n") | |
| # โ ์ด๊ฒฉ์ด ํ๋ณต๋ผ ๋ฆฌ์ ๋์ด๋ '์ง์ ๋งค๋๊ฐ ์ธ์ ์๋์ง'๋ ํญ์ ๋ณด์ฌ์ค๋ค. | |
| ls = res.get("last_sell") | |
| if ls: | |
| ago = st.get("last_i", 0) - ls["i"] | |
| _nowdiv = st.get("div") | |
| verdict += (f"- [์ง์ ๋งค๋(์ฐธ๊ณ )] {ls['date']} @ {ls['price']:,.0f} " | |
| f"(๊ทธ๋ ์ด๊ฒฉ {ls['div']*100:.0f}%) ยท {ago}๊ฑฐ๋์ผ ์ \n") | |
| if _nowdiv is not None: | |
| verdict += f" - โป ์ง๊ธ์ ์ด๊ฒฉ {_nowdiv*100:.0f}%๋ก ํ๋ณต๋ผ์ ์ ํธ ๊บผ์ง\n" | |
| out.append(verdict) | |
| out.append(f"**[์ง๊ธ]** {res['last_date']} ์ข ๊ฐ {res['price_now']:,.0f} ยท {m240s}\n") | |
| if "peak_price" in st: | |
| out.append( | |
| f"- ๊ณ ์ {st['peak_price']:,.0f} ({st['peak_date']})\n" | |
| f"- ์ง๊ธ ์ด๊ฒฉ **{st['div']*100:.1f}%** " | |
| f"= ์ข ๋ชฉ ๊ณ ์ ๋๋น {st['stock_from_peak']*100:.1f}% " | |
| f"โ ์ง์ ๊ฐ์๊ธฐ๊ฐ {st['index_from_peak']*100:+.1f}%\n") | |
| if steps: | |
| out.append(f"**[๋จ๊ณ {len(steps)}๊ฐ]**\n") | |
| for stp in steps: | |
| mark = " โโ " if "์ ๋๋งค๋" in stp["name"] else "" | |
| out.append(f"- `{stp['name']}` {stp['date']} @ {stp['price']:,.0f} " | |
| f"(์ด๊ฒฉ {stp['div']*100:.1f}%){mark}\n") | |
| out.append("\n") | |
| out.append("\n---\n") | |
| if asof: | |
| out.append(f"*์ํ ๊ธฐ์ค: {asof_label(asof)}*\n") | |
| if alerts: | |
| out.append(f"**์ค๋ ํ์ธ ํ์: {len(alerts)}์ข ๋ชฉ โ {', '.join(alerts)}**") | |
| else: | |
| out.append("**์ต๊ทผ ๋งค๋์ ํธ ์์. ์ ๋ถ ๋ณด์ ์ ์ง.**") | |
| return "\n".join(out) | |
| # ==================================================================== | |
| # ํญ: ์ผ์ฑยทETF ๋งค๋ (RS) โ final_sell_monitor_rs.analyze_one ํธ์ถ | |
| # ==================================================================== | |
| def run_rs(text): | |
| raw = [c.strip() for c in text.replace(",", " ").replace("\n", " ").split()] | |
| raw = [c for c in raw if c] | |
| if not raw: | |
| return "์ข ๋ชฉ์ฝ๋ ๋๋ ์ข ๋ชฉ๋ช ์ ์ ๋ ฅํด์ค (์: 005930 ์ผ์ฑ์ ์)" | |
| out = [] | |
| for _q in raw: | |
| code, msg = resolve_to_code(_q) | |
| if code is None: | |
| out.append(msg) | |
| continue | |
| market = market_of(code) | |
| try: | |
| res = final_sell_monitor_rs.analyze_one( | |
| {"ticker": code, "market": market, "name": name_of(code)}) | |
| except Exception as e: | |
| res = f"[์ค๋ฅ] {_label(code)} โ {e}" | |
| out.append(res) | |
| out.append("") | |
| return "\n".join(out) | |
| # ==================================================================== | |
| def run_status(text): | |
| """์ข ๋ชฉ์ ์ผ๋ด '์ง๊ธ ์ํ'(๋์ฐ ๊ทธ๋ฆฌ๋ ์ค / ์์๋ N๋ฒ์งธ / ๋งค์ ๋ฉ์ถค ๋ฑ)๋ฅผ ํ ์ค๋ก.""" | |
| try: | |
| import FinanceDataReader as fdr | |
| import buy_engine_dipfix as be | |
| except Exception as e: | |
| return f"์์ง/๋ฐ์ดํฐ ๋ก๋ ์คํจ: {e}" | |
| raw = [c.strip() for c in text.replace(",", " ").replace("\n", " ").split()] | |
| raw = [c for c in raw if c] | |
| if not raw: | |
| return "์ข ๋ชฉ์ฝ๋ ๋๋ ์ข ๋ชฉ๋ช ์ ์ ๋ ฅํด์ค (์: 096770 DN์คํ ๋ชจํฐ๋ธ)" | |
| seen = set(); out = [] | |
| for _q in raw: | |
| code, msg = resolve_to_code(_q) | |
| if code is None: | |
| out.append(f"- {msg}"); continue | |
| if code in seen: | |
| continue # ๊ฐ์ ์ข ๋ชฉ ์ฝ๋+์ด๋ฆ ๋ ๋ค ๋ฃ์ด๋ ํ ๋ฒ๋ง | |
| seen.add(code) | |
| try: | |
| df = fdr.DataReader(code, "2005-01-01") | |
| except Exception as e: | |
| out.append(f"- {_label(code)}: ๋ฐ์ดํฐ ๋ก๋ ์คํจ ({e})"); continue | |
| if df is None or len(df) < 300: | |
| out.append(f"- {_label(code)}: ์ผ๋ด ๋ฐ์ดํฐ ๋ถ์กฑ(300๋ด ๋ฏธ๋ง)"); continue | |
| try: | |
| be.run(df, "1day") | |
| status = getattr(be, "LAST_STATUS", "") or "(์ํ ์์)" | |
| kind = getattr(be, "LAST_STATUS_KIND", "wait") | |
| except Exception as e: | |
| out.append(f"- {_label(code)}: ๋ถ์ ์คํจ ({e})"); continue | |
| last_date = str(df.index[-1])[:10] | |
| last_px = float(df["Close"].iloc[-1]) | |
| # ์์ ์ผ๊ด์ฑ(์ตํ๋ณด๋ฃจ์ ๋์ผ): ๐ข์ด๋ก=์ข์(๋งค์ ๊ตญ๋ฉด), โ ๏ธ์ฃผํฉ=์ฃผ์(๋ฌด๋์ง๋ ์ค) | |
| if kind == "buy": | |
| line = f'<span style="color:#16a34a;font-weight:700">๐ข {status}</span>' | |
| elif kind == "danger": | |
| line = f'<span style="color:#ea580c;font-weight:700">โ ๏ธ {status}</span>' | |
| else: | |
| line = status | |
| out.append(f"### ๐ {_label(code)} ({market_of(code)})\n" | |
| f"- **[์ผ๋ด]** {line}\n" | |
| f"- ๊ธฐ์ค: {last_date} ์ข ๊ฐ {last_px:,.0f}") | |
| return "\n\n".join(out) if out else "๊ฒฐ๊ณผ ์์" | |
| # UI (ํญ ์์: ์ตํ๋ณด๋ฃจ โ ๋ณผ๋ฆฐ์ โ ์ผ๋ด ์ํ โ ์ผ์ฑยทETF(RS)) | |
| # ==================================================================== | |
| with gr.Blocks(title="WTW Outlook") as demo: | |
| gr.Markdown("# ๐ WTW Outlook") | |
| with gr.Tab("์ ๋๋งค๋ ์ตํ๋ณด๋ฃจ"): | |
| gr.Markdown("๋ณด์ ์ข ๋ชฉ **์ฝ๋ ๋๋ ์ข ๋ชฉ๋ช **์ ๋ฃ์ผ๋ฉด ๋งค๋์ ํธ/ํ์ฌ๋จ๊ณ๋ฅผ ์๋ ค์ค. (์์ฅ ์๋ํ๋ณ, ์ฌ๋ฌ ์ข ๋ชฉ ๊ฐ๋ฅ)") | |
| m_in = gr.Textbox(label="์ข ๋ชฉ์ฝ๋ ๋๋ ์ข ๋ชฉ๋ช (์ฌ๋ฌ ๊ฐ๋ ๊ณต๋ฐฑ/์ค๋ฐ๊ฟ/์ผํ๋ก)", | |
| value="277810 ๋ ์ธ๋ณด์ฐ๋ก๋ณดํฑ์ค", lines=3) | |
| m_btn = gr.Button("๋งค๋์ ํธ ํ์ธ", variant="primary") | |
| m_out = gr.Markdown() | |
| m_btn.click(run_monitor, inputs=m_in, outputs=m_out) | |
| with gr.Tab("๋ณผ๋ฆฐ์ ์นด์ดํธ"): | |
| gr.Markdown("๋ณผ๋ฆฐ์ ๋ฐด๋ ์๋จ ๋ํ ์นด์ดํ (์ต๊ทผ 1๋ ์๋)") | |
| b_in = gr.Textbox(label="์ข ๋ชฉ์ฝ๋ ๋๋ ์ข ๋ชฉ๋ช ", value="420770") | |
| b_btn = gr.Button("๋ถ์", variant="primary") | |
| b_out = gr.Code(label="๊ฒฐ๊ณผ") | |
| b_btn.click(run_boll, inputs=b_in, outputs=b_out) | |
| with gr.Tab("์ผ๋ด ์ํ"): | |
| gr.Markdown("์ข ๋ชฉ์ **์ผ๋ด ์ง๊ธ ์ํ**๋ฅผ ์๋ ค์ค (๋์ฐ ๊ทธ๋ฆฌ๋ ์ค / ์์๋ N๋ฒ์งธ / ๋งค์ ๋ฉ์ถค ๋ฑ). ์ฌ๋ฌ ์ข ๋ชฉ ๊ฐ๋ฅ.") | |
| s_in = gr.Textbox(label="์ข ๋ชฉ์ฝ๋ ๋๋ ์ข ๋ชฉ๋ช (์ฌ๋ฌ ๊ฐ๋ ๊ณต๋ฐฑ/์ค๋ฐ๊ฟ/์ผํ๋ก)", | |
| value="096770 DN์คํ ๋ชจํฐ๋ธ", lines=3) | |
| s_btn = gr.Button("์ผ๋ด ์ํ ํ์ธ", variant="primary") | |
| s_out = gr.Markdown() | |
| s_btn.click(run_status, inputs=s_in, outputs=s_out) | |
| with gr.Tab("์ผ์ฑยทETF ๋งค๋ (RS)"): | |
| gr.Markdown("์ผ์ฑ์ ์ยทETF์ฒ๋ผ ์ด๊ฒฉ์ผ๋ก ์ ์กํ๋ ์ข ๋ชฉ์ RS ๋งค๋์ ํธ. (์์๋ ๋งค์ ๊ตฌ๊ฐ์ด๋ฉด ์ต์ ๋จ)") | |
| rs_in = gr.Textbox(label="์ข ๋ชฉ์ฝ๋ ๋๋ ์ข ๋ชฉ๋ช (์ฌ๋ฌ ๊ฐ๋ ๊ณต๋ฐฑ/์ค๋ฐ๊ฟ/์ผํ๋ก)", | |
| value="005930 ์ผ์ฑ์ ์", lines=3) | |
| rs_btn = gr.Button("RS ๋งค๋์ ํธ ํ์ธ", variant="primary") | |
| rs_out = gr.Code(label="๊ฒฐ๊ณผ") | |
| rs_btn.click(run_rs, inputs=rs_in, outputs=rs_out) | |
| if __name__ == "__main__": | |
| demo.launch() | |