@@ -4876,8 +4876,15 @@ table.market-adr td.adr-breakdown { font-size: 11px; color: #8b8f9a; }
}
}
/ * 주문 모달 ( order - modal ) — 거래내역 보기용 trade - modal과 별개 . 모달 박스 자체 스크롤 X ( 호가창만 스크롤 ) * /
/ * 주문 모달 ( order - modal ) — 거래내역 보기용 trade - modal과 별개 . 모달 박스 자체 스크롤 X ( 호가창만 스크롤 ) * /
. order - box { max - width : 560 px ; overflow - y : hidden ; display : flex ; flex - direction : column ; }
. order - box { max - width : 560 px ; overflow - y : hidden ; display : flex ; flex - direction : column ; transition : border - color 0.15 s , box - shadow 0.15 s , background 0.15 s ; }
. order - box > [ data - order - step = " 1 " ] { display : flex ; flex - direction : column ; flex : 1 ; min - height : 0 ; }
. order - box > [ data - order - step = " 1 " ] { display : flex ; flex - direction : column ; flex : 1 ; min - height : 0 ; }
/ * 매수 / 매도 모드 전체 색조 — 모달 어디를 봐도 side가 한눈에 ( 매수 = 빨강 / 매도 = 파랑 ) * /
. order - box . side - buy { border - color : #c9303e; box-shadow: 0 0 0 1px #c9303e, 0 12px 44px rgba(201,48,62,0.34); background: linear-gradient(180deg, rgba(201,48,62,0.12), rgba(201,48,62,0.02) 130px, #0d1018 300px); }
. order - box . side - sell { border - color : #2a5fb3; box-shadow: 0 0 0 1px #2a5fb3, 0 12px 44px rgba(42,95,179,0.34); background: linear-gradient(180deg, rgba(42,95,179,0.12), rgba(42,95,179,0.02) 130px, #0d1018 300px); }
. order - box . side - buy . modal - head { border - bottom : 2 px solid rgba ( 201 , 48 , 62 , 0.55 ) ; padding - bottom : 10 px ; }
. order - box . side - sell . modal - head { border - bottom : 2 px solid rgba ( 42 , 95 , 179 , 0.55 ) ; padding - bottom : 10 px ; }
. order - box . side - buy . orderbook { border - color : rgba ( 201 , 48 , 62 , 0.38 ) ; }
. order - box . side - sell . orderbook { border - color : rgba ( 42 , 95 , 179 , 0.38 ) ; }
. order - symbol - select { background : #14171f; color: #f0f0f0; border: 1px solid #1f2330; border-radius: 6px; padding: 7px 9px; font-size: 14px; font-weight: 600; font-family: inherit; width: 100%; max-width: 360px; cursor: pointer; color-scheme: dark; }
. order - symbol - select { background : #14171f; color: #f0f0f0; border: 1px solid #1f2330; border-radius: 6px; padding: 7px 9px; font-size: 14px; font-weight: 600; font-family: inherit; width: 100%; max-width: 360px; cursor: pointer; color-scheme: dark; }
. order - symbol - select : focus { outline : none ; border - color : #ff4d5e; }
. order - symbol - select : focus { outline : none ; border - color : #ff4d5e; }
. order - symbol - select option { background : #14171f; color: #f0f0f0; }
. order - symbol - select option { background : #14171f; color: #f0f0f0; }
@@ -4926,8 +4933,9 @@ table.market-adr td.adr-breakdown { font-size: 11px; color: #8b8f9a; }
. qty - step - buttons button [ data - qty - step = " max " ] { background : #2a1f24; color: #ffcc77; }
. qty - step - buttons button [ data - qty - step = " max " ] { background : #2a1f24; color: #ffcc77; }
. qty - step - buttons button [ data - qty - step = " clear " ] { background : #1d1418; color: #ff8a95; }
. qty - step - buttons button [ data - qty - step = " clear " ] { background : #1d1418; color: #ff8a95; }
. order - inputs input : disabled , . order - inputs select : disabled { opacity : 0.5 ; cursor : not - allowed ; }
. order - inputs input : disabled , . order - inputs select : disabled { opacity : 0.5 ; cursor : not - allowed ; }
. order - inputs { display : flex ; flex - direction : column ; gap : 8 px ; min - height : 0 ; }
. order - inputs { display : flex ; flex - direction : column ; gap : 8 px ; min - height : 0 ; overflow - y : auto ; }
. order - inputs . order - actions { margin - top : auto ; }
/ * 입력이 모달 높이를 넘쳐도 매수 / 취소 버튼은 항상 하단 고정 — 잘림 방지 * /
. order - inputs . order - actions { margin - top : auto ; position : sticky ; bottom : 0 ; background : #0d1018; padding-top: 8px; }
. order - inputs label { display : flex ; flex - direction : column ; gap : 4 px ; font - size : 11 px ; color : #8b8f9a; }
. order - inputs label { display : flex ; flex - direction : column ; gap : 4 px ; font - size : 11 px ; color : #8b8f9a; }
. order - inputs label . inline - row { flex - direction : row ; align - items : center ; gap : 8 px ; }
. order - inputs label . inline - row { flex - direction : row ; align - items : center ; gap : 8 px ; }
. order - inputs label . inline - row > select { flex : 1 ; min - width : 0 ; }
. order - inputs label . inline - row > select { flex : 1 ; min - width : 0 ; }
@@ -8100,7 +8108,7 @@ var ACCOUNTS = [
{ label : ' 가희_일반 ' , display : ' 가희 일반 ' , owner : ' 가희 ' } ,
{ label : ' 가희_일반 ' , display : ' 가희 일반 ' , owner : ' 가희 ' } ,
{ label : ' 가희_ISA ' , display : ' 가희 ISA ' , owner : ' 가희 ' }
{ label : ' 가희_ISA ' , display : ' 가희 ISA ' , owner : ' 가희 ' }
] ;
] ;
var state = { code : ' ' , name : ' ' , side : ' BUY ' , pollTimer : null , countdownTimer : null , expiryAt : 0 , isOpen : false , lastBook : null , bookCentered : false , lastCheck : null , marketActive : true , marketPhase : null , symbolsCache : null , pendingMaxOnSell : false , pendingMaxOnBuy : false } ;
var state = { code : ' ' , name : ' ' , side : ' BUY ' , pollTimer : null , countdownTimer : null , expiryAt : 0 , isOpen : false , lastBook : null , bookCentered : false , lastCheck : null , marketActive : true , marketPhase : null , symbolsCache : null , pendingMaxOnSell : false , pendingMaxOnBuy : false , accStatus : null } ;
function $ ( sel , root ) { return ( root | | modal ) . querySelector ( sel ) ; }
function $ ( sel , root ) { return ( root | | modal ) . querySelector ( sel ) ; }
function $ $ ( sel , root ) { return ( root | | modal ) . querySelectorAll ( sel ) ; }
function $ $ ( sel , root ) { return ( root | | modal ) . querySelectorAll ( sel ) ; }
function $ p ( sel ) { return pinModal ? pinModal . querySelector ( sel ) : null ; }
function $ p ( sel ) { return pinModal ? pinModal . querySelector ( sel ) : null ; }
@@ -8136,6 +8144,10 @@ function setMsg(text, kind){
function setSide ( side ) {
function setSide ( side ) {
state . side = side ;
state . side = side ;
$ $ ( ' .order-side-toggle button[data-side] ' ) . forEach ( function ( b ) { b . classList . toggle ( ' active ' , b . getAttribute ( ' data-side ' ) == = side ) ; } ) ;
$ $ ( ' .order-side-toggle button[data-side] ' ) . forEach ( function ( b ) { b . classList . toggle ( ' active ' , b . getAttribute ( ' data-side ' ) == = side ) ; } ) ;
/ / 모달 박스 전체에 side 색조 — 테두리 · 헤더 구분선 · 호가창 테두리가 매수 = 빨강 / 매도 = 파랑으로 물듦
var obox = $ ( ' .order-box ' ) ;
if ( obox ) { obox . classList . toggle ( ' side-buy ' , side == = ' BUY ' ) ; obox . classList . toggle ( ' side-sell ' , side == = ' SELL ' ) ; }
decorateAccounts ( ) ; / / 매수 / 매도에 따라 계좌 색 · 표기 전환
var sb = $ ( ' [data-order-submit] ' ) ; if ( sb ) { sb . setAttribute ( ' data-side ' , side ) ; sb . textContent = ( side == = ' BUY ' ? ' 매수 ' : ' 매도 ' ) ; }
var sb = $ ( ' [data-order-submit] ' ) ; if ( sb ) { sb . setAttribute ( ' data-side ' , side ) ; sb . textContent = ( side == = ' BUY ' ? ' 매수 ' : ' 매도 ' ) ; }
renderQtyButtons ( ) ;
renderQtyButtons ( ) ;
/ / 매수일 때만 금액 input 표시
/ / 매수일 때만 금액 input 표시
@@ -8234,6 +8246,7 @@ function renderBook(book){
}
}
highlightSelectedTick ( ) ;
highlightSelectedTick ( ) ;
updateMarketPhaseDisplay ( ) ;
updateMarketPhaseDisplay ( ) ;
decorateAccounts ( ) ; / / 현재가 갱신 → 매수 예수금 충분 여부 색 갱신
if ( ! state . bookCentered ) {
if ( ! state . bookCentered ) {
var div = ob . querySelector ( ' .ob-divider ' ) ;
var div = ob . querySelector ( ' .ob-divider ' ) ;
if ( div ) {
if ( div ) {
@@ -8284,12 +8297,11 @@ function renderOrderInfo(){
var orderType = ( ( $ ( ' [data-order-type] ' ) | | { } ) . value | | ' LIMIT ' ) ;
var orderType = ( ( $ ( ' [data-order-type] ' ) | | { } ) . value | | ' LIMIT ' ) ;
if ( state . side == = ' BUY ' ) {
if ( state . side == = ' BUY ' ) {
var lines = [
var lines = [
' 예수금 (D+2): <b>' + fmt ( d . d2_entra | | 0 ) + ' </b>원 ' ,
' 매수가능액 (D+2 예수금 ): <b>' + fmt ( d . d2_entra | | 0 ) + ' </b>원 '
' 매수가능액: <b> ' + fmt ( d . ord_alow_amt | | 0 ) + ' </b>원 '
] ;
] ;
if ( orderType == = ' MARKET ' & & d . upper_limit ) {
if ( orderType == = ' MARKET ' & & d . upper_limit ) {
var needed = ( d . upper_limit | | 0 ) * qty ;
var needed = ( d . upper_limit | | 0 ) * qty ;
var overCls = ( needed > ( d . ord_alow_amt | | 0 ) ) ? ' class= " qty-over " ' : ' ' ;
var overCls = ( needed > ( d . d2_entra | | 0 ) ) ? ' class= " qty-over " ' : ' ' ;
lines . push ( ' 필요증거금<span class= " muted small " > (상한가 ' + fmt ( d . upper_limit ) + ' 원 기준)</span>: <b ' + overCls + ' > ' + fmt ( needed ) + ' </b>원 ( ' + fmt ( qty ) + ' 주) ' ) ;
lines . push ( ' 필요증거금<span class= " muted small " > (상한가 ' + fmt ( d . upper_limit ) + ' 원 기준)</span>: <b ' + overCls + ' > ' + fmt ( needed ) + ' </b>원 ( ' + fmt ( qty ) + ' 주) ' ) ;
} else {
} else {
lines . push ( ' 주문예상액: <b> ' + fmt ( amount ) + ' </b>원 ( ' + fmt ( qty ) + ' 주) ' ) ;
lines . push ( ' 주문예상액: <b> ' + fmt ( amount ) + ' </b>원 ( ' + fmt ( qty ) + ' 주) ' ) ;
@@ -8487,12 +8499,16 @@ function populateSymbolSelect(currentCode, currentName){
var groups = { ' 보유 ' : [ ] , ' 관심 ' : [ ] , ' 감시 ' : [ ] , ' 현재 ' : [ ] } ;
var groups = { ' 보유 ' : [ ] , ' 관심 ' : [ ] , ' 감시 ' : [ ] , ' 현재 ' : [ ] } ;
opts . concat ( items ) . forEach ( function ( it ) { ( groups [ it . source ] | | groups [ ' 관심 ' ] ) . push ( it ) ; } ) ;
opts . concat ( items ) . forEach ( function ( it ) { ( groups [ it . source ] | | groups [ ' 관심 ' ] ) . push ( it ) ; } ) ;
var html = ' <option value= " " >종목 선택…</option> ' ;
var html = ' <option value= " " >종목 선택…</option> ' ;
/ / 종목코드 단위 통합 — 같은 종목이 여러 계좌 ( 본인 · 가희 ) 에 있어도 하나만 . 매매 계좌는 별도 select .
/ / 라벨은 종목명만 ( owner · 종목코드 제거 ) . 중복 시 우선순위 그룹 ( 현재 > 보유 > 관심 > 감시 ) 만 노출 .
var seen = { } ;
[ ' 현재 ' , ' 보유 ' , ' 관심 ' , ' 감시 ' ] . forEach ( function ( g ) {
[ ' 현재 ' , ' 보유 ' , ' 관심 ' , ' 감시 ' ] . forEach ( function ( g ) {
if ( ! groups [ g ] | | ! groups [ g ] . length ) return ;
if ( ! groups [ g ] | | ! groups [ g ] . length ) return ;
var rows = groups [ g ] . filter ( function ( it ) { if ( seen [ it . code ] ) return false ; seen [ it . code ] = 1 ; return true ; } ) ;
if ( ! rows . length ) return ;
html + = ' <optgroup label= " ' + g + ' " > ' ;
html + = ' <optgroup label= " ' + g + ' " > ' ;
groups [ g ] . forEach ( function ( it ) {
rows . forEach ( function ( it ) {
var ownerTag = it . owner ? ' [ ' + it . owner + ' ] ' : ' ' ;
var label = ( it . name | | it . code ) ;
var label = ( it . name | | it . code ) + ownerTag + ' ( ' + it . code + ' ) ' ;
var selAttr = ( it . code == = currentCode ) ? ' selected ' : ' ' ;
var selAttr = ( it . code == = currentCode ) ? ' selected ' : ' ' ;
html + = ' <option value= " ' + it . code + ' " data-name= " ' + ( it . name | | ' ' ) . replace ( / " /g, ' " ' )+ ' " ' +selAttr+ ' > ' +label+ ' < / option > ' ;
html + = ' <option value= " ' + it . code + ' " data-name= " ' + ( it . name | | ' ' ) . replace ( / " /g, ' " ' )+ ' " ' +selAttr+ ' > ' +label+ ' < / option > ' ;
} ) ;
} ) ;
@@ -8532,6 +8548,9 @@ function onSymbolChange(){
/ / 종목 변경 시 새 max로 자동 채움 ( SELL : 보유 100 % , BUY : 매수가능 max )
/ / 종목 변경 시 새 max로 자동 채움 ( SELL : 보유 100 % , BUY : 매수가능 max )
if ( state . side == = ' SELL ' ) state . pendingMaxOnSell = true ;
if ( state . side == = ' SELL ' ) state . pendingMaxOnSell = true ;
else state . pendingMaxOnBuy = true ;
else state . pendingMaxOnBuy = true ;
state . accStatus = null ;
decorateAccounts ( ) ;
fetchAccountStatus ( ) ;
pollOnce ( ) ;
pollOnce ( ) ;
updateCheck ( ) ;
updateCheck ( ) ;
}
}
@@ -8542,6 +8561,38 @@ function fillAccounts(allowed){
if ( allowed & & allowed . length ) { list = ACCOUNTS . filter ( function ( a ) { return allowed . indexOf ( a . label ) > = 0 ; } ) ; }
if ( allowed & & allowed . length ) { list = ACCOUNTS . filter ( function ( a ) { return allowed . indexOf ( a . label ) > = 0 ; } ) ; }
list . forEach ( function ( a ) { var opt = document . createElement ( ' option ' ) ; opt . value = a . label ; opt . textContent = a . display ; sel . appendChild ( opt ) ; } ) ;
list . forEach ( function ( a ) { var opt = document . createElement ( ' option ' ) ; opt . value = a . label ; opt . textContent = a . display ; sel . appendChild ( opt ) ; } ) ;
}
}
/ / 계좌 옵션에 색 + 텍스트 병행 표시 . 매수 : 예수금 < 현재가 ( 1 주 ) 면 붉은색 · ' ⚠ 부족 ' . 매도 : 매도가능 주수 표기 , 0 이면 붉은색 .
/ / iOS Safari는 option 글자색 무시 → 텍스트 ( 주수 · 부족 ) 가 폰 fallback .
var ACC_DISPLAY = { } ; ACCOUNTS . forEach ( function ( a ) { ACC_DISPLAY [ a . label ] = a . display ; } ) ;
function decorateAccounts ( ) {
var sel = $ ( ' [data-order-account] ' ) ; if ( ! sel ) return ;
var st = state . accStatus ;
var price = ( state . lastBook & & state . lastBook . price ) | | ( st & & st . cur_price ) | | 0 ;
Array . prototype . forEach . call ( sel . options , function ( opt ) {
var base = ACC_DISPLAY [ opt . value ] | | opt . value ;
if ( ! st | | ! st . byLabel | | ! st . byLabel [ opt . value ] ) { opt . textContent = base ; opt . style . color = ' ' ; return ; }
var a = st . byLabel [ opt . value ] ;
if ( state . side == = ' SELL ' ) {
var q = a . trde_able_qty | | 0 ; / / 매도가능 수량 ( 미체결 매도로 묶인 분 제외 )
opt . textContent = base + ( q > 0 ? ' · ' + fmt ( q ) + ' 주 ' : ' · 미보유 ' ) ;
opt . style . color = q > 0 ? ' #f0f0f0 ' : ' #ff6b75 ' ;
} else {
var ok = price > 0 ? ( a . d2_entra > = price ) : ( a . d2_entra > 0 ) ;
opt . textContent = base + ( ok ? ' ' : ' · ⚠ 부족 ' ) ;
opt . style . color = ok ? ' #f0f0f0 ' : ' #ff6b75 ' ;
}
} ) ;
}
function fetchAccountStatus ( ) {
if ( ! state . code ) { state . accStatus = null ; decorateAccounts ( ) ; return ; }
var code = state . code ;
fetch ( ' /api/order/account_status?code= ' + encodeURIComponent ( code ) ) . then ( function ( r ) { return r . json ( ) ; } ) . then ( function ( d ) {
if ( state . code != = code ) return ; / / 그새 종목 바뀌면 무시
var byLabel = { } ; ( d . accounts | | [ ] ) . forEach ( function ( a ) { byLabel [ a . label ] = a ; } ) ;
state . accStatus = { byLabel : byLabel , cur_price : d . cur_price | | 0 } ;
decorateAccounts ( ) ;
} ) . catch ( function ( ) { } ) ;
}
function reset ( ) {
function reset ( ) {
setMsg ( ' ' ) ;
setMsg ( ' ' ) ;
$ ( ' [data-order-type] ' ) . value = ' LIMIT ' ;
$ ( ' [data-order-type] ' ) . value = ' LIMIT ' ;
@@ -8593,6 +8644,8 @@ function openModal(opts){
fillAccounts ( opts . accounts ) ;
fillAccounts ( opts . accounts ) ;
if ( opts . account ) { var sel = $ ( ' [data-order-account] ' ) ; if ( sel ) sel . value = opts . account ; }
if ( opts . account ) { var sel = $ ( ' [data-order-account] ' ) ; if ( sel ) sel . value = opts . account ; }
populateSymbolSelect ( state . code , state . name ) ;
populateSymbolSelect ( state . code , state . name ) ;
state . accStatus = null ;
fetchAccountStatus ( ) ;
setSide ( opts . side == = ' SELL ' ? ' SELL ' : ' BUY ' ) ;
setSide ( opts . side == = ' SELL ' ? ' SELL ' : ' BUY ' ) ;
if ( opts . price ) { $ ( ' [data-order-price-input] ' ) . value = opts . price ; }
if ( opts . price ) { $ ( ' [data-order-price-input] ' ) . value = opts . price ; }
modal . classList . remove ( ' hidden ' ) ;
modal . classList . remove ( ' hidden ' ) ;
@@ -8676,7 +8729,7 @@ function doPropose(){
var basis = ( orderType == = ' MARKET ' & & lc . upper_limit ) ? ' 상한가 ' : ' 단가 ' ;
var basis = ( orderType == = ' MARKET ' & & lc . upper_limit ) ? ' 상한가 ' : ' 단가 ' ;
var ref = ( orderType == = ' MARKET ' & & lc . upper_limit ) ? lc . upper_limit : price ;
var ref = ( orderType == = ' MARKET ' & & lc . upper_limit ) ? lc . upper_limit : price ;
var needed = ref * qty ;
var needed = ref * qty ;
setMsg ( ' 예수금 부족 — 필요 ' + needed . toLocaleString ( ) + ' 원 ( ' + basis + ' ' + ( ref | | 0 ) . toLocaleString ( ) + ' 원 × ' + qty + ' 주) / 가용 ' + ( lc . ord_alow_amt | | 0 ) . toLocaleString ( ) + ' 원 \n 최대 ' + lc . max_qty + ' 주까지 매수 가능 ' , ' error ' ) ;
setMsg ( ' 예수금 부족 — 필요 ' + needed . toLocaleString ( ) + ' 원 ( ' + basis + ' ' + ( ref | | 0 ) . toLocaleString ( ) + ' 원 × ' + qty + ' 주) / 가용 ' + ( lc . d2_entra | | 0 ) . toLocaleString ( ) + ' 원 \n 최대 ' + lc . max_qty + ' 주까지 매수 가능 ' , ' error ' ) ;
} else {
} else {
setMsg ( ' 보유 부족 — 매도 ' + qty + ' 주 / 매도가능 ' + lc . max_qty + ' 주 ' , ' error ' ) ;
setMsg ( ' 보유 부족 — 매도 ' + qty + ' 주 / 매도가능 ' + lc . max_qty + ' 주 ' , ' error ' ) ;
}
}
@@ -8826,7 +8879,8 @@ document.addEventListener('click', function(e){
if ( subOrderBtn ) {
if ( subOrderBtn ) {
function openWithFirst ( items ) {
function openWithFirst ( items ) {
var first = ( items | | [ ] ) . find ( function ( it ) { return it . source == = ' 보유 ' & & it . owner == = ' 본인 ' ; } ) ;
var first = ( items | | [ ] ) . find ( function ( it ) { return it . source == = ' 보유 ' & & it . owner == = ' 본인 ' ; } ) ;
if ( first ) { openModal ( { code : first . code , name : first . name } ) ; }
/ / 보유종목을 자동선택하므로 행 거래버튼과 동일 규칙으로 매도탭 진입 . 보유분 없으면 빈 모달 ( 매수 기본 ) .
if ( first ) { openModal ( { code : first . code , name : first . name , side : ' SELL ' } ) ; }
else { openModal ( { code : ' ' , name : ' ' } ) ; }
else { openModal ( { code : ' ' , name : ' ' } ) ; }
}
}
if ( state . symbolsCache ) { openWithFirst ( state . symbolsCache ) ; }
if ( state . symbolsCache ) { openWithFirst ( state . symbolsCache ) ; }
@@ -9405,9 +9459,12 @@ class Handler(BaseHTTPRequestHandler):
' order_type ' : order_type , ' price ' : price }
' order_type ' : order_type , ' price ' : price }
if side == ' BUY ' :
if side == ' BUY ' :
bal = kc . get_balance ( account )
bal = kc . get_balance ( account )
ord_alow = int ( bal . get ( ' ord_alow_amt ' ) or 0 )
# 매수가능 기준은 d2_entra(D+2 정산예수금) — propose 가드(validate_balance_for_buy)와
result [ ' ord_alow_amt ' ] = ord_alow
# 동일. kt00001 ord_alow_amt는 오늘 즉시현금(entr)만 잡혀 매도결제분(D+2)을 빠뜨려
result [ ' d2_entra ' ] = int ( bal . get ( ' d2_entra ' ) or 0 )
# max_qty가 과소 계산되고 "예수금 부족" 오탐을 냈다. ord_alow_amt는 참고 표시용만 유지.
d2 = int ( bal . get ( ' d2_entra ' ) or 0 )
result [ ' ord_alow_amt ' ] = int ( bal . get ( ' ord_alow_amt ' ) or 0 )
result [ ' d2_entra ' ] = d2
if order_type == ' MARKET ' :
if order_type == ' MARKET ' :
# 키움 시장가 매수 증거금 = 상한가 × qty. 같은 기준으로 max_qty 표시.
# 키움 시장가 매수 증거금 = 상한가 × qty. 같은 기준으로 max_qty 표시.
try :
try :
@@ -9417,10 +9474,9 @@ class Handler(BaseHTTPRequestHandler):
except Exception :
except Exception :
upper = 0
upper = 0
result [ ' upper_limit ' ] = upper
result [ ' upper_limit ' ] = upper
result [ ' max_qty ' ] = ( ord_alow / / upper ) if upper > 0 else 0
result [ ' max_qty ' ] = ( d2 / / upper ) if upper > 0 else 0
else :
else :
# 키움 ord_alow_amt는 수수료 버퍼 이미 반영된 매수가능액. price>0 일 때만 max_qty.
result [ ' max_qty ' ] = ( d2 / / price ) if price > 0 else 0
result [ ' max_qty ' ] = ( ord_alow / / price ) if price > 0 else 0
else : # SELL
else : # SELL
positions = kc . get_positions ( account )
positions = kc . get_positions ( account )
pos = next ( ( p for p in positions if p [ ' code ' ] == code ) , None )
pos = next ( ( p for p in positions if p [ ' code ' ] == code ) , None )
@@ -9442,6 +9498,38 @@ class Handler(BaseHTTPRequestHandler):
return
return
self . _send_json ( 200 , result )
self . _send_json ( 200 , result )
return
return
if parsed . path == ' /api/order/account_status ' :
# 계좌 select 색/보유주수 표시용 — 한 종목에 대해 4계좌 d2_entra·보유주수 일괄 반환.
qs = parse_qs ( parsed . query )
code = ' ' . join ( ch for ch in ( qs . get ( ' code ' ) or [ ' ' ] ) [ 0 ] . strip ( ) if ch . isalnum ( ) )
if not code :
self . _send_json ( 400 , { ' error ' : ' code required ' } )
return
try :
import kiwoom_client as kc
labels = [ a [ ' label ' ] for a in kc . list_accounts ( ) ]
with ThreadPoolExecutor ( max_workers = len ( labels ) + 2 ) as ex :
pos_f = ex . submit ( kc . get_positions_all , exchange = ' NXT ' , labels = labels )
bal_fs = { lb : ex . submit ( kc . get_balance , lb ) for lb in labels }
pos_by = pos_f . result ( )
bals = { lb : bal_fs [ lb ] . result ( ) for lb in labels }
cur_price = 0
accounts = [ ]
for lb in labels :
p = next ( ( x for x in ( pos_by . get ( lb , [ ] ) or [ ] ) if x [ ' code ' ] == code ) , None )
if p and p . get ( ' cur_price ' ) and not cur_price :
cur_price = p [ ' cur_price ' ]
accounts . append ( {
' label ' : lb ,
' d2_entra ' : int ( bals [ lb ] . get ( ' d2_entra ' ) or 0 ) ,
' hold_qty ' : p [ ' qty ' ] if p else 0 ,
' trde_able_qty ' : p [ ' trde_able_qty ' ] if p else 0 ,
} )
self . _send_json ( 200 , { ' code ' : code , ' cur_price ' : cur_price , ' accounts ' : accounts } )
except Exception as e :
traceback . print_exc ( )
self . _send_json ( 500 , { ' error ' : f ' account_status failed: { e } ' } )
return
if parsed . path == ' /api/chart_svg ' :
if parsed . path == ' /api/chart_svg ' :
qs = parse_qs ( parsed . query )
qs = parse_qs ( parsed . query )
code = ' ' . join ( ch for ch in ( qs . get ( ' code ' ) or [ ' ' ] ) [ 0 ] . strip ( ) if ch . isalnum ( ) )
code = ' ' . join ( ch for ch in ( qs . get ( ' code ' ) or [ ' ' ] ) [ 0 ] . strip ( ) if ch . isalnum ( ) )