(function () {
var list = document.getElementById('position-live-list');
var recommendList = document.getElementById('recommend-list');
var symInput = document.getElementById('trade-symbol');
var lotsInput = document.getElementById('trade-lots');
var priceInput = document.getElementById('trade-price');
var footer = document.getElementById('trade-footer');
var slInput = document.getElementById('trade-sl');
var tpInput = document.getElementById('trade-tp');
var pollTimer = null;
var recommendSource = null;
var quoteTimer = null;
function runWhenReady(fn) {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', fn);
} else {
fn();
}
}
function fmtNum(v, digits) {
if (v === null || v === undefined) return '--';
return Number(v).toFixed(digits === undefined ? 2 : digits);
}
function selectedSymbol() {
return (symInput && symInput.value || '').trim();
}
function refreshQuote() {
var sym = selectedSymbol();
var lots = lotsInput ? lotsInput.value : '1';
if (!sym) return;
fetch('/api/trade/quote?symbol=' + encodeURIComponent(sym) + '&lots=' + encodeURIComponent(lots))
.then(function (r) { return r.json(); })
.then(function (data) {
if (!data.ok) return;
if (priceInput && !priceInput.dataset.manual && data.price) {
priceInput.value = data.price;
}
var px = data.price != null ? data.price : '—';
['px-long', 'px-short'].forEach(function (id) {
var el = document.getElementById(id);
if (el) el.textContent = px;
});
var pl = document.getElementById('pos-long');
var ps = document.getElementById('pos-short');
if (pl) pl.textContent = '≤' + (data.pos_long || 0);
if (ps) ps.textContent = '≤' + (data.pos_short || 0);
if (footer && data.metrics) {
var m = data.metrics;
var hint = footer.querySelector('.hint');
var extra =
'
' + (data.name || sym) + ' 精度 ' + m.price_precision +
' 位 · 每跳 ' + m.tick_value_total + ' 元(' + lots + ' 手)
';
if (hint) {
hint.insertAdjacentHTML('afterend', extra);
var olds = footer.querySelectorAll('p:not(.hint):not(.text-loss)');
for (var i = 0; i < olds.length - 1; i++) olds[i].remove();
}
}
}).catch(function () {});
}
function scheduleQuote() {
clearTimeout(quoteTimer);
quoteTimer = setTimeout(refreshQuote, 400);
}
function postOrder(offset, direction) {
var sym = selectedSymbol();
if (!sym) { alert('请选择品种'); return; }
var body = {
symbol: sym,
offset: offset,
direction: direction,
lots: parseInt(lotsInput.value, 10) || 1,
price: parseFloat(priceInput.value) || 0,
stop_loss: slInput ? parseFloat(slInput.value) : null,
take_profit: tpInput ? parseFloat(tpInput.value) : null
};
fetch('/api/trade/order', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
}).then(function (r) { return r.json(); }).then(function (data) {
if (!data.ok) { alert(data.error || '下单失败'); return; }
alert('已提交 ' + (data.lots || '') + ' 手');
pollPositions();
refreshQuote();
});
}
function buildPosCard(row) {
var pnlClass = row.float_pnl > 0 ? 'pnl-pos' : (row.float_pnl < 0 ? 'pnl-neg' : '');
var pnlText = '--';
if (row.float_pnl != null) {
var sign = row.float_pnl >= 0 ? '+' : '';
pnlText = sign + fmtNum(row.float_pnl) + '元';
if (row.float_pct != null) pnlText += ' (' + sign + fmtNum(row.float_pct) + '%)';
}
var rr = row.rr_ratio != null ? row.rr_ratio + ':1' : '--';
var openT = (row.open_time || '').replace('T', ' ').slice(0, 16);
var dirBadge = row.direction_label || (row.direction === 'long' ? '做多' : '做空');
var closeBtn = '';
if (row.close_url) {
closeBtn = '';
} else if (row.can_close) {
closeBtn = '';
}
return (
'' +
'
' + row.symbol + ' ' + dirBadge + '
' + closeBtn + '
' +
'
来源 ' + (row.source_label || row.source) + '
' +
'
' +
'
' + fmtNum(row.entry_price) + '
' +
'
' + (row.stop_loss != null ? fmtNum(row.stop_loss) : '--') + '
' +
'
' + (row.take_profit != null ? fmtNum(row.take_profit) : '--') + '
' +
'
' +
'
'
);
}
function closePosition(payload) {
var price = payload.mark_price;
if (!price || price <= 0) { alert('无法获取现价'); return; }
if (!confirm('确认以 ' + price + ' 限价平仓 ' + payload.lots + ' 手?')) return;
fetch('/api/trading/close', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
}).then(function (r) { return r.json(); }).then(function (d) {
if (!d.ok) { alert(d.error || '平仓失败'); return; }
pollPositions();
});
}
function pollPositions() {
if (!list) return;
fetch('/api/trading/live')
.then(function (r) {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
})
.then(function (data) {
var cap = document.getElementById('cap-display');
if (cap && data.capital != null) cap.textContent = Number(data.capital).toFixed(2);
var recCap = document.getElementById('rec-capital');
if (recCap && data.capital != null) recCap.textContent = Number(data.capital).toFixed(2);
var riskBadge = document.getElementById('risk-badge');
if (riskBadge && data.risk_status) {
riskBadge.textContent = data.risk_status.status_label;
riskBadge.className = 'badge ' + (data.risk_status.can_trade ? 'profit' : 'loss');
}
var ctpBadge = document.getElementById('ctp-badge');
if (ctpBadge && data.ctp_status) {
ctpBadge.textContent = data.ctp_status.connected ? 'CTP 已连接' : 'CTP 未连接';
ctpBadge.className = 'badge ' + (data.ctp_status.connected ? 'profit' : 'planned');
}
var rows = data.rows || [];
if (!rows.length) {
list.innerHTML = '暂无持仓。可在左侧下单,或通过策略交易开仓。
';
return;
}
list.innerHTML = rows.map(buildPosCard).join('');
list.querySelectorAll('[data-close]').forEach(function (btn) {
btn.addEventListener('click', function () {
closePosition(JSON.parse(btn.getAttribute('data-close')));
});
});
})
.catch(function () {
if (list.innerHTML.indexOf('pos-card') < 0) {
list.innerHTML = '持仓加载失败
';
}
});
}
function badgeClass(status) {
if (status === 'ok') return 'profit';
return 'planned';
}
function renderRecommendations(data) {
if (!recommendList || !data) return;
var recCap = document.getElementById('rec-capital');
if (recCap && data.capital != null) recCap.textContent = Number(data.capital).toFixed(2);
var recUpd = document.getElementById('rec-updated');
if (recUpd && data.updated_at) recUpd.textContent = '更新 ' + data.updated_at;
var rows = data.rows || [];
if (!rows.length) {
recommendList.innerHTML = '| 当前资金下暂无推荐品种 |
';
return;
}
recommendList.innerHTML = rows.map(function (r) {
return (
'' +
'| ' + (r.name || '') + ' ' + (r.ths || '') + ' | ' +
'' + (r.exchange || '') + ' | ' +
'' + (r.price != null ? r.price : '—') + ' | ' +
'' + (r.margin_one_lot != null ? r.margin_one_lot : '—') + ' | ' +
'' + (r.min_capital_one_lot != null ? r.min_capital_one_lot : '—') + ' | ' +
'' + (r.status_label || '') + ' | ' +
'
'
);
}).join('');
}
function connectRecommendStream() {
if (recommendSource) {
recommendSource.close();
recommendSource = null;
}
recommendSource = new EventSource('/api/recommend/stream');
recommendSource.addEventListener('recommend', function (ev) {
try {
renderRecommendations(JSON.parse(ev.data));
} catch (e) { /* ignore */ }
});
recommendSource.onerror = function () {
if (recommendSource) {
recommendSource.close();
recommendSource = null;
}
setTimeout(connectRecommendStream, 5000);
};
}
if (symInput) symInput.addEventListener('input', scheduleQuote);
if (lotsInput) lotsInput.addEventListener('input', scheduleQuote);
if (priceInput) {
priceInput.addEventListener('input', function () { priceInput.dataset.manual = '1'; });
}
var btnLong = document.getElementById('btn-open-long');
var btnShort = document.getElementById('btn-open-short');
var btnCloseL = document.getElementById('btn-close-long');
var btnCloseS = document.getElementById('btn-close-short');
if (btnLong) btnLong.addEventListener('click', function () { postOrder('open', 'long'); });
if (btnShort) btnShort.addEventListener('click', function () { postOrder('open', 'short'); });
if (btnCloseL) btnCloseL.addEventListener('click', function () { postOrder('close', 'long'); });
if (btnCloseS) btnCloseS.addEventListener('click', function () { postOrder('close', 'short'); });
var btnConnect = document.getElementById('btn-ctp-connect');
if (btnConnect) {
btnConnect.addEventListener('click', function () {
btnConnect.disabled = true;
btnConnect.textContent = '连接中…';
fetch('/api/ctp/connect', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' })
.then(function (r) { return r.json(); })
.then(function (d) {
if (!d.ok) { alert(d.error || '连接失败'); return; }
location.reload();
})
.finally(function () {
btnConnect.disabled = false;
btnConnect.textContent = '连接 CTP';
});
});
}
runWhenReady(function () {
pollPositions();
connectRecommendStream();
pollTimer = setInterval(pollPositions, 3000);
scheduleQuote();
});
})();