QQuery / final_sell_monitor.py
johnkim
sync from consolidated
9a35465
Raw
History Blame Contribute Delete
20.9 kB
# -*- 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()