接入 SimNow 模拟盘与期货下单、策略及品种推荐功能。
新增 vnpy CTP 桥接、以损定仓/固定张数、趋势回调与滚仓策略、按资金推荐品种及交易风控;模拟盘走 SimNow,实盘预留期货公司配置。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+101
-37
@@ -242,41 +242,59 @@
|
||||
|
||||
function getDataZoom(c, preserve) {
|
||||
var defStart = getDefaultZoomStart();
|
||||
var zoom = [
|
||||
{
|
||||
type: 'inside',
|
||||
xAxisIndex: [0, 1],
|
||||
start: defStart,
|
||||
end: 100,
|
||||
zoomOnMouseWheel: true,
|
||||
moveOnMouseMove: true,
|
||||
moveOnMouseWheel: false,
|
||||
var xZoom = {
|
||||
type: 'inside',
|
||||
id: 'dzInsideX',
|
||||
xAxisIndex: [0, 1],
|
||||
start: defStart,
|
||||
end: 100,
|
||||
filterMode: 'none',
|
||||
zoomOnMouseWheel: true,
|
||||
moveOnMouseMove: true,
|
||||
moveOnMouseWheel: false,
|
||||
preventDefaultMouseMove: true,
|
||||
minSpan: 2,
|
||||
};
|
||||
var yZoom = {
|
||||
type: 'inside',
|
||||
id: 'dzInsideY',
|
||||
yAxisIndex: [0],
|
||||
orient: 'vertical',
|
||||
filterMode: 'none',
|
||||
zoomOnMouseWheel: true,
|
||||
moveOnMouseMove: true,
|
||||
preventDefaultMouseMove: true,
|
||||
};
|
||||
var slider = {
|
||||
type: 'slider',
|
||||
id: 'dzSlider',
|
||||
xAxisIndex: [0, 1],
|
||||
start: defStart,
|
||||
end: 100,
|
||||
height: 22,
|
||||
bottom: 4,
|
||||
borderColor: c.grid,
|
||||
backgroundColor: c.bg,
|
||||
fillerColor: c.area,
|
||||
handleStyle: { color: c.sliderFill },
|
||||
dataBackground: {
|
||||
lineStyle: { color: c.grid, opacity: 0.35 },
|
||||
areaStyle: { color: c.area },
|
||||
},
|
||||
{
|
||||
type: 'slider',
|
||||
xAxisIndex: [0, 1],
|
||||
start: defStart,
|
||||
end: 100,
|
||||
height: 22,
|
||||
bottom: 4,
|
||||
borderColor: c.grid,
|
||||
backgroundColor: c.bg,
|
||||
fillerColor: c.area,
|
||||
handleStyle: { color: c.sliderFill },
|
||||
dataBackground: {
|
||||
lineStyle: { color: c.grid },
|
||||
areaStyle: { color: c.area },
|
||||
},
|
||||
textStyle: { color: c.text, fontSize: 10 },
|
||||
},
|
||||
];
|
||||
textStyle: { color: c.text, fontSize: 10 },
|
||||
filterMode: 'none',
|
||||
brushSelect: false,
|
||||
};
|
||||
var zoom = [xZoom, yZoom, slider];
|
||||
if (preserve && chart) {
|
||||
var opt = chart.getOption();
|
||||
if (opt && opt.dataZoom) {
|
||||
opt.dataZoom.forEach(function (z, i) {
|
||||
if (zoom[i] && z.start != null && z.end != null) {
|
||||
zoom[i].start = z.start;
|
||||
zoom[i].end = z.end;
|
||||
opt.dataZoom.forEach(function (z) {
|
||||
if (!z.id) return;
|
||||
var target = zoom.find(function (t) { return t.id === z.id; });
|
||||
if (target && z.start != null && z.end != null) {
|
||||
target.start = z.start;
|
||||
target.end = z.end;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -284,6 +302,11 @@
|
||||
return zoom;
|
||||
}
|
||||
|
||||
function isFollowingLatest() {
|
||||
var z = getZoomRange();
|
||||
return z.end >= 98;
|
||||
}
|
||||
|
||||
function mapSeriesData(bars, values, gapDay) {
|
||||
if (!gapDay) return values;
|
||||
return bars.map(function (b, i) {
|
||||
@@ -303,6 +326,7 @@
|
||||
var times = bars.map(function (b) { return b.time; });
|
||||
var isLine = data.chart_type === 'line' || data.period === 'timeshare';
|
||||
var gapDay = chartOpts.gapDay;
|
||||
var followLatest = preserveZoom && isFollowingLatest();
|
||||
var dataZoom = getDataZoom(c, preserveZoom);
|
||||
var zoom = preserveZoom ? getZoomRange() : { start: dataZoom[0].start, end: dataZoom[0].end };
|
||||
var vIdx = visibleIndices(bars, zoom);
|
||||
@@ -326,6 +350,7 @@
|
||||
boundaryGap: gapDay ? false : true,
|
||||
axisLabel: { color: c.text, fontSize: 10 },
|
||||
axisLine: { lineStyle: { color: c.grid } },
|
||||
splitLine: { show: false },
|
||||
};
|
||||
var xAxis1 = {
|
||||
type: xAxisType,
|
||||
@@ -333,6 +358,7 @@
|
||||
boundaryGap: gapDay ? false : true,
|
||||
axisLabel: { show: false },
|
||||
axisLine: { lineStyle: { color: c.grid } },
|
||||
splitLine: { show: false },
|
||||
};
|
||||
if (!gapDay) {
|
||||
xAxis0.data = times;
|
||||
@@ -344,14 +370,27 @@
|
||||
animation: false,
|
||||
tooltip: { trigger: 'axis', axisPointer: { type: 'cross' } },
|
||||
axisPointer: { link: [{ xAxisIndex: 'all' }] },
|
||||
dataZoom: dataZoom,
|
||||
grid: grids,
|
||||
xAxis: [xAxis0, xAxis1],
|
||||
yAxis: [
|
||||
{ scale: true, gridIndex: 0, splitLine: { lineStyle: { color: c.grid } }, axisLabel: { color: c.text } },
|
||||
{ scale: true, gridIndex: 1, splitLine: { show: false }, axisLabel: { color: c.text, fontSize: 10 }, splitNumber: 2 },
|
||||
{
|
||||
scale: true,
|
||||
gridIndex: 0,
|
||||
splitLine: { show: false },
|
||||
axisLabel: { color: c.text },
|
||||
},
|
||||
{
|
||||
scale: true,
|
||||
gridIndex: 1,
|
||||
splitLine: { show: false },
|
||||
axisLabel: { color: c.text, fontSize: 10 },
|
||||
splitNumber: 2,
|
||||
},
|
||||
],
|
||||
};
|
||||
if (!preserveZoom) {
|
||||
base.dataZoom = dataZoom;
|
||||
}
|
||||
|
||||
var series = [];
|
||||
var mainMark = {
|
||||
@@ -465,7 +504,12 @@
|
||||
};
|
||||
}
|
||||
|
||||
chart.setOption(Object.assign(base, { series: series }), true);
|
||||
if (preserveZoom) {
|
||||
chart.setOption(Object.assign(base, { series: series }), false);
|
||||
} else {
|
||||
chart.setOption(Object.assign(base, { series: series }), true);
|
||||
dataZoomBound = false;
|
||||
}
|
||||
|
||||
var title = (data.chart_symbol || data.symbol || '') + ' · ' + periodLabel(data.period);
|
||||
chart.setOption({
|
||||
@@ -478,6 +522,22 @@
|
||||
} : { show: false },
|
||||
});
|
||||
|
||||
if (followLatest) {
|
||||
var span = zoom.end - zoom.start;
|
||||
chart.dispatchAction({
|
||||
type: 'dataZoom',
|
||||
dataZoomIndex: 0,
|
||||
start: Math.max(0, 100 - span),
|
||||
end: 100,
|
||||
});
|
||||
chart.dispatchAction({
|
||||
type: 'dataZoom',
|
||||
dataZoomIndex: 2,
|
||||
start: Math.max(0, 100 - span),
|
||||
end: 100,
|
||||
});
|
||||
}
|
||||
|
||||
bindDataZoomHL();
|
||||
}
|
||||
|
||||
@@ -656,12 +716,16 @@
|
||||
if (start === 0) end = newSpan;
|
||||
else start = end - newSpan;
|
||||
}
|
||||
chart.dispatchAction({ type: 'dataZoom', start: start, end: end });
|
||||
chart.dispatchAction({ type: 'dataZoom', dataZoomIndex: 0, start: start, end: end });
|
||||
chart.dispatchAction({ type: 'dataZoom', dataZoomIndex: 2, start: start, end: end });
|
||||
}
|
||||
|
||||
function resetDataZoom() {
|
||||
if (!chart) return;
|
||||
chart.dispatchAction({ type: 'dataZoom', start: getDefaultZoomStart(), end: 100 });
|
||||
var start = getDefaultZoomStart();
|
||||
chart.dispatchAction({ type: 'dataZoom', dataZoomIndex: 0, start: start, end: 100 });
|
||||
chart.dispatchAction({ type: 'dataZoom', dataZoomIndex: 2, start: start, end: 100 });
|
||||
chart.dispatchAction({ type: 'dataZoom', dataZoomIndex: 1, start: 0, end: 100 });
|
||||
}
|
||||
|
||||
function bindPeriodTabs() {
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
(function () {
|
||||
var trendPayload = null;
|
||||
|
||||
function jsonPost(url, body) {
|
||||
return fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body || {})
|
||||
}).then(function (r) { return r.json(); });
|
||||
}
|
||||
|
||||
function formData(form) {
|
||||
var fd = new FormData(form);
|
||||
var o = {};
|
||||
fd.forEach(function (v, k) { o[k] = v; });
|
||||
return o;
|
||||
}
|
||||
|
||||
var trendForm = document.getElementById('trend-form');
|
||||
var btnPreview = document.getElementById('btn-trend-preview');
|
||||
var btnExec = document.getElementById('btn-trend-exec');
|
||||
var previewEl = document.getElementById('trend-preview');
|
||||
|
||||
if (btnPreview && trendForm) {
|
||||
btnPreview.addEventListener('click', function () {
|
||||
jsonPost('/api/strategy/trend/preview', formData(trendForm)).then(function (d) {
|
||||
if (!d.ok) { previewEl.textContent = d.error || '预览失败'; btnExec.hidden = true; return; }
|
||||
trendPayload = formData(trendForm);
|
||||
previewEl.textContent = JSON.stringify(d.plan, null, 2);
|
||||
btnExec.hidden = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
if (btnExec) {
|
||||
btnExec.addEventListener('click', function () {
|
||||
if (!trendPayload) return;
|
||||
jsonPost('/api/strategy/trend/execute', trendPayload).then(function (d) {
|
||||
if (!d.ok) { alert(d.error); return; }
|
||||
location.reload();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var rollForm = document.getElementById('roll-form');
|
||||
var btnRollP = document.getElementById('btn-roll-preview');
|
||||
var btnRollE = document.getElementById('btn-roll-exec');
|
||||
var rollPrev = document.getElementById('roll-preview');
|
||||
if (btnRollP && rollForm) {
|
||||
btnRollP.addEventListener('click', function () {
|
||||
jsonPost('/api/strategy/roll/preview', formData(rollForm)).then(function (d) {
|
||||
if (!d.ok) { rollPrev.textContent = d.error; btnRollE.hidden = true; return; }
|
||||
rollPrev.textContent = JSON.stringify(d.preview, null, 2);
|
||||
btnRollE.hidden = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
if (btnRollE && rollForm) {
|
||||
btnRollE.addEventListener('click', function () {
|
||||
jsonPost('/api/strategy/roll/execute', formData(rollForm)).then(function (d) {
|
||||
if (!d.ok) { alert(d.error); return; }
|
||||
location.reload();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var btnStop = document.getElementById('btn-trend-stop');
|
||||
if (btnStop) {
|
||||
btnStop.addEventListener('click', function () {
|
||||
var pid = document.querySelector('#trend-stop-form input[name=plan_id]');
|
||||
jsonPost('/api/strategy/trend/stop', { plan_id: pid ? pid.value : 0 }).then(function (d) {
|
||||
if (!d.ok) { alert(d.error); return; }
|
||||
location.reload();
|
||||
});
|
||||
});
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,127 @@
|
||||
(function () {
|
||||
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 debounceTimer;
|
||||
|
||||
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 ml = document.getElementById('max-long');
|
||||
var ms = document.getElementById('max-short');
|
||||
if (ml) ml.textContent = '≤' + (data.max_open_long || '—');
|
||||
if (ms) ms.textContent = '≤' + (data.max_open_short || '—');
|
||||
document.getElementById('pos-long').textContent = '≤' + (data.pos_long || 0);
|
||||
document.getElementById('pos-short').textContent = '≤' + (data.pos_short || 0);
|
||||
if (footer && data.metrics) {
|
||||
var m = data.metrics;
|
||||
footer.innerHTML =
|
||||
'<p><strong>' + (data.name || sym) + '</strong> ' + (data.footer_text || '') + '</p>' +
|
||||
'<p>价格精度 <strong>' + m.price_precision + '</strong> 位 · ' +
|
||||
'最小变动 <strong>' + m.tick_size + '</strong> · ' +
|
||||
'每跳 <strong>' + m.tick_value_per_lot + '</strong> 元/手 · ' +
|
||||
'当前 <strong>' + lots + '</strong> 手每跳合计 <strong class="text-accent">' + m.tick_value_total + '</strong> 元</p>' +
|
||||
(m.margin_total ? '<p class="text-muted">预估保证金约 ' + m.margin_total + ' 元</p>' : '');
|
||||
}
|
||||
}).catch(function () {});
|
||||
}
|
||||
|
||||
function scheduleRefresh() {
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(refreshQuote, 400);
|
||||
}
|
||||
|
||||
if (symInput) symInput.addEventListener('input', scheduleRefresh);
|
||||
if (lotsInput) lotsInput.addEventListener('input', scheduleRefresh);
|
||||
if (priceInput) {
|
||||
priceInput.addEventListener('input', function () {
|
||||
priceInput.dataset.manual = '1';
|
||||
});
|
||||
}
|
||||
|
||||
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 || '') + ' 手');
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
|
||||
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';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setInterval(function () {
|
||||
fetch('/api/account_snapshot').then(function (r) { return r.json(); }).then(function (d) {
|
||||
var cap = document.getElementById('cap-display');
|
||||
if (cap && d.capital != null) cap.textContent = Number(d.capital).toFixed(2);
|
||||
var badge = document.getElementById('risk-badge');
|
||||
if (badge && d.risk_status) badge.textContent = d.risk_status.status_label;
|
||||
var ctpBadge = document.getElementById('ctp-badge');
|
||||
if (ctpBadge && d.ctp_status) {
|
||||
ctpBadge.textContent = d.ctp_status.connected ? 'CTP 已连接' : 'CTP 未连接';
|
||||
ctpBadge.className = 'badge ' + (d.ctp_status.connected ? 'profit' : 'planned');
|
||||
}
|
||||
}).catch(function () {});
|
||||
}, 5000);
|
||||
|
||||
scheduleRefresh();
|
||||
})();
|
||||
Reference in New Issue
Block a user