glorifiedjx commited on
Commit
cd11f92
ยท
verified ยท
1 Parent(s): 60cc4fd

Update app.py

Browse files

์ตœ์ข…๋ณด๋ฃจ ์ „๋Ÿ‰๋งค๋„ ์˜ต์…˜ ์ถ”๊ฐ€

Files changed (1) hide show
  1. app.py +149 -161
app.py CHANGED
@@ -434,27 +434,73 @@ def run_ma240(ticker):
434
  return buf.getvalue()
435
 
436
 
 
437
  # ============================================================
438
- # 3) ์ „๋Ÿ‰๋งค๋„ (RSยท์ด๊ฒฉ) (sell_+10_debug.py ๋กœ์ง ๊ทธ๋Œ€๋กœ)
 
 
439
  # ============================================================
440
  def run_sell(ticker, market_override="์ž๋™"):
441
  buf = io.StringIO()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
442
  try:
443
  with contextlib.redirect_stdout(buf):
444
  TICKER = ticker
 
445
 
446
- DIV_THRESHOLD = -0.10
447
- RS_MA_PERIOD = 20
448
- INDEX_PEAK_LOOKBACK = 180
449
- DATA_START = (datetime.today() - timedelta(days=600)).strftime('%Y-%m-%d')
450
- ANALYZE_TO = datetime.today().strftime('%Y-%m-%d')
451
 
452
- print("=" * 70)
453
- print(f"๋ถ„์„ ์ข…๋ชฉ: {TICKER}")
454
- print("=" * 70)
455
- print("\n[1/3] ์‹œ์žฅ ํŒ๋ณ„ ์ค‘...")
456
- market = None; name = TICKER
457
  mo = (market_override or "์ž๋™").strip()
 
458
  if mo in ('KOSPI', 'KOSDAQ'):
459
  market = mo
460
  print(f" (์ˆ˜๋™ ์ง€์ •) {market}")
@@ -475,166 +521,108 @@ def run_sell(ticker, market_override="์ž๋™"):
475
  break
476
  if market is None:
477
  market = 'KOSDAQ'
478
- print(f" ๋ชฉ๋ก์—์„œ ๋ชป ์ฐพ์Œ -> KOSDAQ ๊ฐ€์ • (์œ„ '์‹œ์žฅ' ๋ฉ”๋‰ด์—์„œ ์ง์ ‘ ์„ ํƒ ๊ฐ€๋Šฅ)")
479
  else:
480
  print(f" {name} ({TICKER}) / {market}")
481
  except Exception as e:
482
  market = 'KOSDAQ'
483
- print(f" ํŒ๋ณ„ ์‹คํŒจ({type(e).__name__}) -> KOSDAQ ๊ฐ€์ •. "
484
- f"์œ„ '์‹œ์žฅ' ๋ฉ”๋‰ด์—์„œ KOSPI/KOSDAQ๋ฅผ ์ง์ ‘ ๊ณจ๋ผ์ค˜.")
485
-
486
- print("\n[2/3] ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์ค‘...")
487
- stock_df = fdr.DataReader(TICKER, DATA_START, ANALYZE_TO)
488
- if stock_df is None or len(stock_df) == 0:
489
- print("์ข…๋ชฉ ๋ฐ์ดํ„ฐ ์—†์Œ"); sys.exit()
490
-
491
- candidates = ['KQ11', '^KQ11'] if market == 'KOSDAQ' else ['KS11', '^KS11']
492
- index_df = None
493
- for c in candidates:
494
- try:
495
- t = fdr.DataReader(c, DATA_START, ANALYZE_TO)
496
- if t is not None and len(t) > 0:
497
- index_df = t; print(f" ์ง€์ˆ˜: {c}"); break
498
- except:
499
- continue
500
- if index_df is None:
501
- print("์ง€์ˆ˜ ๋กœ๋“œ ์‹คํŒจ"); sys.exit()
502
-
503
- merged = pd.concat([
504
- stock_df['Close'].rename('stock'),
505
- index_df['Close'].rename('index')
506
- ], axis=1).dropna()
507
- print(f" ์ข…๋ชฉ {len(stock_df)}์ผ | ์ง€์ˆ˜ {len(index_df)}์ผ")
508
-
509
- print("\n[3/3] ์ง€์ˆ˜ ๊ณ ์  ํƒ์ƒ‰ ์ค‘...")
510
- cutoff = (datetime.today() - timedelta(days=INDEX_PEAK_LOOKBACK)).strftime('%Y-%m-%d')
511
- recent_idx = merged[merged.index >= cutoff]['index']
512
- index_peak_date = recent_idx.idxmax()
513
- index_peak_value = float(recent_idx.max())
514
- stock_at_peak = float(merged.loc[index_peak_date, 'stock']) if index_peak_date in merged.index else None
515
-
516
- latest_date = merged.index[-1]
517
- is_ath = (index_peak_date == latest_date)
518
-
519
- days_from_peak = (latest_date.to_pydatetime().replace(tzinfo=None) -
520
- index_peak_date.to_pydatetime().replace(tzinfo=None)).days
521
-
522
- print(f" ์ง€์ˆ˜ ๊ณ ์ : {index_peak_value:,.2f} ({str(index_peak_date)[:10]})")
523
- print(f" ์ •์ ๋‚  ์ข…๋ชฉ: {stock_at_peak:,.0f}" if stock_at_peak else " ์ •์ ๋‚  ์ข…๋ชฉ: ์—†์Œ")
524
- print(f" ์˜ค๋Š˜ ์ง€์ˆ˜: {float(merged['index'].iloc[-1]):,.2f} ({str(latest_date)[:10]})")
525
- print(f" ATH ์—ฌ๋ถ€: {'์˜ˆ โ†’ RS Line ๋ถ„์„' if is_ath else f'์•„๋‹ˆ์˜ค (๊ณ ์  {days_from_peak}์ผ ๊ฒฝ๊ณผ) โ†’ ์ด๊ฒฉ ๋ถ„์„'}")
526
-
527
- FLAG_START = str(index_peak_date)[:10]
528
-
529
- if not is_ath:
530
- flag_data = merged[merged.index >= FLAG_START].copy()
531
- if len(flag_data) == 0:
532
- print("๋ฐ์ดํ„ฐ ์—†์Œ"); sys.exit()
533
-
534
- print("\n" + "=" * 70)
535
- print("[์ด๊ฒฉ ๋ถ„์„] ์ง€์ˆ˜ ๊ณ ์  ๊ธฐ์ค€")
536
- print(f"์ง€์ˆ˜ ์ •์ : {FLAG_START} ({index_peak_value:,.2f})")
537
- print("=" * 70)
538
- print(f"\n{'๋‚ ์งœ':<12} {'์ข…๋ชฉ๊ฐ€':>8} {'์ง€์ˆ˜':>8} {'์ง€์ˆ˜์ˆ˜์ต':>9} {'์ข…๋ชฉ์ˆ˜์ต':>9} {'์ด๊ฒฉ':>8} {'flag':>5} ํŒ์ •")
539
- print("-" * 78)
540
-
541
- div_flag = False; div_flag_value = None
542
- sell_signal = False; sell_date = None; sell_reason = None
543
-
544
- for date, row in flag_data.iterrows():
545
- s = float(row['stock']); idx = float(row['index'])
546
- if stock_at_peak is None:
547
- continue
548
- ir = (idx / index_peak_value) - 1
549
- sr = (s / stock_at_peak) - 1
550
  div = sr - ir
551
- verdict = ''
552
  if div <= DIV_THRESHOLD:
553
  if not div_flag:
554
- div_flag = True; div_flag_value = div
555
- verdict = '*** FLAG ON (์˜ค๋Š˜ ๋งค๋„X)'
 
 
 
 
 
 
 
 
 
 
556
  else:
557
- if div < div_flag_value:
558
- verdict = '!!! ์ „๋Ÿ‰๋งค๋„!'
559
- if not sell_signal:
560
- sell_signal = True; sell_date = str(date)[:10]
561
- sell_reason = f'์ด๊ฒฉ {div*100:.1f}%'
562
- div_flag_value = div
563
- else:
564
- verdict = 'Watch'; div_flag_value = div
565
  else:
566
- if div_flag:
567
- div_flag = False; div_flag_value = None; verdict = 'FLAG OFF'
568
-
569
- print(f"{str(date)[:10]:<12} {s:>8,.0f} {idx:>8,.0f} "
570
- f"{ir*100:>+8.1f}% {sr*100:>+8.1f}% {div*100:>+7.1f}% "
571
- f"{'ON' if div_flag else '':>5} {verdict}")
572
 
573
- print()
574
- if sell_signal:
575
- print(f">>> ์ „๋Ÿ‰ ๋งค๋„ ์‹ ํ˜ธ: {sell_date} | {sell_reason}")
576
- else:
577
- print(">>> ์•„์ง ์ „๋Ÿ‰ ๋งค๋„ ์‹ ํ˜ธ ์—†์Œ")
578
-
579
- if stock_at_peak:
580
- last = flag_data.iloc[-1]
581
- dv_now = ((float(last['stock'])/stock_at_peak) - 1) - ((float(last['index'])/index_peak_value) - 1)
582
- print(f"\nํ˜„์žฌ ์ด๊ฒฉ ({str(flag_data.index[-1])[:10]}):")
583
- print(f" ์ง€์ˆ˜ ์ •์  : {index_peak_value:,.2f} ({FLAG_START})")
584
- print(f" ์ •์ ๋‚  ์ข…๋ชฉ : {stock_at_peak:,.0f}")
585
- print(f" ํ˜„์žฌ ์ข…๋ชฉ : {float(last['stock']):,.0f}")
586
- print(f" ์ด๊ฒฉ : {dv_now*100:+.1f}% (๊ธฐ์ค€ {DIV_THRESHOLD*100:.0f}%)")
587
- print(f" div_flag : {'ON' if div_flag else 'off'}")
588
-
589
- print("\n" + "=" * 70)
590
- print("์ตœ์ข… ์š”์•ฝ")
591
- print("=" * 70)
592
- print(f"์ข…๋ชฉ : {name} ({TICKER}) / {market}")
593
- print(f"์ง€์ˆ˜ ์ •์  : {FLAG_START} ({index_peak_value:,.2f}) | {days_from_peak}์ผ ๊ฒฝ๊ณผ")
594
- print(f"์ด๊ฒฉ ์‹ ํ˜ธ : {('>>> ' + sell_date) if sell_signal else 'None'}")
595
- print()
596
- if sell_signal:
597
- print(f"==> ๋งค๋„ ์‹ ํ˜ธ: {sell_date}")
598
- else:
599
- print("==> ์•„์ง ๋งค๋„ ์‹ ํ˜ธ ์—†์Œ. ๊ณ„์† ๋ณด์œ .")
600
-
601
- else:
602
- print("\n" + "=" * 70)
603
- print(f"[RS Line ๋ถ„์„] MA{RS_MA_PERIOD} - ์˜ค๋Š˜์ด ์ง€์ˆ˜ ๊ณ ์  (ATH)")
604
- print("=" * 70)
605
-
606
- rs_all = merged.copy()
607
- rs_all['rs'] = rs_all['stock'] / rs_all['index']
608
- rs_all['rs_ma'] = rs_all['rs'].rolling(RS_MA_PERIOD).mean()
609
- rs_flag = rs_all[rs_all.index >= FLAG_START].dropna(subset=['rs_ma'])
610
-
611
- rs_sell_signal = False; rs_sell_date = None
612
-
613
- if len(rs_flag) == 0:
614
- print("RS MA ๊ณ„์‚ฐ ๋ถˆ๊ฐ€ (๋ฐ์ดํ„ฐ ๋ถ€์กฑ)")
615
- else:
616
- print(f"\n{'๋‚ ์งœ':<12} {'์ข…๋ชฉ๊ฐ€':>8} {'RS':>10} {'RS MA20':>10} {'vs MA':>8} ํŒ์ •")
617
- print("-" * 58)
618
- for date, row in rs_flag.iterrows():
619
- rs = float(row['rs']); rma = float(row['rs_ma'])
620
- pct = (rs / rma - 1) * 100
621
- v = '>>> ๋ฐ๋“œํฌ๋กœ์Šค!' if rs < rma else 'Golden'
622
- if rs < rma and not rs_sell_signal:
623
- rs_sell_signal = True; rs_sell_date = str(date)[:10]
624
- print(f"{str(date)[:10]:<12} {float(row['stock']):>8,.0f} "
625
- f"{rs:>10.4f} {rma:>10.4f} {pct:>+7.1f}% {v}")
626
-
627
- print("\n" + "=" * 70)
628
- print("์ตœ์ข… ์š”์•ฝ")
629
- print("=" * 70)
630
- print(f"์ข…๋ชฉ : {name} ({TICKER}) / {market}")
631
- print(f"์ง€์ˆ˜ ์ •์  : {FLAG_START} ({index_peak_value:,.2f}) | ATH")
632
- print(f"RS ์‹ ํ˜ธ : {('>>> ' + rs_sell_date) if rs_sell_signal else 'None'}")
633
- print()
634
- if rs_sell_signal:
635
- print(f"==> RS Line ๋งค๋„ ์‹ ํ˜ธ: {rs_sell_date}")
636
  else:
637
- print("==> RS Line ๋ฐ๋“œํฌ๋กœ์Šค ์—†์Œ. ๊ณ„์† ๋ณด์œ .")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
638
  except SystemExit:
639
  pass
640
  except Exception as e:
@@ -928,7 +916,7 @@ VCP_DETECT = "VCP ๋ŒํŒŒ ๊ฐ์ง€"
928
  VCP_SELL = "VCP ์‹คํŒจ ๋ถ€๋ถ„๋งค๋„"
929
  BOLL = "๋ณผ๋ฆฐ์ € ์นด์šดํŠธ"
930
  MA240 = "240์„  ์ถ”์„ธ์‚ฌ๋ง ์ปท"
931
- SELL = "์ „๋Ÿ‰๋งค๋„ (RSยท์ด๊ฒฉ)"
932
 
933
  CHOICES = [VCP_DETECT, VCP_SELL, BOLL, MA240, SELL]
934
 
 
434
  return buf.getvalue()
435
 
436
 
437
+
438
  # ============================================================
439
+ # 3) ์ „๋Ÿ‰๋งค๋„ (์ตœํ›„๋ณด๋ฃจ) (final_sell_monitor.py ๋กœ์ง ์ด์‹ ยท ๋‹จ์ผ ํ‹ฐ์ปค)
440
+ # ์ฃผ ์‹ ํ˜ธ : ์ž๊ธฐ๊ณ ์  ์ด๊ฒฉ -10% -> 22์ผ๋‚ด lower-low ์žฌํ™•์ธ (240์˜ฌ ๊ตฌ๊ฐ„)
441
+ # ๋ฐฑ์—… : ์ข…๊ฐ€๊ฐ€ 240์„  BREAK_CONFIRM์ผ ์—ฐ์† ์ดํƒˆ
442
  # ============================================================
443
  def run_sell(ticker, market_override="์ž๋™"):
444
  buf = io.StringIO()
445
+
446
+ DIV_THRESHOLD = -0.10 # ์ด๊ฒฉ ๊ธฐ์ค€
447
+ CONFIRM_WINDOW = 22 # lower-low ์žฌํ™•์ธ ์œˆ๋„์šฐ(๊ฑฐ๋ž˜์ผ)
448
+ TREND_MIN_DAYS = 60 # '240์˜ฌ': ์ตœ๊ทผ N๊ฑฐ๋ž˜์ผ ์—ฐ์† 240์„  ์œ„
449
+ TREND_MIN_GAP = 0.05 # '240์˜ฌ': 240์„  +5% ์ด์ƒ
450
+ BREAK_PCT = 0.00 # ๋ฐฑ์—… 240์„  ์ดํƒˆ ๊ธฐ์ค€
451
+ BREAK_CONFIRM = 2 # ๋ฐฑ์—…: ์—ฐ์† N์ผ ์ดํƒˆ
452
+ DATA_BACK_DAYS = 900 # 240MA ์›Œ๋ฐ์—… ํฌํ•จ ๋กœ๋“œ(โ‰ˆ2.5๋…„)
453
+
454
+ def in_240_run(stock, ma240, i):
455
+ if i - TREND_MIN_DAYS + 1 < 0:
456
+ return False
457
+ m = ma240[i]
458
+ if np.isnan(m) or stock[i] <= m * (1 + TREND_MIN_GAP):
459
+ return False
460
+ for k in range(i - TREND_MIN_DAYS + 1, i + 1):
461
+ if np.isnan(ma240[k]) or stock[k] <= ma240[k]:
462
+ return False
463
+ return True
464
+
465
+ def load(tk, market):
466
+ end = datetime.today().strftime('%Y-%m-%d')
467
+ start = (datetime.today() - timedelta(days=DATA_BACK_DAYS)).strftime('%Y-%m-%d')
468
+ sdf = fdr.DataReader(tk, start, end)
469
+ if sdf is None or len(sdf) == 0:
470
+ return None
471
+ idx_code = 'KS11' if market == 'KOSPI' else 'KQ11'
472
+ idf = None
473
+ for c in (idx_code, '^' + idx_code):
474
+ try:
475
+ t = fdr.DataReader(c, start, end)
476
+ if t is not None and len(t) > 0:
477
+ idf = t; break
478
+ except Exception:
479
+ continue
480
+ if idf is None:
481
+ return None
482
+ merged = pd.concat([sdf['Close'].rename('stock'), idf['Close'].rename('index')],
483
+ axis=1, sort=True)
484
+ merged['index'] = merged['index'].ffill()
485
+ merged = merged.dropna()
486
+ merged['ma240'] = merged['stock'].rolling(240).mean()
487
+ return merged
488
+
489
  try:
490
  with contextlib.redirect_stdout(buf):
491
  TICKER = ticker
492
+ name = TICKER
493
 
494
+ print("=" * 72)
495
+ print(f"์ „๋Ÿ‰๋งค๋„ ์ตœํ›„๋ณด๋ฃจ ๋ชจ๋‹ˆํ„ฐ | {datetime.today().strftime('%Y-%m-%d %H:%M')}")
496
+ print(f"์ฃผ ์‹ ํ˜ธ: 240์˜ฌ + ์ด๊ฒฉ {DIV_THRESHOLD*100:.0f}% lower-low ์žฌํ™•์ธ({CONFIRM_WINDOW}์ผ)"
497
+ f" | ๋ฐฑ์—…: 240์„  {BREAK_CONFIRM}์ผ ์—ฐ์† ์ดํƒˆ")
498
+ print("=" * 72)
499
 
500
+ # --- ์‹œ์žฅ ๊ฒฐ์ • (์ˆ˜๋™ ์šฐ์„ , ์ž๋™์€ KRX ๊ฐ€๋Šฅํ•  ๋•Œ๋งŒ) ---
501
+ print("\n[์‹œ์žฅ ํŒ๋ณ„]")
 
 
 
502
  mo = (market_override or "์ž๋™").strip()
503
+ market = None
504
  if mo in ('KOSPI', 'KOSDAQ'):
505
  market = mo
506
  print(f" (์ˆ˜๋™ ์ง€์ •) {market}")
 
521
  break
522
  if market is None:
523
  market = 'KOSDAQ'
524
+ print(f" ๋ชฉ๋ก์—์„œ ๋ชป ์ฐพ์Œ -> KOSDAQ ๊ฐ€์ • (์œ„ '์‹œ์žฅ' ๋ฉ”๋‰ด์—์„œ ์ง์ ‘ ์„ ํƒ)")
525
  else:
526
  print(f" {name} ({TICKER}) / {market}")
527
  except Exception as e:
528
  market = 'KOSDAQ'
529
+ print(f" ์ž๋™ํŒ๋ณ„ ์‹คํŒจ({type(e).__name__}) -> KOSDAQ ๊ฐ€์ •. "
530
+ f"์œ„ '์‹œ์žฅ' ๋ฉ”๋‰ด์—์„œ KOSPI/KOSDAQ ์ง์ ‘ ์„ ํƒ ๊ถŒ์žฅ.")
531
+
532
+ # --- ๋ฐ์ดํ„ฐ ๋กœ๋“œ ---
533
+ df = load(TICKER, market)
534
+ if df is None:
535
+ print(f"\n[{TICKER} {name}] ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์‹คํŒจ (ํ‹ฐ์ปค/์‹œ์žฅ ํ™•์ธ)"); sys.exit()
536
+
537
+ dates = [str(d)[:10] for d in df.index]
538
+ stock = df['stock'].values.astype(float)
539
+ index = df['index'].values.astype(float)
540
+ ma240 = df['ma240'].values.astype(float)
541
+ n = len(stock)
542
+
543
+ # --- ์›Œํฌํฌ์›Œ๋“œ: ์ž๊ธฐ๊ณ ์  + ์ด๊ฒฉ ์ด๋ฒคํŠธ + lower-low ์žฌํ™•์ธ (๊ฐ€์žฅ ์ตœ๊ทผ ๊ฒƒ ์ถ”์ ) ---
544
+ running_peak = -1.0; index_at_peak = None; peak_date = None
545
+ div_flag = False; div_flag_value = None; div_sold = False
546
+ last_event = None
547
+ confirmed = None
548
+
549
+ for t in range(n):
550
+ st = stock[t]; it = index[t]
551
+ if running_peak < 0 or st >= running_peak:
552
+ running_peak = st; index_at_peak = it; peak_date = dates[t]
553
+ div_flag = False; div_flag_value = None; div_sold = False
554
+ last_event = None
555
+ confirmed = None
556
+ else:
557
+ sr = st / running_peak - 1
558
+ ir = (it / index_at_peak - 1) if index_at_peak else 0.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
559
  div = sr - ir
 
560
  if div <= DIV_THRESHOLD:
561
  if not div_flag:
562
+ div_flag = True; div_flag_value = div; div_sold = False
563
+ elif div < div_flag_value:
564
+ if not div_sold:
565
+ if (last_event is not None
566
+ and t - last_event['i'] <= CONFIRM_WINDOW
567
+ and st < last_event['price']):
568
+ confirmed = {'i': t, 'date': dates[t], 'price': st,
569
+ 'prev_date': last_event['date'],
570
+ 'run': in_240_run(stock, ma240, t)}
571
+ last_event = {'i': t, 'price': st, 'date': dates[t]}
572
+ div_sold = True
573
+ div_flag_value = div
574
  else:
575
+ div_flag_value = div
 
 
 
 
 
 
 
576
  else:
577
+ div_flag = False; div_flag_value = None; div_sold = False
 
 
 
 
 
578
 
579
+ # --- ๋ฐฑ์—…: 240์„  ์ดํƒˆ (์—ฐ์† BREAK_CONFIRM์ผ) ---
580
+ break_240 = None
581
+ streak = 0
582
+ for t in range(n):
583
+ if np.isnan(ma240[t]):
584
+ continue
585
+ if stock[t] < ma240[t] * (1 - BREAK_PCT):
586
+ streak += 1
587
+ if streak >= BREAK_CONFIRM and break_240 is None:
588
+ break_240 = dates[t]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
589
  else:
590
+ streak = 0
591
+ break_240 = None
592
+
593
+ # --- ์˜ค๋Š˜ ์ƒํƒœ ---
594
+ t = n - 1
595
+ today = dates[t]
596
+ m = ma240[t]
597
+ run_now = in_240_run(stock, ma240, t)
598
+ sr_now = stock[t] / running_peak - 1 if running_peak > 0 else 0.0
599
+ ir_now = (index[t] / index_at_peak - 1) if index_at_peak else 0.0
600
+ div_now = sr_now - ir_now
601
+
602
+ print("\n" + "=" * 72)
603
+ print(f"[{TICKER} {name}] {market} | {today} ์ข…๊ฐ€ {stock[t]:,.0f}"
604
+ + (f" | 240์„  {m:,.0f} ({(stock[t]/m-1)*100:+.1f}%)" if not np.isnan(m) else " | 240์„  ๋ฐ์ดํ„ฐ๋ถ€์กฑ"))
605
+ print(f" 240์˜ฌ ์—ฌ๋ถ€ : {'์˜ˆ (60์ผ ์—ฐ์† 240์œ„ + 5%โ†‘)' if run_now else '์•„๋‹ˆ์˜ค'}")
606
+ print(f" ์ข…๋ชฉ ๊ณ ์  : {running_peak:,.0f} ({peak_date}) | ํ˜„์žฌ ์ด๊ฒฉ {div_now*100:+.1f}%"
607
+ f" (๊ธฐ์ค€ {DIV_THRESHOLD*100:.0f}%) | flag {'ON' if div_flag else 'off'}")
608
+ if last_event and not confirmed:
609
+ remain = CONFIRM_WINDOW - (t - last_event['i'])
610
+ if remain > 0:
611
+ print(f" 1์ฐจ ์ด๋ฒคํŠธ : {last_event['date']} @ {last_event['price']:,.0f}"
612
+ f" -> ์žฌํ™•์ธ ๋Œ€๊ธฐ {remain}๊ฑฐ๋ž˜์ผ ๋‚จ์Œ (์ด๋ณด๋‹ค ๋‚ฎ์€ 2์ฐจ ์ด๋ฒคํŠธ๋ฉด ์ „๋Ÿ‰๋งค๋„)")
613
+
614
+ # --- ํŒ์ • ---
615
+ if confirmed:
616
+ tag = '240์˜ฌ' if confirmed['run'] else '๋น„240์˜ฌ(์ฐธ๊ณ )'
617
+ ago = (n - 1) - confirmed['i']
618
+ print(f"\n >>> [์ฃผ ์‹ ํ˜ธ] ์ „๋Ÿ‰๋งค๋„: {confirmed['date']} @ {confirmed['price']:,.0f}"
619
+ f" (1์ฐจ {confirmed['prev_date']} -> lower-low ์žฌํ™•์ธ, {tag}) | {ago}๊ฑฐ๋ž˜์ผ ์ „")
620
+ if not confirmed['run']:
621
+ print(" (240์˜ฌ ๊ตฌ๊ฐ„์ด ์•„๋‹ˆ์–ด์„œ ์‹ ๋ขฐ๋„๋Š” ํ•œ ๋‹จ๊ณ„ ๋‚ฎ์Œ โ€” ์ฐจํŠธ๋กœ ํ™•์ธ)")
622
+ if break_240:
623
+ print(f"\n >>> [๋ฐฑ์—…] 240์„  ์ดํƒˆ ๋งค๋„: {break_240} (์—ฐ์† {BREAK_CONFIRM}์ผ ์ดํƒˆ, ํ˜„์žฌ๋„ ์ดํƒˆ ์ค‘)")
624
+ if not confirmed and not break_240:
625
+ print(f"\n ==> ๋งค๋„ ์‹ ํ˜ธ ์—†์Œ. ๋ณด์œ  ์œ ์ง€.")
626
  except SystemExit:
627
  pass
628
  except Exception as e:
 
916
  VCP_SELL = "VCP ์‹คํŒจ ๋ถ€๋ถ„๋งค๋„"
917
  BOLL = "๋ณผ๋ฆฐ์ € ์นด์šดํŠธ"
918
  MA240 = "240์„  ์ถ”์„ธ์‚ฌ๋ง ์ปท"
919
+ SELL = "์ „๋Ÿ‰๋งค๋„ (์ตœํ›„๋ณด๋ฃจ)"
920
 
921
  CHOICES = [VCP_DETECT, VCP_SELL, BOLL, MA240, SELL]
922