QQuery / app.py
glorifiedjx's picture
์ตœํ›„๋ณด๋ฃจ: 240์˜ฌ ๋’ค์—” ์ผฌ + ์ง€์ˆ˜์ฃผ๋„ ๊ฑด๋„ˆ๋œ€ ํ‘œ์‹œ
849bf80
Raw History Blame Contribute Delete
33.4 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 _rng_txt(rg):
"""(์‹œ์ž‘,๋) โ†’ '2025-07-10 ~ 2026-02-04'. ๋์ด None์ด๋ฉด ์•„์ง ์ง„ํ–‰ ์ค‘์ธ ๊ตฌ๊ฐ„."""
a, b = rg
return f"{a} ~ {b}" if b else f"{a} ~ ์ง€๊ธˆ(์ง„ํ–‰์ค‘)"
def _dates_txt(ds, cap=6):
"""๋ฌด์‹œ๋œ ์‹ ํ˜ธ ๋‚ ์งœ ๋ชฉ๋ก. ๋„ˆ๋ฌด ๋งŽ์œผ๋ฉด ์•ž์ชฝ ๋ช‡ ๊ฐœ + ๋งˆ์ง€๋ง‰."""
if len(ds) <= cap:
return ", ".join(ds)
return ", ".join(ds[:cap - 1]) + f" โ€ฆ {ds[-1]} (์ด {len(ds)}๊ฑด)"
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"]
# โ˜…2026-10-14(์‚ฌ์šฉ์ž ํ™•์ •): ๋ชจ์•„๊ฐ€๋Š” ๊ตฌ๊ฐ„ ๋งค๋„๋Š” ์—”์ง„ '์•ˆ์—์„œ' ๋ฌด์‹œ โ†’ ํŒ”์•˜์Œ์œผ๋กœ ์•ˆ ์น˜๊ณ  ๋‹ค์Œ ๋‹จ๊ณ„๋ฅผ ๊ณ„์† ๋ณธ๋‹ค.
# (์˜ˆ์ „: ๋ฌด์‹œํ•ด๋„ ์—”์ง„์€ '๋งค๋„ํ•จ'์œผ๋กœ ๋‚จ์•„ ์ด๊ฒฉ โˆ’10% ํšŒ๋ณต ์ „๊นŒ์ง€ ์ƒˆ ์‹ ํ˜ธ ์—†์Œ โ€” ์ž๋žŒ โˆ’48% ๋ฌด์•Œ๋žŒ)
_pre_rngs = None
try:
import final_sell_monitor as _FM0
if getattr(_FM0, "IGNORE_SELL_IN_ACCUM", True):
_pre_rngs = _FM0.accum_ranges(code)
except Exception:
_pre_rngs = None
_mute = (lambda d: _FM0.in_accum(d, _pre_rngs)) if (_pre_rngs and getattr(BT, "MUTE_KEEPS_WATCHING", False)) else None # 10/14: ์Šค์œ„์น˜ False(์˜ˆ์ „ ๊ทœ์น™)
try:
raw_sells, jumps, state = BT.find_confirmed_sells(
stock, index, r["dates"], r["start_pos"], return_state=True, mute=_mute)
except TypeError: # ์˜› BT(mute ์—†์Œ) โ€” ๋™๊ธฐํ™” ์ „ ์•ˆ์ „์žฅ์น˜
raw_sells, jumps, state = BT.find_confirmed_sells(
stock, index, r["dates"], r["start_pos"], return_state=True)
_eng_muted = list(state.get("muted") or [])
sells = _real_sells(stock, low, ma240, raw_sells) # 4์ข… ํ•„ํ„ฐ
last_i = len(stock) - 1
# โ˜…2026-09-15 ๋ฒ„๊ทธ์ˆ˜์ • โ€” '๋ชจ์•„๊ฐ€๋Š” ๊ตฌ๊ฐ„(๋งค์ˆ˜ ๊ตญ๋ฉด)์˜ ์ „๋Ÿ‰๋งค๋„ ์•Œ๋žŒ์€ ๋ฌด์‹œ' ๊ทœ์น™์ด
# ์—ฌ๊ธฐ์—” **ํ†ต์งธ๋กœ ๋น ์ ธ ์žˆ์—ˆ๋‹ค.** final_sell_monitor.check_one ๊ณผ full_debug ์—๋Š” ์žˆ๋Š”๋ฐ
# app.py ๋Š” ์ž์ฒด check_one ์„ ๊ฐ–๊ณ  ์žˆ์–ด์„œ ํ•„ํ„ฐ๋ฅผ ํ•œ ๋ฒˆ๋„ ์•ˆ ๊ฑธ์—ˆ๋‹ค.
# โ†’ ์„ค๊ณ„๋ณธ์ด '๋ฌด์‹œ'๋กœ ํŒ๋‹จํ•œ ์‹ ํ˜ธ๊ฐ€ ์›น์—์„  [โ˜…์ง€๊ธˆ ๋งค๋„] ๋กœ ๋–ด๋‹ค
# (์‹ค์ธก 217730: 2026-09-09 ์ „๋Ÿ‰๋งค๋„ โ€” full_debug ๋Š” '๋ฌด์‹œ๋จ', ์—ฌ๊ธฐ์„  ๋นจ๊ฐ„ ๊ฒฝ๊ณ ).
# ๊ทœ์น™์€ final_sell_monitor(โ†’ phase_ranges, ๋‹จ์ผ ์ •๋ณธ)์—์„œ ๊ฐ€์ ธ์˜จ๋‹ค. ์—ฌ๊ธฐ์„œ ์žฌ๊ตฌํ˜„ ๊ธˆ์ง€.
# โ˜…2026-09-18 2์ฐจ์ˆ˜์ • โ€” ๋ฌด์‹œ๋œ ๊ฑด์ˆ˜๋ฅผ '์ง€๊ธˆ ์–˜๊ธฐ'์ฒ˜๋Ÿผ ๋ณด์—ฌ์ฃผ๋˜ ํ‘œ์‹œ ๋ฒ„๊ทธ.
# ๋ฌด์‹œ ์ž์ฒด๋Š” ์˜ณ์€๋ฐ(ํ•„ํ„ฐ๋Š” ์ •์ƒ), n_muted ์— **๋ช‡ ๋…„ ์ „ ์‹ ํ˜ธ๊นŒ์ง€** ๋‹ค ์„ธ์„œ
# ์˜ค๋Š˜ ํŒ์ • ์˜†์— '๋ชจ์•„๊ฐ€๋Š” ๊ตฌ๊ฐ„์ด๋ผ ๋ฌด์‹œ๋จ'์ด ๋ถ™์—ˆ๋‹ค โ†’ ์ง€๊ธˆ์ด ๋งค์ˆ˜ ๊ตญ๋ฉด์ธ ์ค„๋กœ ์ฝํžŒ๋‹ค.
# (์‹ค์ธก 078520: ๋งˆ์ง€๋ง‰ ๋ชจ์•„๊ฐ€๋Š” ๊ตฌ๊ฐ„์€ 2025-07-10~2026-02-04 ๋กœ ์ด๋ฏธ ๋๋‚ฌ๊ณ 
# 2026-08-11 ๋ถ€ํ„ฐ๋Š” '๋งค์ˆ˜์ค‘๋‹จ(๋ˆŒ๋ฆผ ํŒ์ •)' ์ธ๋ฐ๋„ ๊ทธ ๋ฌธ๊ตฌ๊ฐ€ ๋–ด๋‹ค.)
# โ‡’ **๋‚ ์งœ๋ฅผ ์ „๋ถ€ ๊ฐ™์ด ๋Œ๋ ค์ค€๋‹ค.** ๊ฑด์ˆ˜๋งŒ ๋ณด์—ฌ์ฃผ๋ฉด ๊ทธ๊ฒŒ ์–ธ์ œ ์–˜๊ธด์ง€ ์•Œ ์ˆ˜๊ฐ€ ์—†๋‹ค
# (2026-09-19 ์ง€์ : '๋ชจ์•„๊ฐ€๋Š” ๊ตฌ๊ฐ„์ด ์–ธ์ œ๊ณ  ๋ฌด์‹œ๋œ 2๊ฑด์ด ์–ธ์ œ๊ณ  ์ง€๊ธˆ์„
# ์–˜๊ธฐํ•˜๋Š” ๊ฑด์ง€ ์ „ํ˜€ ์•Œ ์ˆ˜๊ฐ€ ์—†๋‹ค'). ์ˆซ์ž ๋ง๊ณ  ๋‚ ์งœ๊ฐ€ ๋‹ต์ด๋‹ค.
accum_now = False; n_muted = 0; muted_dates = []; muted_rngs = []; cur_rng = None
try:
import final_sell_monitor as _FM
if getattr(_FM, "IGNORE_SELL_IN_ACCUM", True):
rngs = _FM.accum_ranges(code)
if rngs:
def _rng_of(ds):
for _a, _b in rngs:
if ds >= _a and (_b is None or ds < _b):
return (_a, _b)
return None
_all = sells
sells = [s for s in _all if not _FM.in_accum(s["date"], rngs)]
_muted = sorted([s for s in _all if _FM.in_accum(s["date"], rngs)] + _eng_muted,
key=lambda x: x["i"]) # โ˜…10/14 ์—”์ง„ ์•ˆ์—์„œ ๋ฌด์‹œ๋œ ๊ฒƒ๋„ ํ‘œ์‹œ
n_muted = len(_muted)
muted_dates = [s["date"] for s in _muted]
for _s in _muted: # ๋ฌด์‹œ๋œ ์‹ ํ˜ธ๊ฐ€ '์–ด๋А ๊ตฌ๊ฐ„' ์•ˆ์ด์—ˆ๋‚˜
_rg = _rng_of(_s["date"])
if _rg and _rg not in muted_rngs:
muted_rngs.append(_rg)
_today = r["dates"][last_i]
accum_now = _FM.in_accum(_today, rngs)
cur_rng = _rng_of(_today) if accum_now else None
except Exception:
pass # ๊ทœ์น™์„ ๋ชป ๋ถˆ๋Ÿฌ์˜ค๋ฉด ์˜ˆ์ „ ๋™์ž‘(ํ•„ํ„ฐ ์—†์Œ)์œผ๋กœ โ€” ์กฐ์šฉํžˆ ์ฃฝ์ง€ ์•Š๊ฒŒ ์•„๋ž˜์„œ ํ‘œ์‹œ
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:
# โ˜…10/14 ๋ฌธ๊ตฌ ๋ฒ„๊ทธ: 4์ข… ํ•„ํ„ฐ์— ์•ˆ ๊ฑธ๋ ธ๋Š”๋ฐ ๋น ์ง„ ๊ฑฐ๋ฉด = ๋ชจ์•„๊ฐ€๋Š” ๊ตฌ๊ฐ„ ๋•Œ๋ฌธ("์กฐ๊ฑด ๋ฏธ๋‹ฌ" ์•„๋‹˜)
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,
"accum_now": accum_now, # ์ง€๊ธˆ ๋ชจ์•„๊ฐ€๋Š” ๊ตฌ๊ฐ„์ธ๊ฐ€
"n_muted": n_muted, # ๊ทธ ๊ตฌ๊ฐ„์ด๋ผ ๋ฌด์‹œํ•œ ๋งค๋„์‹ ํ˜ธ ์ˆ˜(์ „ ๊ธฐ๊ฐ„)
"muted_dates": muted_dates, # ๋ฌด์‹œ๋œ ์‹ ํ˜ธ๊ฐ€ ๋‚œ ๋‚ ์งœ๋“ค โ† '์–ธ์ œ ๊ฑด์ง€'
"muted_rngs": muted_rngs, # ๊ทธ ์‹ ํ˜ธ๋“ค์ด ๋“ค์–ด์žˆ๋˜ ๋ชจ์•„๊ฐ€๋Š” ๊ตฌ๊ฐ„(๋“ค)
"cur_rng": cur_rng, # ์ง€๊ธˆ์ด ๋ชจ์•„๊ฐ€๋Š” ๊ตฌ๊ฐ„์ด๋ฉด ๊ทธ ๊ตฌ๊ฐ„, ์•„๋‹ˆ๋ฉด None
"last_sell": sells[-1] if sells else None,
"trigger": trigger,
"skipped": skipped,
"state": state,
# โ˜…2026-10-13: ๋ฐ์ดํ„ฐ ๋‚ ์งœ(์ง€์ˆ˜ ๋Šฆ์Œ ๊ฐ์ง€). ์ง€์ˆ˜๊ฐ€ ์ข…๋ชฉ๋ณด๋‹ค ๋Šฆ์œผ๋ฉด ์˜ค๋Š˜ ์ด๊ฒฉ์€ ๊ฐ€์งœ์ผ ์ˆ˜ ์žˆ๋‹ค.
"stock_last": r.get("stock_last"),
"index_last": r.get("index_last"),
"stale": bool(r.get("stock_last") and r.get("index_last")
and r.get("index_last") < r.get("stock_last")),
}
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 = []
held = [] # โ˜…10/13 ์ง€์ˆ˜ ๋ฐ์ดํ„ฐ ๋Šฆ์–ด์„œ ํŒ๋‹จ ๋ณด๋ฅ˜ํ•œ ์ข…๋ชฉ
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์„  ์—†์Œ")
# ์™œ ์กฐ์šฉํ•œ์ง€ ๋ณด์ด๊ฒŒ ํ•œ๋‹ค(์•ˆ ๊ทธ๋Ÿฌ๋ฉด '์‹ ํ˜ธ๊ฐ€ ์—†๋Š” ๊ฒƒ'๊ณผ ๊ตฌ๋ถ„์ด ์•ˆ ๋จ).
# โ˜… ๊ฑด์ˆ˜๋งŒ ์“ฐ์ง€ ์•Š๋Š”๋‹ค โ€” **๊ตฌ๊ฐ„์ด ์–ธ์ œ๋ถ€ํ„ฐ ์–ธ์ œ๊นŒ์ง€ / ๋ฌด์‹œ๋œ ์‹ ํ˜ธ๊ฐ€ ์–ธ์ œ /
# ์ง€๊ธˆ์ด ๊ทธ ๊ตฌ๊ฐ„์ธ์ง€** ์…‹ ๋‹ค ๋‚ ์งœ๋กœ ์ ๋Š”๋‹ค. ๊ฑด์ˆ˜๋งŒ ์žˆ์œผ๋ฉด ์ง€๊ธˆ ์–˜๊ธด ์ค„ ์ฝํžŒ๋‹ค.
_md = res.get("muted_dates") or []
_cur = res.get("cur_rng"); _mrg = res.get("muted_rngs") or []
if res.get("accum_now") and _cur:
verdict_note = (f"\n> ๐Ÿ”‡ **์ง€๊ธˆ({res['last_date']})์ด ๋ชจ์•„๊ฐ€๋Š” ๊ตฌ๊ฐ„"
f"({_rng_txt(_cur)}) โ€” ์ „๋Ÿ‰๋งค๋„ ์•Œ๋žŒ ๊บผ๋‘ **\n")
# โ˜…10/14 ๋ฌธ๊ตฌ ๋ฒ„๊ทธ: '์ด ๊ตฌ๊ฐ„' ์ด๋ผ๋ฉด์„œ ๋‹ค๋ฅธ(๊ณผ๊ฑฐ) ๊ตฌ๊ฐ„ ๋‚ ์งœ๊นŒ์ง€ ์„ž์–ด ๋ณด์—ฌ์คฌ๋‹ค โ†’ ์ง€๊ธˆ ๊ตฌ๊ฐ„ ๊ฒƒ๋งŒ / ๊ณผ๊ฑฐ ๊ฒƒ์€ ๋”ฐ๋กœ
_a0, _b0 = _cur
_md_now = [d for d in _md if d >= _a0 and (_b0 is None or d < _b0)]
_md_old = [d for d in _md if d not in _md_now]
if _md_now:
verdict_note += (f"> ใ†์ด ๊ตฌ๊ฐ„์—์„œ ๋ฌด์‹œ๋œ ์‹ ํ˜ธ {len(_md_now)}๊ฑด: "
f"{_dates_txt(_md_now)}\n")
if _md_old:
verdict_note += (f"> ใ†(์ฐธ๊ณ ) ์ง€๋‚œ ๋ชจ์•„๊ฐ€๋Š” ๊ตฌ๊ฐ„๋“ค์—์„œ ๋ฌด์‹œ๋œ ์‹ ํ˜ธ {len(_md_old)}๊ฑด: "
f"{_dates_txt(_md_old)}\n")
elif _md:
_g = (" (" + ", ".join(_rng_txt(g) for g in _mrg) + ")") if _mrg else ""
verdict_note = (f"\n> ๐Ÿ”‡ **์ง€๋‚œ** ๋ชจ์•„๊ฐ€๋Š” ๊ตฌ๊ฐ„{_g} ์•ˆ์—์„œ ๋‚œ "
f"์ „๋Ÿ‰๋งค๋„ ์‹ ํ˜ธ {len(_md)}๊ฑด์€ ๋ฌด์‹œ๋จ: {_dates_txt(_md)}\n"
f"> ใ†**์ง€๊ธˆ({res['last_date']})์€ ๋ชจ์•„๊ฐ€๋Š” ๊ตฌ๊ฐ„์ด ์•„๋‹˜** โ€” "
f"์ง€๊ธˆ ์‹ ํ˜ธ๊ฐ€ ๋‚˜๋ฉด ๋ฌด์‹œ ์•ˆ ํ•˜๊ณ  ์•Œ๋žŒ ์šธ๋ฆผ\n")
else:
verdict_note = ""
_stale = res.get("stale")
_il = res.get("index_last") or ""
_stale_note = (f"\n> โš ๏ธ **{res['market']} ์ง€์ˆ˜ ๋ฐ์ดํ„ฐ๊ฐ€ {_il} ์— ๋ฉˆ์ถฐ ์žˆ์Œ** "
f"(์ข…๋ชฉ ๋ฐ์ดํ„ฐ๋Š” {res.get('stock_last')}) โ€” ๊ทธ ๋’ค ๋‚ ์งœ์˜ ์ด๊ฒฉ์€ ์ง€์ˆ˜ ์›€์ง์ž„์„ ๋ฐ˜์˜ ๋ชป ํ•ด "
f"**ํ‹€๋ฆฌ๊ฒŒ** ๋‚˜์˜ต๋‹ˆ๋‹ค(์ง€์ˆ˜๊ฐ€ ๋น ์ง„ ๋‚ ์ด๋ฉด ์‹ค์ œ๋ณด๋‹ค ๋‚˜์˜๊ฒŒ). ์ด ์ƒํƒœ์˜ ๋นจ๊ฐ„ ๋ถˆ์€ ๋ฏฟ์ง€ ๋งˆ์„ธ์š”. ์ž ์‹œ ๋’ค ๋‹ค์‹œ ์กฐํšŒ.\n") if _stale else ""
if res["recent"] and _stale and res["recent"][-1]["date"] > _il:
sl = res["recent"][-1]
held.append(_label(code))
verdict = (f"### ๐ŸŸ  [ํŒ๋‹จ ๋ณด๋ฅ˜ โ€” ์ง€์ˆ˜ ๋ฐ์ดํ„ฐ ๋Šฆ์Œ] {_label(code)} ({res['market']})\n"
f"- ๊ณ„์‚ฐ์ƒ ์ „๋Ÿ‰๋งค๋„ {sl['date']} @ {sl['price']:,.0f} (์ด๊ฒฉ {sl['div']*100:.0f}%) ์ด์ง€๋งŒ, "
f"๊ทธ๋‚  ์ง€์ˆ˜๊ฐ€ ์•„์ง ์•ˆ ๋“ค์–ด์™€์„œ(๋งˆ์ง€๋ง‰ {_il}) **๊ฐ€์งœ์ผ ๊ฐ€๋Šฅ์„ฑ์ด ํผ**. ํŒ”์ง€ ๋ง๊ณ  ๋‹ค์‹œ ์กฐํšŒ.\n")
elif 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")
# โ˜…2026-10-14 '๋ฌด์‹œ ํ›„ ๋” ๋น ์ง' ๊ฒฝ๊ณ (๋ด‡ ํ…”๋ ˆ๊ทธ๋žจ๊ณผ ๊ฐ™์€ ๊ธฐ์ค€: ๊ตฌ๊ฐ„ ๋๋‚œ ๋’ค 5%p+ ๋” ๊นŠ์–ด์ง). ๋งค๋งค์—” ์˜ํ–ฅ ์—†์Œ.
_cd = st.get("div")
if not res.get("accum_now") and _cd is not None \
and (sl["div"] - _cd) >= 0.05:
verdict = verdict.replace("### ๐ŸŸข [๋ณด์œ ]", "### ๐ŸŸ  [๋ณด์œ  ยท ํ™•์ธ ํ•„์š”]", 1)
verdict += (f"- โš ๏ธ **๋ฌด์‹œํ•œ ๋’ค๋กœ ์ด๊ฒฉ์ด {(sl['div'] - _cd)*100:.0f}%p ๋” ๋น ์ง** "
f"(๊ทธ๋•Œ {sl['div']*100:.0f}% โ†’ ์ง€๊ธˆ {_cd*100:.1f}%). ๊ทœ์น™์ƒ ์ƒˆ ๋งค๋„ ์‹ ํ˜ธ๋Š” ์•ˆ ๋‚˜์š”"
f"(์ด๊ฒฉ โˆ’10% ํšŒ๋ณต ์ „๊นŒ์ง€) โ€” ๋ด‡์€ ์•ˆ ํŒ๋‹ˆ๋‹ค. **์ง์ ‘ ํŒ๋‹จํ•˜์„ธ์š”.**\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"
# โ˜…2026-10-16 ์ง€์ˆ˜ ์ฃผ๋„ํ˜• ์ด๊ฒฉ ๊ฑด๋„ˆ๋œ€(BT.INDEX_LED_SKIP='half', watch) โ€” ์™œ ์•ˆ ์šธ๋ ธ๋Š”์ง€ ๋ณด์ด๊ฒŒ
_isk = st.get("idx_skip") or []
if _isk and (st.get("last_i", 0) - _isk[-1]["i"]) <= 60:
_k = _isk[-1]; _ago = st.get("last_i", 0) - _k["i"]
verdict += (f"\n> โ„น๏ธ **{_k['date']} ์ „๋Ÿ‰๋งค๋„ ์กฐ๊ฑด์ด์—ˆ์ง€๋งŒ ๊ฑด๋„ˆ๋œ€** @ {_k['price']:,.0f} "
f"(์ด๊ฒฉ {_k['div']*100:.0f}%) ยท {_ago}๊ฑฐ๋ž˜์ผ ์ „\n"
f"> ใ†์ด์œ : ์ข…๋ชฉ์ด ๋น ์ง„ ๊ฒƒ๋ณด๋‹ค **์ง€์ˆ˜๊ฐ€ ์˜ค๋ฅธ ๊ฒŒ ๋” ์ปค์„œ** ์ƒ๊ธด ์ด๊ฒฉ(ํญ๋ฝ ์•„๋‹˜)\n"
f"> ใ†ํŒ”์•˜์Œ์œผ๋กœ ์•ˆ ์น˜๊ณ  ๊ณ„์† ์ง€์ผœ๋ด„ โ€” ์ด๊ฒฉ์ด 2.5%p ๋” ๊นŠ์–ด์ง€๋ฉด ๋‹ค์‹œ ํŒ๋‹จ(๊ทธ๋•Œ ์ข…๋ชฉ ํƒ“์ด๋ฉด ๋งค๋„)\n")
if verdict_note:
verdict += verdict_note # โ˜… ์™œ ์กฐ์šฉํ•œ์ง€ ๋ณด์ด๊ฒŒ(๋ฌด์‹œ๋œ ๊ฑด์ˆ˜)
if _stale_note:
verdict += _stale_note # โ˜…10/13 ์ง€์ˆ˜ ๋Šฆ์Œ ๊ฒฝ๊ณ 
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 (" (๋ฌด์‹œ โ€” ํŒ”์•˜์Œ ์•„๋‹˜, ๊ณ„์† ์ง€์ผœ๋ด„)" if "๋ฌด์‹œ" in stp["name"]
else (" (์ง€์ˆ˜๋งŒ ์˜ฌ๋ผ์„œ ์ƒ๊ธด ์ด๊ฒฉ โ€” ๊ฑด๋„ˆ๋œ€, ๊ณ„์† ์ง€์ผœ๋ด„)" 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 held:
out.append(f"๐ŸŸ  **์ง€์ˆ˜ ๋ฐ์ดํ„ฐ ๋Šฆ์–ด์„œ ํŒ๋‹จ ๋ณด๋ฅ˜: {len(held)}์ข…๋ชฉ โ†’ {', '.join(held)}** (์ž ์‹œ ๋’ค ๋‹ค์‹œ ์กฐํšŒ)\n\n")
if alerts:
out.append(f"**์˜ค๋Š˜ ํ™•์ธ ํ•„์š”: {len(alerts)}์ข…๋ชฉ โ†’ {', '.join(alerts)}**")
elif not held:
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()