Spaces:
Running
Running
| # -*- coding: utf-8 -*- | |
| """ | |
| ์ ๋๋งค๋ ์ตํ๋ณด๋ฃจ - ๋ณด์ ์ข ๋ชฉ ๋ชจ๋ํฐ | |
| ===================================== | |
| confirm_sell_backtest.py ์ ํต์ฌ ๋ก์ง์ ๊ทธ๋๋ก import ํด์ ์ด๋ค(ํ๋ฆฌํฐ 100%). | |
| ๋ฐฑํ ์คํฐ๋ ๊ณผ๊ฑฐ ์ ์ฒด ๋งค๋๋ด์ญ์ ๋ฝ์ง๋ง, ์ด ๋ชจ๋ํฐ๋ | |
| "๋ด ๋ณด์ ์ข ๋ชฉ์ด '์ค๋/์ต๊ทผ' ๋งค๋์ ํธ๊ฐ ๋ด๋๊ฐ"๋ง ๋ณธ๋ค. | |
| ์ฐ๋ ๋ฒ: | |
| 1) ๊ฐ์ ํด๋์ confirm_sell_backtest.py ๊ฐ ์์ด์ผ ํจ. | |
| 2) (์ ํ) ๊ฐ์ ํด๋์ market_map.csv (code,market,name) ์์ผ๋ฉด ์์ฅ+์ข ๋ชฉ๋ช ์๋ํ์. | |
| ์์ผ๋ฉด fdr.StockListing ์ผ๋ก ์์ฅ๋ง ํ๋ณ(์ด๋ฆ์ ์ฝ๋๋ก ๋์ฒด). | |
| 3) ์๋ WATCH ์ ๋ณด์ ์ข ๋ชฉ ์ฝ๋๋ง ๋ฃ๋๋ค. | |
| 4) python final_sell_monitor.py | |
| 5) [๋งค๋] ๋ก ๋จ๋ฉด ๊ทธ ์ข ๋ชฉ์ ์ต๊ทผ ALERT_DAYS ๊ฑฐ๋์ผ ์์ ์ ๋๋งค๋ ์ ํธ๊ฐ ๋ ๊ฒ. | |
| ์๊ณ๊ฐ(A-4ยท๊ฐ๋ณ์ด๊ฒฉ ๋ฑ)์ ์ ๋ถ confirm_sell_backtest.py ๋ฅผ ๋ฐ๋ผ๊ฐ๋ค. | |
| """ | |
| import sys, ssl, os | |
| from datetime import datetime, timezone, timedelta | |
| import FinanceDataReader as fdr | |
| import confirm_sell_backtest as BT # ํต์ฌ ๋ก์ง/์ค์ ๊ทธ๋๋ก (main()์ __main__ ๊ฐ๋๋ผ ์คํ ์ ๋จ) | |
| import buy_engine_dipfix as be # ๋งค์ ์์ง (์ผ๋ด ๋์ฐ/์์๋ ๊ตญ๋ฉด โ '๋ชจ์๊ฐ๋ ๊ตฌ๊ฐ' ํ๋ณ). dipfix=ํ์ฑ ์ต์ ๋ณธ์ผ๋ก ํต์ผ | |
| 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} ์ข ๊ฐ ๊ธฐ์ค" | |
| ssl._create_default_https_context = ssl._create_unverified_context | |
| if sys.platform == 'win32': | |
| try: | |
| sys.stdout.reconfigure(encoding='utf-8') | |
| except Exception: | |
| pass | |
| HERE = os.path.dirname(os.path.abspath(__file__)) | |
| # build_market_map.py / market_map.csv / ๋ชจ๋ํฐ ๋ค ๊ฐ์ ํด๋(sell_debug)์ ๋ . | |
| MAP_PATH = os.path.join(HERE, 'market_map.csv') | |
| # ===================================================== | |
| # [์ฌ๊ธฐ๋ง ๋ฐ๊พธ๋ฉด ๋จ] ๋ณด์ ์ข ๋ชฉ ์ฝ๋ | |
| # ===================================================== | |
| # ===================================================== | |
| # ๋ชจ์๊ฐ๋ ๊ตฌ๊ฐ (๋งค์ ๊ตญ๋ฉด) โ ์ด ๊ตฌ๊ฐ์ ์ ๋๋งค๋ ์๋์ ๋ฌด์ | |
| # ๊ตฌ๊ฐ = ๋์ฐ์ฑ๊ณต(์์๋ ์์) ~ 2๋ฒ์งธ ์๋ ์ข ๋ฃ(240์ฌ) | |
| # ์ค๊ฐ์ ์๋ช ์ ์ดํ/์ฌ์ดํด์ข ๋ฃ/๋๋ธ๋์ฐ ๋์ค๋ฉด ๊ฑฐ๊ธฐ์ ๋ | |
| # ===================================================== | |
| IGNORE_SELL_IN_ACCUM = True # False ๋ก ํ๋ฉด ๊ธฐ์กด์ฒ๋ผ ํญ์ ์๋ | |
| _accum_cache = {} | |
| def accum_ranges(tk): | |
| """buy_engine ์ผ๋ด ๊ตญ๋ฉด์์ '๋ชจ์๊ฐ๋ ๊ตฌ๊ฐ' [(์์์ผ, ๋์ผ|None)]""" | |
| if tk in _accum_cache: | |
| return _accum_cache[tk] | |
| out = [] | |
| try: | |
| df = fdr.DataReader(tk, '2000-01-01') | |
| if df is not None and len(df) > 300 and 'Close' in df.columns: | |
| ev, buys, mts = be.run(df, '1day') | |
| start = None | |
| for d, tag, dt in ev: | |
| ds = str(d)[:10] | |
| if tag == '๋์ฐ์ฑ๊ณตโ์์๋์์': | |
| start = ds | |
| elif start and (('2๋ฒ์งธ ์๋ ์ข ๋ฃ' in tag) or ('240์ฌ' in tag) | |
| or ('์๋ช ์ ์ดํ' in tag) or ('์ฌ์ดํด์ข ๋ฃ' in tag) | |
| or ('๋๋ธ๋์ฐ' in tag)): | |
| out.append((start, ds)); start = None | |
| if start: | |
| out.append((start, None)) # ์งํ ์ค | |
| except Exception: | |
| out = [] | |
| _accum_cache[tk] = out | |
| return out | |
| def in_accum(ds, ranges): | |
| return any(ds >= a and (b is None or ds < b) for a, b in ranges) | |
| WATCH = [ | |
| # '277810', # ๋ ์ธ๋ณด์ฐ๋ก๋ณดํฑ์ค | |
| # '373220', # LG์๋์ง์๋ฃจ์ | |
| # ์ฝ๋ ๋์ ์ข ๋ชฉ๋ช ์ผ๋ก๋ ๋จ! ์: '๋ ์ธ๋ณด์ฐ๋ก๋ณดํฑ์ค', 'LG์๋์ง์๋ฃจ์ ' | |
| # '060370', | |
| '483650', # ๋ฌ๋ฐ๊ธ๋ก๋ฒ | |
| '096770', # SK์ด๋ ธ๋ฒ ์ด์ | |
| '090430', # ์๋ชจ๋ ํผ์ํฝ | |
| '326030', # SK๋ฐ์ด์คํ | |
| '035420', # NAVER | |
| '352820', # ํ์ด๋ธ | |
| '003490', # ๋ํํญ๊ณต | |
| '025540', # ํ๊ตญ๋จ์ | |
| '373220', # LG์๋์ง์๋ฃจ์ | |
| '443060', # HDํ๋๋ง๋ฆฐ์๋ฃจ์ | |
| '032350', # ๋กฏ๋ฐ๊ด๊ด๊ฐ๋ฐ | |
| '005380', # ํ๋์ฐจ | |
| '003670', # ํฌ์ค์ฝํจ์ฒ์ | |
| '011790', # SKC | |
| '005930', # ์ผ์ฑ์ ์ | |
| '000660', # SKํ์ด๋์ค | |
| '078520', # ์์ด๋ธ์จ์์จ | |
| '206640', # ๋ฐ๋ํ ๋ฉ๋ | |
| '302440', # SK๋ฐ์ด์ค์ฌ์ด์ธ์ค | |
| '328130', # ๋ฃจ๋ | |
| '005250', # ๋ น์ญ์ํ๋ฉ์ค | |
| '234690', # ๋ น์ญ์์ฐ๋น | |
| '083930', # ์๋ฐ์ฝ | |
| '195940', # HK์ด๋ ธ์ | |
| '122870', # ์์ด์ง์ํฐํ ์ธ๋จผํธ | |
| '112610', # ์จ์์ค์๋ | |
| '257720', # ์ค๋ฆฌ์ฝํฌ | |
| '086710', # ์ ์ง๋ทฐํฐ์ฌ์ด์ธ์ค | |
| '217730', # ๊ฐ์คํ ๋ฐ์ด์คํ | |
| '003570', # SNT๋ค์ด๋ด๋ฏน์ค | |
| '194370', # ์ ์ด์์ค์ฝํผ๋ ์ด์ | |
| '131030', # ์ตํฌ์ค์ ์ฝ | |
| '204320', # HL๋ง๋ | |
| '096530', # ์จ์ | |
| '035900', # JYP Ent. | |
| '178920', # PI์ฒจ๋จ์์ฌ | |
| '216080', # ์ ํ ๋ง | |
| '099320', # ์ํธ๋ ์์ด | |
| '088130', # ๋์์ํ | |
| '420770', # ๊ธฐ๊ฐ๋น์ค | |
| '222080', # SFA๋ฐ๋์ฒด | |
| '147830', # ์ ๋ฃก์ฐ์ | |
| '281820', # ์ผ์ด์จํ | |
| '017510', # ์ธ๋ช ์ ๊ธฐ | |
| '112290', # ์์ด์จ์ผ | |
| '089010', # ์ผํธ๋ก๋์ค | |
| '102120', # ์ด๋ณด๋ธ๋ฐ๋์ฒด | |
| '251370', # ์์ด์์ด์นํฐ | |
| '094360', # ์นฉ์ค์ค๋ฏธ๋์ด | |
| '166090', # ํ๋๋จธํฐ๋ฆฌ์ผ์ฆ | |
| '277810', # ๋ ์ธ๋ณด์ฐ๋ก๋ณดํฑ์ค | |
| '389020', # ์๋ํ ํฌ๋๋ก์ง | |
| '161580', # ํ์ตํฑ์ค | |
| '232140', # ์์ด์จ | |
| '033100', # ์ ๋ฃก์ ๊ธฐ | |
| '031980', # ํผ์์ค์ผ์ดํ๋ฉ์ค | |
| '403870', # HPSP | |
| '183300', # ์ฝ๋ฏธ์ฝ | |
| '445090', # ์์ด์ง๋๋ | |
| '347850', # ๋์ค๋ํ๋งํ | |
| '030530', # ์์ตํ๋ฉ์ค | |
| '399720', # ๊ฐ์จ์นฉ์ค | |
| '042700', # ํ๋ฏธ๋ฐ๋์ฒด | |
| '440110', # ํ๋ | |
| '039030', # ์ด์คํ ํฌ๋์ค | |
| ] | |
| ALERT_DAYS = 5 # ์ต๊ทผ ์ด ๊ฑฐ๋์ผ ์์ ๋งค๋์ ํธ๊ฐ ๋ฌ์ผ๋ฉด [๋งค๋]๋ก ์๋ฆผ | |
| # ===================================================== | |
| # market_map.csv ๋ก๋ (code -> (market, name)). ์์ผ๋ฉด StockListing ํด๋ฐฑ. | |
| # ===================================================== | |
| def load_market_map(): | |
| m = {} | |
| if os.path.exists(MAP_PATH): | |
| 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 or parts[0].lower() == 'code': | |
| continue | |
| name = parts[2] if len(parts) >= 3 else '' | |
| m[parts[0].zfill(6)] = (parts[1].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 _code, (_mkt, _nm) in MARKET_MAP.items(): | |
| if _nm: | |
| NAME_TO_CODE[_norm(_nm)] = _code | |
| _NORM_NAMES.append(_norm(_nm)) | |
| 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}" | |
| # ์คํ ํ์ฉ: ๊ฐ์ฅ ๋น์ทํ ์ด๋ฆ (difflib) | |
| 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 detect_via_stocklisting(tickers): | |
| """market_map.csv ์์ ๋ ํด๋ฐฑ (ํ๊ตญ IP ๋ฐ์คํฌํ์์๋ง ๋จ).""" | |
| try: | |
| kospi = set(fdr.StockListing('KOSPI')['Code'].astype(str)) | |
| except Exception: | |
| kospi = set() | |
| try: | |
| kosdaq = set(fdr.StockListing('KOSDAQ')['Code'].astype(str)) | |
| except Exception: | |
| kosdaq = set() | |
| out = {} | |
| for tk in tickers: | |
| out[tk] = 'KOSPI' if tk in kospi else 'KOSDAQ' | |
| return out | |
| def market_of(code, fallback): | |
| if code.zfill(6) in MARKET_MAP: | |
| return MARKET_MAP[code.zfill(6)][0] | |
| return fallback.get(code, 'KOSDAQ') | |
| def label_of(code): | |
| """'๋ ์ธ๋ณด์ฐ๋ก๋ณดํฑ์ค(277810)' ํํ. ์ด๋ฆ ์์ผ๋ฉด ์ฝ๋๋ง.""" | |
| nm = MARKET_MAP.get(code.zfill(6), ('', ''))[1] | |
| return f"{nm}({code.zfill(6)})" if nm else code.zfill(6) | |
| def _skip_reason(stock, low, ma240, s): | |
| """์ด ๋งค๋๋ฅผ ์ ๋ฌด์ํ๋์ง ์ฌ๋ ๋ง๋ก. ๋ฌด์ ์ ํ๋ฉด None.""" | |
| 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': | |
| tol = getattr(BT, 'BELOW_240_TOL', 0.05) | |
| dpct = f'{days*100:.1f}%' if days is not None else '' | |
| return f'์ ํธ ๋น์ผ ์ข ๊ฐ๊ฐ 240์ผ์ -{tol*100:.0f}% ๋๊ฒ ํ์ฐธ ์๋(-{dpct}) โ ์ด๋ฏธ ๋ฆ์ ์ ํธ๋ผ์ (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 # PENDING(์ต๊ทผ์ ํธ)์ด๋ฉด ์ฌ๊ธฐ๋ก -> ์ ์ง์ | |
| def _div_explain(st): | |
| """์ด๊ฒฉ์ด ๋ฌด์จ ๋ป์ธ์ง ๊ณ์ฐ ๋ด์ญ์ ํ์ด์.""" | |
| if not st or 'stock_from_peak' not in st: | |
| return '' | |
| return (f"์ง๊ธ ์ด๊ฒฉ {st['div']*100:.1f}% @ํ์ฌ๊ฐ " | |
| f"= ์ข ๋ชฉ์ด ๊ณ ์ ๋๋น {st['stock_from_peak']*100:.1f}% " | |
| f"(๊ณ ์ {st['peak_price']:,.0f} @ {st['peak_date']}) " | |
| f"โ ์ง์๋ ๊ฐ์๊ธฐ๊ฐ {st['index_from_peak']*100:+.1f}%") | |
| 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 | |
| # cat == 'PENDING' (์ต๊ทผ ์ ํธ๋ผ ํ์ ๋ถ๊ฐ) -> ์ ๋ ์ ์ง์. ์๋ ์ด๋ฆผ! | |
| out.append(s) | |
| return out | |
| def check_one(tk, market): | |
| r = BT.load_one(tk, market) | |
| if r is None: | |
| return {'tk': tk, '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) | |
| # โ ๋ชจ์๊ฐ๋ ๊ตฌ๊ฐ(๋งค์ ๊ตญ๋ฉด)์ ๋งค๋์ ํธ๋ ๋ฌด์ | |
| accum_now = False; n_muted = 0 | |
| if IGNORE_SELL_IN_ACCUM: | |
| rngs = accum_ranges(tk) | |
| if rngs: | |
| before = len(sells) | |
| sells = [s for s in sells if not in_accum(s['date'], rngs)] | |
| n_muted = before - len(sells) | |
| accum_now = in_accum(r['dates'][len(stock) - 1], rngs) | |
| last_i = len(stock) - 1 | |
| recent = [s for s in sells if s['i'] >= last_i - ALERT_DAYS] | |
| # ์ง๊ธ sold ์ํ๋ฅผ ๋ง๋ '๋ฐ๋ก ๊ทธ ๋งค๋'๋ฅผ ์ฐพ๋๋ค. | |
| # ํํฐ์ ๊ฑธ๋ฌ์ง ๋งค๋๊ฐ ๋ง๋ ์ํ๋ฉด -> ๊ทธ ์ํ๋ ํ์ ์ ํจ(๋ฌด์ํ๊ธฐ๋ก ํ ์๋์ด๋ฏ๋ก) | |
| trigger = None; skipped = None | |
| if state.get('sold') and state.get('sold_i') is not None: | |
| trigger = next((s for s in sells if s['i'] == state['sold_i']), None) | |
| if trigger is None: | |
| # ๊ทธ ๋งค๋๋ ํํฐ๋ก ๋ฌด์๋ ๊ฒ -> ์ ๋ฌด์ํ๋์ง ์ฌ์ ๋ฅผ ์ฐพ์๋๋ค | |
| raw = next((s for s in raw_sells if s['i'] == state['sold_i']), 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 { | |
| 'tk': tk, '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, | |
| 'accum_now': accum_now, 'n_muted': n_muted, | |
| } | |
| def status_line(res): | |
| """ํ์ฌ ๋จ๊ณ๋ฅผ ์งง๊ฒ: ๊ด๋ง / flagON ๋๊ธฐ / 1์ฐจ / 2์ฐจflagON ๋๊ธฐ + ์ด๊ฒฉ.""" | |
| if res.get('accum_now'): | |
| mut = f" ยท ๋ฌด์๋ ๋งค๋์ ํธ {res['n_muted']}๊ฑด" if res.get('n_muted') else "" | |
| return f"โ ๋ชจ์๊ฐ๋ ๊ตฌ๊ฐ (๋์ฐ ์์๋ ๋งค์ ๊ตญ๋ฉด) โ ๋งค๋์๋ ๋ฌด์ ์ค{mut}" | |
| st = res.get('state') | |
| if not st: | |
| return '๊ด๋ง (์ ํธ ๋๊ธฐ)' | |
| div_now = st['div'] * 100 | |
| if st.get('sold'): | |
| return (f'๊ณผ๊ฑฐ ๋งค๋์ ํธ ์ ์ง์ค (์ง๊ธ ํ๋ผ๋ ์ ํธ ์๋) ยท ์ง๊ธ ์ด๊ฒฉ {div_now:.1f}%') | |
| nm = {'์์': '๊ด๋ง โ ์์ง ์๋ฌด ์ ํธ ์์', | |
| 'flagON': '1์ฐจ flag ON โ ์ด๊ฒฉ์ด -10% ๋๋ฌ, ๊ด์ฐฐ ์์', | |
| '1์ฐจ': '1์ฐจ ๊ฒฝ๊ณ โ ๋ ๊น์ด์ง (๋ค์: 2์ฐจ flag)', | |
| '2์ฐจflagON': '2์ฐจ flag ON โ ํ ๋ฒ ๋ ๊น์ด์ง๋ฉด ์ ๋๋งค๋!', | |
| '2์ฐจ๋งค๋': '๊ด๋ง โ ์์ง ์๋ฌด ์ ํธ ์์'}.get(st['stage_name'], st['stage_name']) | |
| return nm | |
| def main(): | |
| if getattr(BT, 'ENGINE_VERSION', 0) < 3: | |
| print("!" * 66) | |
| print("[๊ฒฝ๊ณ ] confirm_sell_backtest.py ๊ฐ ๊ตฌ๋ฒ์ ์ด์ผ!") | |
| print(" -> ๋จ๊ณ ์งํ๋ด์ญ/๊ณ ์ ์ ๋ณด๊ฐ ์ ๋์. ์ต์ ๋ฐฑํ ์คํฐ๋ก ๊ต์ฒดํด์ค.") | |
| print("!" * 66) | |
| print("=" * 66) | |
| print(f"์ ๋๋งค๋ ์ตํ๋ณด๋ฃจ - ๋ณด์ ์ข ๋ชฉ ๋ชจ๋ํฐ (์ต๊ทผ {ALERT_DAYS}๊ฑฐ๋์ผ ๊ธฐ์ค)") | |
| if not MARKET_MAP: | |
| print("(market_map.csv ์์ -> StockListing ์ผ๋ก ์์ฅ๋ง ํ๋ณ, ์ด๋ฆ์ ์ฝ๋๋ก ํ์)") | |
| print("=" * 66) | |
| fallback = {} if MARKET_MAP else detect_via_stocklisting(WATCH) | |
| alerts = [] | |
| asof = None | |
| for _q in WATCH: | |
| tk, msg = resolve_to_code(_q) | |
| if tk is None: | |
| print(f" [๊ฑด๋๋] {msg}") | |
| continue | |
| market = market_of(tk, fallback) | |
| res = check_one(tk, market) | |
| lbl = label_of(tk) + (f" {msg}" if msg else "") | |
| if not res['ok']: | |
| print(f" {lbl} ({market}) - {res['msg']}") | |
| continue | |
| asof = res['last_date'] | |
| st = res.get('state') or {} | |
| steps = st.get('steps') or [] | |
| trig = res.get('trigger') | |
| skp = res.get('skipped') | |
| # --- ํค๋: [์ฝ๋ ์ข ๋ชฉ๋ช ] ์์ฅ | ๋ ์ง ์ข ๊ฐ | 240์ (๊ดด๋ฆฌ%) --- | |
| m240 = res.get('ma240_now') | |
| m240s = (f"240์ {m240:,.0f} ({(res['price_now']/m240-1)*100:+.1f}%)" | |
| if m240 else "240์ ์์") | |
| print(f"\n[{tk} {MARKET_MAP.get(tk, ('', ''))[1] or ''}] {res['market']} | " | |
| f"{res['last_date']} ์ข ๊ฐ {res['price_now']:,.0f} | {m240s}") | |
| # --- ๊ณ ์ / ์ง๊ธ ์ด๊ฒฉ / ๋จ๊ณ ์ --- | |
| if 'peak_price' in st: | |
| print(f" ๊ณ ์ {st['peak_price']:,.0f}({st['peak_date']}) | " | |
| f"์ง๊ธ ์ด๊ฒฉ {st['div']*100:.1f}% " | |
| f"(์ข ๋ชฉ ๊ณ ์ ๋๋น {st['stock_from_peak']*100:.1f}% โ ์ง์ {st['index_from_peak']*100:+.1f}%) | " | |
| f"๋จ๊ณ {len(steps)}๊ฐ") | |
| # --- ๋จ๊ณ ์งํ ๋ด์ญ --- | |
| for stp in steps: | |
| mark = ' <<<' if '์ ๋๋งค๋' in stp['name'] else '' | |
| print(f" {stp['name']:<14} {stp['date']} @ {stp['price']:,.0f} " | |
| f"(์ด๊ฒฉ {stp['div']*100:.1f}%){mark}") | |
| # --- ์ฃผ ์ ํธ --- | |
| if res['recent']: | |
| sl = res['recent'][-1] | |
| alerts.append(lbl) | |
| ago = st.get('last_i', 0) - sl['i'] | |
| print(f" >>> [โ ์ง๊ธ ๋งค๋] ์ ๋๋งค๋: {sl['date']} @ {sl['price']:,.0f} " | |
| f"(์ ํธ์์ ์ด๊ฒฉ {sl['div']*100:.0f}%) | {ago}๊ฑฐ๋์ผ ์ ") | |
| elif trig and st.get('sold'): | |
| ago = st.get('last_i', 0) - trig['i'] | |
| print(f" >>> [๊ณผ๊ฑฐ ์ ํธ] ์ ๋๋งค๋: {trig['date']} @ {trig['price']:,.0f} " | |
| f"(๊ทธ๋ ์ด๊ฒฉ {trig['div']*100:.0f}%) | {ago}๊ฑฐ๋์ผ ์ ") | |
| print(f" >>> โป ์ง๊ธ ํ๋ผ๋ ์ ํธ ์๋ (์ด๊ฒฉ {BT.DIV_THRESHOLD*100:.0f}% ์๋ก ํ๋ณตํ๋ฉด ๋ฆฌ์ )") | |
| elif skp: | |
| sl = skp['sell'] | |
| ago = st.get('last_i', 0) - sl['i'] | |
| print(f" >>> [๋ฌด์๋ ์ ํธ] ์ ๋๋งค๋: {sl['date']} @ {sl['price']:,.0f} " | |
| f"(๊ทธ๋ ์ด๊ฒฉ {sl['div']*100:.0f}%) | {ago}๊ฑฐ๋์ผ ์ ") | |
| print(f" >>> ๋ฌด์ํ ์ด์ โ {skp['reason']}") | |
| else: | |
| print(f" >>> ๋งค๋ ์ ํธ ์์. ๋ณด์ ์ ์ง. (ํ์ฌ ๋จ๊ณ: {status_line(res)})") | |
| # โ ์ด๊ฒฉ์ด ํ๋ณต๋์ด ๋ฆฌ์ ๋์ด๋ '์ง์ ๋งค๋๊ฐ ์ธ์ ์๋์ง'๋ ํญ์ ๋ณด์ฌ์ค๋ค. | |
| # (์ฌ์ฉ์: ๊ทธ ์ ๊น์ง ์ฌ๋ผ์ค๋ฉด ์ ๋ฆฌ ํ๋จํ๋ ค๋ฉด ๋ง์ง๋ง ๋งค๋ ์์ ์ ๊ผญ ์์์ผ ํจ) | |
| ls = res.get('last_sell') | |
| if ls: | |
| ago = st.get('last_i', 0) - ls['i'] | |
| _nowdiv = st.get('div') | |
| print(f" >>> [์ง์ ๋งค๋(์ฐธ๊ณ )] {ls['date']} @ {ls['price']:,.0f} " | |
| f"(๊ทธ๋ ์ด๊ฒฉ {ls['div']*100:.0f}%) | {ago}๊ฑฐ๋์ผ ์ ") | |
| if _nowdiv is not None: | |
| print(f" >>> โป ์ง๊ธ์ ์ด๊ฒฉ {_nowdiv*100:.0f}%๋ก ํ๋ณต๋ผ์ ์ ํธ ๊บผ์ง") | |
| print("\n" + "=" * 66) | |
| if asof: | |
| print(f"โป ์ํ ๊ธฐ์ค: {asof_label(asof)}") | |
| if alerts: | |
| print(f"์ค๋ ํ์ธ ํ์: {len(alerts)}์ข ๋ชฉ -> " + ", ".join(alerts)) | |
| else: | |
| print("์ต๊ทผ ๋งค๋์ ํธ ์์. ์ ๋ถ ๋ณด์ ์ ์ง.") | |
| print("=" * 66) | |
| if __name__ == '__main__': | |
| main() |