QQuery / app.py
johnkim
๋ฉ”๋‰ด ์ˆœ์„œ ๋ณ€๊ฒฝ
b857a2c
Raw
History Blame Contribute Delete
22.7 kB
# -*- 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()