@@ -27,7 +27,7 @@ BIND_HOST = ''
BIND_PORT = 18792
STATE_BADGE = {
' SELL ' : ( ' 매도 ' , ' #ef4444 ' ) , ' ADD ' : ( ' 추격매수 ' , ' #16 a34a ' ) , ' BUY ' : ( ' 매수 ' , ' #22c55e ' ) ,
' SELL ' : ( ' 매도 ' , ' #ef4444 ' ) , ' ADD ' : ( ' 추격매수 ' , ' #0f7 a34 ' ) , ' BUY ' : ( ' 매수 ' , ' #22c55e ' ) ,
' HOLD ' : ( ' 보유 ' , ' #3b82f6 ' ) , ' WAIT ' : ( ' 대기 ' , ' #eab308 ' ) , ' SKIP ' : ( ' 제외 ' , ' #6b7280 ' ) ,
' NODATA ' : ( ' 데이터없음 ' , ' #374151 ' ) ,
}
@@ -745,6 +745,56 @@ def _market_fit_words(p: dict) -> str:
return ' · ' . join ( [ m ] + adj + [ style ] )
def _cat_labels ( p : dict ) - > tuple [ str , str , str , str ] :
""" 전략을 4카테고리 두글자 이름으로 — (추세, 진입, 매도, 베팅). """
p = p or { }
sma = p . get ( ' SMA_LONG ' , 20 )
trend = { 10 : ' 민감 ' , 20 : ' 표준 ' , 40 : ' 둔감 ' , 60 : ' 장기 ' } . get ( sma , ' 표준 ' )
entry = ' 적극 ' if p . get ( ' VOLUME_BREAKOUT_MULT ' , 1.5 ) < = 1.5 else ' 신중 '
rr = p . get ( ' RR_RATIO ' , 2.0 )
sell = ' 한방 ' if rr > = 2.5 else ' 단타 ' if rr < = 1.5 else ' 균형 '
bet = ' 집중 ' if p . get ( ' RISK_PER_TRADE_PCT ' , 0.01 ) > = 0.02 else ' 분산 '
return trend , entry , sell , bet
def _cat_name ( p : dict ) - > str :
""" 4카테고리 이름 한 줄 — 예 ' 표준·적극·균형·분산 ' . """
return ' · ' . join ( _cat_labels ( p ) )
def _cat_detail_html ( p : dict ) - > str :
""" 펼침용 — 4카테고리 각각의 선택값·쉬운 설명. """
p = p or { }
trend , entry , sell , bet = _cat_labels ( p )
sma = p . get ( ' SMA_LONG ' , 20 ) ; vol = p . get ( ' VOLUME_BREAKOUT_MULT ' , 1.5 )
rr = p . get ( ' RR_RATIO ' , 2.0 ) ; risk = p . get ( ' RISK_PER_TRADE_PCT ' , 0.01 )
stop = p . get ( ' STOP_ATR_MULT ' , 2.0 ) ; hold = p . get ( ' MAX_HOLD_DAYS ' , 0 )
rows = [
( ' 🟦 추세 ' , trend , f ' { sma : g } 일선 기준 ' , ' 추세 — 며칠짜리 평균선으로 보나 ' ,
f ' 추세를 며칠짜리 평균선으로 보는지예요. 이 전략은 { sma : g } 일선 기준( { trend } ). \n \n '
' · 민감(10일선): 빨리 반응 — 기회 빨리 잡지만 속임수(휩쏘)도 많음 \n '
' · 표준(20일선): 무난한 중간 \n · 둔감(40일선): 천천히 — 늦지만 안정적 \n · 장기(60일선): 큰 흐름만 ' ) ,
( ' 🟩 진입 ' , entry , f ' 돌파 거래량 { vol : g } 배 ' , ' 진입 — 얼마나 쉽게 사나 ' ,
f ' 돌파로 살 때 거래량이 평소의 몇 배는 터져야 사는지예요. 이 전략은 { vol : g } 배( { entry } ). \n \n '
' · 적극(1.3배): 문턱 낮음 — 자주 삼 \n · 신중(2.0배): 확실히 터질 때만 삼 ' ) ,
( ' 🟥 매도 ' , sell , f ' 손익비 { rr : g } · 손절 ATR { stop : g } 배 ' + ( f ' · 시간손절 { hold } 일 ' if hold else ' ' ) ,
' 매도 — 얼마나 길게 먹나 ' ,
f ' 목표 수익을 손절폭의 몇 배로 잡는지예요. 이 전략은 손익비 { rr : g } ( { sell } ). \n \n '
' · 한방(3.0): 길게 들고 크게 먹기 \n · 균형(2.0): 중간 \n · 단타(1.5): 짧게 자주 먹기 \n \n '
f ' 손절은 변동성(ATR)의 { stop : g } 배 빠지면 실행 ' + ( f ' , { hold } 일 지나도 안 오르면 정리해요. ' if hold else ' 해요. ' ) ) ,
( ' 🟨 베팅 ' , bet , f ' 종목당 리스크 { risk * 100 : g } % ' , ' 베팅 — 한 종목에 얼마나 ' ,
f ' 한 종목에 자산의 몇 %까지 거는지예요. 이 전략은 { risk * 100 : g } %( { bet } ). \n \n '
' · 집중(2 % ): 크게 베팅 — 수익·손실 둘 다 큼 \n · 분산(1 % ): 작게 — 안정적 ' ) ,
]
out = ' '
for cat , name , val , title , desc in rows :
out + = ( f ' <div class= " kv " ><span class= " k " > { cat } </span> '
f ' <span><b> { _esc ( name ) } </b> <span class= " muted " >— { _esc ( val ) } </span> '
f ' <button type= " button " class= " chk-i " data-title= " { _esc ( title ) } " '
f ' data-detail= " { _esc ( desc ) } " >ⓘ</button></span></div> ' )
return out
# 약세장 모드 단어 칩 — 비교군 행·드롭다운에서 한눈에 구분 (긴 접미사 대신).
_BEAR_WORD = { 0 : ' 회피 ' , 1 : ' 안정 ' , 2 : ' 적극 ' }
_BEAR_NAME = { 0 : ' 회피 (현금화) ' , 1 : ' 안정 (돌파만·기본) ' , 2 : ' 적극 (눌림허용) ' }
@@ -771,6 +821,51 @@ def _bear_chip_html(p) -> str:
f ' data-detail= " { _esc ( _BEAR_DESC [ m ] + _BEAR_LEGEND ) } " > { _BEAR_WORD [ m ] } </span> ' )
def _gate_info ( p ) :
""" 진입 게이트 차원 — (이름, 설명). 유형·약세장대처와 독립된 3번째 축. """
p = p or { }
off = [ ]
if not int ( p . get ( ' TREND_SLOPE_GATE ' , 1 ) ) :
off . append ( ' 장기선 우상향 요구 ' )
if not int ( p . get ( ' MID_ARRAY_GATE ' , 1 ) ) :
off . append ( ' 중기 정배열(5>60) 요구 ' )
if not off :
return ( ' 보수 (현행) ' ,
' 추세 조건을 모두 요구해요 — 현재가>장기선·정배열·장기선 우상향·중기 정배열(5>60). '
' 안전하지만 매수 기회가 적어요(보수적). ' )
return ( ' 완화 — ' + ' · ' . join ( ' 장기선 기울기 ' if ' 우상향 ' in o else ' 중기 정배열 ' for o in off ) + ' 면제 ' ,
' 추세 게이트를 완화한 변이예요. 꺼진 조건: \n · ' + ' \n · ' . join ( off )
+ ' \n \n 기본 조건(현재가>장기선·정배열)은 그대로라, 단기 반등 초입 종목도 잡아 더 적극적으로 매수해요. '
' 보수형(현행)과 라이브로 비교 중입니다. ' )
def _relax_chip_html ( p ) - > str :
""" 추세 게이트 완화 칩 — 장기선 기울기/중기정배열 게이트를 끈 변이 표시. 현행(둘다 ON)이면 빈 문자열. """
p = p or { }
off = [ ]
if not int ( p . get ( ' TREND_SLOPE_GATE ' , 1 ) ) :
off . append ( ' 장기선 우상향 요구 ' )
if not int ( p . get ( ' MID_ARRAY_GATE ' , 1 ) ) :
off . append ( ' 중기 정배열(5>60) 요구 ' )
if not off :
return ' '
desc = ( ' 추세 게이트 완화 변이 — 다음 조건을 꺼서 더 적극적으로 매수해요: \n · '
+ ' \n · ' . join ( off ) + ' \n \n 보수형(현행)과 라이브로 비교 중인 실험 전략입니다. ' )
return ( f ' <span class= " bic bic-relax " data-title= " 추세 게이트 완화 " '
f ' data-detail= " { _esc ( desc ) } " >완화</span> ' )
def _immediate_chip_html ( p ) - > str :
""" 즉시매수 칩 — 돌파·눌림목 안 기다리고 조건 통과 시 현재가 즉시 매수하는 변이. 아니면 빈 문자열. """
if not int ( ( p or { } ) . get ( ' IMMEDIATE_ENTRY ' , 0 ) ) :
return ' '
desc = ( ' 즉시매수 변이 — 추세·정배열·수급·애널을 다 통과한 종목이면, 돌파나 눌림목을 안 기다리고 '
' 강세장에서 현재가에 바로 사요. 꾸준히 오르기만 하는 좋은 종목을 놓치지 않으려는 실험 전략이에요. '
' (기다렸다 사는 기존 전략과 라이브 비교 중) ' )
return ( f ' <span class= " bic bic-now " data-title= " 즉시매수 " '
f ' data-detail= " { _esc ( desc ) } " >즉시</span> ' )
def _backtest_panel ( ) - > str :
""" 백테스트 결과 — 유형별(세부 라벨) 대표(1위) 조합 요약. 비교군은 이 대표들로 자동 구성되므로
수동 추가 / 제거 없이 읽기 전용 요약만 보여준다 . """
@@ -1028,12 +1123,14 @@ def _compare_panel() -> str:
rows + = (
f ' <details class= " row " data-reg= " { i } " data-rank= " { rank_of [ v [ " id " ] ] } " data-bt= " { bt_attr } " '
f ' data-held= " { s . get ( " open_positions " , 0 ) } " data-trades= " { s . get ( " closed_trades " , 0 ) } " ><summary> '
f ' <span class= " vid " > { rank_of [ v [ " id " ] ] } 위</span> { _bear_chip_html ( mp ) } <span class= " muted " > { _vid_disp ( vid ) } </span> '
f ' <span class= " nm " > { _esc ( mf_w ) } </span> { tag } '
f ' <span class= " vid " > { rank_of [ v [ " id " ] ] } 위</span> { _bear_chip_html ( mp ) } { _relax_chip_html ( mp ) } { _immediate_chip_html ( mp ) } <span class= " muted " > { _vid_disp ( vid ) } </span> '
f ' <span class= " nm " > { _esc ( _cat_name ( mp ) ) } </span> { tag } '
f ' <span class= " rt " ><span class= " muted " >🛒 { s . get ( " open_positions " , 0 ) } </span> '
f ' <b style= " color: { rc } " > { ret : +g } %</b></span></summary> '
f ' <div class= " body " > '
f ' <div class= " kv " ><span class= " k " >유형 </span><span> { _esc ( mf_sum ) } '
f ' <div class= " kv " ><span class= " k " >전략 4요소 </span><span class= " muted " >추세·진입·매도·베팅</span></div >'
f ' { _cat_detail_html ( mp ) } '
f ' <div class= " kv " ><span class= " k " >시장적합</span><span> { _esc ( mf_sum ) } '
f ' <button type= " button " class= " chk-i " data-title= " { _esc ( mf_t ) } " '
f ' data-detail= " { _esc ( mf_b ) } " >ⓘ</button></span></div> '
f ' <div class= " kv " ><span class= " k " >약세장대처</span><span><b> { _BEAR_NAME [ bm ] } </b> '
@@ -1197,6 +1294,7 @@ h1{font-size:17px;margin:10px 0 2px}h2{font-size:14px;margin:22px 0 8px;color:#c
. vid { background : #1e293b;color:#7dd3fc;border-radius:5px;padding:1px 6px;font-size:11px;font-weight:700}
. bic { border - radius : 5 px ; padding : 1 px 5 px ; font - size : 10 px ; font - weight : 700 ; margin - left : 4 px }
. bic0 { background : #334155;color:#cbd5e1}.bic1{background:#1e3a5f;color:#7dd3fc}.bic2{background:#4a2e12;color:#fbbf24}
. bic - relax { background : #3b1d4a;color:#d8b4fe}.bic-now{background:#0c3a2e;color:#6ee7b7}
. cmp - btlink { background : #0f2a3f;color:#7dd3fc;border:1px solid #1e3a5f;border-radius:6px;padding:3px 8px;font-size:11px;font-weight:600;display:inline-block}
. note { white - space : pre - wrap ; line - height : 1.65 ; font - size : 13 px ; color : #cbd5e1}
ul . an { margin : 6 px 0 ; padding - left : 18 px ; line - height : 1.7 ; font - size : 13 px }
@@ -1326,12 +1424,15 @@ td:first-child,th:first-child{text-align:left}
< meta name = " viewport " content = " width=device-width,initial-scale=1 " >
< title > { _esc ( vid ) } 전략 상세 < / title > { style } < / head > < body > < div id = " page " >
{ back }
< h1 > < span class = " vid " > { rank or " — " } 위 < / span > < span class = " muted " > { _vid_disp ( vid ) } < / span > { _esc ( _market_fit_words ( params ) ) }
< span class = " bic bic {bm} " > { _BEAR_WORD [ bm ] } < / span > { icon } < / h1 >
< h1 > < span class = " vid " > { rank or " — " } 위 < / span > < span class = " muted " > { _vid_disp ( vid ) } < / span > { _esc ( _cat_name ( params ) ) }
< span class = " bic bic {bm} " > { _BEAR_WORD [ bm ] } < / span > { _relax_chip_html ( params ) } { _immediate_chip_html ( params ) } { icon } < / h1 >
< div class = " sub " > { _esc ( _market_fit_summary ( params ) ) } · 약세장대처 : { _BEAR_NAME [ bm ] } < / div >
< div class = " card " > { notes_html } < / div >
< h2 > 🧩 전략 4 요소 ( 이름 풀이 ) < / h2 >
< div class = " card " > { _cat_detail_html ( params ) } < / div >
< h2 > 📈 수익률 추이 < / h2 >
< div class = " card " > { _ret_chart_svg ( series , idx_series ) } < / div >
@@ -1380,7 +1481,9 @@ _DAY_CAL_JS = """<script>(function(){
function render ( ) {
var p = cur . split ( ' - ' ) , y = + p [ 0 ] , m = + p [ 1 ] ;
var startDow = new Date ( y , m - 1 , 1 ) . getDay ( ) , ndays = new Date ( y , m , 0 ) . getDate ( ) ;
var minYM = ( TD [ TD . length - 1 ] | | ' ' ) . slice ( 0 , 7 ) , maxYM = ( TD [ 0 ] | | ' ' ) . slice ( 0 , 7 ) ;
var allYM = TD . concat ( [ TODAY ] ) ; / / 월 이동 범위에 오늘 포함 ( 거래 없어도 오늘 월까지 이동 가능 )
var minYM = allYM . reduce ( function ( a , b ) { return a < b ? a : b ; } ) . slice ( 0 , 7 ) ;
var maxYM = allYM . reduce ( function ( a , b ) { return a > b ? a : b ; } ) . slice ( 0 , 7 ) ;
var h = ' <div class= " cal-hd " > '
+ ' <button type= " button " data-nav= " -1 " ' + ( cur < = minYM ? ' disabled ' : ' ' ) + ' >‹ </button> '
+ ' <span> ' + y + ' . ' + pad ( m ) + ' </span> '
@@ -1390,9 +1493,12 @@ _DAY_CAL_JS = """<script>(function(){
for ( var i = 0 ; i < startDow ; i + + ) h + = ' <div></div> ' ;
for ( var d = 1 ; d < = ndays ; d + + ) {
var ds = y + ' - ' + pad ( m ) + ' - ' + pad ( d ) ;
var cls = ' cal-d ' + ( ds == = TODAY ? ' tdy ' : ' ' ) ;
if ( tset [ ds ] ) { h + = ' <a class= " ' + cls + ' on ' + ( ds == = SEL ? ' sel ' : ' ' ) + ' " data-d= " ' + ds + ' " href= " /?t=lab&s=day&d= ' + ds + ' " > ' + d + ' </a> ' ; }
else { h + = ' <span class= " ' + cls + ' off " > ' + d + ' </span> ' ; }
var isToday = ( ds == = TODAY ) , hasTr = ! ! tset [ ds ] ;
var cls = ' cal-d ' + ( isToday ? ' tdy ' : ' ' ) ;
if ( hasTr | | isToday ) { / / 거래일 = 밝게 ( on ) / 오늘은 거래없어도 클릭가능 ( today - open )
cls + = hasTr ? ' on ' : ' today-open ' ;
h + = ' <a class= " ' + cls + ( ds == = SEL ? ' sel ' : ' ' ) + ' " data-d= " ' + ds + ' " href= " /?t=lab&s=day&d= ' + ds + ' " > ' + d + ' </a> ' ;
} else { h + = ' <span class= " ' + cls + ' off " > ' + d + ' </span> ' ; }
}
h + = ' </div> ' ;
box . innerHTML = h ;
@@ -1404,7 +1510,7 @@ _DAY_CAL_JS = """<script>(function(){
cur = ny + ' - ' + pad ( nm ) ; render ( ) ;
} ) ;
} ) ;
box . querySelectorAll ( ' .cal-d.on ' ) . forEach ( function ( a ) {
box . querySelectorAll ( ' .cal-d[data-d] ' ) . forEach ( function ( a ) {
a . addEventListener ( ' click ' , function ( e ) { e . preventDefault ( ) ; pick ( a . getAttribute ( ' data-d ' ) ) ; } ) ;
} ) ;
}
@@ -1468,19 +1574,18 @@ def _day_trades_panel(sel_date: str | None = None) -> str:
if d :
all_rows . append ( ( d , v [ ' id ' ] , r ) )
dates = sorted ( { d for d , _ , _ in all_rows } , reverse = True ) # 최신순
dates = sorted ( { d for d , _ , _ in all_rows } , reverse = True ) # 거래일만, 최신순
today = datetime . now ( config . KST ) . strftime ( ' % Y- % m- %d ' )
selectable = set ( dates ) | { today } # 오늘은 거래 없어도 선택 가능
back = ' <label for= " sub-cmp " class= " cmp-btlink " role= " button " tabindex= " 0 " >← 비교로</label> '
if not dates :
return ( f ' <h2 class= " cmp-title " >📋 거래목록 { back } </h2> '
' <div class= " empty " >아직 거래 기록이 없어요 — 스캔이 진입 조건을 기다리는 중</div> ' )
if sel_date not in dates :
sel_date = dates [ 0 ] # 기본 = 최근 거래일
if sel_date not in selectable :
sel_date = today # 기본 = 오늘 (거래 없으면 '이 날은 거래 없어요' 표시)
# 달력형 날짜 선택 — 거래일만 밝게·클릭가능, 거래 없는 날은 어둡게. 월 이동은 JS(새로고침 X).
is_today = sel_date == datetime . now ( config . KST ) . strftime ( ' % Y- % m- %d ' )
is_today = sel_date == today
cal_js = ( _DAY_CAL_JS . replace ( ' __DATES__ ' , json . dumps ( dates ) )
. replace ( ' __SEL__ ' , sel_date )
. replace ( ' __TODAY__ ' , datetime . now ( config . KST ) . strftime ( ' % Y- % m- %d ' ) ) )
. replace ( ' __TODAY__ ' , today ) )
picker = ( f ' <details class= " day-cal-wrap " id= " day-cal-wrap " ><summary> '
f ' 📅 <b id= " day-sel-label " > { sel_date } { " (오늘) " if is_today else " " } </b> '
f ' <span class= " muted " >▾ 날짜 선택</span> '
@@ -1513,7 +1618,9 @@ def _day_trades_panel(sel_date: str | None = None) -> str:
a [ ' n ' ] = len ( a [ ' vids ' ] )
if not agg :
return head + ' <div class= " empty " >이 날은 거래가 없어요</div> '
# 빈 날도 #day-sub·#day-rows 컨테이너 유지 — 다른 날로 fetch 전환 시 갱신되게
return ( head + ' <div class= " sub " id= " day-sub " >이 날은 거래가 없어요</div> '
+ ' <div id= " day-rows " ><div class= " empty " >이 날은 거래가 없어요 — 스캔이 진입 조건을 기다렸어요</div></div> ' )
# (종목, 방향) 단위로 한 아이템 — 같은 종목에 매수·매도 동시면 2개 아이템으로 분리.
# 정렬: 매도(청산) 먼저, 그다음 전략 수 많은 순.
@@ -1686,14 +1793,18 @@ def _trades_table(trades) -> str:
return tr_rows or ' <div class= " empty " >거래 없음</div> '
def _decision_card ( d , watch_n : int = 0 , held_n : int = 0 , acc_detail : str = ' ' ) - > str :
""" 관심종목 탭 종목 카드 (일반·제외목록 공용). watch_n/held_n = 이 종목을 관심/보유 중인 계좌 수 .
acc_detail 이 있으면 👁 🛒 클릭 시 ' 어느 전략이 보는지 ' 목록 토스트 . """
def _decision_card ( d , watch_n : int = 0 , held_n : int = 0 , acc_detail : str = ' ' , buy_n : int = 0 ) - > str :
""" 관심종목 탭 종목 카드 (일반·제외목록 공용). watch_n=관심(대기+매수준비) 총합, buy_n=매수준비중, held_n=보유 .
acc_detail 이 있으면 배지 클릭 시 ' 어느 전략이 보는지 ' 목록 토스트 . """
chips = _chips ( d . get ( ' checks ' , [ ] ) )
src = ' / ' . join ( ORIGIN_LABEL . get ( s , s ) for s in d . get ( ' sources ' , [ ] ) )
acc = ' '
wait_n = max ( 0 , watch_n - buy_n ) # 대기중 = 관심 총합 − 매수준비중
if watch_n or held_n :
parts = ( [ f ' 👁 { watch_n } ' ] if watch_n else [ ] ) + ( [ f ' 🛒 { held_n } ' ] if held_n else [ ] )
# 🟢 매수준비중 · 🟡 대기중 · 🛒 보유 — 0인 항목은 생략
parts = ( [ f ' <span style= " color:#22c55e " >🟢 { buy_n } </span> ' ] if buy_n else [ ] ) \
+ ( [ f ' <span style= " color:#eab308 " >🟡 { wait_n } </span> ' ] if wait_n else [ ] ) \
+ ( [ f ' 🛒 { held_n } ' ] if held_n else [ ] )
attrs = ( f ' data-title= " { _esc ( d . get ( " name " ) ) } — 보는 전략 " data-detail= " { _esc ( acc_detail ) } " '
if acc_detail else ' ' )
acc = f ' <span class= " muted acc-n " { attrs } > { " " . join ( parts ) } </span> '
@@ -1706,7 +1817,7 @@ def _decision_card(d, watch_n: int = 0, held_n: int = 0, acc_detail: str = '') -
f ' <button type= " submit " title= " 관찰목록에서 삭제 " >✕</button></form> ' )
return (
f ' <div class= " card " ><div class= " card-h " > '
f ' <b> { _esc ( d . get ( " name " ) ) } </b> <span class= " muted " > { _esc ( d . get ( " code " ) ) } · { _esc ( src ) } </span> { acc } '
f ' <b> { _esc ( d . get ( " name " ) ) } </b> <span class= " muted " > { _esc ( src ) } </span> { acc } '
f ' <span class= " price " > { price_h } { pnl_h } </span> { del_btn } </div> '
f ' <div class= " reason " > { _esc ( d . get ( " reason " , " " ) ) } </div> '
f ' <div class= " chks " > { chips } </div></div> '
@@ -1792,7 +1903,9 @@ def render_html(view: str | None = None, day_date: str | None = None,
if ws :
if lines :
lines . append ( ' ' )
lines . append ( f ' 👁 관심 { len ( ws ) } 개 전략: ' )
_nb = sum ( 1 for _ , st , _ in ws if st in ( ' BUY ' , ' ADD ' ) )
_nw = len ( ws ) - _nb
lines . append ( f ' 관심 { len ( ws ) } 개 — 🟢매수준비 { _nb } · 🟡대기 { _nw } : ' )
for vid , st , rsn in ws :
em = ' 🟢 ' if st in ( ' BUY ' , ' ADD ' ) else ' 🟡 '
rk = rank_map . get ( vid )
@@ -1838,7 +1951,7 @@ def render_html(view: str | None = None, day_date: str | None = None,
judge_html + = f ' <h3 class= " grp " ><span class= " badge " style= " background: { color } " > { label } </span> { len ( items ) } </h3> '
for d in items :
judge_html + = _decision_card ( d , watch_n . get ( d . get ( ' code ' ) , 0 ) , held_n . get ( d . get ( ' code ' ) , 0 ) ,
_acc_detail ( d . get ( ' code ' ) ) )
_acc_detail ( d . get ( ' code ' ) ) , buy_n . get ( d . get ( ' code ' ) , 0 ) )
# 관찰목록 중 아직 스캔 안 된(방금 추가 등) 종목 → '스캔 대기'로 즉시 표시
wl = umod . _load_watchlist ( )
@@ -1854,7 +1967,7 @@ def render_html(view: str | None = None, day_date: str | None = None,
f ' <button type= " submit " title= " 관찰목록에서 삭제 " >✕</button></form> ' )
judge_html + = (
f ' <div class= " card " ><div class= " card-h " > '
f ' <b> { _esc ( e . get ( " name " ) or code ) } </b> <span class= " muted " > { _esc ( code ) } · { _esc ( src ) } </span> '
f ' <b> { _esc ( e . get ( " name " ) or code ) } </b> <span class= " muted " > { _esc ( src ) } </span> '
f ' <span class= " price muted " >다음 스캔 대기</span> { del_btn } </div></div> '
)
@@ -1866,7 +1979,7 @@ def render_html(view: str | None = None, day_date: str | None = None,
f ' <span class= " bt-type-d " style= " display:inline;margin-left:6px " >스캔은 하되 매수 안 함</span></summary> '
f ' <div class= " body " > ' + ' ' . join (
_decision_card ( d , watch_n . get ( d . get ( ' code ' ) , 0 ) , held_n . get ( d . get ( ' code ' ) , 0 ) ,
_acc_detail ( d . get ( ' code ' ) ) )
_acc_detail ( d . get ( ' code ' ) ) , buy_n . get ( d . get ( ' code ' ) , 0 ) )
for d in excluded ) + ' </div></details> '
)
@@ -1879,8 +1992,10 @@ def render_html(view: str | None = None, day_date: str | None = None,
_sel = ' selected ' if view == vv [ ' id ' ] else ' '
_fav = f ' { icon_of [ vv [ " id " ] ] } ' if vv [ ' id ' ] in icon_of else ' '
_vp = vv . get ( ' params ' ) or { }
_rx = ' 완화 ' if ( not int ( _vp . get ( ' TREND_SLOPE_GATE ' , 1 ) ) or not int ( _vp . get ( ' MID_ARRAY_GATE ' , 1 ) ) ) else ' '
_im = ' 즉시 ' if int ( _vp . get ( ' IMMEDIATE_ENTRY ' , 0 ) ) else ' '
opts + = ( f ' <option value= " { _esc ( vv [ " id " ] ) } " { _sel } > { _fav } { _rk ( vv [ " id " ] ) } { _vid_disp ( vv [ " id " ] ) } '
f ' [ { _BEAR_WORD [ _bear_mode ( _vp ) ] } ] · { _esc ( _market_fit_words ( _vp ) ) } </option> ' )
f ' [ { _BEAR_WORD [ _bear_mode ( _vp ) ] } { _rx } { _im } ] · { _esc ( _cat_name ( _vp ) ) } </option> ' )
view_sel = ( f ' <div class= " sub " style= " margin:4px 0 8px " >체크리스트 시각: '
f ' <select id= " judge-view " '
f ' onchange= " location.href= \' /?t=watch&view= \' +encodeURIComponent(this.value) " > { opts } </select></div> ' )
@@ -1935,6 +2050,7 @@ h3.grp{{font-size:13px;margin:16px 0 8px;color:#cbd5e1}}
. chks { { display : flex ; flex - wrap : wrap ; gap : 5 px } }
. chk { { background : #0f151c;border:1px solid #1f2937;border-radius:6px;padding:2px 7px;font-size:11px}}
. chk . cd { { color : #6b7280;margin-left:5px}}
. acc - n { { font - size : 9 px } }
. acc - n [ data - detail ] { { cursor : pointer ; text - decoration : underline dotted #475569;text-underline-offset:3px}}
#chk-toast .ct-d{{max-height:55vh;overflow-y:auto}}
. chk - i { { background : none ; border : none ; color : #60a5fa;font-size:11px;cursor:pointer;padding:0 0 0 4px;line-height:1;vertical-align:baseline}}
@@ -1976,6 +2092,7 @@ h3.grp{{font-size:13px;margin:16px 0 8px;color:#cbd5e1}}
. cal - wd { { text - align : center ; font - size : 9 px ; color : #64748b;padding:1px 0}}
. cal - d { { position : relative ; text - align : center ; padding : 5 px 0 ; border - radius : 5 px ; font - size : 11 px ; font - variant - numeric : tabular - nums } }
. cal - d . on { { background : #0f2a3f;color:#7dd3fc;border:1px solid #1e3a5f;font-weight:700;text-decoration:none;cursor:pointer}}
. cal - d . today - open { { color : #7dd3fc;border:1px dashed #334155;text-decoration:none;cursor:pointer}}
. cal - d . off { { color : #2c3848}}
. cal - d . sel { { background : #2563eb;color:#fff;border-color:#60a5fa;box-shadow:0 0 0 1px #60a5fa}}
. cal - d . tdy : : after { { content : ' ' ; position : absolute ; bottom : 2 px ; left : 50 % ; transform : translateX ( - 50 % ) ; width : 3 px ; height : 3 px ; border - radius : 50 % ; background : #7dd3fc}}
@@ -1996,6 +2113,8 @@ h3.grp{{font-size:13px;margin:16px 0 8px;color:#cbd5e1}}
. bic0 { { background : #334155;color:#cbd5e1}}
. bic1 { { background : #1e3a5f;color:#7dd3fc}}
. bic2 { { background : #4a2e12;color:#fbbf24}}
. bic - relax { { background : #3b1d4a;color:#d8b4fe}}
. bic - now { { background : #0c3a2e;color:#6ee7b7}}
. cmp - sortbar { { display : flex ; gap : 6 px ; margin : 0 0 10 px } }
. cmp - sortbar button { { flex : 1 ; background : #141b24;border:1px solid #1f2937;border-radius:8px;color:#9ca3af;font-size:11px;font-weight:600;padding:7px 4px;cursor:pointer}}
. cmp - sortbar button . active { { background : #15406e;color:#fff;border-color:#2952cc}}
@@ -2100,7 +2219,7 @@ input[name="tab"],input[name="subtab"]{{display:none}}
< input type = " text " id = " wq " placeholder = " 종목명 또는 6자리 코드 검색 " aria - label = " 종목 검색 " autocomplete = " off " >
< button type = " button " id = " wq-btn " class = " btn-addcmp " > 검색 < / button > < / div >
< div id = " wq-results " class = " wsearch-results hidden " > < / div >
< div class = " sub " > 상태 그룹은 전체 전략 ( 메인 + 변이 ) 합산 — 한 전략이라도 보유 / 매수대기면 그 그룹으로 · 체크리스트는 기준전략 ( M ) 시각 · 👁 관심 🛒 보유 전략 수 < / div >
< div class = " sub " > 상태 그룹은 전체 전략 ( 메인 + 변이 ) 합산 — 한 전략이라도 보유 / 매수대기면 그 그룹으로 · 배지 : 🟢 매수준비 · 🟡 대기 · 🛒 보유 전략 수 ( 탭하면 어떤 전략인지 ) < / div >
< div class = " sub " > 종목명 일부만 입력해도 후보가 떠요 · 후보를 누르면 추가 · 자산 ( 비하이브 워치 · 관심 · 보유 ) 은 자동 편입 · 삭제는 카드의 ✕ ( 수동만 ) · 보유 중이면 삭제해도 청산까지 추적 < / div >
{ view_sel }
{ judge_html }