Spaces:
Running
Running
johnkim commited on
Commit ·
d8835bf
1
Parent(s): 9a35465
직전 매도 이력 표시 + 재베이스 메시지 상세화 + market_map 자동갱신
Browse files- app.py +39 -0
- buy_engine_dipfix.py +104 -14
- 배포_규칙_README.md +20 -8
app.py
CHANGED
|
@@ -49,6 +49,36 @@ MAP_PATH = os.path.join(HERE, "market_map.csv")
|
|
| 49 |
BOLL_PATH = os.path.join(HERE, "bollinger_count_debug.py")
|
| 50 |
VCP_PATH = os.path.join(HERE, "vcp_debug.py")
|
| 51 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
ALERT_DAYS = 5 # 전량매도: 최근 이 거래일 안의 매도신호를 [매도]로 본다
|
| 53 |
|
| 54 |
_TICKER_RE = re.compile(r"TICKER\s*=\s*['\"][0-9A-Za-z]+['\"]")
|
|
@@ -318,6 +348,15 @@ def run_monitor(text):
|
|
| 318 |
verdict = (f"### 🟢 [보유] {_label(code)} ({res['market']})\n"
|
| 319 |
f"- 매도 신호 없음. 보유 유지.\n"
|
| 320 |
f"- 현재 단계: {_status_line(res)}\n")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 321 |
out.append(verdict)
|
| 322 |
|
| 323 |
out.append(f"**[지금]** {res['last_date']} 종가 {res['price_now']:,.0f} · {m240s}\n")
|
|
|
|
| 49 |
BOLL_PATH = os.path.join(HERE, "bollinger_count_debug.py")
|
| 50 |
VCP_PATH = os.path.join(HERE, "vcp_debug.py")
|
| 51 |
|
| 52 |
+
# ── 실행 시 market_map.csv 백그라운드 자동 갱신 (오래됐을 때만) ────────────────
|
| 53 |
+
# build_market_map.py 를 찾아 --if-stale N --out <내 market_map.csv> 로 백그라운드 실행.
|
| 54 |
+
# fdr/한국IP 없는 배포 서버에선 조용히 실패 → 커밋된 market_map.csv 그대로 사용(앱은 정상).
|
| 55 |
+
# 맥 로컬(fdr 됨)에선 앱 켤 때마다 N일 넘었으면 알아서 최신화. 맥 스케줄러와 이중 안전망.
|
| 56 |
+
MARKET_MAP_STALE_DAYS = 7 # 이 일수 넘으면 갱신 시도. 0이면 자동갱신 끔.
|
| 57 |
+
|
| 58 |
+
def _refresh_market_map_bg():
|
| 59 |
+
import threading, subprocess, sys as _sys
|
| 60 |
+
def _run():
|
| 61 |
+
try:
|
| 62 |
+
cands = [
|
| 63 |
+
os.path.join(HERE, "build_market_map.py"),
|
| 64 |
+
os.path.join(HERE, "..", "quant", "sell_debug", "build_market_map.py"),
|
| 65 |
+
os.path.expanduser("~/Projects/quant/sell_debug/build_market_map.py"),
|
| 66 |
+
]
|
| 67 |
+
script = next((p for p in cands if os.path.exists(p)), None)
|
| 68 |
+
if not script:
|
| 69 |
+
return # 배포 서버 등 생성기 없음 → 조용히 스킵
|
| 70 |
+
subprocess.run(
|
| 71 |
+
[_sys.executable, script, "--if-stale", str(MARKET_MAP_STALE_DAYS),
|
| 72 |
+
"--out", MAP_PATH],
|
| 73 |
+
timeout=180, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
| 74 |
+
except Exception:
|
| 75 |
+
pass # fdr 실패/해외IP/타임아웃 등 전부 무시 — 앱 동작에 영향 없음
|
| 76 |
+
if MARKET_MAP_STALE_DAYS:
|
| 77 |
+
threading.Thread(target=_run, daemon=True).start()
|
| 78 |
+
|
| 79 |
+
_refresh_market_map_bg()
|
| 80 |
+
# ──────────────────────────────────────────────────────────────────────────────
|
| 81 |
+
|
| 82 |
ALERT_DAYS = 5 # 전량매도: 최근 이 거래일 안의 매도신호를 [매도]로 본다
|
| 83 |
|
| 84 |
_TICKER_RE = re.compile(r"TICKER\s*=\s*['\"][0-9A-Za-z]+['\"]")
|
|
|
|
| 348 |
verdict = (f"### 🟢 [보유] {_label(code)} ({res['market']})\n"
|
| 349 |
f"- 매도 신호 없음. 보유 유지.\n"
|
| 350 |
f"- 현재 단계: {_status_line(res)}\n")
|
| 351 |
+
# ★ 이격이 회복돼 리셋됐어도 '직전 매도가 언제였는지'는 항상 보여준다.
|
| 352 |
+
ls = res.get("last_sell")
|
| 353 |
+
if ls:
|
| 354 |
+
ago = st.get("last_i", 0) - ls["i"]
|
| 355 |
+
_nowdiv = st.get("div")
|
| 356 |
+
verdict += (f"- [직전 매도(참고)] {ls['date']} @ {ls['price']:,.0f} "
|
| 357 |
+
f"(그때 이격 {ls['div']*100:.0f}%) · {ago}거래일 전\n")
|
| 358 |
+
if _nowdiv is not None:
|
| 359 |
+
verdict += f" - ※ 지금은 이격 {_nowdiv*100:.0f}%로 회복돼서 신호 꺼짐\n"
|
| 360 |
out.append(verdict)
|
| 361 |
|
| 362 |
out.append(f"**[지금]** {res['last_date']} 종가 {res['price_now']:,.0f} · {m240s}\n")
|
buy_engine_dipfix.py
CHANGED
|
@@ -34,6 +34,34 @@ LIFELINE_BASE_MIN = True # [생명선] True면 '베이스 구간 전체의 최
|
|
| 34 |
# ※ 60선이 240선 위/아래인지 무관하게 WAIT(베이스) 내내 추적 —
|
| 35 |
# 상장초기 평행연장 240 구간에서 60선이 위아래로 왔다갔다해도 급락을 놓치지 않음.
|
| 36 |
LIFELINE_MARGIN = 0.01 # 생명선 = 시작점 BB하단 × (1-1%)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
LIFELINE_KEEP_MTN = True # [일봉/PEAK] ★생명선 이탈해도 동산 구조는 유지:
|
| 38 |
# 주가가 생명선을 깨면 전량매도(손절)는 하되, 동산을 폐기하지 않고 계속 관찰.
|
| 39 |
# 이후 60선이 240선 위로 복귀할 때 '끝지점 60선 > 시작점 60선'(더 높은 저점)이면
|
|
@@ -218,6 +246,10 @@ def run(df, tf='5min', sf=None, sell_dates=None, vcp_wins=None):
|
|
| 218 |
dip_ma60=HI; dip_bb60=None; dip_px=HI; dip_bbpx=None; dip_dt=None; dip_gap=0.0; wave_peak_ma60=0.0; prev_mtn_peak=None; dip_peak=0.0
|
| 219 |
dip_start_dt=None # DIP 시작일 (REBASE_DIP_DAYS 기간 판정용)
|
| 220 |
dip_floor_logged=False # '조기재베이스 보류(생명선바닥)' 이벤트 1회 출력용
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
wave_confirmed=False; conf_logged=False; touch_logged=False # 위아래 복귀 확인 여부(가짜 복귀 차단)
|
| 222 |
cross_up_dt=None # 이번 위아래에서 60선이 240선 위(gap>0)로 처음 올라간 날 (시간 기반 확인용)
|
| 223 |
wave_hat=0.0 # 이번 위아래에서 60선이 240선 위로 만든 최대 gap (모자 봉우리 감지 — 스침 오판 방지)
|
|
@@ -261,6 +293,32 @@ def run(df, tf='5min', sf=None, sell_dates=None, vcp_wins=None):
|
|
| 261 |
above = m60 > m240
|
| 262 |
gap = m60/m240 - 1
|
| 263 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 264 |
if phase=='WAIT':
|
| 265 |
# ★ 베이스 구간 전체의 최저 BB하단 추적 (above/below 무관 — 평행연장 구간 대응)
|
| 266 |
if bl < base_bbmin: base_bbmin = bl
|
|
@@ -319,6 +377,7 @@ def run(df, tf='5min', sf=None, sell_dates=None, vcp_wins=None):
|
|
| 319 |
life_broken=False
|
| 320 |
mtn_count=1 # ★ 새 시퀀스의 첫 동산 → 위아래 매수 시작
|
| 321 |
wave_fresh=True # 동산 완성 직후 복귀 → 모자 체크 대상
|
|
|
|
| 322 |
wave_confirmed=False; conf_logged=False; touch_logged=False; cross_up_dt=None; wave_hat=0.0 # 복귀확인 리셋
|
| 323 |
phase='WAVE'; wave=0; armed=False; b240={}; flag240up=False; hold=False; lwl=None; lwl_c=None; daily_open=False; pend=[]; up_peak=0.0; up_days=0; pend=[]
|
| 324 |
mts.append({'start':d,'s60':mtn_s60,'e60':endlow_ma60,'life':life,'endpx':endlow_px,'sdt':mtn_sdt,'edt':endlow_dt})
|
|
@@ -329,12 +388,14 @@ def run(df, tf='5min', sf=None, sell_dates=None, vcp_wins=None):
|
|
| 329 |
+ (f' ★생명선 깼었지만 끝지점 60선이 시작점보다 높아 구조유지 → 동산성공 인정 / 생명선 재설정 {life:,.0f}' if _revived else '')))
|
| 330 |
elif not higher:
|
| 331 |
# 끝지점이 시작점보다 낮음 = 더 낮은 저점(구조 붕괴) → 그 저점으로 재베이스
|
|
|
|
|
|
|
| 332 |
mtn_s60 = endlow_ma60; mtn_sdt = endlow_dt
|
| 333 |
life = min(endlow_bb60, endlow_bbpx)*(1-LIFELINE_MARGIN)
|
| 334 |
mtn_count=0 # 구조 붕괴 → 시퀀스 리셋
|
| 335 |
life_broken=False
|
| 336 |
ev.append((d,'동산실패→재베이스(더낮은저점)',
|
| 337 |
-
f'
|
| 338 |
peak_gap=gap; peak_m60=m60; endlow_gap=0.0; endlow_ma60=HI; endlow_px=HI # 새 산봉우리로
|
| 339 |
else:
|
| 340 |
# 봉우리/끝지점이 작음(노이즈) → 무시하고 같은 PEAK 유지
|
|
@@ -360,10 +421,11 @@ def run(df, tf='5min', sf=None, sell_dates=None, vcp_wins=None):
|
|
| 360 |
phase='DIP'
|
| 361 |
dip_ma60=m60; dip_bb60=bl; dip_px=L; dip_bbpx=bl; dip_dt=d; dip_gap=gap
|
| 362 |
dip_start_dt=d # ★ DIP 시작일 (기간 기반 재베이스 REBASE_DIP_DAYS 판정용)
|
|
|
|
| 363 |
dip_floor_logged=False
|
| 364 |
dip_peak=wave_peak_ma60 # DIP 직전까지 = 이번 산의 피크 확정
|
| 365 |
ev.append((d,'끝지점(2) 추적중→매수중단',
|
| 366 |
-
f'
|
| 367 |
elif above or (daily and WAVE_TOUCH_BUY and gap <= 0):
|
| 368 |
# above=60>240 (정상 위아래) / gap<=0 = 스침구간(240선 위로 못 올라감, DIP 전) → 바닥 다지기로 보고 매수 허용
|
| 369 |
if m60 > wave_peak_ma60: wave_peak_ma60 = m60 # 현재 위아래 구간의 60선 최고(피크)만 추적
|
|
@@ -444,7 +506,8 @@ def run(df, tf='5min', sf=None, sell_dates=None, vcp_wins=None):
|
|
| 444 |
if deep_ok:
|
| 445 |
# 확정! → 새 위아래 시작, 못 산 날들 소급 매수
|
| 446 |
wave+=1
|
| 447 |
-
|
|
|
|
| 448 |
daily_open=True; hold=True
|
| 449 |
amt = d1 if wave==1 else d2
|
| 450 |
d0 = pend[0][0]
|
|
@@ -501,6 +564,7 @@ def run(df, tf='5min', sf=None, sell_dates=None, vcp_wins=None):
|
|
| 501 |
elif not above:
|
| 502 |
# 끝지점(2) 최저 갱신 (★ 사이클종료 없음 — 끝지점(1)과 동일하게 생명선만이 사망조건)
|
| 503 |
if gap < dip_gap: dip_gap=gap
|
|
|
|
| 504 |
if m60 < dip_ma60: dip_ma60=m60; dip_bb60=bl; dip_dt=d
|
| 505 |
if L < dip_px: dip_px=L; dip_bbpx=bl
|
| 506 |
# ★ 조기 '속음' 판정: 240 복귀를 기다리지 않고, 깊이가 임계를 찍는 순간 재베이스.
|
|
@@ -526,7 +590,7 @@ def run(df, tf='5min', sf=None, sell_dates=None, vcp_wins=None):
|
|
| 526 |
if _deep_hit and (not _long_hit) and _floor0 and (not _endok0) and _blk and (not dip_floor_logged):
|
| 527 |
dip_floor_logged=True
|
| 528 |
ev.append((d,'조기재베이스 보류(생명선바닥)',
|
| 529 |
-
f'
|
| 530 |
if not _blk:
|
| 531 |
_why0 = ('계단식하락' if (_pg0 is not None and _pg0 < DOUBLE_MTN_GAP_MIN)
|
| 532 |
else ('끝지점 낮아짐' if not _endok0
|
|
@@ -536,13 +600,13 @@ def run(df, tf='5min', sf=None, sell_dates=None, vcp_wins=None):
|
|
| 536 |
_dur0 = (d - dip_start_dt).days if dip_start_dt is not None else 0
|
| 537 |
if _deep_hit:
|
| 538 |
_tag0 = '속음→재베이스(조기판정)'
|
| 539 |
-
_rsn0 = f'
|
| 540 |
else:
|
| 541 |
_tag0 = '속음→재베이스(장기DIP)'
|
| 542 |
-
_rsn0 = (f'
|
| 543 |
-
f'깊이는 {dip_gap*100:.1f}%로 얕
|
| 544 |
ev.append((d,_tag0,
|
| 545 |
-
f'{_rsn0} /
|
| 546 |
phase='PEAK'; peak_gap=gap; peak_m60=m60
|
| 547 |
endlow_gap=0.0; endlow_ma60=HI; endlow_px=HI
|
| 548 |
hold=False; wave=0; armed=False; b240={}; flag240up=False
|
|
@@ -575,6 +639,16 @@ def run(df, tf='5min', sf=None, sell_dates=None, vcp_wins=None):
|
|
| 575 |
# ※ 생명선(life)은 리셋하지 않음 — 첫 동산 시작점 생명선을 시퀀스 내내 그대로 유지.
|
| 576 |
# (더블/트리플 눌림에서 얕게 팔려나가지 않도록. 재베이스는 새 시퀀스라 별도)
|
| 577 |
gap_pct = (p_now/p_prev - 1)*100
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 578 |
mtn_s60 = dip_ma60; mtn_sdt = dip_dt
|
| 579 |
mts.append({'start':d,'s60':mtn_s60,'e60':dip_ma60,'life':life,
|
| 580 |
'endpx':dip_px,'sdt':mtn_sdt,'edt':dip_dt})
|
|
@@ -583,7 +657,8 @@ def run(df, tf='5min', sf=None, sell_dates=None, vcp_wins=None):
|
|
| 583 |
_over = mtn_count > MAX_MTN_WAVE # 트리플(3)+ 이면 위아래 매수 안 함
|
| 584 |
_nm = {2:'더블',3:'트리플',4:'쿼드러플'}.get(mtn_count, f'{mtn_count}중')
|
| 585 |
ev.append((d,f'{_nm}동산 확정→위아래 리셋',
|
| 586 |
-
f'피크갭 {gap_pct:+.1f}% < {DOUBLE_MTN_GAP*100:.0f}% (제자리) — {mtn_count}번째 동산 / 생명선 {life:,.0f}
|
|
|
|
| 587 |
+ (' | ★끝지점 낮아졌지만 저가>첫생명선 → 더블 인정(생명선바닥)' if (_floor_ok and not _end_ok) else '')
|
| 588 |
+ (f' | ★{MAX_MTN_WAVE}개 초과 → 위아래 매수 안 함(관찰만)' if _over else '')))
|
| 589 |
wave_fresh=False # 시퀀스 진행 중(더블동산) → 모자 무시
|
|
@@ -597,7 +672,7 @@ def run(df, tf='5min', sf=None, sell_dates=None, vcp_wins=None):
|
|
| 597 |
mtn_s60 = dip_ma60; mtn_sdt = dip_dt
|
| 598 |
life = min(dip_bb60, dip_bbpx)*(1-LIFELINE_MARGIN)
|
| 599 |
ev.append((d,'깊은눌림→새동산(재베이스)',
|
| 600 |
-
f'
|
| 601 |
phase='PEAK'; peak_gap=gap; peak_m60=m60
|
| 602 |
endlow_gap=0.0; endlow_ma60=HI; endlow_px=HI
|
| 603 |
hold=False; wave=0; armed=False; b240={}; flag240up=False
|
|
@@ -615,7 +690,7 @@ def run(df, tf='5min', sf=None, sell_dates=None, vcp_wins=None):
|
|
| 615 |
_whyR = ('계단식하락' if (_pg is not None and _pg < DOUBLE_MTN_GAP_MIN)
|
| 616 |
else ('끝지점 낮아짐' if not _end_ok else '피크데이터 없음'))
|
| 617 |
ev.append((d,'속음→재베이스(깊은붕괴)',
|
| 618 |
-
f'
|
| 619 |
phase='PEAK'; peak_gap=gap; peak_m60=m60
|
| 620 |
endlow_gap=0.0; endlow_ma60=HI; endlow_px=HI
|
| 621 |
hold=False; wave=0; armed=False; b240={}; flag240up=False
|
|
@@ -628,11 +703,26 @@ def run(df, tf='5min', sf=None, sell_dates=None, vcp_wins=None):
|
|
| 628 |
# 하지만 첫 동산 생명선을 안 깼으니 시퀀스(동산+더블동산)는 살아있음 (M자도 더블동산).
|
| 629 |
# 60>240 복귀 = 새 매수구간 → 1번째 위아래부터 재개. 생명선은 계속 첫 동산 것.
|
| 630 |
# (생명선이 유일한 사망조건 — 깨지면 DIP 중에 이미 전량매도됐을 것)
|
| 631 |
-
_why3 = (
|
| 632 |
-
else ('끝지점 낮아짐' if not _end_ok else '피크데이터 없음'))
|
| 633 |
wave_fresh=False # 시퀀스 진행 중(재개) → 모자 무시
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 634 |
ev.append((d,'작은봉우리(동산아님)→위아래 재개',
|
| 635 |
-
f'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 636 |
phase='WAVE'; wave=0; armed=False; b240={}; flag240up=False
|
| 637 |
lwl=None; lwl_c=None; daily_open=False; pend=[]; up_peak=0.0; up_days=0
|
| 638 |
wave_confirmed=False; conf_logged=False; touch_logged=False; cross_up_dt=None; wave_hat=0.0 # 복귀확인 리셋
|
|
|
|
| 34 |
# ※ 60선이 240선 위/아래인지 무관하게 WAIT(베이스) 내내 추적 —
|
| 35 |
# 상장초기 평행연장 240 구간에서 60선이 위아래로 왔다갔다해도 급락을 놓치지 않음.
|
| 36 |
LIFELINE_MARGIN = 0.01 # 생명선 = 시작점 BB하단 × (1-1%)
|
| 37 |
+
VIRTUAL_LIFELINE = True # [일봉] ★가상 생명선 (사용자 아이디어 2026-08):
|
| 38 |
+
# 진짜 생명선(첫 동산 것)은 '구조용'으로 그대로 두고,
|
| 39 |
+
# 더블동산/위아래 재개로 '작은 언덕'이 생길 때마다 그 구간 전용 보호선을 따로 긋는다.
|
| 40 |
+
# 가상 생명선 = 직전 눌림(DIP) 구간의 최저 BB하단 ×(1-마진). 진짜 생명선보다 위일 때만 유효.
|
| 41 |
+
# 이 선을 깨면: ① 그 작은 언덕에서 산 물량만 매도(전량 아님) ② 재베이스로 동산부터 다시 그림.
|
| 42 |
+
# → 물타기는 끊되 전체 포지션·구조는 안 날림. False면 이 기능 끔.
|
| 43 |
+
DEEP_DIP_ONE_WAVE = False # [일봉] ★깊게 빠진 뒤 재개할 땐 '1번째 위아래'만 매수 (2026-08 사용자):
|
| 44 |
+
# 직전 눌림이 DEEP_DIP_ONE_WAVE_PCT 보다 깊었으면, 그 뒤 열리는 위아래는
|
| 45 |
+
# 1번째만 사고 2번째는 안 삼(구조가 이미 상한 구간이라 추가 물타기 축소).
|
| 46 |
+
# False면 기존처럼 1·2번째 다 매수.
|
| 47 |
+
DEEP_DIP_ONE_WAVE_PCT = 0.20 # 위 규칙 적용 기준 깊이(끝지점이 240선 대비 -20% 이하)
|
| 48 |
+
VLIFE_GRACE_DAYS = 20 # [일봉] ★가상생명선 이탈 유예일 (2026-08):
|
| 49 |
+
# 선을 깬 즉시 팔지 않고, 이 일수 안에 종가가 선 위로 회복하면 없던 일로 함.
|
| 50 |
+
# 유예 내내 회복 못 하면 그때 구간매도.
|
| 51 |
+
# 이유: 즉시매도는 '패닉 투매 바닥'에서 파는 경향(166090 2020-03-19=코로나 바닥 당일,
|
| 52 |
+
# 033100 2016-01-20, 236200 2022-06-23 모두 매도 직후가 최저점이었음).
|
| 53 |
+
# 진짜 붕괴는 계속 선 아래 머물고, 급락 바닥은 며칠 만에 되돌아오므로 시간으로 걸러냄.
|
| 54 |
+
# 0 = 유예 없음(즉시 매도).
|
| 55 |
+
DOUBLE_MTN_RELIFE = False # [일봉] (실험) 더블동산/재개 시 '진짜 생명선' 자체를 올려잡기 — 전수조사 결과 성적 악화로 OFF.
|
| 56 |
+
# 대신 위 VIRTUAL_LIFELINE 방식 사용. True로 켜면 진짜 생명선을 올림(옛 실험).
|
| 57 |
+
_DOUBLE_MTN_RELIFE_DOC = """[일봉] ★더블동산/재개 시 생명선을 '직전 눌림(DIP) 구간의 최저 BB하단'으로 올려잡기.
|
| 58 |
+
# 문제(096770): 첫 동산 생명선(58,739)을 시퀀스 내내 유지하다 보니, 주가가 15만원대인데
|
| 59 |
+
# 보호선이 5.8만이라 사실상 보호가 없음 → 계속 떨어지는데 위아래 매수만 반복.
|
| 60 |
+
# 해결(사용자): 60선이 240선을 다시 만나 더블동산/재개가 확정되는 그 시점에,
|
| 61 |
+
# 방금 지나온 DIP 구간에서 가장 낮았던 BB하단을 새 생명선으로 삼는다.
|
| 62 |
+
# (096770: 2022-10-11 BB하단 127,575 × 0.99 = 126,299)
|
| 63 |
+
# ※ 생명선은 '올리기만' 함(내리지 않음) — 옛 생명선보다 낮으면 그대로 둠.
|
| 64 |
+
# False면 예전처럼 첫 동산 생명선을 계속 유지."""
|
| 65 |
LIFELINE_KEEP_MTN = True # [일봉/PEAK] ★생명선 이탈해도 동산 구조는 유지:
|
| 66 |
# 주가가 생명선을 깨면 전량매도(손절)는 하되, 동산을 폐기하지 않고 계속 관찰.
|
| 67 |
# 이후 60선이 240선 위로 복귀할 때 '끝지점 60선 > 시작점 60선'(더 높은 저점)이면
|
|
|
|
| 246 |
dip_ma60=HI; dip_bb60=None; dip_px=HI; dip_bbpx=None; dip_dt=None; dip_gap=0.0; wave_peak_ma60=0.0; prev_mtn_peak=None; dip_peak=0.0
|
| 247 |
dip_start_dt=None # DIP 시작일 (REBASE_DIP_DAYS 기간 판정용)
|
| 248 |
dip_floor_logged=False # '조기재베이스 보류(생명선바닥)' 이벤트 1회 출력용
|
| 249 |
+
vlife=None; vseg_i=0 # 가상 생명선 / 이 작은언덕 매수 시작 인덱스(buys 기준)
|
| 250 |
+
vbreak_dt=None # 가상생명선 아래로 처음 내려간 날 (유예 판정용)
|
| 251 |
+
one_wave=False # 이번 위아래는 1번째만 매수(깊은 눌림 뒤 재개)
|
| 252 |
+
dip_bbmin=HI # DIP 구간 전체의 최저 BB하단 (DOUBLE_MTN_RELIFE: 새 생명선 후보)
|
| 253 |
wave_confirmed=False; conf_logged=False; touch_logged=False # 위아래 복귀 확인 여부(가짜 복귀 차단)
|
| 254 |
cross_up_dt=None # 이번 위아래에서 60선이 240선 위(gap>0)로 처음 올라간 날 (시간 기반 확인용)
|
| 255 |
wave_hat=0.0 # 이번 위아래에서 60선이 240선 위로 만든 최대 gap (모자 봉우리 감지 — 스침 오판 방지)
|
|
|
|
| 293 |
above = m60 > m240
|
| 294 |
gap = m60/m240 - 1
|
| 295 |
|
| 296 |
+
# ★ 가상 생명선 이탈: 이 작은언덕에서 산 물량만 매도 + 재베이스(동산부터 다시)
|
| 297 |
+
if VIRTUAL_LIFELINE and vlife and phase in ('WAVE','DIP'):
|
| 298 |
+
if C >= vlife:
|
| 299 |
+
vbreak_dt=None # 종가가 선 위로 회복 → 유예 리셋(없던 일)
|
| 300 |
+
elif L < vlife and vbreak_dt is None:
|
| 301 |
+
vbreak_dt=d # 처음 깬 날 기록
|
| 302 |
+
if (VIRTUAL_LIFELINE and vlife and phase in ('WAVE','DIP') and vbreak_dt is not None
|
| 303 |
+
and (d - vbreak_dt).days >= VLIFE_GRACE_DAYS):
|
| 304 |
+
_sq = buys[vseg_i:]
|
| 305 |
+
_amt = sum(x[2] for x in _sq)
|
| 306 |
+
_sh = sum(x[2]/x[1] for x in _sq)
|
| 307 |
+
_avg = (_amt/_sh) if _sh else 0
|
| 308 |
+
ev.append((d,'가상생명선이탈→구간매도',
|
| 309 |
+
f'저가 {L:,.0f} < 가상생명선 {vlife:,.0f} — 이 작은언덕 매수분만 매도(진짜 생명선 {life:,.0f}는 유지) '
|
| 310 |
+
f'| 구간매수 {len(_sq)}회 {_amt:,.0f}원 평단 {_avg:,.0f} | {VLIFE_GRACE_DAYS}일 유예 내내 회복 실패({str(vbreak_dt)[:10]}부터) → 동산부터 다시'))
|
| 311 |
+
vlife=None; vseg_i=len(buys); vbreak_dt=None
|
| 312 |
+
mtn_s60 = m60; mtn_sdt = d
|
| 313 |
+
life = min(bl, life if life else bl)
|
| 314 |
+
phase='PEAK'; peak_gap=gap; peak_m60=m60
|
| 315 |
+
endlow_gap=0.0; endlow_ma60=HI; endlow_px=HI
|
| 316 |
+
hold=False; wave=0; armed=False; b240={}; flag240up=False
|
| 317 |
+
daily_open=False; pend=[]; up_peak=0.0; up_days=0
|
| 318 |
+
mtn_count=0; life_broken=False
|
| 319 |
+
wave_confirmed=False; conf_logged=False; touch_logged=False; cross_up_dt=None; wave_hat=0.0
|
| 320 |
+
continue
|
| 321 |
+
|
| 322 |
if phase=='WAIT':
|
| 323 |
# ★ 베이스 구간 전체의 최저 BB하단 추적 (above/below 무관 — 평행연장 구간 대응)
|
| 324 |
if bl < base_bbmin: base_bbmin = bl
|
|
|
|
| 377 |
life_broken=False
|
| 378 |
mtn_count=1 # ★ 새 시퀀스의 첫 동산 → 위아래 매수 시작
|
| 379 |
wave_fresh=True # 동산 완성 직후 복귀 → 모자 체크 대상
|
| 380 |
+
one_wave=False # 새 동산 → 1·2번째 다 매수
|
| 381 |
wave_confirmed=False; conf_logged=False; touch_logged=False; cross_up_dt=None; wave_hat=0.0 # 복귀확인 리셋
|
| 382 |
phase='WAVE'; wave=0; armed=False; b240={}; flag240up=False; hold=False; lwl=None; lwl_c=None; daily_open=False; pend=[]; up_peak=0.0; up_days=0; pend=[]
|
| 383 |
mts.append({'start':d,'s60':mtn_s60,'e60':endlow_ma60,'life':life,'endpx':endlow_px,'sdt':mtn_sdt,'edt':endlow_dt})
|
|
|
|
| 388 |
+ (f' ★생명선 깼었지만 끝지점 60선이 시작점보다 높아 구조유지 → 동산성공 인정 / 생명선 재설정 {life:,.0f}' if _revived else '')))
|
| 389 |
elif not higher:
|
| 390 |
# 끝지점이 시작점보다 낮음 = 더 낮은 저점(구조 붕괴) → 그 저점으로 재베이스
|
| 391 |
+
_os60=mtn_s60; _osdt=mtn_sdt # 실패한 산의 시작점(비교용, 덮어쓰기 전 저장)
|
| 392 |
+
_drop=(endlow_ma60/_os60-1)*100 if _os60 else 0
|
| 393 |
mtn_s60 = endlow_ma60; mtn_sdt = endlow_dt
|
| 394 |
life = min(endlow_bb60, endlow_bbpx)*(1-LIFELINE_MARGIN)
|
| 395 |
mtn_count=0 # 구조 붕괴 → 시퀀스 리셋
|
| 396 |
life_broken=False
|
| 397 |
ev.append((d,'동산실패→재베이스(더낮은저점)',
|
| 398 |
+
f'이 산 실패 — 시작 바닥 {str(_osdt)[:10]}(60선 {_os60:,.0f})보다 끝 바닥 {str(endlow_dt)[:10]}(60선 {endlow_ma60:,.0f})이 {_drop:+.1f}% 더 낮음(=더 낮은 저점, 구조 붕괴). 이 낮은 끝 바닥을 새 시작점으로 산 다시 그림 / 새 생명선 {life:,.0f}'))
|
| 399 |
peak_gap=gap; peak_m60=m60; endlow_gap=0.0; endlow_ma60=HI; endlow_px=HI # 새 산봉우리로
|
| 400 |
else:
|
| 401 |
# 봉우리/끝지점이 작음(노이즈) → 무시하고 같은 PEAK 유지
|
|
|
|
| 421 |
phase='DIP'
|
| 422 |
dip_ma60=m60; dip_bb60=bl; dip_px=L; dip_bbpx=bl; dip_dt=d; dip_gap=gap
|
| 423 |
dip_start_dt=d # ★ DIP 시작일 (기간 기반 재베이스 REBASE_DIP_DAYS 판정용)
|
| 424 |
+
dip_bbmin=bl # ★ DIP 구간 최저 BB하단 추적 시작
|
| 425 |
dip_floor_logged=False
|
| 426 |
dip_peak=wave_peak_ma60 # DIP 직전까지 = 이번 산의 피크 확정
|
| 427 |
ev.append((d,'끝지점(2) 추적중→매수중단',
|
| 428 |
+
f'매수 잠시 멈춤 — 60선 {m60:,.0f}이 240선 밑으로 -{DOUBLE_MTN_PCT*100:.0f}% 넘게 꺾임. 여기가 이번 눌림 시작({str(d)[:10]}). 더블동산인지 무너진 건지 지켜보는 중'))
|
| 429 |
elif above or (daily and WAVE_TOUCH_BUY and gap <= 0):
|
| 430 |
# above=60>240 (정상 위아래) / gap<=0 = 스침구간(240선 위로 못 올라감, DIP 전) → 바닥 다지기로 보고 매수 허용
|
| 431 |
if m60 > wave_peak_ma60: wave_peak_ma60 = m60 # 현재 위아래 구간의 60선 최고(피크)만 추적
|
|
|
|
| 506 |
if deep_ok:
|
| 507 |
# 확정! → 새 위아래 시작, 못 산 날들 소급 매수
|
| 508 |
wave+=1
|
| 509 |
+
_cap = 1 if (DEEP_DIP_ONE_WAVE and one_wave) else 2
|
| 510 |
+
if wave<=_cap:
|
| 511 |
daily_open=True; hold=True
|
| 512 |
amt = d1 if wave==1 else d2
|
| 513 |
d0 = pend[0][0]
|
|
|
|
| 564 |
elif not above:
|
| 565 |
# 끝지점(2) 최저 갱신 (★ 사이클종료 없음 — 끝지점(1)과 동일하게 생명선만이 사망조건)
|
| 566 |
if gap < dip_gap: dip_gap=gap
|
| 567 |
+
if bl < dip_bbmin: dip_bbmin=bl # ★ DIP 구간 최저 BB하단
|
| 568 |
if m60 < dip_ma60: dip_ma60=m60; dip_bb60=bl; dip_dt=d
|
| 569 |
if L < dip_px: dip_px=L; dip_bbpx=bl
|
| 570 |
# ★ 조기 '속음' 판정: 240 복귀를 기다리지 않고, 깊이가 임계를 찍는 순간 재베이스.
|
|
|
|
| 590 |
if _deep_hit and (not _long_hit) and _floor0 and (not _endok0) and _blk and (not dip_floor_logged):
|
| 591 |
dip_floor_logged=True
|
| 592 |
ev.append((d,'조기재베이스 보류(생명선바닥)',
|
| 593 |
+
f'다시 안 그림 — 많이({dip_gap*100:.1f}%) 빠졌지만 최저가 {dip_px:,.0f}가 첫 생명선 {life:,.0f}보다 위. 아직 구조 안 무너짐 → 더블동산 가능성 살려둠'))
|
| 594 |
if not _blk:
|
| 595 |
_why0 = ('계단식하락' if (_pg0 is not None and _pg0 < DOUBLE_MTN_GAP_MIN)
|
| 596 |
else ('끝지점 낮아짐' if not _endok0
|
|
|
|
| 600 |
_dur0 = (d - dip_start_dt).days if dip_start_dt is not None else 0
|
| 601 |
if _deep_hit:
|
| 602 |
_tag0 = '속음→재베이스(조기판정)'
|
| 603 |
+
_rsn0 = f'60선이 240선 밑으로 {dip_gap*100:.1f}% 깊이 빠짐(기준 -{REBASE_DEEP_PCT*100:.0f}% 이하). 눌림 시작 {str(dip_start_dt)[:10]}부터 {_dur0}일째'
|
| 604 |
else:
|
| 605 |
_tag0 = '속음→재베이스(장기DIP)'
|
| 606 |
+
_rsn0 = (f'눌림이 {_dur0}일(약 {_dur0//365}년)이나 지속({str(dip_start_dt)[:10]}~{str(d)[:10]}, 기준 {REBASE_DIP_DAYS}일). '
|
| 607 |
+
f'깊이는 {dip_gap*100:.1f}%로 얕아도 이렇게 오래 끌면 눌림 아니라 무너짐')
|
| 608 |
ev.append((d,_tag0,
|
| 609 |
+
f'속았음 → 처음부터 다시 — {_rsn0} / 이번 봉우리는 직전 산보다 {(_pg0*100 if _pg0 is not None else 0):+.1f}%({_why0}) / 240선 복귀 안 기다리고 위아래 접음 / 새 시작점 {str(dip_dt)[:10]}(60선 {dip_ma60:,.0f}) / 새 생명선 {life:,.0f}'))
|
| 610 |
phase='PEAK'; peak_gap=gap; peak_m60=m60
|
| 611 |
endlow_gap=0.0; endlow_ma60=HI; endlow_px=HI
|
| 612 |
hold=False; wave=0; armed=False; b240={}; flag240up=False
|
|
|
|
| 639 |
# ※ 생명선(life)은 리셋하지 않음 — 첫 동산 시작점 생명선을 시퀀스 내내 그대로 유지.
|
| 640 |
# (더블/트리플 눌림에서 얕게 팔려나가지 않도록. 재베이스는 새 시퀀스라 별도)
|
| 641 |
gap_pct = (p_now/p_prev - 1)*100
|
| 642 |
+
# ★ 생명선 올려잡기: 방금 지나온 DIP 구간 최저 BB하단 기준 (내리지는 않음)
|
| 643 |
+
_oldlife = life
|
| 644 |
+
if VIRTUAL_LIFELINE and dip_bbmin < HI:
|
| 645 |
+
_v = dip_bbmin*(1-LIFELINE_MARGIN)
|
| 646 |
+
vlife = _v if (life is None or _v > life) else None
|
| 647 |
+
vseg_i = len(buys); vbreak_dt=None
|
| 648 |
+
one_wave = bool(DEEP_DIP_ONE_WAVE and dip_gap <= -DEEP_DIP_ONE_WAVE_PCT)
|
| 649 |
+
if DOUBLE_MTN_RELIFE and dip_bbmin < HI:
|
| 650 |
+
_cand = dip_bbmin*(1-LIFELINE_MARGIN)
|
| 651 |
+
if life is None or _cand > life: life = _cand
|
| 652 |
mtn_s60 = dip_ma60; mtn_sdt = dip_dt
|
| 653 |
mts.append({'start':d,'s60':mtn_s60,'e60':dip_ma60,'life':life,
|
| 654 |
'endpx':dip_px,'sdt':mtn_sdt,'edt':dip_dt})
|
|
|
|
| 657 |
_over = mtn_count > MAX_MTN_WAVE # 트리플(3)+ 이면 위아래 매수 안 함
|
| 658 |
_nm = {2:'더블',3:'트리플',4:'쿼드러플'}.get(mtn_count, f'{mtn_count}중')
|
| 659 |
ev.append((d,f'{_nm}동산 확정→위아래 리셋',
|
| 660 |
+
f'피크갭 {gap_pct:+.1f}% < {DOUBLE_MTN_GAP*100:.0f}% (제자리) — {mtn_count}번째 동산 / 생명선 {life:,.0f}'
|
| 661 |
+
+ (f'(직전 눌림 최저BB로 올림, 기존 {_oldlife:,.0f})' if (_oldlife and life>_oldlife) else '(첫동산 유지)')
|
| 662 |
+ (' | ★끝지점 낮아졌지만 저가>첫생명선 → 더블 인정(생명선바닥)' if (_floor_ok and not _end_ok) else '')
|
| 663 |
+ (f' | ★{MAX_MTN_WAVE}개 초과 → 위아래 매수 안 함(관찰만)' if _over else '')))
|
| 664 |
wave_fresh=False # 시퀀스 진행 중(더블동산) → 모자 무시
|
|
|
|
| 672 |
mtn_s60 = dip_ma60; mtn_sdt = dip_dt
|
| 673 |
life = min(dip_bb60, dip_bbpx)*(1-LIFELINE_MARGIN)
|
| 674 |
ev.append((d,'깊은눌림→새동산(재베이스)',
|
| 675 |
+
f'한 바퀴 다 돌고 무너짐 → 처음부터 다시 — 봉우리가 직전 산보다 +{_pg*100:.1f}% 크게 올랐다가(기준 +{DOUBLE_MTN_GAP*100:.0f}% 넘음) 60선이 240선 밑으로 {dip_gap*100:.1f}% 깊이 빠짐(기준 -{emin*100:.0f}% 이하) / 새 시작점 {str(dip_dt)[:10]}(60선 {dip_ma60:,.0f}) / 새 생명선 {life:,.0f}'))
|
| 676 |
phase='PEAK'; peak_gap=gap; peak_m60=m60
|
| 677 |
endlow_gap=0.0; endlow_ma60=HI; endlow_px=HI
|
| 678 |
hold=False; wave=0; armed=False; b240={}; flag240up=False
|
|
|
|
| 690 |
_whyR = ('계단식하락' if (_pg is not None and _pg < DOUBLE_MTN_GAP_MIN)
|
| 691 |
else ('끝지점 낮아짐' if not _end_ok else '피크데이터 없음'))
|
| 692 |
ev.append((d,'속음→재베이스(깊은붕괴)',
|
| 693 |
+
f'속았음 → 처음부터 다시 — 60선이 240선 밑으로 {dip_gap*100:.1f}% 깊이 빠짐(기준 -{REBASE_DEEP_PCT*100:.0f}% 이하) + 봉우리가 직전 산보다 {(_pg*100 if _pg is not None else 0):+.1f}%({_whyR}). 위아래 아님 / 새 시작점 {str(dip_dt)[:10]}(60선 {dip_ma60:,.0f}) / 새 생명선 {life:,.0f}'))
|
| 694 |
phase='PEAK'; peak_gap=gap; peak_m60=m60
|
| 695 |
endlow_gap=0.0; endlow_ma60=HI; endlow_px=HI
|
| 696 |
hold=False; wave=0; armed=False; b240={}; flag240up=False
|
|
|
|
| 703 |
# 하지만 첫 동산 생명선을 안 깼으니 시퀀스(동산+더블동산)는 살아있음 (M자도 더블동산).
|
| 704 |
# 60>240 복귀 = 새 매수구간 → 1번째 위아래부터 재개. 생명선은 계속 첫 동산 것.
|
| 705 |
# (생명선이 유일한 사망조건 — 깨지면 DIP 중에 이미 전량매도됐을 것)
|
| 706 |
+
_why3 = ('계단식 하락' if (_pg is not None and _pg < DOUBLE_MTN_GAP_MIN)
|
| 707 |
+
else ('끝지점이 직전보다 낮아짐' if not _end_ok else '피크데이터 없음'))
|
| 708 |
wave_fresh=False # 시퀀스 진행 중(재개) → 모자 무시
|
| 709 |
+
# ★ 재개 때도 생명선 올려잡기 (직전 눌림 최저 BB하단). 내리지는 않음.
|
| 710 |
+
_oldlife3 = life
|
| 711 |
+
if VIRTUAL_LIFELINE and dip_bbmin < HI:
|
| 712 |
+
_v3 = dip_bbmin*(1-LIFELINE_MARGIN)
|
| 713 |
+
vlife = _v3 if (life is None or _v3 > life) else None
|
| 714 |
+
vseg_i = len(buys); vbreak_dt=None
|
| 715 |
+
one_wave = bool(DEEP_DIP_ONE_WAVE and dip_gap <= -DEEP_DIP_ONE_WAVE_PCT)
|
| 716 |
+
if DOUBLE_MTN_RELIFE and dip_bbmin < HI:
|
| 717 |
+
_c3 = dip_bbmin*(1-LIFELINE_MARGIN)
|
| 718 |
+
if life is None or _c3 > life: life = _c3
|
| 719 |
ev.append((d,'작은봉우리(동산아님)→위아래 재개',
|
| 720 |
+
f'이번 60선 꼭대기 {(p_now if p_now else 0):,.0f} vs 직전 산 꼭대기 {(p_prev if p_prev else 0):,.0f} = {(_pg*100 if _pg is not None else 0):+.1f}% '
|
| 721 |
+
f'(꼭대기가 직전과 비슷해야[-{abs(DOUBLE_MTN_GAP_MIN)*100:.0f}%~+{DOUBLE_MTN_GAP*100:.0f}%] 더블동산인데 벗어남 → {_why3}) / '
|
| 722 |
+
f'60선이 240선 밑으로 {dip_gap*100:.1f}% 빠짐(재베이스 기준 -{REBASE_DEEP_PCT*100:.0f}% 미달이라 동산 다시 안 그림) '
|
| 723 |
+
f'→ 새 산 아님(동산 개수 안 올림). 생명선 {life:,.0f}'
|
| 724 |
+
+ (f'(직전 눌림 최저BB로 올림, 기존 {_oldlife3:,.0f})' if (_oldlife3 and life>_oldlife3) else '')
|
| 725 |
+
+ f' → 위아래 매수만 1번째부터 다시'))
|
| 726 |
phase='WAVE'; wave=0; armed=False; b240={}; flag240up=False
|
| 727 |
lwl=None; lwl_c=None; daily_open=False; pend=[]; up_peak=0.0; up_days=0
|
| 728 |
wave_confirmed=False; conf_logged=False; touch_logged=False; cross_up_dt=None; wave_hat=0.0 # 복귀확인 리셋
|
배포_규칙_README.md
CHANGED
|
@@ -34,24 +34,36 @@ push 하면 웹에 반영된다.
|
|
| 34 |
|
| 35 |
---
|
| 36 |
|
| 37 |
-
## 스크립트가 동기화하는 파일 (consolidated → QQuery)
|
| 38 |
-
- `buy_engine_dipfix.py`
|
| 39 |
-
- `final_sell_monitor.py`
|
| 40 |
-
- `confirm_sell_backtest.py`
|
| 41 |
- `confirm_sell_backtest_rs.py` ← quant/consolidated/rs_sell/
|
| 42 |
- `final_sell_monitor_rs.py` ← quant/consolidated/rs_sell/
|
|
|
|
| 43 |
|
| 44 |
## 스크립트가 건드리지 않는 파일 (QQuery가 정본)
|
| 45 |
-
- `app.py` — QQuery 전용 웹 앱
|
| 46 |
-
- `bollinger_count_debug.py` — QQuery 버전이 웹용으로 개선됨(오늘 날짜 자동). quant 것으로 덮지 말 것.
|
| 47 |
- `market_map.csv` — 양쪽 스냅샷이 조금씩 다름. 204320 등 핵심은 이미 맞음. 건드릴 필요 없음.
|
| 48 |
- `README.md`, `requirements.txt`, `vcp_debug.py` — 웹 전용.
|
| 49 |
|
| 50 |
-
>
|
| 51 |
-
> `sync_to_qquery.sh`의 MAP 목록에
|
|
|
|
| 52 |
|
| 53 |
---
|
| 54 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
## 최근 반영된 것 (2026-08-06)
|
| 56 |
- 매수 엔진 최신 전체 (WAVE_HAT_ONLY_FRESH, REBASE_DIP_DAYS, LIFELINE_KEEP_MTN 등)
|
| 57 |
- `confirm_sell_backtest.py`: **204320 HL만도를 KOSDAQ→KOSPI로 수정** (이격을 KS11로 계산)
|
|
|
|
| 34 |
|
| 35 |
---
|
| 36 |
|
| 37 |
+
## 스크립트가 동기화하는 파일 (consolidated → QQuery) — 편집은 여기서만!
|
| 38 |
+
- `buy_engine_dipfix.py` ← quant/consolidated/
|
| 39 |
+
- `final_sell_monitor.py` ← quant/consolidated/
|
| 40 |
+
- `confirm_sell_backtest.py` ← quant/consolidated/
|
| 41 |
- `confirm_sell_backtest_rs.py` ← quant/consolidated/rs_sell/
|
| 42 |
- `final_sell_monitor_rs.py` ← quant/consolidated/rs_sell/
|
| 43 |
+
- `bollinger_count_debug.py` ← quant/consolidated/ (2026-08-06 QQuery 최신본을 consolidated로 끌어와 정본화)
|
| 44 |
|
| 45 |
## 스크립트가 건드리지 않는 파일 (QQuery가 정본)
|
| 46 |
+
- `app.py` — QQuery 전용 웹 앱. 여기서 직접 관리.
|
|
|
|
| 47 |
- `market_map.csv` — 양쪽 스냅샷이 조금씩 다름. 204320 등 핵심은 이미 맞음. 건드릴 필요 없음.
|
| 48 |
- `README.md`, `requirements.txt`, `vcp_debug.py` — 웹 전용.
|
| 49 |
|
| 50 |
+
> app.py나 market_map을 앞으로 consolidated에서 관리하고 싶으면: 먼저 QQuery 최신본을
|
| 51 |
+
> consolidated로 복사한 뒤, `sync_to_qquery.sh`의 MAP 목록에 한 줄 추가하면 된다.
|
| 52 |
+
> (bollinger_count_debug.py 를 이 방식으로 정본화한 예시가 스크립트에 있음)
|
| 53 |
|
| 54 |
---
|
| 55 |
|
| 56 |
+
## market_map.csv 자동 갱신 (2026-08-06 추가)
|
| 57 |
+
종목→시장(KOSPI/KOSDAQ) 표. **두 겹으로 최신 유지:**
|
| 58 |
+
1. **맥 스케줄러** `com.glorifiedjx.marketmap` (launchd) — 주기적으로 `build_market_map.py` 실행 →
|
| 59 |
+
`quant/sell_debug/market_map.csv` 갱신. 확인: `launchctl list | grep marketmap`
|
| 60 |
+
2. **app.py 실행 시 자동 갱신** — `app.py`가 켜질 때 백그라운드로 `build_market_map.py --if-stale 7
|
| 61 |
+
--out <자기 market_map.csv>` 를 돌림. **7일 넘었으면** 그 자리(QQuery/market_map.csv)에서 바로 최신화.
|
| 62 |
+
- fdr/한국IP 없는 배포 서버에선 조용히 스킵(커밋된 csv 사용). 맥 로컬(fdr O)에선 실제 갱신.
|
| 63 |
+
- 끄려면 app.py의 `MARKET_MAP_STALE_DAYS = 0`. 주기 바꾸려면 그 숫자(일) 조정.
|
| 64 |
+
- `build_market_map.py`는 `quant/sell_debug/`에 있고, app.py가 알아서 찾음(같은폴더 / ../quant/sell_debug / ~/Projects/quant/sell_debug 순).
|
| 65 |
+
- 로컬에서 app 켠 뒤 갱신됐으면 그 QQuery/market_map.csv 를 git commit&push 하면 웹 반영.
|
| 66 |
+
|
| 67 |
## 최근 반영된 것 (2026-08-06)
|
| 68 |
- 매수 엔진 최신 전체 (WAVE_HAT_ONLY_FRESH, REBASE_DIP_DAYS, LIFELINE_KEEP_MTN 등)
|
| 69 |
- `confirm_sell_backtest.py`: **204320 HL만도를 KOSDAQ→KOSPI로 수정** (이격을 KS11로 계산)
|