okx同步

This commit is contained in:
dekun
2026-05-19 15:10:43 +08:00
parent 5af1251687
commit 738cf0eccd
3 changed files with 1250 additions and 110 deletions
+189 -16
View File
@@ -103,6 +103,9 @@
.export-bar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:12px;font-size:.85rem}
.export-bar a{color:#8fc8ff;text-decoration:none;padding:6px 10px;border:1px solid #304164;border-radius:8px;background:#151a2a}
.export-bar a:hover{background:#1f2740}
.list-window-bar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:12px;padding:10px 12px;background:#151a2a;border:1px solid #304164;border-radius:10px;font-size:.82rem}
.list-window-bar label{color:#9aa;display:flex;align-items:center;gap:6px}
.stats-segment-block{margin-top:20px;padding-top:14px;border-top:1px solid #3a4468}
.key-history{margin-top:12px;padding-top:10px;border-top:1px solid #2a3150}
.key-history h3{font-size:.88rem;color:#b8c4ff;margin-bottom:6px}
.key-history .sub{font-size:.72rem;color:#8892b0;margin-bottom:6px}
@@ -145,6 +148,23 @@
</div>
{% with msg=get_flashed_messages() %}{% if msg %}<div class="flash">{{ msg[0] }}</div>{% endif %}{% endwith %}
<div class="list-window-bar">
<span style="color:#cfd3ef">列表筛选(<strong>UTC</strong>,默认当日):{{ list_window.label }}</span>
<label>预设
<select id="win-preset-select" onchange="toggleListWindowCustom()">
<option value="utc_today" {% if list_window.preset == 'utc_today' %}selected{% endif %}>UTC 当日</option>
<option value="utc_last24h" {% if list_window.preset == 'utc_last24h' %}selected{% endif %}>近 24 小时</option>
<option value="utc_last7d" {% if list_window.preset == 'utc_last7d' %}selected{% endif %}>近 7 天</option>
<option value="custom" {% if list_window.preset == 'custom' %}selected{% endif %}>自定义</option>
</select>
</label>
<span id="win-custom-range" style="{% if list_window.preset != 'custom' %}display:none{% endif %}">
<label>起(UTC) <input type="datetime-local" id="win-from-utc" value="{{ list_window.start_utc.strftime('%Y-%m-%dT%H:%M') }}"></label>
<label>止(UTC) <input type="datetime-local" id="win-to-utc" value="{{ list_window.end_utc.strftime('%Y-%m-%dT%H:%M') }}"></label>
</span>
<button type="button" style="padding:6px 12px" onclick="applyListWindow()">应用</button>
<span style="color:#8892b0;font-size:.75rem">统计页仍按北京时间 {{ stats_bundle.stats_reset_hour|default(reset_hour) }}:00 切日</span>
</div>
<div class="export-bar">
<span style="color:#9aa">数据导出(v{{ data_export_version }} CSVUTF-8;交易记录含开仓类型列,复盘单独导出):</span>
<a href="/export/trade_records">交易记录</a>
@@ -178,6 +198,8 @@
<select name="type" required>
<option value="箱体突破">箱体突破</option>
<option value="收敛突破">收敛突破</option>
<option value="斐波回调0.618">斐波回调0.618</option>
<option value="斐波回调0.786">斐波回调0.786</option>
<option value="关键阻力位">关键阻力位</option>
<option value="关键支撑位">关键支撑位</option>
</select>
@@ -188,12 +210,14 @@
<input name="lower" step="0.0001" placeholder="下沿/支撑" required>
<button type="submit">添加</button>
</form>
<div class="rule-tip">{{ key_gate_rule_text }}</div>
<div class="list">
{% for k in key %}
<div class="list-item" id="key-row-{{ k.id }}">
<div><strong>{{ k.symbol }}</strong> | {{ k.monitor_type }} | <span class="badge direction">{{ '做多' if k.direction == 'long' else '做空' }}</span></div>
<div>
上:{{ k.upper }} 下:{{ k.lower }}
{% if k.fib_entry_price %}| 挂E:{{ k.fib_entry_price }}{% endif %}
| 已提醒:{{ k.notification_count or 0 }}/{{ k.max_notify or 3 }}
| 现价:<span id="key-price-{{ k.id }}">-</span>
| 距上沿:<span id="key-up-diff-{{ k.id }}">-</span>
@@ -207,7 +231,7 @@
</div>
<div class="key-history">
<h3>关键位历史(满次提醒或手动删除)</h3>
<div class="sub">满 {{ key_alert_max_times }} 次企业微信提醒后自动移入此处;手动删除也会归档。</div>
<div class="sub">满 {{ key_alert_max_times }} 次企业微信提醒后自动移入此处;手动删除也会归档。受顶栏 UTC 列表时间窗筛选。</div>
<div class="list">
{% for h in key_history %}
<div class="list-item">
@@ -276,6 +300,9 @@
<label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
<input type="checkbox" name="order_chart" value="true"> 开仓后生成多周期K线图(4h/1h/15m/5m 各100)
</label>
<label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
<input type="checkbox" name="breakeven_enabled" value="1" checked> 启用移动保本
</label>
<span style="display:flex;align-items:center;padding:0 10px;font-size:.8rem;color:#8fc8ff">成交价自动取交易所实时+成交回报</span>
<input id="order-sl" name="sl" step="any" placeholder="止损价格" required>
<input id="order-tp" name="tgt" step="any" placeholder="止盈价格" required>
@@ -289,7 +316,7 @@
<div><strong>{{ o.symbol }}</strong> | <span class="badge direction">{{ '做多' if o.direction == 'long' else '做空' }}</span></div>
<div>
风格:{{ o.trade_style or 'trend' }} | 风险:{{ o.risk_percent or '-' }}%≈{{ o.risk_amount or '-' }}U
| 移动保本:{{ o.breakeven_rr_trigger or '-' }}R→{{ o.breakeven_price or '-' }}
| {% if o.breakeven_enabled %}移动保本:开{% else %}移动保本:关{% endif %} {{ o.breakeven_rr_trigger or '-' }}R→{{ o.breakeven_price or '-' }}
<br>
成交:{{ o.trigger_price }} 止损:{{ o.stop_loss }} 止盈:{{ o.take_profit }}
| 盈亏比:<span id="order-rr-{{ o.id }}">{% if o.rr_ratio is not none %}1:{{ '%.2f'|format(o.rr_ratio) }}{% else %}-{% endif %}</span>
@@ -315,15 +342,15 @@
</div>
<div class="table-wrap">
<table>
<tr><th>品种</th><th>类型</th><th>方向</th><th>成交</th><th>止损</th><th>止盈</th><th>基数</th><th>杠杆</th><th>持仓分钟</th><th>开仓时间(北京)</th><th>平仓时间(北京)</th><th>盈亏U</th><th>结果</th><th>操作</th></tr>
<tr><th>品种</th><th>类型</th><th>方向</th><th>成交</th><th>止损(开仓)</th><th>止盈</th><th>基数</th><th>杠杆</th><th>持仓分钟</th><th>开仓时间(北京)</th><th>平仓时间(北京)</th><th>盈亏U</th><th>结果</th><th>操作</th></tr>
{% for r in record %}
<tr id="trade-row-{{ r.id }}">
{% set pnl_val = (r.pnl_amount or 0)|float %}
<td>{{ r.symbol }}</td>
<td>{{ r.monitor_type }}</td>
<td>{{ r.monitor_type }}{% if r.key_signal_type %} · {{ r.key_signal_type }}{% endif %}</td>
<td><span class="badge {{ 'direction-long' if r.direction == 'long' else 'direction-short' }}">{{ '做多' if r.direction == 'long' else '做空' }}</span></td>
<td>{{ r.trigger_price }}</td>
{% set stop_show = r.effective_stop_loss or r.initial_stop_loss or r.stop_loss %}
{% set stop_show = r.display_open_stop_loss or r.initial_stop_loss or r.stop_loss %}
{% set tp_show = r.effective_take_profit or r.take_profit %}
<td>{{ price_fmt(r.symbol, stop_show) }}</td>
<td>{{ price_fmt(r.symbol, tp_show) }}</td>
@@ -348,9 +375,10 @@
onclick='fillJournalFromTrade({{ {
"symbol": r.symbol,
"monitor_type": r.monitor_type,
"key_signal_type": r.key_signal_type or "",
"direction": r.direction,
"trigger_price": r.trigger_price,
"stop_loss": r.effective_stop_loss or r.initial_stop_loss or r.stop_loss,
"stop_loss": r.display_open_stop_loss or r.initial_stop_loss or r.stop_loss,
"take_profit": r.effective_take_profit or r.take_profit,
"opened_at": r.effective_opened_at,
"closed_at": r.effective_closed_at,
@@ -418,16 +446,19 @@
<input name="coin" placeholder="BTC" required>
<input name="tf" placeholder="5m" required>
<input name="pnl" placeholder="盈亏(U)" required>
<select name="entry_reason" required title="仅五种固定开仓逻辑">
<select name="entry_reason" id="journal-entry-reason" required title="固定枚举或选其他手写">
<option value="">开仓类型(必选)</option>
{% for er in entry_reason_options %}
<option value="{{ er }}">{{ er }}</option>
{% endfor %}
<option value="{{ entry_reason_other_value }}">其他(自定义,见下方说明框)</option>
</select>
<input type="text" name="entry_reason_custom" id="journal-entry-reason-custom" maxlength="2000" placeholder="选「其他」时在此填写开仓类型说明" autocomplete="off" style="display:none">
<input name="expect_rr" placeholder="预期RR">
<input name="real_rr" placeholder="实际RR">
<select name="early_exit_trigger" required title="平仓如何触发">
<option value="">离场触发(必选)</option>
<option value="止盈">止盈</option>
<option value="保本止盈">保本止盈</option>
<option value="移动止盈">移动止盈</option>
<option value="手动平仓">手动平仓</option>
@@ -499,12 +530,26 @@
<div class="stat-item"><div class="label">持仓占用导致错过(累计)</div><div class="value">{{ occupied_miss_total }}</div></div>
</div>
<div class="sub" style="margin-bottom:12px;color:#8892b0;font-size:.82rem">
已平仓「下单监控」按平仓时间归入<strong>北京时间</strong>下的交易日;胜率按盈笔数/(盈+亏)。历史总开仓(累计):
统计分析按<strong>北京时间 {{ stats_bundle.stats_reset_hour }}:00</strong>切日计入(与顶栏 UTC 列表窗无关)。历史总开仓(累计):
<strong style="color:#cfd3ef">{{ stats_bundle.total_opens_all }}</strong>
</div>
{{ period_stats("日统计", stats_bundle.day) }}
{{ period_stats("周统计", stats_bundle.week) }}
{{ period_stats("月统计", stats_bundle.month) }}
<div class="form-row" style="margin-bottom:14px;align-items:center">
<label style="display:flex;align-items:center;gap:8px;font-size:.88rem;color:#cfd3ef">
统计品类
<select id="stats-segment-select" onchange="switchStatsSegment()" style="min-width:200px">
{% for seg in stats_bundle.segments %}
<option value="{{ seg.key }}">{{ seg.title }}</option>
{% endfor %}
</select>
</label>
</div>
{% for seg in stats_bundle.segments %}
<div class="stats-segment-block stats-segment-panel" data-stats-segment="{{ seg.key }}"{% if not loop.first %} style="display:none"{% endif %}>
{{ period_stats("日统计", seg.day) }}
{{ period_stats("周统计", seg.week) }}
{{ period_stats("月统计", seg.month) }}
</div>
{% endfor %}
</div>
</div>
{% endif %}
@@ -526,6 +571,107 @@
<script>
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }};
const JOURNAL_ENTRY_REASON_OTHER = {{ entry_reason_other_value | tojson }};
function validateJournalEntryReason(){
const form = document.getElementById("journal-form");
if(!form) return true;
const sel = form.querySelector('[name="entry_reason"]');
const box = form.querySelector('[name="entry_reason_custom"]');
if(!sel || !sel.value){
alert("请选择开仓类型");
return false;
}
if(sel.value === JOURNAL_ENTRY_REASON_OTHER){
const t = (box && box.value || "").trim();
if(!t){
alert("选择「其他」时请填写自定义开仓类型说明");
return false;
}
}
return true;
}
function syncJournalEntryReasonOtherUi(){
const form = document.getElementById("journal-form");
if(!form) return;
const sel = form.querySelector('[name="entry_reason"]');
const box = form.querySelector('[name="entry_reason_custom"]');
if(!sel || !box) return;
if(sel.value === JOURNAL_ENTRY_REASON_OTHER){
box.style.display = "";
box.required = true;
} else {
box.style.display = "none";
box.required = false;
box.value = "";
}
}
function listWindowQueryString(){
const presetEl = document.getElementById("win-preset-select");
const preset = (presetEl && presetEl.value) || new URLSearchParams(window.location.search).get("win_preset") || "utc_today";
const q = new URLSearchParams(window.location.search);
q.set("win_preset", preset);
if(preset === "custom"){
const fromEl = document.getElementById("win-from-utc");
const toEl = document.getElementById("win-to-utc");
if(fromEl && fromEl.value) q.set("from_utc", fromEl.value.replace("T", " ") + ":00");
else q.delete("from_utc");
if(toEl && toEl.value) q.set("to_utc", toEl.value.replace("T", " ") + ":00");
else q.delete("to_utc");
} else {
q.delete("from_utc");
q.delete("to_utc");
}
return q.toString();
}
function toggleListWindowCustom(){
const preset = document.getElementById("win-preset-select");
const box = document.getElementById("win-custom-range");
if(!preset || !box) return;
box.style.display = preset.value === "custom" ? "" : "none";
}
function applyListWindow(){
const qs = listWindowQueryString();
const path = window.location.pathname || "/trade";
window.location.href = qs ? (path + "?" + qs) : path;
}
function attachListWindowToExports(){
const qs = listWindowQueryString();
if(!qs) return;
document.querySelectorAll('.export-bar a[href^="/export/trade_records"], .export-bar a[href^="/export/key_monitor_history"]').forEach(a=>{
const base = a.getAttribute("href").split("?")[0];
a.setAttribute("href", base + "?" + qs);
});
}
function switchStatsSegment(){
const sel = document.getElementById("stats-segment-select");
if(!sel) return;
const key = sel.value;
document.querySelectorAll(".stats-segment-panel").forEach(p=>{
p.style.display = p.getAttribute("data-stats-segment") === key ? "block" : "none";
});
const q = new URLSearchParams(window.location.search);
q.set("stats_segment", key);
const qs = q.toString();
history.replaceState(null, "", qs ? (window.location.pathname + "?" + qs) : window.location.pathname);
}
function initStatsSegmentFromUrl(){
const sel = document.getElementById("stats-segment-select");
if(!sel) return;
const key = new URLSearchParams(window.location.search).get("stats_segment");
if(key && sel.querySelector('option[value="' + key.replace(/"/g, "") + '"]')){
sel.value = key;
}
switchStatsSegment();
}
function showImage(src){document.getElementById("bigImg").src=src;document.getElementById("imgModal").style.display="flex";}
function closeModal(){document.getElementById("imgModal").style.display="none";}
function forceCloseDetailModal(){document.getElementById("detailModal").style.display="none";}
@@ -693,7 +839,8 @@ function deleteKeyHistory(id){
}
function loadJournals(){
fetch("/api/journals").then(r=>r.json()).then(data=>{
const qs = listWindowQueryString();
fetch("/api/journals" + (qs ? "?" + qs : "")).then(r=>r.json()).then(data=>{
Object.keys(journalCache).forEach(k=>delete journalCache[k]);
let html="";
data.forEach(o=>{
@@ -715,7 +862,8 @@ function loadJournals(){
}
function loadReviews(){
fetch("/api/reviews").then(r=>r.json()).then(data=>{
const qs = listWindowQueryString();
fetch("/api/reviews" + (qs ? "?" + qs : "")).then(r=>r.json()).then(data=>{
Object.keys(reviewCache).forEach(k=>delete reviewCache[k]);
let html="";
data.forEach(r=>{
@@ -787,7 +935,13 @@ function setJournalField(name, value){
el.value = String(value);
}
const EARLY_EXIT_TRIGGERS = new Set(["保本止盈","移动止盈","手动平仓","止损","其他"]);
const EARLY_EXIT_TRIGGERS = new Set(["止盈","保本止盈","移动止盈","手动平仓","止损","其他"]);
const KEY_ENTRY_REASON_BY_SIGNAL = {
"箱体突破": "关键位箱体突破",
"收敛突破": "关键位收敛突破",
"斐波回调0.618": "关键位斐波0.618",
"斐波回调0.786": "关键位斐波0.786"
};
function splitLegacyEarlyExitReason(raw){
const s = String(raw || "").trim();
@@ -877,9 +1031,17 @@ function fillJournalFromTrade(t){
if(dirHint){ dirHint.value = t.direction || "long"; }
setJournalField("early_exit_trigger", "");
setJournalField("early_exit_note", "");
setJournalField("entry_reason", "");
const kst = String(t.key_signal_type || "").trim();
const erFromKey = KEY_ENTRY_REASON_BY_SIGNAL[kst] || "";
if(erFromKey && JOURNAL_ENTRY_REASON_OPTIONS.includes(erFromKey)){
setJournalField("entry_reason", erFromKey);
} else {
setJournalField("entry_reason", "");
}
setJournalField("entry_reason_custom", "");
syncJournalEntryReasonOtherUi();
const er = String(t.result || "").trim();
const exitTrigMap = { 保本止盈: "保本止盈", 移动止盈: "移动止盈", 手动平仓: "手动平仓", 止损: "止损" };
const exitTrigMap = { 止盈: "止盈", 保本止盈: "保本止盈", 移动止盈: "移动止盈", 手动平仓: "手动平仓", 止损: "止损" };
if(exitTrigMap[er]) setJournalField("early_exit_trigger", exitTrigMap[er]);
const note = `来自交易记录自动填充:${t.symbol || "-"} ${t.direction || "-"} | 入场:${t.trigger_price || "-"} 止损:${t.stop_loss || "-"} 止盈:${t.take_profit || "-"} | 类型:${t.monitor_type || "-"}`;
setJournalField("note", note);
@@ -962,6 +1124,9 @@ function toggleStatsCard(){
btn.innerText = collapsed ? "展开" : "折叠";
}
attachListWindowToExports();
toggleListWindowCustom();
initStatsSegmentFromUrl();
if(document.getElementById("journal-list")) loadJournals();
if(document.getElementById("review-list")) loadReviews();
const reviewToggle = document.getElementById("review-mode-toggle");
@@ -993,6 +1158,14 @@ if(journalForm){
earlyTrig.addEventListener("change", syncEarlyExitNoteRequired);
syncEarlyExitNoteRequired();
}
const erSel = journalForm.querySelector('[name="entry_reason"]');
if(erSel){
erSel.addEventListener("change", syncJournalEntryReasonOtherUi);
syncJournalEntryReasonOtherUi();
}
journalForm.addEventListener("submit", function(e){
if(!validateJournalEntryReason()){ e.preventDefault(); }
});
}
const keyForm = document.getElementById("key-form");