auto: 일일 백업 2026-08-28 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -12570,7 +12570,8 @@ function showProgressTab(){
|
||||
});
|
||||
return;
|
||||
}
|
||||
if((d.open_orders_count || 0) > 0){
|
||||
// 최근 종료분만 있어도 연다 — 재등록 목록에 닿을 유일한 경로다.
|
||||
if((d.open_orders_count || 0) > 0 || (d.recent_count || 0) > 0){
|
||||
openOpenOrdersModal();
|
||||
return;
|
||||
}
|
||||
@@ -12640,10 +12641,16 @@ function loadOpenOrders(){
|
||||
list.innerHTML = '<div class="loading" style="padding:24px 0;"><div class="loading-spin"></div>불러오는 중…</div>';
|
||||
fetch('/api/orders/open').then(function(r){ return r.json(); }).then(function(d){
|
||||
var rows = d.rows || [];
|
||||
if(!rows.length){
|
||||
// 최근 종료분은 캐시해 둔다 — 재등록 버튼이 인덱스로 이 배열을 되짚는다.
|
||||
state.recentEnded = d.recent || [];
|
||||
if(!rows.length && !state.recentEnded.length){
|
||||
list.innerHTML = '<div class="empty">미체결 주문 없음</div>';
|
||||
return;
|
||||
}
|
||||
if(!rows.length){
|
||||
list.innerHTML = '<div class="empty">미체결 주문 없음</div>' + recentSectionHtml();
|
||||
return;
|
||||
}
|
||||
// 대기 예약을 맨 위로 — 아직 주문이 아니니 미체결과 섞이면 성격이 흐려진다.
|
||||
rows.sort(function(a, b){ return (b.kind === 'arm') - (a.kind === 'arm'); });
|
||||
list.innerHTML = rows.map(function(r){
|
||||
@@ -12723,11 +12730,62 @@ function loadOpenOrders(){
|
||||
'</div>' +
|
||||
'</div>'
|
||||
);
|
||||
}).join('');
|
||||
}).join('') + recentSectionHtml();
|
||||
}).catch(function(e){
|
||||
list.innerHTML = '<div class="empty">조회 실패: ' + e + '</div>';
|
||||
});
|
||||
}
|
||||
// 최근 종료(재등록 후보) 섹션 — 비어 있으면 아예 그리지 않는다.
|
||||
function recentSectionHtml(){
|
||||
var list = state.recentEnded || [];
|
||||
if(!list.length) return '';
|
||||
var rows = list.map(function(e, i){
|
||||
var isArm = (e.kind === 'arm');
|
||||
var badge = isArm ? '예약 만료' : '장마감 소멸';
|
||||
var bits = [];
|
||||
if(isArm && e.trigger_price) bits.push('발동가 ' + fmt(e.trigger_price) + '원');
|
||||
(e.steps || []).forEach(function(s){
|
||||
bits.push('−' + s.pct + '% ' + (s.cum >= 100 ? '전량' : '누적 ' + s.cum + '%'));
|
||||
});
|
||||
if(e.min_sell_price) bits.push('최저 ' + fmt(e.min_sell_price) + '원');
|
||||
var when = String(e.ended_at || '').slice(5, 16).replace('T', ' ');
|
||||
return (
|
||||
'<div class="open-order-row" data-recent-idx="' + i + '">' +
|
||||
'<div class="row-info">' +
|
||||
'<div class="row-line1">' +
|
||||
'<span class="badge-side sell">' + badge + '</span>' +
|
||||
'<span>' + (e.symbol_name || e.symbol) + '</span>' +
|
||||
'<span style="color:#8b8f9a; font-weight:400; font-size:11px;">(' + e.symbol + ')</span>' +
|
||||
'</div>' +
|
||||
'<div class="row-line2">[' + e.account + '] ' + fmt(e.qty) + '주 · ' + when + ' 종료</div>' +
|
||||
'<div class="row-line2" style="color:#d9a84a; overflow-wrap:anywhere;">' +
|
||||
bits.join(' · ') + '</div>' +
|
||||
'</div>' +
|
||||
'<div class="row-action">' +
|
||||
'<button type="button" data-rearm>↺ 재등록</button>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
);
|
||||
}).join('');
|
||||
return '<div class="row-line2" style="margin:14px 0 6px; color:#8b8f9a;">' +
|
||||
'── 최근 종료 (재등록 가능) ──</div>' + rows;
|
||||
}
|
||||
// 재등록 — 이전 설정을 채운 거래 모달을 연다. 발주는 기존 매도 → 확인 → PIN 경로 그대로다.
|
||||
function rearmFromRecent(idx){
|
||||
var e = (state.recentEnded || [])[idx];
|
||||
if(!e){ showToast('재등록 정보를 찾을 수 없습니다', 'error'); return; }
|
||||
// open-orders(z 1010) 가 order-modal(z 1000) 보다 위라, 먼저 닫지 않으면 뒤에 깔린다.
|
||||
closeOpenOrdersModal();
|
||||
openModal({
|
||||
code: e.symbol, name: e.symbol_name || e.symbol, side: 'SELL', account: e.account,
|
||||
orderType: (e.kind === 'arm') ? 'TRAILING_ARM' : 'TRAILING_STOP',
|
||||
qty: e.qty,
|
||||
armPrice: (e.kind === 'arm') ? e.trigger_price : 0,
|
||||
trailMinPrice: e.min_sell_price || 0,
|
||||
trailSteps: e.steps || []
|
||||
});
|
||||
showToast('이전 설정을 불러왔습니다 — 확인 후 매도를 눌러주세요', 'info', 4000);
|
||||
}
|
||||
function cancelOpenOrder(ordNo, account, btn){
|
||||
if(!confirm('주문 #' + ordNo + ' 취소하시겠습니까?')) return;
|
||||
if(btn){ btn.disabled = true; btn.textContent = '취소중…'; }
|
||||
@@ -13050,6 +13108,33 @@ function openModal(opts){
|
||||
fetchAccountStatus();
|
||||
setSide(opts.side === 'SELL' ? 'SELL' : 'BUY');
|
||||
if(opts.price){ $('[data-order-price-input]').value = opts.price; }
|
||||
// ── 재등록 프리필 (2026-08-27) ──
|
||||
// ⚠️ 순서가 전부다. setSide 뒤라야 하고, 아래 세 줄의 차례를 바꾸면 조용히 망가진다:
|
||||
// · orderType 은 side=SELL 확정 뒤에 — refreshStopUI 가 BUY 면 TRAILING_* 를 LIMIT 로 되돌린다
|
||||
// · 계단은 refreshStopUI 뒤에 — 프리셋 select 가 비어 있으면 계단을 프리셋 0번으로 덮는다
|
||||
// · 수량은 pendingMax 플래그를 꺼야 한다 — 안 그러면 async /api/order/check 가 max_qty 로 덮는다
|
||||
if(opts.orderType){
|
||||
var otPre = $('[data-order-type]');
|
||||
if(otPre){ otPre.value = opts.orderType; refreshStopUI(); }
|
||||
}
|
||||
if(opts.orderType){
|
||||
// ⚠️ 항상 대입한다(0이면 빈 문자열). reset() 이 이 두 칸을 안 지워서, 건너뛰면 이전
|
||||
// 종목·이전 세션의 절대가격이 그대로 남는다.
|
||||
var apEl = $('[data-order-arm-price]');
|
||||
if(apEl) apEl.value = (opts.armPrice > 0) ? opts.armPrice : '';
|
||||
var mnEl = $('[data-order-trail-min]');
|
||||
if(mnEl) mnEl.value = (opts.trailMinPrice > 0) ? opts.trailMinPrice : '';
|
||||
}
|
||||
if(opts.qty){
|
||||
var qEl = $('[data-order-qty]');
|
||||
if(qEl) qEl.value = opts.qty;
|
||||
state.pendingMaxOnSell = false;
|
||||
state.pendingMaxOnBuy = false;
|
||||
}
|
||||
if(opts.trailSteps && opts.trailSteps.length){
|
||||
renderTrailStepRows(opts.trailSteps);
|
||||
renderTrailPreview(); // 내부 첫 줄이 refreshSubmitGate — 별도 호출 불필요
|
||||
}
|
||||
modal.classList.remove('hidden');
|
||||
modal.setAttribute('aria-hidden', 'false');
|
||||
document.body.classList.add('modal-open');
|
||||
@@ -13465,6 +13550,13 @@ if(openOrdersModal) openOrdersModal.addEventListener('click', function(e){
|
||||
cancelArm(arow.getAttribute('data-arm-id'), armBtn);
|
||||
return;
|
||||
}
|
||||
var reBtn = t.closest && t.closest('[data-rearm]');
|
||||
if(reBtn){
|
||||
var rrow = reBtn.closest('.open-order-row');
|
||||
if(!rrow) return;
|
||||
rearmFromRecent(parseInt(rrow.getAttribute('data-recent-idx'), 10));
|
||||
return;
|
||||
}
|
||||
});
|
||||
// PIN 모달 click 핸들러
|
||||
if(pinModal) pinModal.addEventListener('click', function(e){
|
||||
@@ -14465,7 +14557,16 @@ class Handler(BaseHTTPRequestHandler):
|
||||
# 대기 예약은 키움에 주문이 없어 ka10075 에 안 잡힌다 → 합성 행으로 합류시킨다.
|
||||
# 안 보이는 자동 발주 예약은 그 자체로 사고 요인이고, 이 팝업이 유일한 창구다.
|
||||
rows.extend(_arm_rows())
|
||||
self._send_json(200, {'rows': rows, 'count': len(rows)})
|
||||
# 최근 종료분(재등록 후보). 상태파일 읽기라 키움 콜 0.
|
||||
# ⚠️ rows 에 섞지 않는다 — '진행중'이 아니라 '지난 일'이고, 배지 카운트에도
|
||||
# 들어가면 안 된다(처리할 게 남은 것처럼 보인다).
|
||||
try:
|
||||
from orders import trailing as _trl
|
||||
recent = _trl.list_recent()
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
recent = []
|
||||
self._send_json(200, {'rows': rows, 'count': len(rows), 'recent': recent})
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
self._send_json(500, {'error': f'open orders failed: {e}'})
|
||||
@@ -14508,11 +14609,16 @@ class Handler(BaseHTTPRequestHandler):
|
||||
traceback.print_exc()
|
||||
# 대기 예약도 진행중 항목으로 센다 — 그래야 대기 예약만 있을 때도 배지가 뜨고
|
||||
# 버튼이 팝업을 연다(키움 미체결이 0건이면 '진행 중 거래 없음' 토스트로 끝난다).
|
||||
recent_count = 0
|
||||
try:
|
||||
from orders import trailing as _trl
|
||||
open_orders_count += len(_trl.list_arms())
|
||||
recent_count = len(_trl.list_recent())
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
# 최근 종료분은 **배지 숫자에 넣지 않는다**(처리할 게 남은 것처럼 보인다).
|
||||
# 다만 이것만 있어도 팝업은 열려야 한다 — 아니면 재등록 목록에 닿을 길이 없다.
|
||||
result['recent_count'] = recent_count
|
||||
result['open_orders_count'] = open_orders_count
|
||||
# 진행중 아이템 수 = 미체결 주문 건수(팝업 행과 동일) + 활성 PIN 카드(있으면 1).
|
||||
# 종목코드 중복제거 X — 같은 종목 2계좌 매도도 2건으로 표시(팝업과 일치).
|
||||
|
||||
Reference in New Issue
Block a user