glorifiedjx commited on
Commit
be61c19
ยท
verified ยท
1 Parent(s): 565d5fc

back to stage

Browse files
Files changed (1) hide show
  1. confirm_sell_backtest.py +114 -602
confirm_sell_backtest.py CHANGED
@@ -4,7 +4,8 @@
4
 
5
  ๊ทœ์น™:
6
  - ์ด๋ฒคํŠธ = ์ข…๋ชฉ ์ž๊ธฐ๊ณ ์  ๊ธฐ์ค€ ์ด๊ฒฉ์ด -10% ์œ„์—์„œ -10% ์ดํ•˜๋กœ '์ƒˆ๋กœ ์ง„์ž…'ํ•œ ๊ตฌ๊ฐ„ 1๊ฐœ (๊นŠ์ด ๋ฌด๊ด€)
7
- - 1์ฐจ ํ›„ ๋‹ค์Œ ์ด๋ฒคํŠธ -> 2์ฐจ,3์ฐจ... ์—ฐ์‡„ (์ผ์ˆ˜ ์ œํ•œ ์—†์Œ)
 
8
  - 2์ฐจ๊ฐ€ ์„ฑ๋ฆฝํ•˜๋ฉด ๊ทธ ๋‚  '์ „๋Ÿ‰๋งค๋„' (๊นŠ์ด ๋น„๊ต ์—†์Œ)
9
  - ์‹ ๊ณ ๊ฐ€ ๊ฐฑ์‹  ์‹œ ๋ฆฌ์…‹
10
 
@@ -18,9 +19,7 @@ import sys, ssl
18
  import pandas as pd
19
  import numpy as np
20
  import FinanceDataReader as fdr
21
- from datetime import datetime, timedelta, timezone
22
-
23
- _KST = timezone(timedelta(hours=9))
24
 
25
  ssl._create_default_https_context = ssl._create_unverified_context
26
  if sys.platform == 'win32':
@@ -31,142 +30,58 @@ if sys.platform == 'win32':
31
  # =============================================
32
  TICKERS = {
33
  'KOSPI': [
34
- # '005930', '000660', '373220', '207940',
35
- # '005380',
36
  # '000270', '068270', '035420', '035720', '005490',
37
- # '105560', '055550', '012330',
38
- # '051910',
39
- # '012450',
40
- # '006400',
41
  # '028260', '066570', '096770', '034730', '015760',
42
- # '178920',
43
  # '204320',
44
- # '005250',
45
  ],
46
  'KOSDAQ': [
47
  # '247540', '086520', '196170', '028300', '348370',
48
- # '058470', '068760', '263750', '035900', '277810',
 
 
49
  # '041510', '035760', '112040', '293490', '145020',
50
  # '039030', '240810', '357780',
51
  # '403870',
52
  # '121600',
53
  # '399720',
54
- # '277810',
55
- # '257720',
56
- # '017510',
57
- # '389020',
58
- # '195940',
59
- '251370',
60
  ],
61
  }
62
 
63
  # =============================================
64
  # ์„ค์ •
65
  # =============================================
66
- DIV_THRESHOLD = -0.10 # ์ด๊ฒฉ ๊ธฐ์ค€(๊ณ ์ •). ์ด๊ฒฉ์ด ์ด๋ณด๋‹ค ๊นŠ์–ด์ง€๋ฉด flag ON ์‹œ์ž‘.
67
- MIN_DEEPEN = 0.020 # ๋‹ค์Œ ๋‹จ๊ณ„๋กœ ๊ฐ€๋ ค๋ฉด ์ง์ „ ๋‹จ๊ณ„๋ณด๋‹ค ์ตœ์†Œ ์ด๋งŒํผ(%p) ๋” ๊นŠ์–ด์•ผ (๋…ธ์ด์ฆˆ ๋ฐฉ์ง€)
68
- DAILY_JUMP = 0.10 # ํ•˜๋ฃจ๋งŒ์— ์ด๊ฒฉ์ด ์ „์ผๆฏ” ์ด๋งŒํผ(%p)+ ๊ธ‰๋ฝํ•˜๋ฉด '๋Œ€๊ธฐ' ๊ฑด๋„ˆ๋›ฐ๊ณ  ์ ํ”„
69
-
70
- # --- ๋‹จ๊ณ„ ์ˆ˜ ์†์žก์ด: flag ON ํ›„ '๋ช‡ ๋ฒˆ ๋” ๊นŠ์–ด์ ธ์•ผ' ์ „๋Ÿ‰๋งค๋„๊ฐ€ ๋ฐœ๋™ํ•˜๋‚˜ ---
71
- # 3 = ๊ธฐ์กด(flagโ†’1์ฐจโ†’2์ฐจflagโ†’2์ฐจ๋งค๋„, ๊นŠ์–ด์ง 3๋ฒˆ). ์ค„์ด๋ฉด ๋” ์ผ์ฐ ์šธ๋ฆฌ์ง€๋งŒ ๋” ์ž์ฃผ ์šธ๋ฆผ.
72
- # 2 = flagโ†’1์ฐจโ†’๋งค๋„ (๊นŠ์–ด์ง 2๋ฒˆ) / 1 = flagโ†’๋งค๋„ (๊นŠ์–ด์ง 1๋ฒˆ, ๊ฐ€์žฅ ์ผ์ฐ/๊ฐ€์žฅ ์ž์ฃผ)
73
- CONFIRM_STAGES = 0
74
-
75
- # =============================================
76
- # ๋งค๋„ ์—”์ง„ ์„ ํƒ
77
- # 'STAGE' = ๊ธฐ์กด: ์ด๊ฒฉ flagโ†’1์ฐจโ†’2์ฐจflagโ†’2์ฐจ (๋‹จ๊ณ„๋ˆ„์ ). ํ—›์•Œ๋žŒ ์ ์ง€๋งŒ ํ›„ํ–‰(๋А๋ฆผ).
78
- # 'RS_A4' = ์ƒˆ๊ฑฐ: RS์„ ์ด ์‹œ์žฅ๋ณด๋‹ค ์•ฝํ•ด์ง€๋ฉด(๋ฐ๋“œํฌ๋กœ์Šค) '๋ฌด์žฅ' -> A-4(์ถ”์„ธ์ฃฝ์Œ) ํ™•์ธ๋˜๋Š”
79
- # ์ฆ‰์‹œ ๋งค๋„. ๋‹จ๊ณ„ ์•ˆ ๊ฑฐ์นจ -> ๋” ์ผ์ฐ. RS๋Š” ์„ ํ–‰์ง€ํ‘œ๋ผ 240 ๊นจ์ง€๊ธฐ ์ „์— ๊ฒฝ๊ณ .
80
- # =============================================
81
- ENGINE = 'RS_A4' # 'STAGE' / 'RS_A4'
82
- RS_MA_PERIOD = 20 # RS์„ (์ข…๋ชฉ/์ง€์ˆ˜)์˜ ์ด๋™ํ‰๊ท  ๊ธฐ๊ฐ„. RS < ์ด ํ‰๊ท ์ด๋ฉด ๋ฐ๋“œํฌ๋กœ์Šค(์•ฝํ™”)
83
- RS_SMOOTH = 5 # RS์„  ์ž์ฒด๋ฅผ ์ด ์ผ์ˆ˜ EMA๋กœ ๋ถ€๋“œ๋Ÿฝ๊ฒŒ(ํ†ฑ๋‹ˆ ์ œ๊ฑฐ). 1์ด๋ฉด smoothing ์•ˆํ•จ.
84
- RS_BUFFER = 0.02 # ๋ฐ๋“œํฌ๋กœ์Šค ๋ฒ„ํผ: RS๊ฐ€ MA๋ณด๋‹ค '์ด๋งŒํผ(2%) ํ™•์‹คํžˆ ์•„๋ž˜'์—ฌ์•ผ ์•ฝํ™” ์ธ์ •
85
- # (RSยทMA ๋”ฑ ๋ถ™์–ด์„œ 0.0001 ์ฐจ์ด๋กœ ํ†กํ†ก ๊ต์ฐจํ•˜๋Š” ์žฆ์€ ์•Œ๋žŒ ๋ฐฉ์ง€)
86
- RS_PERSIST = 3 # RS๊ฐ€ ์ด๋งŒํผ(๊ฑฐ๋ž˜์ผ) ์—ฐ์† ์•ฝํ•ด์•ผ '๋ฌด์žฅ' (ํ•˜๋ฃจ ๋…ธ์ด์ฆˆ ๋ฐฉ์ง€)
87
- RS_CONFIRM = 'MA60' # ๋ฌด์žฅ ํ›„ '๋งค๋„ ํ™•์ธ' ์‹ ํ˜ธ:
88
- # 'A4' = ์ดํ‰์„  ํ‰ํ‰(๋А๋ฆผ/์•ˆ์ „, ์ฒœ์ฒœํžˆ ์ฃฝ๋Š” ์ถ”์„ธ์šฉ)
89
- # 'MA60' = ์ข…๊ฐ€๊ฐ€ 60์„  ๊นจ๋ฉด(๋น ๋ฆ„! ๊ธ‰๋ฝ์— ๊ฐ•ํ•จ) โ† ๋Šฆ๊ฒŒ ์šฐ๋Š” ๊ฑฐ ํ•ด๊ฒฐ
90
- # 'MA120' = ์ข…๊ฐ€๊ฐ€ 120์„  ๊นจ๋ฉด(์ค‘๊ฐ„)
91
- # 'NONE' = RS ๋ฐ๋“œํฌ๋กœ์Šค๋งŒ์œผ๋กœ(์ œ์ผ ๋น ๋ฆ„/์ œ์ผ ์‹œ๋„๋Ÿฌ์›€)
92
- CONFIRM_NEEDS_FLAT = 'MARGIN' # MA60/MA120 ํ™•์ธ์— ์ถ”๊ฐ€ ๊ฒŒ์ดํŠธ (์˜ค๋ฅด๋Š” ์ค‘์—” ๋งค๋„ ๊ธˆ์ง€):
93
- # 'OFF' = ์ข…๊ฐ€๋งŒ (๋น ๋ฅด์ง€๋งŒ ์˜ค๋ฅผ ๋•Œ๋„ ์ฐํž˜)
94
- # 'SELF' = ํ™•์ธ์„  ๊ธฐ์šธ๊ธฐ ์šฐ์ƒํ–ฅ์ด๋ฉด ๊ธˆ์ง€ (๊ธฐ์šธ๊ธฐ ํ›„ํ–‰โ†’๋А๋ฆผ)
95
- # 'MARGIN' = ์ข…๊ฐ€๊ฐ€ ํ™•์ธ์„ ๋ณด๋‹ค CONFIRM_MARGIN ์•„๋ž˜์—ฌ์•ผ (๋น ๋ฆ„+๋ˆŒ๋ฆผ๋ฌด์‹œ)
96
- # 'MARGIN+240' = MARGIN + 240์„  ๊ฐ€ํŒŒ๋ฅธ์ƒ์Šน(>FLAT_MAX_240)์ด๋ฉด ๊ธˆ์ง€ โ† ์ถ”์ฒœ
97
- # '240' = 240์„ ์ด ํ‰ํ‰/๊บพ์ž„์ด์–ด์•ผ (๋” ์—„๊ฒฉ)
98
- # (RS_CONFIRM='A4'/'NONE'์ผ ๋• ์ด ์„ค์ • ๋ฌด์‹œ)
99
- CONFIRM_MARGIN = 0.035 # 'MARGIN' ๋ชจ๋“œ์—์„œ: ์ข…๊ฐ€๊ฐ€ ํ™•์ธ์„ ๋ณด๋‹ค ์ด๋งŒํผ(3%) ์•„๋ž˜์—ฌ์•ผ '๋ถ•๊ดด'๋กœ ์ธ์ •.
100
- # ์ž ๊น ๋ˆŒ๋ฆผ(์„  ์‚ด์ง ์Šค์นจ)์€ ๋ฌด์‹œ, ์ง„์งœ ๊ธ‰๋ฝ๋งŒ. ๊ธฐ์šธ๊ธฐ ์•ˆ ๊ธฐ๋‹ค๋ ค์„œ ๋น ๋ฆ„.
101
- SELL_ONCE = True # ์ฒซ ๋งค๋„ ํ›„์—” '์ง„์งœ ์ถ”์„ธ๋ณต๊ท€'๊นŒ์ง€ ์žฌ๋ฌด์žฅ ๊ธˆ์ง€.
102
- # ๋–จ์–ด์ง€๋Š” ์ข…๋ชฉ์ด -30/-40/-50/-80%์„œ ๊ณ„์† ๋˜ ์šฐ๋Š” ํ†ฑ๋‹ˆ ์ œ๊ฑฐ.
103
- RELOCK_LINE = 240 # ์ž ๊ธˆํ•ด์ œ ๊ธฐ์ค€์„ : 240=ํ™•์‹ค(ํญ๋ฝ์ฃผ ๋‚ด๋‚ด ์ž ๊น€) / 120=๋А์Šจ(๋นจ๋ฆฌ ํ’€๋ฆผ)
104
- # ํญ๋ฝ ์ค‘์—” 120์„ ์ด ์ฃผ๊ฐ€์— ๋ฐ”์ง ๋ถ™์–ด ์ž ๊น ๋ฐ˜๋“ฑ์—๋„ ํ’€๋ ค๋ฒ„๋ฆผ -> 240 ๊ถŒ์žฅ
105
- RELOCK_MIN_DAYS = 60 # + ์ฒซ ๋งค๋„ ํ›„ ์ตœ์†Œ ์ด ์ผ์ˆ˜๋Š” ๋ฌด์กฐ๊ฑด ์ž ๊ธˆ(๋‘˜ ๋‹ค ์ถฉ์กฑํ•ด์•ผ ํ•ด์ œ)
106
- NO_ARM_BELOW_240 = -0.10 # ์ข…๊ฐ€๊ฐ€ 240์„ ๋ณด๋‹ค ์ด๋งŒํผ(-10%) ๋” ์•„๋ž˜๋ฉด '๋ฌด์žฅ ๊ธˆ์ง€'.
107
- # ์ด๋ฏธ ์ถ”์„ธ ๋ฌด๋„ˆ์ง„(240 ํ•œ์ฐธ ์•„๋ž˜) ์ข…๋ชฉ์€ ์ตœํ›„๋ณด๋ฃจ ๋Œ€์ƒ ์•„๋‹˜.
108
- # None ์ด๋ฉด ์ด ํ•„ํ„ฐ ๋”(240 ์•„๋ž˜์—ฌ๋„ ๋ฌด์žฅ ํ—ˆ์šฉ).
109
- NO_ARM_DIV_BELOW = -0.30 # ์ด๊ฒฉ(๊ณ ์  ๋Œ€๋น„ ๋‚™ํญ)์ด ์ด๋ฏธ ์ด๋งŒํผ(-25%) ๋„˜๊ฒŒ ๊นŠ์œผ๋ฉด '๋ฌด์žฅ ๊ธˆ์ง€'.
110
- # 240์„ ์€ ํญ๋ฝ์ฃผ ๋”ฐ๋ผ ๋ฐ”๋‹ฅ๊นŒ์ง€ ๋‚ด๋ ค์™€์„œ ๋ชป ๊ฑฐ๋ฆ„ -> '๊ณ ์  ๋Œ€๋น„'๋กœ ๋ง‰์Œ.
111
- # ์ด๋ฏธ -50/-80% ๋น ์ง„ ์‹œ์ฒด์— 'ํŒ”์•„๋ผ' ์•ˆ ํ•จ. None=ํ•„ํ„ฐ ๋”.
112
  PEAK_WINDOW = 252 # ๊ณ ์  ๊ธฐ์ค€ = ์ตœ๊ทผ N๊ฑฐ๋ž˜์ผ(252=1๋…„) ๋‚ด ์ตœ๊ณ ์  (์˜ค๋ž˜๋œ ๊ณ ์ ์— ์•ˆ ๋ฌถ์ž„)
113
- # CONFIRM_WINDOW ์ œ๊ฑฐ๋จ: 1์ฐจ->2์ฐจ ์‚ฌ์ด ์ผ์ˆ˜ ์ œํ•œ ์—†์Œ (๋ฉฐ์น ์ด ๊ฑธ๋ฆฌ๋“  ์—ฐ์‡„ ํ—ˆ์šฉ)
114
-
115
- # =============================================
116
- # ์ถ”์„ธ ํ•„ํ„ฐ: A-4(๊ธฐ์šธ๊ธฐ ํ‰ํ‰/ํ•˜๋ฝ)๋งŒ ์‚ฌ์šฉ
117
- # [๊ณตํ†ต ์ „์ œ AND] 60์„ ์ด 240์„  ๋ŒํŒŒ ํ›„ RUN_MIN_DAYS ๊ฒฝ๊ณผ
118
- # [๊ทธ ์œ„ A-4] 60/120/240์„  ๊ธฐ์šธ๊ธฐ๊ฐ€ ๊ฐ ๋ฐด๋“œ ์•ˆ์ด๋ฉด ๋งค๋„ ํ—ˆ์šฉ. 240์„  ์—†์œผ๋ฉด ๋งค๋„ ๋”.
119
- # =============================================
120
- USE_TREND = True # ์ถ”์„ธํ•„ํ„ฐ ์ „์ฒด on/off (๊บผ์ง€๋ฉด ํ•„ํ„ฐ ์—†์Œ)
121
-
122
- # ๊ณตํ†ต ์ „์ œ: 60์„ ์ด 240์„  ๋ŒํŒŒ ํ›„ ์ด๋งŒํผ(๊ฑฐ๋ž˜์ผ) ์ง€๋‚˜์•ผ ๋งค๋„ ํ—ˆ์šฉ
123
- RUN_MIN_DAYS = 40 # 60์„ ์ด 240์„  ๋ŒํŒŒ ํ›„ ๊ฒฝ๊ณผ์ผ (๋ง‰ ๋ŒํŒŒ ์งํ›„=240์„  ๋„ˆ๋ฌด ๊ทผ์ฒ˜๋ผ ์˜๋ฏธ์—†์Œ)
124
-
125
- # 240์„  ์ถ”์ •: 240์ผ์น˜ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์œผ๋ฉด(์ƒ์žฅ์ดˆ๊ธฐ ๋“ฑ) ์žˆ๋Š” ๋งŒํผ ์ตœ์žฅ ํ‰๊ท ์œผ๋กœ ๋Œ€์ฒด
126
- ESTIMATE_MA240 = True # True๋ฉด 240์ผ ๋ฏธ๋งŒ์ด์–ด๋„ ์ถ”์ •ํ•ด์„œ ํŒ์ • (๋งค๋„ ๋ผ์ธ์— [240์ถ”์ •] ํ‘œ์‹œ)
127
- MA240_MIN = 120 # ์ตœ์†Œ ์ด๋งŒํผ(๊ฑฐ๋ž˜์ผ) ์žˆ์–ด์•ผ ์ถ”์ •. ์ด ๋ฏธ๋งŒ์ด๋ฉด ์ถ”์ • ์•ˆํ•จ(๋งค๋„ ๋”)
128
-
129
- # A-4) ๊ธฐ์šธ๊ธฐ๊ฐ€ '๋‚ฎ์œผ๋ฉด'(ํ‰ํ‰/ํ•˜๋ฝ) ๋งค๋„ ํ—ˆ์šฉ โ€” 60/120/240์„  ๊ฐ๊ฐ ๋ฐด๋“œ(ํ•˜ํ•œ<๊ธฐ์šธ๊ธฐโ‰ค์ƒํ•œ)
130
- # ์ƒ์Šน ๋™๋ ฅ์ด ์ฃฝ์—ˆ๋‹ค๊ณ  ๋ณด๊ณ  ๋˜์ง. AND/OR๋กœ ๊ฒฐํ•ฉ.
131
- # ํ•˜ํ•œ์ด -1 ์ดํ•˜(์˜ˆ -9)๋ฉด 'ํ•˜ํ•œ ์—†์Œ'. 240์€ ํ•˜ํ•œ 0% (๋งˆ์ด๋„ˆ์Šค๋˜๋ฉด ์ด๋ฏธ ๋Šฆ์Œ).
132
- USE_TREND_FLAT = True # A-4 ์‚ฌ์šฉ?
133
- FLAT_SLOPE_DAYS = 10 # ๊ธฐ์šธ๊ธฐ ์ธก์ • ๊ฐ„๊ฒฉ(๊ฑฐ๋ž˜์ผ)
134
- FLAT_SMOOTH = 5 # ๊ธฐ์šธ๊ธฐ ์Šค๋ฌด๋”ฉ ์ผ์ˆ˜
135
- FLAT_MIN_60 = -9.0 # 60์„  ๊ธฐ์šธ๊ธฐ ํ•˜ํ•œ (-9=ํ•˜ํ•œ์—†์Œ)
136
- FLAT_MAX_60 = 0.00 # 60์„  ๊ธฐ์šธ๊ธฐ ์ƒํ•œ (โ‰ค0%=๊บพ์—ฌ ๋‚ด๋ ค๊ฐ€๊ฑฐ๋‚˜ ์™„์ „ ํ‰ํ‰)
137
- FLAT_MIN_120 = -9.0 # 120์„  ๊ธฐ์šธ๊ธฐ ํ•˜ํ•œ (-9=ํ•˜ํ•œ์—†์Œ)
138
- FLAT_MAX_120 = 0.05 # 120์„  ๊ธฐ์šธ๊ธฐ ์ƒํ•œ
139
- FLAT_MIN_240 = 0.00 # 240์„  ๊ธฐ์šธ๊ธฐ ํ•˜ํ•œ (0%=๋งˆ์ด๋„ˆ์Šค๋ฉด ๋„ˆ๋ฌด ๋Šฆ์Œ -> ์ œ์™ธ)
140
- FLAT_MAX_240 = 0.04 # 240์„  ๊ธฐ์šธ๊ธฐ ์ƒํ•œ (์ด๋ณด๋‹ค ๊ฐ€ํŒŒ๋ฅด๊ฒŒ ์˜ค๋ฅด๋ฉด=์ƒ์Šน์ถ”์„ธ ํ•œ๋ณตํŒ ๋ˆŒ๋ฆผ -> ๋งค๋„X)
141
- FLAT_COMBINE = 'AND' # 'AND'(์„ธ ์„  ๋‹ค ๋งŒ์กฑ) / 'OR'(ํ•˜๋‚˜๋ผ๋„ ๋งŒ์กฑ)
142
-
143
- SHOW_STEPS = True # True๋ฉด ๋งค๋„๋งˆ๋‹ค [๋‹จ๊ณ„ ์ƒ์„ธ](flag/1์ฐจ/2์ฐจ ์–ธ์ œ ์–ด๋–ป๊ฒŒ) ์ถœ๋ ฅ
144
-
145
  LOOK_AHEAD = 200 # ๋งค๋„ ํ›„ ๊ด€์ฐฐ๊ธฐ๊ฐ„(๊ฑฐ๋ž˜์ผ)
146
  PROTECT_DROP = -0.10 # ๋งค๋„๊ฐ€ ๋Œ€๋น„ ์ด๋งŒํผ ๋” ๋น ์ง€๋ฉด '์ž˜ ํŒ ๊ฒƒ'
147
 
148
- TREND_MIN_DAYS = 60 # '240์˜ฌ'(๊ฐ•ํ•œ ์ƒ์Šน์ถ”์„ธ): ์ตœ๊ทผ N๊ฑฐ๋ž˜์ผ ์—ฐ์† 240์„  ์œ„
149
- TREND_MIN_GAP = 0.05 # '240์˜ฌ': ์ข…๊ฐ€๊ฐ€ 240์„  +5% ์ด์ƒ
150
 
151
- # ๊ฐ•ํ•œ ์ƒ์Šน์ถ”์„ธ(240์˜ฌ) ์ค‘์ด๋ฉด ๋งค๋„ ๊ธˆ์ง€ โ€” ๊ธ‰๋“ฑ์ฃผ๊ฐ€ ์ƒ์Šน ํ•œ๋ณตํŒ์—์„œ 60์„  ์ž ๊น ๊นจ๋Š”
152
- # '๋ˆŒ๋ฆผ'์„ ๋ถ•๊ดด๋กœ ์˜ค์ธํ•˜๋Š” false alarm ๋ฐฉ์ง€. (240 ๊ธฐ์šธ๊ธฐ๋ณด๋‹ค ํŠผํŠผ: 60์ผ์—ฐ์† 240์œ„+5%๊ฐญ)
153
- NO_SELL_STRONG_UPTREND = False # True๋ฉด in_240_run(๊ฐ•ํ•œ์ƒ์Šน) ์‹œ ๋งค๋„ ๋ณด๋ฅ˜
 
 
 
154
 
155
  ANALYZE_FROM = '2020-01-01' # ์ฑ„์ (๋งค๋„์ง‘๊ณ„) ์‹œ์ž‘์ผ โ€” ์ด๊ฑด '์–ธ์ œ๋ถ€ํ„ฐ ๊ฒฐ๊ณผ๋ฅผ ๋ณผ์ง€'์ผ ๋ฟ
156
- ANALYZE_TO = None # None=์˜ค๋Š˜(๋งค๋ฒˆ ํ˜ธ์ถœ์‹œ์  KST ๊ธฐ์ค€). ํŠน์ •์ผ ๊ณ ์ •ํ•˜๋ ค๋ฉด 'YYYY-MM-DD'.
157
 
158
  # โ˜… ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ์€ ANALYZE_FROM ๊ณผ ๋ฌด๊ด€ํ•˜๊ฒŒ 'ํ•ญ์ƒ ์ถฉ๋ถ„ํžˆ ๊ณผ๊ฑฐ๋ถ€ํ„ฐ' ๋ถˆ๋Ÿฌ์˜จ๋‹ค.
159
  # (๊ณ ์ /์ด๊ฒฉ์€ ์ „์ฒด ๊ฐ€๊ฒฉ์ด๋ ฅ์œผ๋กœ ๊ณ„์‚ฐ๋ผ์•ผ ํ•˜๋ฏ€๋กœ. ANALYZE_FROM ๋งŒ ๋Šฆ์ถ”๋ฉด ๊ณ ์ ์„
160
  # ์ž˜๋ชป ์žก์•„ ๋งค๋„์‹œ์ ์ด ๋‹ฌ๋ผ์ง€๋Š” ๋ฒ„๊ทธ๊ฐ€ ์ƒ๊น€ -> ๊ทธ๋ž˜์„œ DATA_START ๋Š” ๊ณ ์ •.)
161
  DATA_START = '2018-01-01' # ๊ณ ์ •. ๋” ๊ณผ๊ฑฐ๋ฅผ ๋ณด๋ ค๋ฉด ์ด ๋‚ ์งœ๋งŒ ์•ž๋‹น๊ธฐ๋ฉด ๋จ.
162
 
163
-
164
- def _end_date():
165
- """๋ฐ์ดํ„ฐ ๋๋‚ ์งœ. ANALYZE_TO ๊ฐ€ None ์ด๋ฉด '์ง€๊ธˆ KST ์˜ค๋Š˜'์„ ๋งค๋ฒˆ ์ƒˆ๋กœ ๊ณ„์‚ฐ
166
- (์•ฑ์ด ๋ฉฐ์น ์งธ ์ผœ์ ธ ์žˆ์–ด๋„ ํ•ญ์ƒ ์ตœ์‹  ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋„๋ก)."""
167
- if ANALYZE_TO:
168
- return ANALYZE_TO
169
- return datetime.now(_KST).strftime('%Y-%m-%d')
170
 
171
 
172
  # =============================================
@@ -188,113 +103,21 @@ def _trailing_peaks(stock, index, win):
188
  return pk, ipk
189
 
190
 
191
- def _days_since_cross(t, ma60, ma240):
192
- """t ์‹œ์  ๊ธฐ์ค€, 60์„ ์ด 240์„ ์„ ์œ„๋กœ ๋ŒํŒŒํ•œ ๋’ค ๋ฉฐ์น (๊ฑฐ๋ž˜์ผ) ์ง€๋‚ฌ๋‚˜.
193
- ํ˜„์žฌ 60>240๊ฐ€ ์•„๋‹ˆ๋ฉด -1. ๋ŒํŒŒ ์‹œ์ ์„ ๋ชป ์ฐพ์œผ๋ฉด t(์ฒ˜์Œ๋ถ€ํ„ฐ ์œ„๋กœ ๊ฐ„์ฃผ)."""
194
- if ma60 is None or np.isnan(ma60[t]) or np.isnan(ma240[t]) or ma60[t] <= ma240[t]:
195
- return -1
196
- k = t
197
- while k > 0:
198
- if np.isnan(ma60[k - 1]) or np.isnan(ma240[k - 1]):
199
- break
200
- if ma60[k - 1] <= ma240[k - 1]: # ์ง์ „๋‚  60<=240 -> ์—ฌ๊ธฐ๊ฐ€ ๋ŒํŒŒ ์งํ›„
201
- break
202
- k -= 1
203
- return t - k
204
-
205
-
206
- def _smoothed_slope(line, days, smooth):
207
- """์„ ์˜ days๊ฐ„๊ฒฉ ๊ธฐ์šธ๊ธฐ((line[t]/line[t-days])-1)๋ฅผ ๊ตฌํ•˜๊ณ  smooth์ผ ์ด๋™ํ‰๊ท ."""
208
- n = len(line)
209
- raw = np.full(n, np.nan)
210
- for t in range(days, n):
211
- a = line[t]; b = line[t - days]
212
- if not np.isnan(a) and not np.isnan(b) and b > 0:
213
- raw[t] = a / b - 1
214
- if smooth <= 1:
215
- return raw
216
- out = np.full(n, np.nan)
217
- for t in range(n):
218
- lo = max(0, t - smooth + 1)
219
- seg = raw[lo:t + 1]
220
- seg = seg[~np.isnan(seg)]
221
- if len(seg) > 0:
222
- out[t] = seg.mean()
223
- return out
224
-
225
-
226
- def trend_ok(t, stock, ma240, ma60=None, ma120=None, flat_slopes=None):
227
- """์ถ”์„ธ ํ•„ํ„ฐ(USE_TREND).
228
- [๊ณตํ†ต ์ „์ œ AND] 60์„ ์ด 240์„  ๋ŒํŒŒ ํ›„ RUN_MIN_DAYS ๊ฒฝ๊ณผ
229
- [๊ทธ ์œ„ A-4] 60/120/240์„  ๊ธฐ์šธ๊ธฐ๊ฐ€ ๊ฐ ๋ฐด๋“œ ์•ˆ์ด๋ฉด ๋งค๋„ ํ—ˆ์šฉ.
230
- 240์„  ์—†์œผ๋ฉด ๋งค๋„ ๋”. ์‚ฌ์œ ์— ์‹ค์ œ ๊ฐ’/๊ธฐ์šธ๊ธฐ ํฌํ•จ(๋””๋ฒ„๊น…)."""
231
- if not USE_TREND:
232
- return True, ''
233
- m = ma240[t]
234
- m60v = ma60[t] if (ma60 is not None) else float('nan')
235
- vals = f'[ma60={m60v:,.0f} ma240={m:,.0f} ์ข…๊ฐ€={stock[t]:,.0f}]' if not np.isnan(m) else ''
236
- if np.isnan(m) or m <= 0:
237
- return False, '240์—†์Œ'
238
- since = _days_since_cross(t, ma60, ma240)
239
- if since < 0:
240
- return False, f'60<240(์šฐ์ƒํ–ฅ์•„๋‹˜){vals}'
241
- if since < RUN_MIN_DAYS:
242
- return False, f'๋ŒํŒŒํ›„{since}์ผ(<{RUN_MIN_DAYS}){vals}'
243
- a4 = False; a4msg = ''
244
- if USE_TREND_FLAT and flat_slopes is not None:
245
- checks = [] # (์„ , ๊ธฐ์šธ๊ธฐ๊ฐ’, ํ•˜ํ•œ, ์ƒํ•œ, ํ†ต๊ณผ?)
246
- for ln, lo, hi in ((60, FLAT_MIN_60, FLAT_MAX_60),
247
- (120, FLAT_MIN_120, FLAT_MAX_120),
248
- (240, FLAT_MIN_240, FLAT_MAX_240)):
249
- arr = flat_slopes.get(ln)
250
- if arr is not None and not np.isnan(arr[t]):
251
- v = float(arr[t])
252
- checks.append((ln, v, lo, hi, (lo < v <= hi)))
253
- if checks:
254
- a4 = all(c[4] for c in checks) if FLAT_COMBINE == 'AND' else any(c[4] for c in checks)
255
- def _fm(ln, v, lo, hi, ok):
256
- mk = 'O' if ok else 'X'
257
- if lo <= -1:
258
- return f'{ln}:{v*100:.1f}โ‰ค{hi*100:.1f}%{mk}'
259
- return f'{ln}:{v*100:.1f}โˆˆ({lo*100:.0f},{hi*100:.1f}]{mk}'
260
- a4msg = ' '.join(_fm(*c) for c in checks)
261
- if a4:
262
- return True, f'A4(ํ‰ํ‰ยท{a4msg})'
263
- ex = (' ' + a4msg) if a4msg else ''
264
- return False, f'์ถ”์„ธ๋ฏธ๋‹ฌ{vals}{ex}(๋ŒํŒŒํ›„{since}์ผ)'
265
-
266
-
267
- def find_confirmed_sells(stock, index, dates, start_pos, ma60=None, ma240=None,
268
- low=None, bbl=None, ma240_est=None, ma120=None):
269
- """ํ™•์ •๋งค๋„ ์ฐพ๊ธฐ โ€” ๋‹จ๊ณ„๋ˆ„์ (CONFIRM_STAGES๋‹จ๊ณ„)+๊ธ‰๋ณ€์ ํ”„ + ์ถ”์„ธํ•„ํ„ฐ(A-4)."""
270
  n = len(stock)
271
  twpk, twip = _trailing_peaks(stock, index, PEAK_WINDOW)
272
-
273
- flat_slopes = None
274
- if USE_TREND_FLAT:
275
- flat_slopes = {}
276
- for ln, arr in ((60, ma60), (120, ma120), (240, ma240)):
277
- if arr is not None:
278
- flat_slopes[ln] = _smoothed_slope(arr, FLAT_SLOPE_DAYS, FLAT_SMOOTH)
279
-
280
- SELL_STAGE = max(1, int(CONFIRM_STAGES)) + 1 # ์ด ๋‹จ๊ณ„์— ๋„๋‹ฌํ•˜๋ฉด ์ „๋Ÿ‰๋งค๋„
281
-
282
- def _sname(stg):
283
- """๋‹จ๊ณ„ ์ด๋ฆ„. ๊ธฐ๋ณธ(CONFIRM_STAGES=3)์ด๋ฉด ์นœ์ˆ™ํ•œ 1์ฐจ/2์ฐจ ์ด๋ฆ„, ์•„๋‹ˆ๋ฉด N๋‹จ๊ณ„."""
284
- if CONFIRM_STAGES == 3:
285
- return {1: 'flag ON', 2: '1์ฐจ', 3: '2์ฐจ flag ON'}.get(stg, f'{stg}๋‹จ๊ณ„')
286
- return 'flag ON' if stg == 1 else f'{stg}๋‹จ๊ณ„'
287
-
288
- def _gate(t):
289
- ok, why = (True, '')
290
- if ma240 is not None:
291
- ok, why = trend_ok(t, stock, ma240, ma60, ma120, flat_slopes)
292
- return ok, why
293
-
294
  stage = 0; ref_div = None; first_i = None; sold = False
295
  prev_div = None
296
  sells = []
297
- jumps = []
298
  _SNAME = {0: '์—†์Œ', 1: 'flagON', 2: '1์ฐจ', 3: '2์ฐจflagON', 4: '2์ฐจ๋งค๋„'}
299
 
300
  def _log_jump(t, frm, to, div, pdv, st):
@@ -304,325 +127,60 @@ def find_confirmed_sells(stock, index, dates, start_pos, ma60=None, ma240=None,
304
  'from': _SNAME[frm], 'to': _SNAME[to],
305
  'skip': (frm == 0 and to == 2) or (frm == 2 and to == 4)})
306
 
307
- def _sell(t, st, div, via=''):
308
- if t < start_pos:
309
- return False, '๊ด€์ฐฐ๊ธฐ๊ฐ„๋ฐ–'
310
- ok, why = _gate(t)
311
- if ok:
312
- est = bool(ma240_est[t]) if ma240_est is not None else False
313
- sells.append({'i': t, 'price': st, 'div': div, 'date': dates[t],
314
- 'trend': why, 'via': via, 'est': est})
315
- return True, why
316
- return False, why # ์ถ”์„ธํ•„ํ„ฐ์— ๋ง‰ํž˜ (why=์ฐจ๋‹จ์‚ฌ์œ )
317
-
318
- steps = [] # ๋‹จ๊ณ„ ์ „์ด ์ƒ์„ธ ๋กœ๊ทธ
319
- def _step(t, name, div, note=''):
320
- if t >= start_pos:
321
- steps.append({'date': dates[t], 'name': name, 'div': div, 'note': note})
322
-
323
  for t in range(n):
324
  st = float(stock[t]); it = float(index[t])
325
  peak = float(twpk[t]); iap = float(twip[t])
326
- s_term = (st / peak - 1) # ์ข…๋ชฉ: ์ž๊ธฐ๊ณ ์  ๋Œ€๋น„
327
- i_term = ((it / iap - 1) if iap else 0.0) # ์ง€์ˆ˜: ๊ณ ์ ์ผ ๋Œ€๋น„
328
- div = s_term - i_term
329
- _bd = f'[์ข…๋ชฉ {s_term*100:+.0f}% โˆ’ ์ง€์ˆ˜ {i_term*100:+.0f}%]' # ์ด๊ฒฉ ๋ถ„ํ•ด(๋””๋ฒ„๊น…)
330
 
 
331
  if div > DIV_THRESHOLD:
332
- if stage != 0 and t >= start_pos:
333
- _step(t, f'๋ฆฌ์…‹(์ด๊ฒฉ {DIV_THRESHOLD*100:.0f}%์œ„ ํšŒ๋ณต)', div)
334
  stage = 0; ref_div = None; first_i = None; sold = False
335
  prev_div = div
336
  continue
337
- if sold:
 
338
  prev_div = div
339
  continue
340
 
 
341
  crash = (prev_div is not None) and ((div - prev_div) <= -DAILY_JUMP)
342
- cnote = f'ํ•˜๋ฃจ๊ธ‰๋ณ€ {(div-prev_div)*100:.1f}%p (์ ํ”„๊ธฐ์ค€ {DAILY_JUMP*100:.0f}%p)' if crash else ''
343
-
344
- def _try_sell(note, via):
345
- """๋งค๋„ ์‹œ๋„. ๋ฐœ๋™ํ•˜๋ฉด True(+๋ฐœ๋™๋กœ๊ทธ), ์ฐจ๋‹จ๋˜๋ฉด False(+์ฐจ๋‹จ๋กœ๊ทธ). ๋Œ€๊ธฐ ์•ˆํ•จ."""
346
- fired, why = _sell(t, st, div, via)
347
- if fired:
348
- _step(t, '์ „๋Ÿ‰๋งค๋„ โ˜…๋ฐœ๋™', div, note + ' ' + _bd)
349
- return True
350
- _step(t, f'๋งค๋„์กฐ๊ฑด ์ถฉ์กฑํ–ˆ์œผ๋‚˜ ์ฐจ๋‹จ({why}) โ€” ์ด๋ฒˆ ์‹ ํ˜ธ ๋ฌด์‹œ', div, note)
351
- return False
352
 
353
- # --- ์ผ๋ฐ˜ํ™”๋œ ๋‹จ๊ณ„ ๋จธ์‹  ---
354
- # ๋ฌด์žฅ(stage>=1) ํ›„ ๊นŠ์–ด์งˆ ๋•Œ๋งˆ๋‹ค stage+1 (๊ธ‰๋ณ€์ด๋ฉด +2๋กœ ํ•œ ๋‹จ๊ณ„ ๊ฑด๋„ˆ๋œ€).
355
- # stage ๊ฐ€ SELL_STAGE ์— ๋„๋‹ฌํ•˜๋ฉด ์ „๋Ÿ‰๋งค๋„.
356
- if stage == 0:
357
  if crash:
358
- nx = min(2, SELL_STAGE)
359
- _log_jump(t, 0, nx, div, prev_div, st)
360
- ref_div = div; first_i = t
361
- if nx >= SELL_STAGE:
362
- if _try_sell(cnote + ' (๋‹จ๊ณ„ ๊ฑด๋„ˆ๋œ€)', '๊ธ‰๋ณ€์ ํ”„'):
363
- sold = True
364
- else:
365
- stage = SELL_STAGE - 1 # ์ฐจ๋‹จ -> ๋งค๋„์ง์ „ ๋‹จ๊ณ„๋กœ park
366
- else:
367
- stage = nx
368
- _step(t, f'{_sname(nx)}(flag ๊ฑด๋„ˆ๋œ€)', div, cnote)
369
  else:
370
- stage = 1; ref_div = div; first_i = t
371
- if stage >= SELL_STAGE: # CONFIRM_STAGES=0 ๊ฐ™์€ ๊ทน๋‹จ ์„ค์ •
372
- if _try_sell('flag์ฆ‰์‹œ', 'flag'):
373
- sold = True
374
- else:
375
- stage = SELL_STAGE - 1
376
- else:
377
- _step(t, _sname(1), div, f'๊ธฐ์ค€ {DIV_THRESHOLD*100:.0f}%')
378
- else:
379
- adv = 0
380
  if crash:
381
- adv = 2; note = cnote
 
382
  elif div <= ref_div - MIN_DEEPEN:
383
- adv = 1; note = f'์ง์ „ๆฏ” {(div-ref_div)*100:.1f}%p'
384
- if adv:
385
- nx = min(stage + adv, SELL_STAGE)
386
- ref_div = div
387
- if nx >= SELL_STAGE:
388
- if crash:
389
- _log_jump(t, stage, SELL_STAGE, div, prev_div, st)
390
- if _try_sell(note, '๊ธ‰๋ณ€์ ํ”„' if crash else '๋‹จ๊ณ„๋ˆ„์ '):
391
- sold = True
392
- else:
393
- stage = SELL_STAGE - 1 # ์ฐจ๋‹จ -> ๋งค๋„์ง์ „ ๋‹จ๊ณ„ park, ๋” ๊นŠ์–ด์ง€๋ฉด ์žฌ์‹œ๋„
394
- else:
395
- stage = nx
396
- _step(t, _sname(nx), div, note)
397
- prev_div = div
398
-
399
- # ๊ด€์ฐฐ ์ข…๋ฃŒ ์‹œ์ , ์•„์ง ๋งค๋„ ์•ˆ ๋‚ฌ๊ณ  ๋‹จ๊ณ„ ๋Œ€๊ธฐ ์ค‘์ด๋ฉด ํ˜„์žฌ ์ƒํƒœ ๊ธฐ๋ก
400
- if not sold and 1 <= stage < SELL_STAGE and ref_div is not None:
401
- last = n - 1
402
- if CONFIRM_STAGES == 3:
403
- nm = {1: '1์ฐจ flag', 2: '1์ฐจ (๋‹ค์Œ: 2์ฐจ flag)',
404
- 3: '2์ฐจ flag (์ „๋Ÿ‰๋งค๋„ ์ž„๋ฐ•!)'}.get(stage, f'{stage}๋‹จ๊ณ„')
405
- else:
406
- tail = ' (๋‹ค์Œ: ์ „๋Ÿ‰๋งค๋„!)' if stage == SELL_STAGE - 1 else ''
407
- nm = f'{stage}๋‹จ๊ณ„ / ๋งค๋„๊นŒ์ง€ {SELL_STAGE}๋‹จ๊ณ„{tail}'
408
- cur = float(stock[last] / twpk[last] - 1) - ((float(index[last] / twip[last] - 1)) if twip[last] else 0.0)
409
- trigger = ref_div - MIN_DEEPEN # ๋‹ค์Œ ๋‹จ๊ณ„ ๋ฐœ๋™ ์ด๊ฒฉ
410
- need = cur - trigger # >0 ์ด๋ฉด ์ด๋งŒํผ ๋” ๊นŠ์–ด์ ธ์•ผ ๋‹ค์Œ ๋‹จ๊ณ„
411
- # ํ˜„์žฌ ์ด๊ฒฉ์ด '๋ฌด์žฅ๋œ ์ตœ์ €์ (ref_div)'๋ณด๋‹ค ๋šœ๋ ท์ด ์œ„๋กœ ํšŒ๋ณต๋์œผ๋ฉด = ๋ฐ˜๋“ฑ ์ค‘ (์ž„๋ฐ• ์•„๋‹˜)
412
- if cur > ref_div + MIN_DEEPEN:
413
- nm = nm.replace(' (์ „๋Ÿ‰๋งค๋„ ์ž„๋ฐ•!)', '').replace(' (๋‹ค์Œ: ์ „๋Ÿ‰๋งค๋„!)', '')
414
- note = (f'๋ฐ˜๋“ฑ ์ค‘ โ€” ํ˜„์žฌ ์ด๊ฒฉ {cur*100:.1f}%, ์ตœ์ € {ref_div*100:.1f}%๊นŒ์ง€ ๊ฐ”๋‹ค ํšŒ๋ณต. '
415
- f'๋งค๋„ํŠธ๋ฆฌ๊ฑฐ {trigger*100:.1f}% (๋ฆฌ์…‹์€ ์ด๊ฒฉ {DIV_THRESHOLD*100:.0f}% ํšŒ๋ณต ์‹œ)')
416
- _step(last, f'[ํ˜„์žฌ] {nm} ยท ๋ฐ˜๋“ฑ ์ค‘', cur, note)
417
- elif need > 0:
418
- _step(last, f'[ํ˜„์žฌ] {nm}', cur, f'๋‹ค์Œ ๋‹จ๊ณ„๊นŒ์ง€ {need*100:.1f}%p ๋” ํ•˜๋ฝ ํ•„์š”')
419
- else:
420
- _step(last, f'[ํ˜„์žฌ] {nm}', cur, '์กฐ๊ฑด ์ถฉ์กฑ์ƒํƒœ(๋‹ค์Œ๋ด‰ ํ™•์ธ)')
421
- return sells, jumps, steps
422
-
423
-
424
- def find_rs_a4_sells(stock, index, dates, start_pos, ma60=None, ma240=None,
425
- low=None, bbl=None, ma240_est=None, ma120=None):
426
- """RS_A4 ์—”์ง„: RS์„ (์ข…๋ชฉ/์ง€์ˆ˜)์ด ์‹œ์žฅ๋ณด๋‹ค ์•ฝํ•ด์ง€๋ฉด(RS<RS_MA, RS_PERSIST์ผ ์ง€์†) '๋ฌด์žฅ',
427
- ๊ทธ ์ƒํƒœ์—์„œ A-4 ์ถ”์„ธํ•„ํ„ฐ๊ฐ€ ํ™•์ธ๋˜๋Š” ์ฆ‰์‹œ ์ „๋Ÿ‰๋งค๋„. ๋‹จ๊ณ„ ์•ˆ ๊ฑฐ์นจ -> ์ผ์ฐ ์šด๋‹ค.
428
- ๋ฐ˜ํ™˜์€ STAGE ์—”์ง„๊ณผ ๋™์ผ ํ˜•์‹ (sells, jumps, steps)."""
429
- n = len(stock)
430
- twpk, twip = _trailing_peaks(stock, index, PEAK_WINDOW)
431
-
432
- flat_slopes = None
433
- if USE_TREND_FLAT:
434
- flat_slopes = {}
435
- for ln, arr in ((60, ma60), (120, ma120), (240, ma240)):
436
- if arr is not None:
437
- flat_slopes[ln] = _smoothed_slope(arr, FLAT_SLOPE_DAYS, FLAT_SMOOTH)
438
-
439
- # RS์„ (์ข…๋ชฉ/์ง€์ˆ˜) -> ํ†ฑ๋‹ˆ ์ค„์ด๋ ค EMA๋กœ ๋ถ€๋“œ๋Ÿฝ๊ฒŒ -> ๊ทธ ์ด๋™ํ‰๊ท ๊ณผ ๋น„๊ต(๋ฒ„ํผ๋ฐด๋“œ)
440
- rs_raw = pd.Series([stock[t] / index[t] if index[t] else np.nan for t in range(n)])
441
- rs_s = rs_raw.ewm(span=max(1, RS_SMOOTH), adjust=False).mean() if RS_SMOOTH > 1 else rs_raw
442
- rs = rs_s.values
443
- rs_ma = rs_s.rolling(RS_MA_PERIOD).mean().values
444
- # ๋ฐ๋“œํฌ๋กœ์Šค: RS๊ฐ€ MA๋ณด๋‹ค RS_BUFFER ๋งŒํผ 'ํ™•์‹คํžˆ' ์•„๋ž˜์—ฌ์•ผ ์•ฝํ™” ์ธ์ • (์Šค์นจ ๋ฌด์‹œ)
445
- rs_weak = np.array([(not np.isnan(rs_ma[t])) and rs[t] < rs_ma[t] * (1 - RS_BUFFER)
446
- for t in range(n)])
447
-
448
- sells = []; jumps = []; steps = []
449
- def _step(t, name, div, note=''):
450
- if t >= start_pos:
451
- steps.append({'date': dates[t], 'name': name, 'div': div, 'note': note})
452
-
453
- def _gate(t):
454
- if ma240 is None:
455
- return True, ''
456
- return trend_ok(t, stock, ma240, ma60, ma120, flat_slopes)
457
-
458
- def _flat_gate(line_no):
459
- """CONFIRM_NEEDS_FLAT์— ๋”ฐ๋ผ ์ถ”๊ฐ€ ์กฐ๊ฑด. (ํ†ต๊ณผ์—ฌ๋ถ€, ์‚ฌ์œ ๊ผฌ๋ฆฌํ‘œ) ๋ฐ˜ํ™˜."""
460
- def slope_at(ln, t):
461
- arr = flat_slopes.get(ln) if flat_slopes else None
462
- return arr[t] if (arr is not None and not np.isnan(arr[t])) else None
463
- line_arr = {60: ma60, 120: ma120}.get(line_no)
464
- if CONFIRM_NEEDS_FLAT == 'SELF':
465
- def chk(t):
466
- s = slope_at(line_no, t)
467
- if s is None:
468
- return True, ''
469
- if s > 0: # ํ™•์ธ์„ ์ด ์šฐ์ƒํ–ฅ -> ๋งค๋„ ๊ธˆ์ง€
470
- return False, f'{line_no}์„  ์šฐ์ƒํ–ฅ({s*100:+.1f}%)โ†’๋ณด๋ฅ˜'
471
- return True, f'{line_no}์„  ๊ธฐ์šธ๊ธฐ{s*100:+.1f}%'
472
- return chk
473
- if CONFIRM_NEEDS_FLAT in ('MARGIN', 'MARGIN+240'):
474
- # ๊ธฐ์šธ๊ธฐ(ํ›„ํ–‰) ๋Œ€์‹  ๋งˆ์ง„: ์ข…๊ฐ€๊ฐ€ ํ™•์ธ์„ ๋ณด๋‹ค CONFIRM_MARGIN ํ™•์‹คํžˆ ์•„๋ž˜์—ฌ์•ผ ๋ฐœ๋™.
475
- # ์ž ๊น ๋ˆŒ๋ฆผ(์„  ์‚ด์ง ์Šค์นจ)=๋ฌด์‹œ, ์ง„์งœ ๋ถ•๊ดด(์„  ํ›จ์”ฌ ์•„๋ž˜)=์ฆ‰์‹œ(์„  ๊บพ์ž„ ์•ˆ ๊ธฐ๋‹ค๋ฆผ).
476
- # '+240'์ด๋ฉด ์ถ”๊ฐ€๋กœ 240์„ ์ด ๋„ˆ๋ฌด ๊ฐ€ํŒŒ๋ฅธ ์šฐ์ƒํ–ฅ(>FLAT_MAX_240)์ด๋ฉด ๋งค๋„ ๊ธˆ์ง€(์ƒ์Šน ํ•œ๋ณตํŒ ๋ˆŒ๋ฆผ).
477
- def chk(t):
478
- if line_arr is None or np.isnan(line_arr[t]):
479
- return True, ''
480
- margin = (1 - stock[t] / line_arr[t]) # ์„  ๋Œ€๋น„ ์–ผ๋งˆ๋‚˜ ์•„๋ž˜(%)
481
- if margin < CONFIRM_MARGIN:
482
- return False, f'{line_no}์„  ์‚ด์ง(-{margin*100:.1f}%<{CONFIRM_MARGIN*100:.0f}%)โ†’๋ณด๋ฅ˜'
483
- if CONFIRM_NEEDS_FLAT == 'MARGIN+240':
484
- s240 = slope_at(240, t)
485
- if s240 is not None and s240 > FLAT_MAX_240:
486
- return False, f'240 ๊ฐ€ํŒŒ๋ฅธ์ƒ์Šน({s240*100:+.1f}%>{FLAT_MAX_240*100:.0f}%)=๋ˆŒ๋ฆผโ†’๋ณด๋ฅ˜'
487
- return True, f'{line_no}์„  -{margin*100:.1f}%(๋ถ•๊ดด)'
488
- return chk
489
- if CONFIRM_NEEDS_FLAT == '240':
490
- def chk(t):
491
- s = slope_at(240, t)
492
- if s is None:
493
- return True, ''
494
- if not (FLAT_MIN_240 < s <= FLAT_MAX_240):
495
- return False, f'240๊ธฐ์šธ๊ธฐ {s*100:+.1f}%โˆ‰({FLAT_MIN_240*100:.0f},{FLAT_MAX_240*100:.0f}]โ†’๋ณด๋ฅ˜'
496
- return True, f'240๊ธฐ์šธ๊ธฐ {s*100:+.1f}%'
497
- return chk
498
- return lambda t: (True, '') # 'OFF'
499
-
500
- _g60 = _flat_gate(60); _g120 = _flat_gate(120)
501
-
502
- def _confirm(t):
503
- """๋ฌด์žฅ ํ›„ '๋งค๋„ ํ™•์ธ' ์‹ ํ˜ธ. ๋ชจ๋“œ๋ณ„:
504
- A4=์ดํ‰์„  ํ‰ํ‰(๋А๋ฆผ/์•ˆ์ „) / MA60=์ข…๊ฐ€<60์„ (๋น ๋ฆ„) / MA120=์ข…๊ฐ€<120์„ (์ค‘๊ฐ„) / NONE=RS๋งŒ.
505
- MA60/120์€ CONFIRM_NEEDS_FLAT ๊ธฐ์šธ๊ธฐ ๊ฒŒ์ดํŠธ๋„ ํ†ต๊ณผํ•ด์•ผ ํ•จ(์˜ค๋ฅผ ๋•Œ ๋งค๋„ ๊ธˆ์ง€)."""
506
- if RS_CONFIRM == 'NONE':
507
- return True, 'RS๋‹จ๋…'
508
- if RS_CONFIRM == 'MA60':
509
- if ma60 is None or np.isnan(ma60[t]):
510
- return False, '60์„ ์—†์Œ'
511
- if stock[t] >= ma60[t]:
512
- return False, f'60์„ ์œ„(์ข…๊ฐ€ {stock[t]:,.0f}>{ma60[t]:,.0f})'
513
- gok, gwhy = _g60(t)
514
- if not gok:
515
- return False, gwhy
516
- return True, f'์ข…๊ฐ€<60์„ ({ma60[t]:,.0f}) {gwhy}'.strip()
517
- if RS_CONFIRM == 'MA120':
518
- if ma120 is None or np.isnan(ma120[t]):
519
- return False, '120์„ ์—†์Œ'
520
- if stock[t] >= ma120[t]:
521
- return False, f'120์„ ์œ„(์ข…๊ฐ€ {stock[t]:,.0f}>{ma120[t]:,.0f})'
522
- gok, gwhy = _g120(t)
523
- if not gok:
524
- return False, gwhy
525
- return True, f'์ข…๊ฐ€<120์„ ({ma120[t]:,.0f}) {gwhy}'.strip()
526
- return _gate(t) # 'A4'
527
-
528
- armed = False; armed_date = None; sold = False; weak_run = 0
529
- locked = False; lock_i = -1
530
- for t in range(n):
531
- div = (stock[t] / twpk[t] - 1) - ((index[t] / twip[t] - 1) if twip[t] else 0.0)
532
-
533
- # SELL_ONCE ์ž ๊ธˆ: ๋งค๋„ ํ›„ (1)RELOCK_LINE ํšŒ๋ณต AND (2)์ตœ์†Œ๊ธฐ๊ฐ„ ๊ฒฝ๊ณผ ๋‘˜ ๋‹ค์—ฌ์•ผ ํ’€๋ฆผ
534
- if locked:
535
- line = {60: ma60, 120: ma120, 240: ma240}.get(RELOCK_LINE, ma240)
536
- if line is None:
537
- line = ma120 if ma120 is not None else ma60
538
- above = (line is not None and not np.isnan(line[t]) and stock[t] > line[t])
539
- enough_days = (t - lock_i) >= RELOCK_MIN_DAYS
540
- if above and enough_days:
541
- locked = False; armed = False; weak_run = 0; sold = False
542
  if t >= start_pos:
543
- _step(t, f'๋Œ€๊ธฐ ์žฌ๊ฐœ ๊ฐ€๋Šฅ({RELOCK_LINE}์„  ํšŒ๋ณต+{t-lock_i}์ผ๊ฒฝ๊ณผ=์ƒˆ ์‚ฌ์ดํด)', div)
544
- else:
545
- continue # ์ž ๊ฒจ์žˆ์œผ๋ฉด ์นจ๋ฌต
546
-
547
- if rs_weak[t]:
548
- weak_run += 1
549
- else:
550
- # RS๊ฐ€ ๋‹ค์‹œ ์‹œ์žฅ๋ณด๋‹ค ๊ฐ•ํ•ด์ง -> ๋ฌด์žฅ ํ•ด์ œ
551
- if armed and t >= start_pos:
552
- _step(t, '๋Œ€๊ธฐ ํ•ด์ œ(RS ํšŒ๋ณต)', div)
553
- armed = False; weak_run = 0
554
- if not SELL_ONCE:
555
- sold = False
556
- continue
557
-
558
- # RS_PERSIST์ผ ์—ฐ์† ์•ฝํ•˜๋ฉด ๋ฌด์žฅ (์ด๋ฏธ ํŒ ์ƒํƒœ๋ฉด ๋ฌด์žฅ ์•ˆ ํ•จ)
559
- if not armed and not sold and weak_run >= RS_PERSIST:
560
- # (1) ์ข…๊ฐ€๊ฐ€ 240์„ ๋ณด๋‹ค ๊นŠ์ด ์•„๋ž˜ = ์ถ”์„ธ ๋ฌด๋„ˆ์ง -> ๋ฌด์žฅ ๊ธˆ์ง€
561
- deep_below = False
562
- if NO_ARM_BELOW_240 is not None and ma240 is not None and not np.isnan(ma240[t]):
563
- deep_below = (stock[t] / ma240[t] - 1) <= NO_ARM_BELOW_240
564
- # (2) ์ด๊ฒฉ(๊ณ ์  ๋Œ€๋น„ ๋‚™ํญ)์ด ์ด๋ฏธ ๋„ˆ๋ฌด ๊นŠ์Œ = ์ตœํ›„๋ณด๋ฃจ ์‹œ์  ์ง€๋‚จ -> ๋ฌด์žฅ ๊ธˆ์ง€
565
- deep_div = (NO_ARM_DIV_BELOW is not None and div <= NO_ARM_DIV_BELOW)
566
- if deep_below or deep_div:
567
- if t >= start_pos:
568
- why = (f'์ด๊ฒฉ {div*100:.0f}%โ‰ค{NO_ARM_DIV_BELOW*100:.0f}%(๊ณ ์ ๋Œ€๋น„ ํ•œ์ฐธ ๋น ์ง)'
569
- if deep_div else
570
- f'์ข…๊ฐ€ 240์„  {(stock[t]/ma240[t]-1)*100:.0f}%(์ถ”์„ธ์ดํƒˆ)')
571
- _step(t, '๋Œ€๊ธฐ ๋ณด๋ฅ˜(์ตœํ›„๋ณด๋ฃจ ๋Œ€์ƒ ์•„๋‹˜)', div, why)
572
- else:
573
- armed = True; armed_date = dates[t]
574
- rsd = (rs[t] / rs_ma[t] - 1) * 100
575
- _step(t, '๋งค๋„๋Œ€๊ธฐ ์ง„์ž…(RS ์•ฝํ™”)', div, f'RS {rsd:.1f}% < MA{RS_MA_PERIOD}')
576
-
577
- if armed and not sold:
578
- ok, why = _confirm(t)
579
- # ๊ฐ•ํ•œ ์ƒ์Šน์ถ”์„ธ(240์˜ฌ) ์ค‘์ด๋ฉด ๋งค๋„ ๋ณด๋ฅ˜ = ์ƒ์Šน ํ•œ๋ณตํŒ ๋ˆŒ๋ฆผ false alarm ๋ฐฉ์ง€
580
- if ok and NO_SELL_STRONG_UPTREND and ma240 is not None and in_240_run(stock, ma240, t):
581
- ok = False
582
  if t >= start_pos:
583
- _step(t, '๋งค๋„ ๋ณด๋ฅ˜(๊ฐ•ํ•œ ์ƒ์Šน์ถ”์„ธ=240์˜ฌ, ๋ˆŒ๋ฆผ)', div,
584
- f'์ข…๊ฐ€ {stock[t]:,.0f} > 240์„ +{TREND_MIN_GAP*100:.0f}%, {TREND_MIN_DAYS}์ผ์—ฐ์† 240์œ„')
585
- if ok:
 
586
  if t >= start_pos:
587
- est = bool(ma240_est[t]) if ma240_est is not None else False
588
- sells.append({'i': t, 'price': float(stock[t]), 'div': div,
589
- 'date': dates[t], 'trend': why,
590
- 'via': f'RS์•ฝํ™”({armed_date})', 'est': est})
591
- _step(t, '์ „๋Ÿ‰๋งค๋„ โ˜…๋ฐœ๋™', div, f'RS์•ฝํ™” ํ›„ ํ™•์ธ[{RS_CONFIRM}]: {why}')
592
  sold = True
593
- if SELL_ONCE:
594
- locked = True; lock_i = t # ํ•œ ๋ฒˆ ํŒ”๋ฉด ์ž ๊ธˆ(ํ•ด์ œ์„ +๊ธฐ๊ฐ„ ๋‘˜ ๋‹ค ์ถฉ์กฑ๊นŒ์ง€)
595
- else:
596
- pass # ๋ฌด์žฅ๋์ง€๋งŒ ํ™•์ธ์‹ ํ˜ธ ์•„์ง(๋˜๋Š” ์ƒ์Šน์ถ”์„ธ ๋ณด๋ฅ˜) -> ๋Œ€๊ธฐ
597
-
598
- # ํ˜„์žฌ ์ƒํƒœ
599
- last = n - 1
600
- if not sold:
601
- div = (stock[last] / twpk[last] - 1) - ((index[last] / twip[last] - 1) if twip[last] else 0.0)
602
- if armed:
603
- ok, why = _confirm(last)
604
- # ๋งค๋„ ํ™•์ธ์„ ๊นŒ์ง€ ๋‚จ์€ ๊ฑฐ๋ฆฌ(๊ฐ€๊ฒฉ ๊ธฐ์ค€) = ์ž„๋ฐ• ๊ฒฝ๊ณ 
605
- line_arr = {'MA60': ma60, 'MA120': ma120}.get(RS_CONFIRM)
606
- dist = None; lname = ''
607
- if line_arr is not None and not np.isnan(line_arr[last]):
608
- use_margin = CONFIRM_NEEDS_FLAT in ('MARGIN', 'MARGIN+240')
609
- trigger = line_arr[last] * (1 - CONFIRM_MARGIN) if use_margin else line_arr[last]
610
- dist = stock[last] / trigger - 1 # >0 ์ด๋ฉด ์ด๋งŒํผ ๋” ๋น ์ ธ์•ผ ๋งค๋„
611
- lname = '60์„ ' if RS_CONFIRM == 'MA60' else '120์„ '
612
- if dist is not None and dist > 0:
613
- if dist <= 0.03:
614
- nm = f'โš ๏ธ ๋งค๋„ ๋Œ€๊ธฐ ({lname}๊นŒ์ง€ -{dist*100:.1f}%) ๋งค๋„ ์ž„๋ฐ•!'
615
- else:
616
- nm = f'๋งค๋„ ๋Œ€๊ธฐ ({lname}๊นŒ์ง€ -{dist*100:.1f}%)'
617
- note = ''
618
- elif dist is not None: # ๋ถ•๊ดด์„  ๋„๋‹ฌํ–ˆ์œผ๋‚˜ ๋‹ค๋ฅธ ์กฐ๊ฑด(์ƒ์Šน์ถ”์„ธ ๋“ฑ)์— ๋ง‰ํž˜
619
- nm = '๋งค๋„ ๋Œ€๊ธฐ'; note = f'{lname} ๋„๋‹ฌํ–ˆ์œผ๋‚˜ ๋ณด๋ฅ˜ ({why})'
620
- else:
621
- nm = '๋งค๋„ ๋Œ€๊ธฐ'; note = f'ํ™•์ธ[{RS_CONFIRM}] ๋Œ€๊ธฐ์ค‘ ({why})'
622
- _step(last, f'[ํ˜„์žฌ] {nm}', div, note)
623
- else:
624
- _step(last, '[ํ˜„์žฌ] ๊ด€๋ง', div, '')
625
- return sells, jumps, steps
626
 
627
 
628
  def in_240_run(stock, ma240, i):
@@ -637,16 +195,17 @@ def in_240_run(stock, ma240, i):
637
  return True
638
 
639
 
640
- def classify_240(stock, ma240, i, price):
641
- """('EARLY'|'STILL_UP'|'ALREADY_DOWN'|'UNKNOWN', ์ดํƒˆ๊นŒ์ง€ ์ผ์ˆ˜)"""
 
642
  m = ma240[i]
643
  if np.isnan(m):
644
  return ('UNKNOWN', None)
645
- if stock[i] <= m:
646
  return ('ALREADY_DOWN', None)
647
  end = min(i + 1 + LOOK_AHEAD, len(stock))
648
  for k in range(i + 1, end):
649
- if not np.isnan(ma240[k]) and stock[k] < ma240[k]:
650
  return ('EARLY', k - i)
651
  return ('STILL_UP', None)
652
 
@@ -661,7 +220,7 @@ def protected(stock, i, price):
661
 
662
  def load_one(ticker, market):
663
  try:
664
- sdf = fdr.DataReader(ticker, DATA_START, _end_date())
665
  except Exception:
666
  return None
667
  if not isinstance(sdf, pd.DataFrame) or 'Close' not in sdf.columns or len(sdf) == 0:
@@ -670,7 +229,7 @@ def load_one(ticker, market):
670
  idf = None
671
  for c in (idx_code, '^' + idx_code):
672
  try:
673
- t = fdr.DataReader(c, DATA_START, _end_date())
674
  if isinstance(t, pd.DataFrame) and 'Close' in t.columns and len(t) > 0:
675
  idf = t; break
676
  except Exception:
@@ -684,20 +243,7 @@ def load_one(ticker, market):
684
  merged = merged.dropna()
685
  if len(merged) == 0:
686
  return None
687
- # 240์„ : ์ •์‹(240์ผ ๋‹ค ์žˆ์Œ) ์šฐ์„ . ๋ถ€์กฑํ•˜๋ฉด '์žˆ๋Š” ๋งŒํผ ์ตœ์žฅ ํ‰๊ท '์œผ๋กœ ์ถ”์ •.
688
- ma240_full = merged['stock'].rolling(240).mean()
689
- if ESTIMATE_MA240:
690
- # min_periods=MA240_MIN: ๋ฐ์ดํ„ฐ๊ฐ€ MA240_MIN์ผ ์ด์ƒ์ด๋ฉด ์žˆ๋Š” ๋งŒํผ ํ‰๊ท ์œผ๋กœ ์ฑ„์›€
691
- ma240_est = merged['stock'].rolling(240, min_periods=MA240_MIN).mean()
692
- merged['ma240'] = ma240_est
693
- merged['ma240_est'] = ma240_full.isna() & ma240_est.notna() # ์ถ”์ •์œผ๋กœ ์ฑ„์šด ๊ตฌ๊ฐ„ True
694
- else:
695
- merged['ma240'] = ma240_full
696
- merged['ma240_est'] = False
697
- merged['ma60'] = merged['stock'].rolling(60).mean()
698
- merged['ma120'] = merged['stock'].rolling(120).mean()
699
- merged['bbl'] = (merged['stock'].rolling(20).mean()
700
- - 2 * merged['stock'].rolling(20).std()) # BB ํ•˜๋‹จ(์ƒ๋ช…์„  ์•ต์ปค์šฉ)
701
  dates = [str(d)[:10] for d in merged.index]
702
  start_pos = next((k for k, d in enumerate(dates) if d >= ANALYZE_FROM), 0)
703
  return {
@@ -705,10 +251,6 @@ def load_one(ticker, market):
705
  'low': merged['low'].values.astype(float),
706
  'index': merged['index'].values.astype(float),
707
  'ma240': merged['ma240'].values.astype(float),
708
- 'ma240_est': merged['ma240_est'].values.astype(bool),
709
- 'ma60': merged['ma60'].values.astype(float),
710
- 'ma120': merged['ma120'].values.astype(float),
711
- 'bbl': merged['bbl'].values.astype(float),
712
  'dates': dates, 'start_pos': start_pos,
713
  }
714
 
@@ -719,28 +261,9 @@ def pct(a, b):
719
 
720
  def main():
721
  print("=" * 88)
722
- print(f"์ด๊ฒฉ ์žฌํ™•์ธ ๋ฐฑํ…Œ์ŠคํŠธ [lower-low] | {ANALYZE_FROM} ~ {_end_date()} | ์ข…๋ชฉ "
723
  f"{sum(len(v) for v in TICKERS.values())}๊ฐœ")
724
- if USE_TREND and USE_TREND_FLAT:
725
- cb = FLAT_COMBINE
726
- def _b(lo, hi):
727
- return f"โ‰ค{hi*100:.0f}%" if lo <= -1 else f"({lo*100:.0f},{hi*100:.0f}]%"
728
- tf_s = (f"์ถ”์„ธํ•„ํ„ฐ=ON[๋ŒํŒŒํ›„{RUN_MIN_DAYS}์ผ AND A-4ํ‰ํ‰ "
729
- f"60{_b(FLAT_MIN_60,FLAT_MAX_60)} {cb} "
730
- f"120{_b(FLAT_MIN_120,FLAT_MAX_120)} {cb} "
731
- f"240{_b(FLAT_MIN_240,FLAT_MAX_240)}]")
732
- else:
733
- tf_s = "์ถ”์„ธํ•„ํ„ฐ=OFF"
734
- if ENGINE == 'RS_A4':
735
- rs_confirm = {'A4': 'A-4ํ‰ํ‰', 'MA60': '์ข…๊ฐ€<60์„ ', 'MA120': '์ข…๊ฐ€<120์„ ',
736
- 'NONE': 'RS๋‹จ๋…'}.get(RS_CONFIRM, RS_CONFIRM)
737
- print(f"์—”์ง„=RS_A4 | RS(EMA{RS_SMOOTH})<MA{RS_MA_PERIOD}ร—(1-{RS_BUFFER*100:.0f}%) "
738
- f"{RS_PERSIST}์ผ์—ฐ์† RS์•ฝํ™” -> ํ™•์ธ[{rs_confirm}] ์ฆ‰์‹œ๋งค๋„ | {tf_s}")
739
- else:
740
- div_s = f"์ด๊ฒฉ๊ธฐ์ค€={DIV_THRESHOLD*100:.0f}%(๊ณ ์ •)"
741
- jmp_s = f"ํ•˜๋ฃจ๊ธ‰๋ณ€({DAILY_JUMP*100:.0f}%p)์ ํ”„"
742
- print(f"์—”์ง„=STAGE | ๊ณ ์ =์ตœ๊ทผ{PEAK_WINDOW}์ผ๋‚ด์ตœ๊ณ  | {div_s} | ๋‹จ๊ณ„๋ˆ„์ ({MIN_DEEPEN*100:.1f}%p+, "
743
- f"๋งค๋„๊นŒ์ง€ {CONFIRM_STAGES+1}๋‹จ๊ณ„) | {jmp_s} | {tf_s}")
744
  print(f"์ฑ„์ : ๊ด€์ฐฐ {LOOK_AHEAD}์ผ | E์œจ=240์ดํƒˆ(์กฐ๊ธฐ๊ฒฝ๋ณด) | P์œจ=๋งค๋„๊ฐ€๋Œ€๋น„ {PROTECT_DROP*100:.0f}% ์ถ”๊ฐ€ํ•˜๋ฝ ํšŒํ”ผ")
745
  print("=" * 88)
746
 
@@ -760,27 +283,29 @@ def main():
760
  all_jumps = [] # (tk, market, jump) ์ „์ฒด ๊ธ‰๋ณ€ ์ ํ”„
761
  for (tk, market), r in cache.items():
762
  stock, index, ma240 = r['stock'], r['index'], r['ma240']
763
- ma60 = r.get('ma60'); ma120 = r.get('ma120')
764
- # ๋””๋ฒ„๊น…์šฉ ๊ธฐ์šธ๊ธฐ: A-4๊ฐ€ ์“ฐ๋Š” ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๊ณ„์‚ฐ(๋ผ์ธ ํ‘œ์‹œ๊ฐ’=์‹ค์ œ ํŒ์ •๊ฐ’)
765
- _sd, _sm = FLAT_SLOPE_DAYS, FLAT_SMOOTH
766
- sl60 = _smoothed_slope(ma60, _sd, _sm) if ma60 is not None else None
767
- sl120 = _smoothed_slope(ma120, _sd, _sm) if ma120 is not None else None
768
- sl240 = _smoothed_slope(ma240, _sd, _sm) if ma240 is not None else None
769
- _engine = find_rs_a4_sells if ENGINE == 'RS_A4' else find_confirmed_sells
770
- sells, jumps, steps = _engine(stock, index, r['dates'], r['start_pos'],
771
- ma60=ma60, ma240=ma240,
772
- low=r.get('low'), bbl=r.get('bbl'),
773
- ma240_est=r.get('ma240_est'), ma120=ma120)
774
  for j in jumps:
775
  all_jumps.append((tk, market, j))
776
  lines = []
777
  for s in sells:
778
  i = s['i']; price = s['price']
779
- cat, days = classify_240(stock, ma240, i, price) # ์š”์•ฝํ†ต๊ณ„์šฉ(๋ผ์ธ์—” ์•ˆ์ฐ์Œ)
 
 
 
 
 
 
 
 
 
 
780
  isP = bool(protected(stock, i, price))
781
  is_run = in_240_run(stock, ma240, i)
782
  if cat == 'ALREADY_DOWN':
783
  down_n += 1
 
784
  elif cat == 'EARLY':
785
  b = run if is_run else edge
786
  b['n'] += 1; b['E'] += 1
@@ -788,29 +313,17 @@ def main():
788
  if is_run:
789
  drop = stock[min(i + 1 + LOOK_AHEAD, len(stock)) - 1] / price - 1
790
  run['drops'].append(drop)
 
791
  elif cat == 'STILL_UP':
792
  b = run if is_run else edge
793
  b['n'] += 1
794
  if isP: b['P'] += 1
795
- avoid = 'ํ•˜๋ฝํšŒํ”ผO' if isP else 'ํ•˜๋ฝํšŒํ”ผX'
796
- via = s.get('via', '')
797
- tr = s.get('trend', '')
798
- est = '240์ถ”์ •' if s.get('est') else ''
799
- # ๊ธฐ์šธ๊ธฐ ๋””๋ฒ„๊น…: ๋งค๋„ ์‹œ์ ์˜ 60/120/240์„  ๊ธฐ์šธ๊ธฐ(์Šค๋ฌด๋”ฉ) ํ•ญ์ƒ ํ‘œ์‹œ
800
- def _slv(arr):
801
- if arr is None or i >= len(arr) or np.isnan(arr[i]):
802
- return '-'
803
- return f'{arr[i]*100:.1f}%'
804
- slp = f'๊ธฐ์šธ๊ธฐ[60={_slv(sl60)} 120={_slv(sl120)} 240={_slv(sl240)}]'
805
- parts = [p for p in (via, tr, est, avoid, slp) if p]
806
  lines.append(f" ๋งค๋„ {s['date']} @ {price:,.0f} (์ด๏ฟฝ๏ฟฝ {s['div']*100:.0f}%) "
807
- f"[{', '.join(parts)}]")
808
- # ๋‹จ๊ณ„ ์ƒ์„ธ ๋กœ๊ทธ
809
- if SHOW_STEPS and steps:
810
- lines.append(f" [๋‹จ๊ณ„ ์ƒ์„ธ]")
811
- for stp in steps:
812
- nt = f" ({stp['note']})" if stp['note'] else ''
813
- lines.append(f" {stp['date']} {stp['name']:<24} ์ด๊ฒฉ {stp['div']*100:+.1f}%{nt}")
814
  detail.append((tk, market, lines))
815
 
816
  tot = run['n'] + edge['n'] + down_n
@@ -833,23 +346,22 @@ def main():
833
  else:
834
  print(f" {tk} {market} - ๋งค๋„ ์—†์Œ")
835
 
836
- # ---- ํ•˜๋ฃจ ๊ธ‰๋ณ€ ์ ํ”„ ๋ฐœ๋™ ๋‚ด์—ญ (STAGE ์—”์ง„ ์ „์šฉ. RS_A4๋Š” ๋‹จ๊ณ„๊ฐœ๋… ์—†์–ด์„œ ์ƒ๋žต) ----
837
- if ENGINE != 'RS_A4':
838
- print("\n" + "=" * 88)
839
- print(f"[ํ•˜๋ฃจ ๊ธ‰๋ณ€ ์ ํ”„ ๋ฐœ๋™ ๋‚ด์—ญ] (์ „์ผ ์ด๊ฒฉ ๋Œ€๋น„ {DAILY_JUMP*100:.0f}%p+ ๊ธ‰๋ฝ -> ๋Œ€๊ธฐ๋‹จ๊ณ„ ๊ฑด๋„ˆ๋œ€)")
840
- print("=" * 88)
841
- if not all_jumps:
842
- print(" ๋ฐœ๋™ ์—†์Œ")
843
- else:
844
- n_skip = sum(1 for _, _, j in all_jumps if j['skip'])
845
- print(f" ์ด {len(all_jumps)}ํšŒ ๋ฐœ๋™ (์ด ์ค‘ '๋Œ€๊ธฐ ๊ฑด๋„ˆ๋œ€(SKIP)' {n_skip}ํšŒ)\n")
846
- print(f" {'์ข…๋ชฉ':<8}{'์‹œ์žฅ':<7}{'๋‚ ์งœ':<12}{'ํ•˜๋ฃจ๋‚™ํญ':>8}{'์ด๊ฒฉ':>8} ๋‹จ๊ณ„์ „ํ™˜")
847
- print(" " + "-" * 70)
848
- for tk, market, j in sorted(all_jumps, key=lambda z: z[2]['date']):
849
- mark = ' โ˜…๊ฑด๋„ˆ๋œ€' if j['skip'] else ''
850
- print(f" {tk:<8}{market:<7}{j['date']:<12}"
851
- f"{j['drop']*100:>7.1f}%{j['div']*100:>7.1f}% "
852
- f"{j['from']} -> {j['to']}{mark}")
853
 
854
 
855
  if __name__ == '__main__':
 
4
 
5
  ๊ทœ์น™:
6
  - ์ด๋ฒคํŠธ = ์ข…๋ชฉ ์ž๊ธฐ๊ณ ์  ๊ธฐ์ค€ ์ด๊ฒฉ์ด -10% ์œ„์—์„œ -10% ์ดํ•˜๋กœ '์ƒˆ๋กœ ์ง„์ž…'ํ•œ ๊ตฌ๊ฐ„ 1๊ฐœ (๊นŠ์ด ๋ฌด๊ด€)
7
+ - 1์ฐจ ํ›„ '์ง์ „ ์ด๋ฒคํŠธ๋กœ๋ถ€ํ„ฐ 22๊ฑฐ๋ž˜์ผ ์ด๋‚ด'์— ๋‹ค์Œ ์ด๋ฒคํŠธ -> 2์ฐจ,3์ฐจ... ์—ฐ์‡„
8
+ (22์ผ ๋„˜๊ธฐ๋ฉด ๋Š๊ธฐ๊ณ  ๊ทธ๊ฒŒ ์ƒˆ 1์ฐจ)
9
  - 2์ฐจ๊ฐ€ ์„ฑ๋ฆฝํ•˜๋ฉด ๊ทธ ๋‚  '์ „๋Ÿ‰๋งค๋„' (๊นŠ์ด ๋น„๊ต ์—†์Œ)
10
  - ์‹ ๊ณ ๊ฐ€ ๊ฐฑ์‹  ์‹œ ๋ฆฌ์…‹
11
 
 
19
  import pandas as pd
20
  import numpy as np
21
  import FinanceDataReader as fdr
22
+ from datetime import datetime, timedelta
 
 
23
 
24
  ssl._create_default_https_context = ssl._create_unverified_context
25
  if sys.platform == 'win32':
 
30
  # =============================================
31
  TICKERS = {
32
  'KOSPI': [
33
+ # '005930', '000660', '373220', '207940', '005380',
 
34
  # '000270', '068270', '035420', '035720', '005490',
35
+ # '105560', '055550', '012330', '051910', '006400',
 
 
 
36
  # '028260', '066570', '096770', '034730', '015760',
 
37
  # '204320',
38
+ # '012450',
39
  ],
40
  'KOSDAQ': [
41
  # '247540', '086520', '196170', '028300', '348370',
42
+ # '058470', '068760', '263750', '035900',
43
+ # '277810',
44
+ '234690',
45
  # '041510', '035760', '112040', '293490', '145020',
46
  # '039030', '240810', '357780',
47
  # '403870',
48
  # '121600',
49
  # '399720',
50
+ # '234340',
 
 
 
 
 
51
  ],
52
  }
53
 
54
  # =============================================
55
  # ์„ค์ •
56
  # =============================================
57
+ DIV_THRESHOLD = -0.10 # ์ด๊ฒฉ ๊ธฐ์ค€
58
+ MIN_DEEPEN = 0.025 # ๋‹ค์Œ ๋‹จ๊ณ„๋กœ ๊ฐ€๋ ค๋ฉด ์ง์ „ ๋‹จ๊ณ„๋ณด๋‹ค ์ตœ์†Œ ์ด๋งŒํผ(%p) ๋” ๊นŠ์–ด์•ผ (๋…ธ์ด์ฆˆ ๋ฐฉ์ง€)
59
+ DAILY_JUMP = 0.10 # ํ•˜๋ฃจ๋งŒ์— ์ด๊ฒฉ์ด ์ „์ผ ๋Œ€๋น„ ์ด๋งŒํผ(%p)+ ๊ธ‰๋ฝํ•˜๋ฉด '๋Œ€๊ธฐ' ๊ฑด๋„ˆ๋›ฐ๊ณ  ๋ฐ”๋กœ ํ™•์ • ์ ํ”„
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  PEAK_WINDOW = 252 # ๊ณ ์  ๊ธฐ์ค€ = ์ตœ๊ทผ N๊ฑฐ๋ž˜์ผ(252=1๋…„) ๋‚ด ์ตœ๊ณ ์  (์˜ค๋ž˜๋œ ๊ณ ์ ์— ์•ˆ ๋ฌถ์ž„)
61
+ CONFIRM_WINDOW = 9999 # 1์ฐจ ์ด๋ฒคํŠธ๋กœ๋ถ€ํ„ฐ 2์ฐจ(๋งค๋„)๊นŒ์ง€ N์ผ ์ด๋‚ด
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  LOOK_AHEAD = 200 # ๋งค๋„ ํ›„ ๊ด€์ฐฐ๊ธฐ๊ฐ„(๊ฑฐ๋ž˜์ผ)
63
  PROTECT_DROP = -0.10 # ๋งค๋„๊ฐ€ ๋Œ€๋น„ ์ด๋งŒํผ ๋” ๋น ์ง€๋ฉด '์ž˜ ํŒ ๊ฒƒ'
64
 
65
+ TREND_MIN_DAYS = 60 # '240์˜ฌ': ์ตœ๊ทผ N๊ฑฐ๋ž˜์ผ ์—ฐ์† 240์„  ์œ„
66
+ TREND_MIN_GAP = 0.05 # '240์˜ฌ': ๋งค๋„์ผ ์ข…๊ฐ€๊ฐ€ 240์„  +5% ์ด์ƒ
67
 
68
+ # --- ๋งค๋„ ์•Œ๋žŒ ์–ต์ œ ํ•„ํ„ฐ (์š”์ฒญ์‚ฌํ•ญ) ---
69
+ SKIP_IF_BELOW_240 = True # ๋งค๋„ ์‹œ์  ์ข…๊ฐ€๊ฐ€ 240์„  ์•„๋ž˜๋ฉด ์•Œ๋žŒ ๋” (์ด๋ฏธ ๋Šฆ์Œ)
70
+ SKIP_IF_NO_240 = True # 240์„ ์ด ์—†์œผ๋ฉด(๋ฐ์ดํ„ฐ ๋ถ€์กฑ) ์•Œ๋žŒ ๋”
71
+ MAX_EARLY_DAYS = 129 # ๋งค๋„ ํ›„ ์ด ์ผ์ˆ˜๊นŒ์ง€๋งŒ ์œ ํšจ. ์ด๋ณด๋‹ค ๋Šฆ๊ฒŒ(130์ผ+) 240์ดํƒˆํ•˜๋ฉด
72
+ # = ๋„ˆ๋ฌด ์ผ์ฐ ํŒ ๊ฒƒ -> ์•Œ๋žŒ ๋”. (None์ด๋ฉด ์ด ํ•„ํ„ฐ ๋”)
73
+ SKIP_IF_STILL_UP = True # ๋งค๋„ ํ›„ ๊ด€์ฐฐ๊ธฐ๊ฐ„ ๋‚ด๋‚ด 240 ์•ˆ ๊นจ๊ณ  ์ถ”์„ธ์œ ์ง€=ํ—›๋งค๋„(false alarm) -> ์•Œ๋žŒ ๋”
74
 
75
  ANALYZE_FROM = '2020-01-01' # ์ฑ„์ (๋งค๋„์ง‘๊ณ„) ์‹œ์ž‘์ผ โ€” ์ด๊ฑด '์–ธ์ œ๋ถ€ํ„ฐ ๊ฒฐ๊ณผ๋ฅผ ๋ณผ์ง€'์ผ ๋ฟ
76
+ ANALYZE_TO = None # None=์˜ค๋Š˜
77
 
78
  # โ˜… ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ์€ ANALYZE_FROM ๊ณผ ๋ฌด๊ด€ํ•˜๊ฒŒ 'ํ•ญ์ƒ ์ถฉ๋ถ„ํžˆ ๊ณผ๊ฑฐ๋ถ€ํ„ฐ' ๋ถˆ๋Ÿฌ์˜จ๋‹ค.
79
  # (๊ณ ์ /์ด๊ฒฉ์€ ์ „์ฒด ๊ฐ€๊ฒฉ์ด๋ ฅ์œผ๋กœ ๊ณ„์‚ฐ๋ผ์•ผ ํ•˜๋ฏ€๋กœ. ANALYZE_FROM ๋งŒ ๋Šฆ์ถ”๋ฉด ๊ณ ์ ์„
80
  # ์ž˜๋ชป ์žก์•„ ๋งค๋„์‹œ์ ์ด ๋‹ฌ๋ผ์ง€๋Š” ๋ฒ„๊ทธ๊ฐ€ ์ƒ๊น€ -> ๊ทธ๋ž˜์„œ DATA_START ๋Š” ๊ณ ์ •.)
81
  DATA_START = '2018-01-01' # ๊ณ ์ •. ๋” ๊ณผ๊ฑฐ๋ฅผ ๋ณด๋ ค๋ฉด ์ด ๋‚ ์งœ๋งŒ ์•ž๋‹น๊ธฐ๋ฉด ๋จ.
82
 
83
+ _to_dt = datetime.strptime(ANALYZE_TO, '%Y-%m-%d') if ANALYZE_TO else datetime.today()
84
+ ANALYZE_TO = _to_dt.strftime('%Y-%m-%d')
 
 
 
 
 
85
 
86
 
87
  # =============================================
 
103
  return pk, ipk
104
 
105
 
106
+ def find_confirmed_sells(stock, index, dates, start_pos):
107
+ """ํ™•์ •๋งค๋„(2์ฐจ) ์ฐพ๊ธฐ โ€” ๋‹จ๊ณ„๋ณ„ ๋ˆ„์  ํ•˜๋ฝ + ํ•˜๋ฃจ ์ด๊ฒฉ ๊ธ‰๋ณ€ ์ ํ”„.
108
+ ๊ณ ์  = '์ตœ๊ทผ PEAK_WINDOW(252) ๊ฑฐ๋ž˜์ผ ๋‚ด ์ตœ๊ณ ์ ' (ํŠธ๋ ˆ์ผ๋ง).
109
+ ๋‹จ๊ณ„: flagON(๋Œ€๊ธฐ) -> 1์ฐจ(ํ™•์ •) -> 2์ฐจflagON(๋Œ€๊ธฐ) -> 2์ฐจ๋งค๋„(ํ™•์ •)
110
+ - ํ‰์†Œ: ์ง์ „ ๋‹จ๊ณ„๋ณด๋‹ค ์ด๊ฒฉ MIN_DEEPEN(2.5%p)+ ๋” ๊นŠ์œผ๋ฉด ๋‹ค์Œ ๋‹จ๊ณ„
111
+ - ํ•˜๋ฃจ ๊ธ‰๋ณ€: ์ „์ผ ์ด๊ฒฉ ๋Œ€๋น„ ์˜ค๋Š˜ ์ด๊ฒฉ์ด DAILY_JUMP(10%p)+ ๊ธ‰๋ฝํ•˜๋ฉด
112
+ '๋Œ€๊ธฐ(flagON)' ๊ฑด๋„ˆ๋›ฐ๊ณ  ๋ฐ”๋กœ ํ™•์ •์œผ๋กœ ์ ํ”„
113
+ - 1์ฐจ -> 2์ฐจ(๋งค๋„)๋Š” 22์ผ ์ด๋‚ด
114
+ - ์ด๊ฒฉ -10% ์œ„๋กœ ํšŒ๋ณตํ•˜๋ฉด ์—ํ”ผ์†Œ๋“œ ๋ฆฌ์…‹(๋งค๋„ํ–ˆ๋˜ ๊ฒƒ๋„ ํ’€๋ฆผ)."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  n = len(stock)
116
  twpk, twip = _trailing_peaks(stock, index, PEAK_WINDOW)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  stage = 0; ref_div = None; first_i = None; sold = False
118
  prev_div = None
119
  sells = []
120
+ jumps = [] # ํ•˜๋ฃจ๊ธ‰๋ณ€ ์ ํ”„ ๋ฐœ๋™ ๋‚ด์—ญ
121
  _SNAME = {0: '์—†์Œ', 1: 'flagON', 2: '1์ฐจ', 3: '2์ฐจflagON', 4: '2์ฐจ๋งค๋„'}
122
 
123
  def _log_jump(t, frm, to, div, pdv, st):
 
127
  'from': _SNAME[frm], 'to': _SNAME[to],
128
  'skip': (frm == 0 and to == 2) or (frm == 2 and to == 4)})
129
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  for t in range(n):
131
  st = float(stock[t]); it = float(index[t])
132
  peak = float(twpk[t]); iap = float(twip[t])
133
+ div = (st / peak - 1) - ((it / iap - 1) if iap else 0.0)
 
 
 
134
 
135
+ # ์ด๊ฒฉ -10% ์œ„๋กœ(=์ตœ๊ทผ ๊ณ ์  ๊ทผ์ฒ˜๋กœ ํšŒ๋ณต/์‹ ๊ณ ๊ฐ€) -> ์—ํ”ผ์†Œ๋“œ ๋ฆฌ์…‹
136
  if div > DIV_THRESHOLD:
 
 
137
  stage = 0; ref_div = None; first_i = None; sold = False
138
  prev_div = div
139
  continue
140
+
141
+ if sold: # ๋งค๋„ ํ›„, ์•„์ง ํšŒ๋ณต ๋ชปํ•จ -> ์ •์ง€ ์œ ์ง€
142
  prev_div = div
143
  continue
144
 
145
+ # ํ•˜๋ฃจ ๊ธ‰๋ณ€: ์ „์ผ ์ด๊ฒฉ ๋Œ€๋น„ DAILY_JUMP ์ด์ƒ ๊ธ‰๋ฝ
146
  crash = (prev_div is not None) and ((div - prev_div) <= -DAILY_JUMP)
147
+ within = (first_i is None) or ((t - first_i) <= CONFIRM_WINDOW)
 
 
 
 
 
 
 
 
 
148
 
149
+ if stage == 0: # ์•„๋ฌด๊ฒƒ๋„ ์—†์Œ
 
 
 
150
  if crash:
151
+ _log_jump(t, 0, 2, div, prev_div, st)
152
+ stage = 2; ref_div = div; first_i = t # flagON ๊ฑด๋„ˆ๋›ฐ๊ณ  -> 1์ฐจ
 
 
 
 
 
 
 
 
 
153
  else:
154
+ stage = 1; ref_div = div # flag ON(๋Œ€๊ธฐ)
155
+ elif stage == 1: # flag ON(๋Œ€๊ธฐ)
 
 
 
 
 
 
 
 
156
  if crash:
157
+ _log_jump(t, 1, 2, div, prev_div, st)
158
+ stage = 2; ref_div = div; first_i = t # -> 1์ฐจ(ํ™•์ •)
159
  elif div <= ref_div - MIN_DEEPEN:
160
+ stage = 2; ref_div = div; first_i = t # -> 1์ฐจ(ํ™•์ •)
161
+ elif stage == 2: # 1์ฐจ(ํ™•์ •)
162
+ if crash and within:
163
+ _log_jump(t, 2, 4, div, prev_div, st)
164
+ stage = 4; ref_div = div # 2์ฐจflagON ๊ฑด๋„ˆ๋›ฐ๊ณ  -> ๋งค๋„
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
  if t >= start_pos:
166
+ sells.append({'i': t, 'price': st, 'div': div, 'date': dates[t]})
167
+ sold = True
168
+ elif div <= ref_div - MIN_DEEPEN and within:
169
+ stage = 3; ref_div = div # -> 2์ฐจ flag ON(๋Œ€๊ธฐ)
170
+ elif stage == 3: # 2์ฐจ flag ON(๋Œ€๊ธฐ)
171
+ if crash and within:
172
+ _log_jump(t, 3, 4, div, prev_div, st)
173
+ stage = 4; ref_div = div
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  if t >= start_pos:
175
+ sells.append({'i': t, 'price': st, 'div': div, 'date': dates[t]})
176
+ sold = True
177
+ elif div <= ref_div - MIN_DEEPEN and within:
178
+ stage = 4; ref_div = div # -> 2์ฐจ ๋งค๋„(ํ™•์ •)
179
  if t >= start_pos:
180
+ sells.append({'i': t, 'price': st, 'div': div, 'date': dates[t]})
 
 
 
 
181
  sold = True
182
+ prev_div = div
183
+ return sells, jumps
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
 
185
 
186
  def in_240_run(stock, ma240, i):
 
195
  return True
196
 
197
 
198
+ def classify_240(stock, low, ma240, i, price):
199
+ """('EARLY'|'STILL_UP'|'ALREADY_DOWN'|'UNKNOWN', ์ดํƒˆ๊นŒ์ง€ ์ผ์ˆ˜)
200
+ ALREADY_DOWN ์€ '์ €๊ฐ€(์žฅ์ค‘)๊ฐ€ 240์„ ์„ ํ„ฐ์น˜/์ดํƒˆ'ํ•˜๋ฉด ํ•ด๋‹น (์ข…๊ฐ€ ์•„๋‹˜)."""
201
  m = ma240[i]
202
  if np.isnan(m):
203
  return ('UNKNOWN', None)
204
+ if low[i] <= m: # ์žฅ์ค‘ ์ €๊ฐ€๊ฐ€ 240์„  ํ„ฐ์น˜/์ดํƒˆ -> ์ด๋ฏธ ์œ„ํ—˜
205
  return ('ALREADY_DOWN', None)
206
  end = min(i + 1 + LOOK_AHEAD, len(stock))
207
  for k in range(i + 1, end):
208
+ if not np.isnan(ma240[k]) and low[k] <= ma240[k]:
209
  return ('EARLY', k - i)
210
  return ('STILL_UP', None)
211
 
 
220
 
221
  def load_one(ticker, market):
222
  try:
223
+ sdf = fdr.DataReader(ticker, DATA_START, ANALYZE_TO)
224
  except Exception:
225
  return None
226
  if not isinstance(sdf, pd.DataFrame) or 'Close' not in sdf.columns or len(sdf) == 0:
 
229
  idf = None
230
  for c in (idx_code, '^' + idx_code):
231
  try:
232
+ t = fdr.DataReader(c, DATA_START, ANALYZE_TO)
233
  if isinstance(t, pd.DataFrame) and 'Close' in t.columns and len(t) > 0:
234
  idf = t; break
235
  except Exception:
 
243
  merged = merged.dropna()
244
  if len(merged) == 0:
245
  return None
246
+ merged['ma240'] = merged['stock'].rolling(240).mean()
 
 
 
 
 
 
 
 
 
 
 
 
 
247
  dates = [str(d)[:10] for d in merged.index]
248
  start_pos = next((k for k, d in enumerate(dates) if d >= ANALYZE_FROM), 0)
249
  return {
 
251
  'low': merged['low'].values.astype(float),
252
  'index': merged['index'].values.astype(float),
253
  'ma240': merged['ma240'].values.astype(float),
 
 
 
 
254
  'dates': dates, 'start_pos': start_pos,
255
  }
256
 
 
261
 
262
  def main():
263
  print("=" * 88)
264
+ print(f"์ด๊ฒฉ ์žฌํ™•์ธ ๋ฐฑํ…Œ์ŠคํŠธ [lower-low] | {ANALYZE_FROM} ~ {ANALYZE_TO} | ์ข…๋ชฉ "
265
  f"{sum(len(v) for v in TICKERS.values())}๊ฐœ")
266
+ print(f"๊ทœ์น™: ๊ณ ์ =์ตœ๊ทผ{PEAK_WINDOW}์ผ๋‚ด์ตœ๊ณ  | ๋‹จ๊ณ„๋ˆ„์ ({MIN_DEEPEN*100:.1f}%p+) | ํ•˜๋ฃจ๊ธ‰๋ณ€({DAILY_JUMP*100:.0f}%p)์ ํ”„ | 1์ฐจ~2์ฐจ 22์ผ๋‚ด | ํšŒ๋ณต์‹œ์žฌ์‹œ์ž‘")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
  print(f"์ฑ„์ : ๊ด€์ฐฐ {LOOK_AHEAD}์ผ | E์œจ=240์ดํƒˆ(์กฐ๊ธฐ๊ฒฝ๋ณด) | P์œจ=๋งค๋„๊ฐ€๋Œ€๋น„ {PROTECT_DROP*100:.0f}% ์ถ”๊ฐ€ํ•˜๋ฝ ํšŒํ”ผ")
268
  print("=" * 88)
269
 
 
283
  all_jumps = [] # (tk, market, jump) ์ „์ฒด ๊ธ‰๋ณ€ ์ ํ”„
284
  for (tk, market), r in cache.items():
285
  stock, index, ma240 = r['stock'], r['index'], r['ma240']
286
+ low = r['low']
287
+ sells, jumps = find_confirmed_sells(stock, index, r['dates'], r['start_pos'])
 
 
 
 
 
 
 
 
 
288
  for j in jumps:
289
  all_jumps.append((tk, market, j))
290
  lines = []
291
  for s in sells:
292
  i = s['i']; price = s['price']
293
+ cat, days = classify_240(stock, low, ma240, i, price)
294
+ # --- ์•Œ๋žŒ ์–ต์ œ ํ•„ํ„ฐ ---
295
+ if SKIP_IF_NO_240 and cat == 'UNKNOWN':
296
+ continue # 240์„  ์—†์Œ -> ์•Œ๋žŒ ๋”
297
+ if SKIP_IF_BELOW_240 and cat == 'ALREADY_DOWN':
298
+ continue # ๋งค๋„ ์‹œ์  ์ด๋ฏธ 240 ์•„๋ž˜ -> ์•Œ๋žŒ ๋”
299
+ if MAX_EARLY_DAYS is not None and cat == 'EARLY' and days is not None \
300
+ and days > MAX_EARLY_DAYS:
301
+ continue # 130์ผ+ ๋’ค์—์•ผ 240์ดํƒˆ = ๋„ˆ๋ฌด ์ผ์ฐ ํŒ ๊ฒƒ -> ๋”
302
+ if SKIP_IF_STILL_UP and cat == 'STILL_UP':
303
+ continue # ๋งค๋„ ํ›„ ์ถ”์„ธ์œ ์ง€(240 ์•ˆ๊นจ์ง) = ํ—›๋งค๋„ -> ๋”
304
  isP = bool(protected(stock, i, price))
305
  is_run = in_240_run(stock, ma240, i)
306
  if cat == 'ALREADY_DOWN':
307
  down_n += 1
308
+ where = '์ด๋ฏธ240์•„๋ž˜'
309
  elif cat == 'EARLY':
310
  b = run if is_run else edge
311
  b['n'] += 1; b['E'] += 1
 
313
  if is_run:
314
  drop = stock[min(i + 1 + LOOK_AHEAD, len(stock)) - 1] / price - 1
315
  run['drops'].append(drop)
316
+ where = f"{'240์˜ฌ' if is_run else '๊ฒฝ๊ณ„'}, {days}์ผ๋’ค 240์ดํƒˆ"
317
  elif cat == 'STILL_UP':
318
  b = run if is_run else edge
319
  b['n'] += 1
320
  if isP: b['P'] += 1
321
+ where = f"{'240์˜ฌ' if is_run else '๊ฒฝ๊ณ„'}, ์ถ”์„ธ์œ ์ง€"
322
+ else:
323
+ where = 'ํŒ์ •๋ถˆ๊ฐ€'
324
+ avoid = ', ํ•˜๋ฝํšŒํ”ผO' if isP else ''
 
 
 
 
 
 
 
325
  lines.append(f" ๋งค๋„ {s['date']} @ {price:,.0f} (์ด๏ฟฝ๏ฟฝ {s['div']*100:.0f}%) "
326
+ f"[{where}{avoid}]")
 
 
 
 
 
 
327
  detail.append((tk, market, lines))
328
 
329
  tot = run['n'] + edge['n'] + down_n
 
346
  else:
347
  print(f" {tk} {market} - ๋งค๋„ ์—†์Œ")
348
 
349
+ # ---- ํ•˜๋ฃจ ๊ธ‰๋ณ€ ์ ํ”„(flagON/2์ฐจflagON ๋Œ€๊ธฐ ๊ฑด๋„ˆ๋œ€) ๋ฐœ๋™ ๋‚ด์—ญ ----
350
+ print("\n" + "=" * 88)
351
+ print(f"[ํ•˜๋ฃจ ๊ธ‰๋ณ€ ์ ํ”„ ๋ฐœ๋™ ๋‚ด์—ญ] (์ „์ผ ์ด๊ฒฉ ๋Œ€๋น„ {DAILY_JUMP*100:.0f}%p+ ๊ธ‰๋ฝ -> ๋Œ€๊ธฐ๋‹จ๊ณ„ ๊ฑด๋„ˆ๋œ€)")
352
+ print("=" * 88)
353
+ if not all_jumps:
354
+ print(" ๋ฐœ๋™ ์—†์Œ")
355
+ else:
356
+ n_skip = sum(1 for _, _, j in all_jumps if j['skip'])
357
+ print(f" ์ด {len(all_jumps)}ํšŒ ๋ฐœ๋™ (์ด ์ค‘ '๋Œ€๊ธฐ ๊ฑด๋„ˆ๋œ€(SKIP)' {n_skip}ํšŒ)\n")
358
+ print(f" {'์ข…๋ชฉ':<8}{'์‹œ์žฅ':<7}{'๋‚ ์งœ':<12}{'ํ•˜๋ฃจ๋‚™ํญ':>8}{'์ด๊ฒฉ':>8} ๋‹จ๊ณ„์ „ํ™˜")
359
+ print(" " + "-" * 70)
360
+ for tk, market, j in sorted(all_jumps, key=lambda z: z[2]['date']):
361
+ mark = ' โ˜…๊ฑด๋„ˆ๋œ€' if j['skip'] else ''
362
+ print(f" {tk:<8}{market:<7}{j['date']:<12}"
363
+ f"{j['drop']*100:>7.1f}%{j['div']*100:>7.1f}% "
364
+ f"{j['from']} -> {j['to']}{mark}")
 
365
 
366
 
367
  if __name__ == '__main__':