Unify key support/resistance monitor type and fix form parity.

Merge 关键阻力位/关键支撑位 into 关键支撑阻力, share key_monitor_form.js across hub and new-tab views, and add hub shortcut to /key_monitor.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-19 08:31:14 +08:00
parent ce172a7cee
commit 073a382d41
15 changed files with 233 additions and 470 deletions
+4 -2
View File
@@ -158,6 +158,7 @@ from key_monitor_lib import (
KEY_DIRECTION_WATCH, KEY_DIRECTION_WATCH,
KEY_MONITOR_ALERT_ONLY_TYPES, KEY_MONITOR_ALERT_ONLY_TYPES,
KEY_MONITOR_AUTO_TYPES, KEY_MONITOR_AUTO_TYPES,
KEY_MONITOR_RS_TYPE,
KEY_MONITOR_RS_TYPES, KEY_MONITOR_RS_TYPES,
auto_amp_ok, auto_amp_ok,
auto_confirm_ok, auto_confirm_ok,
@@ -7792,6 +7793,7 @@ def add_key():
return redirect("/key_monitor") return redirect("/key_monitor")
if mt in KEY_MONITOR_RS_TYPES: if mt in KEY_MONITOR_RS_TYPES:
direction_sel = KEY_DIRECTION_WATCH direction_sel = KEY_DIRECTION_WATCH
mt = KEY_MONITOR_RS_TYPE
elif direction_sel not in ("long", "short"): elif direction_sel not in ("long", "short"):
flash("箱体/收敛突破请选择做多或做空") flash("箱体/收敛突破请选择做多或做空")
return redirect("/key_monitor") return redirect("/key_monitor")
@@ -7828,7 +7830,7 @@ def add_key():
conn.close() conn.close()
flash( flash(
f"当前持仓已达上限({occupied}/{MAX_ACTIVE_POSITIONS}):无法添加「箱体突破 / 收敛突破」。" f"当前持仓已达上限({occupied}/{MAX_ACTIVE_POSITIONS}):无法添加「箱体突破 / 收敛突破」。"
"请平仓后再试,或使用「关键阻力位/关键支撑位」(仅单次提醒)。" "请平仓后再试,或使用「关键支撑阻力」(仅提醒)。"
) )
return redirect("/key_monitor") return redirect("/key_monitor")
ex_sym_key = normalize_exchange_symbol(symbol) ex_sym_key = normalize_exchange_symbol(symbol)
@@ -8004,7 +8006,7 @@ def add_key():
extra = f"|方案:{sl_tp_mode_label(sl_tp_mode)}|移动保本:{'' if be_flag else ''}" extra = f"|方案:{sl_tp_mode_label(sl_tp_mode)}|移动保本:{'' if be_flag else ''}"
if mt in KEY_MONITOR_RS_TYPES: if mt in KEY_MONITOR_RS_TYPES:
flash( flash(
f"添加成功({symbol} 日成交量排名 {rank}/{total})|阻力/支撑:双向监控上/下沿," f"添加成功({symbol} 日成交量排名 {rank}/{total})|关键支撑阻力:双向监控上/下沿,"
f"5m 收盘突破后微信提醒 {KEY_ALERT_MAX_TIMES} 次(间隔 {KEY_ALERT_INTERVAL_MINUTES} 分钟)" f"5m 收盘突破后微信提醒 {KEY_ALERT_MAX_TIMES} 次(间隔 {KEY_ALERT_INTERVAL_MINUTES} 分钟)"
) )
else: else:
+1 -113
View File
@@ -629,8 +629,7 @@
<select name="type" required> <select name="type" required>
<option value="箱体突破">箱体突破</option> <option value="箱体突破">箱体突破</option>
<option value="收敛突破">收敛突破</option> <option value="收敛突破">收敛突破</option>
<option value="关键阻力">关键阻力</option> <option value="关键支撑阻力">关键支撑阻力</option>
<option value="关键支撑位">关键支撑位</option>
</select> </select>
<select name="direction" required> <select name="direction" required>
<option value="">方向</option><option value="long">做多</option><option value="short">做空</option> <option value="">方向</option><option value="long">做多</option><option value="short">做空</option>
@@ -1550,120 +1549,9 @@ if(journalForm){
} }
} }
function syncKeyMonitorFormFields(){
const typeEl = document.querySelector('#key-form [name="type"]');
const dirEl = document.getElementById("key-direction");
const modeEl = document.getElementById("key-sl-tp-mode");
const manualTp = document.getElementById("key-manual-tp");
const beWrap = document.getElementById("key-breakeven-wrap");
if(!typeEl) return;
const t = (typeEl.value || "").trim();
const autoTypes = new Set(["箱体突破","收敛突破"]);
const fibTypes = new Set(["斐波回调0.618","斐波回调0.786"]);
const fbTypes = new Set(["假突破"]);
const teTypes = new Set(["触价开仓"]);
const rsTypes = new Set(["关键阻力位","关键支撑位"]);
const showAuto = autoTypes.has(t);
const showFb = fbTypes.has(t);
const showTe = teTypes.has(t);
const showBe = showAuto || fibTypes.has(t) || showFb || showTe;
const showDir = !rsTypes.has(t);
const upperEl = document.getElementById("key-upper");
const lowerEl = document.getElementById("key-lower");
const fbPriceEl = document.getElementById("key-fb-price");
const teEntryEl = document.getElementById("key-trigger-entry");
const teSlEl = document.getElementById("key-trigger-sl");
const teTpEl = document.getElementById("key-trigger-tp");
if(dirEl){
dirEl.style.display = showDir ? "" : "none";
dirEl.required = showDir;
if(!showDir) dirEl.value = "";
}
if(modeEl) modeEl.style.display = showAuto ? "" : "none";
if(manualTp){
const trend = showAuto && modeEl && modeEl.value === "trend_manual";
manualTp.style.display = trend ? "" : "none";
manualTp.required = !!trend;
}
if(beWrap) beWrap.style.display = showBe ? "inline-flex" : "none";
if(window.TimeCloseUI) TimeCloseUI.syncKeyTimeCloseVisibility(showBe);
const hideBounds = showFb || showTe;
if(upperEl){
upperEl.style.display = hideBounds ? "none" : "";
upperEl.required = !hideBounds;
if(hideBounds) upperEl.value = "";
}
if(lowerEl){
lowerEl.style.display = hideBounds ? "none" : "";
lowerEl.required = !hideBounds;
if(hideBounds) lowerEl.value = "";
}
if(fbPriceEl){
fbPriceEl.style.display = showFb ? "" : "none";
fbPriceEl.required = showFb;
if(!showFb) fbPriceEl.value = "";
fbPriceEl.placeholder = (dirEl && dirEl.value === "short") ? "高点(阻力)" : ((dirEl && dirEl.value === "long") ? "低点(支撑)" : "做空填高点/做多填低点");
}
[teEntryEl, teSlEl, teTpEl].forEach((el)=>{
if(!el) return;
el.style.display = showTe ? "" : "none";
el.required = showTe;
if(!showTe) el.value = "";
});
}
const keyTypeSel = document.querySelector('#key-form [name="type"]');
const keyModeSel = document.getElementById("key-sl-tp-mode");
const keyDirSel = document.getElementById("key-direction");
if(keyTypeSel) keyTypeSel.addEventListener("change", syncKeyMonitorFormFields);
if(keyModeSel) keyModeSel.addEventListener("change", syncKeyMonitorFormFields);
if(keyDirSel) keyDirSel.addEventListener("change", syncKeyMonitorFormFields);
syncKeyMonitorFormFields();
if(window.TimeCloseUI){ if(window.TimeCloseUI){
TimeCloseUI.bindTimeCloseForm("key-time-close-cb", "key-time-close-hours", "key-time-close-wrap");
TimeCloseUI.bindTimeCloseForm("order-time-close-cb", "order-time-close-hours", "order-time-close-wrap"); TimeCloseUI.bindTimeCloseForm("order-time-close-cb", "order-time-close-hours", "order-time-close-wrap");
} }
const keyForm = document.getElementById("key-form");
if(keyForm){
keyForm.addEventListener("submit", (e)=>{
e.preventDefault();
if(window.FormSubmitGuard && FormSubmitGuard.isLocked(keyForm)) return;
const symbolEl = keyForm.querySelector('[name="symbol"]');
const symbol = (symbolEl ? symbolEl.value : "").trim();
if(!symbol){
alert("请先输入交易对");
return;
}
const typeVal = (keyForm.querySelector('[name="type"]') || {}).value || "";
if(typeVal === "假突破"){
if(window.FormSubmitGuard) FormSubmitGuard.nativeSubmitOnce(keyForm, "提交中…");
else keyForm.submit();
return;
}
if(window.FormSubmitGuard) FormSubmitGuard.lock(keyForm, "校验排名中…");
fetch(`/api/symbol_liquidity_rank?symbol=${encodeURIComponent(symbol)}`)
.then(r=>r.json().then(d=>({status:r.status, data:d})))
.then(({status,data})=>{
if(status >= 400 || !data.ok){
alert((data && data.msg) || "日成交量排名读取失败");
if(window.FormSubmitGuard) FormSubmitGuard.unlock(keyForm);
return;
}
const rankMax = data.rank_max || 30;
if(!data.in_top30){
alert(`${data.symbol} 当前日成交量排名 ${data.rank}/${data.total},不在前${rankMax},已拦截。`);
if(window.FormSubmitGuard) FormSubmitGuard.unlock(keyForm);
return;
}
if(window.FormSubmitGuard) FormSubmitGuard.nativeSubmitOnce(keyForm, "提交中…");
else keyForm.submit();
})
.catch(()=>{
alert("日成交量排名检查失败,请稍后重试");
if(window.FormSubmitGuard) FormSubmitGuard.unlock(keyForm);
});
});
}
// 复盘/AI列表:初次进入页面后再异步刷新一次,避免浏览器 bfcache/重定向后仍显示旧缓存 // 复盘/AI列表:初次进入页面后再异步刷新一次,避免浏览器 bfcache/重定向后仍显示旧缓存
setTimeout(() => { setTimeout(() => {
if(document.getElementById("journal-list")) loadJournals(); if(document.getElementById("journal-list")) loadJournals();
+4 -2
View File
@@ -157,6 +157,7 @@ from key_monitor_lib import (
KEY_DIRECTION_WATCH, KEY_DIRECTION_WATCH,
KEY_MONITOR_ALERT_ONLY_TYPES, KEY_MONITOR_ALERT_ONLY_TYPES,
KEY_MONITOR_AUTO_TYPES, KEY_MONITOR_AUTO_TYPES,
KEY_MONITOR_RS_TYPE,
KEY_MONITOR_RS_TYPES, KEY_MONITOR_RS_TYPES,
auto_amp_ok, auto_amp_ok,
auto_confirm_ok, auto_confirm_ok,
@@ -7684,6 +7685,7 @@ def add_key():
direction_sel = (d.get("direction") or "").strip().lower() direction_sel = (d.get("direction") or "").strip().lower()
if mt in KEY_MONITOR_RS_TYPES: if mt in KEY_MONITOR_RS_TYPES:
direction_sel = KEY_DIRECTION_WATCH direction_sel = KEY_DIRECTION_WATCH
mt = KEY_MONITOR_RS_TYPE
elif direction_sel not in ("long", "short"): elif direction_sel not in ("long", "short"):
flash("箱体/收敛突破请选择做多或做空") flash("箱体/收敛突破请选择做多或做空")
return redirect("/key_monitor") return redirect("/key_monitor")
@@ -7723,7 +7725,7 @@ def add_key():
conn = None conn = None
flash( flash(
f"当前持仓已达上限({occupied}/{MAX_ACTIVE_POSITIONS}):无法添加「箱体突破 / 收敛突破」。" f"当前持仓已达上限({occupied}/{MAX_ACTIVE_POSITIONS}):无法添加「箱体突破 / 收敛突破」。"
"请平仓后再试,或使用「关键阻力位/关键支撑位」(仅单次提醒)。" "请平仓后再试,或使用「关键支撑阻力」(仅提醒)。"
) )
return redirect("/key_monitor") return redirect("/key_monitor")
ex_sym_key = normalize_exchange_symbol(symbol) ex_sym_key = normalize_exchange_symbol(symbol)
@@ -7925,7 +7927,7 @@ def add_key():
extra += f"{time_close_label(tc_h)}" extra += f"{time_close_label(tc_h)}"
if mt in KEY_MONITOR_RS_TYPES: if mt in KEY_MONITOR_RS_TYPES:
flash( flash(
f"添加成功({symbol} 日成交量排名 {rank}/{total})|阻力/支撑:双向监控上/下沿," f"添加成功({symbol} 日成交量排名 {rank}/{total})|关键支撑阻力:双向监控上/下沿,"
f"5m 收盘突破后微信提醒 {KEY_ALERT_MAX_TIMES} 次(间隔 {KEY_ALERT_INTERVAL_MINUTES} 分钟)" f"5m 收盘突破后微信提醒 {KEY_ALERT_MAX_TIMES} 次(间隔 {KEY_ALERT_INTERVAL_MINUTES} 分钟)"
) )
else: else:
+1 -113
View File
@@ -596,8 +596,7 @@
<select name="type" required> <select name="type" required>
<option value="箱体突破">箱体突破</option> <option value="箱体突破">箱体突破</option>
<option value="收敛突破">收敛突破</option> <option value="收敛突破">收敛突破</option>
<option value="关键阻力">关键阻力</option> <option value="关键支撑阻力">关键支撑阻力</option>
<option value="关键支撑位">关键支撑位</option>
</select> </select>
<select name="direction" required> <select name="direction" required>
<option value="">方向</option><option value="long">做多</option><option value="short">做空</option> <option value="">方向</option><option value="long">做多</option><option value="short">做空</option>
@@ -1517,120 +1516,9 @@ if(journalForm){
} }
} }
function syncKeyMonitorFormFields(){
const typeEl = document.querySelector('#key-form [name="type"]');
const dirEl = document.getElementById("key-direction");
const modeEl = document.getElementById("key-sl-tp-mode");
const manualTp = document.getElementById("key-manual-tp");
const beWrap = document.getElementById("key-breakeven-wrap");
if(!typeEl) return;
const t = (typeEl.value || "").trim();
const autoTypes = new Set(["箱体突破","收敛突破"]);
const fibTypes = new Set(["斐波回调0.618","斐波回调0.786"]);
const fbTypes = new Set(["假突破"]);
const teTypes = new Set(["触价开仓"]);
const rsTypes = new Set(["关键阻力位","关键支撑位"]);
const showAuto = autoTypes.has(t);
const showFb = fbTypes.has(t);
const showTe = teTypes.has(t);
const showBe = showAuto || fibTypes.has(t) || showFb || showTe;
const showDir = !rsTypes.has(t);
const upperEl = document.getElementById("key-upper");
const lowerEl = document.getElementById("key-lower");
const fbPriceEl = document.getElementById("key-fb-price");
const teEntryEl = document.getElementById("key-trigger-entry");
const teSlEl = document.getElementById("key-trigger-sl");
const teTpEl = document.getElementById("key-trigger-tp");
if(dirEl){
dirEl.style.display = showDir ? "" : "none";
dirEl.required = showDir;
if(!showDir) dirEl.value = "";
}
if(modeEl) modeEl.style.display = showAuto ? "" : "none";
if(manualTp){
const trend = showAuto && modeEl && modeEl.value === "trend_manual";
manualTp.style.display = trend ? "" : "none";
manualTp.required = !!trend;
}
if(beWrap) beWrap.style.display = showBe ? "inline-flex" : "none";
if(window.TimeCloseUI) TimeCloseUI.syncKeyTimeCloseVisibility(showBe);
const hideBounds = showFb || showTe;
if(upperEl){
upperEl.style.display = hideBounds ? "none" : "";
upperEl.required = !hideBounds;
if(hideBounds) upperEl.value = "";
}
if(lowerEl){
lowerEl.style.display = hideBounds ? "none" : "";
lowerEl.required = !hideBounds;
if(hideBounds) lowerEl.value = "";
}
if(fbPriceEl){
fbPriceEl.style.display = showFb ? "" : "none";
fbPriceEl.required = showFb;
if(!showFb) fbPriceEl.value = "";
fbPriceEl.placeholder = (dirEl && dirEl.value === "short") ? "高点(阻力)" : ((dirEl && dirEl.value === "long") ? "低点(支撑)" : "做空填高点/做多填低点");
}
[teEntryEl, teSlEl, teTpEl].forEach((el)=>{
if(!el) return;
el.style.display = showTe ? "" : "none";
el.required = showTe;
if(!showTe) el.value = "";
});
}
const keyTypeSel = document.querySelector('#key-form [name="type"]');
const keyModeSel = document.getElementById("key-sl-tp-mode");
const keyDirSel = document.getElementById("key-direction");
if(keyTypeSel) keyTypeSel.addEventListener("change", syncKeyMonitorFormFields);
if(keyModeSel) keyModeSel.addEventListener("change", syncKeyMonitorFormFields);
if(keyDirSel) keyDirSel.addEventListener("change", syncKeyMonitorFormFields);
syncKeyMonitorFormFields();
if(window.TimeCloseUI){ if(window.TimeCloseUI){
TimeCloseUI.bindTimeCloseForm("key-time-close-cb", "key-time-close-hours", "key-time-close-wrap");
TimeCloseUI.bindTimeCloseForm("order-time-close-cb", "order-time-close-hours", "order-time-close-wrap"); TimeCloseUI.bindTimeCloseForm("order-time-close-cb", "order-time-close-hours", "order-time-close-wrap");
} }
const keyForm = document.getElementById("key-form");
if(keyForm){
keyForm.addEventListener("submit", (e)=>{
e.preventDefault();
if(window.FormSubmitGuard && FormSubmitGuard.isLocked(keyForm)) return;
const symbolEl = keyForm.querySelector('[name="symbol"]');
const symbol = (symbolEl ? symbolEl.value : "").trim();
if(!symbol){
alert("请先输入交易对");
return;
}
const typeVal = (keyForm.querySelector('[name="type"]') || {}).value || "";
if(typeVal === "假突破"){
if(window.FormSubmitGuard) FormSubmitGuard.nativeSubmitOnce(keyForm, "提交中…");
else keyForm.submit();
return;
}
if(window.FormSubmitGuard) FormSubmitGuard.lock(keyForm, "校验排名中…");
fetch(`/api/symbol_liquidity_rank?symbol=${encodeURIComponent(symbol)}`)
.then(r=>r.json().then(d=>({status:r.status, data:d})))
.then(({status,data})=>{
if(status >= 400 || !data.ok){
alert((data && data.msg) || "日成交量排名读取失败");
if(window.FormSubmitGuard) FormSubmitGuard.unlock(keyForm);
return;
}
const rankMax = data.rank_max || 30;
if(!data.in_top30){
alert(`${data.symbol} 当前日成交量排名 ${data.rank}/${data.total},不在前${rankMax},已拦截。`);
if(window.FormSubmitGuard) FormSubmitGuard.unlock(keyForm);
return;
}
if(window.FormSubmitGuard) FormSubmitGuard.nativeSubmitOnce(keyForm, "提交中…");
else keyForm.submit();
})
.catch(()=>{
alert("日成交量排名检查失败,请稍后重试");
if(window.FormSubmitGuard) FormSubmitGuard.unlock(keyForm);
});
});
}
// 复盘/AI列表:初次进入页面后再异步刷新一次,避免浏览器 bfcache/重定向后仍显示旧缓存 // 复盘/AI列表:初次进入页面后再异步刷新一次,避免浏览器 bfcache/重定向后仍显示旧缓存
setTimeout(() => { setTimeout(() => {
if(document.getElementById("journal-list")) loadJournals(); if(document.getElementById("journal-list")) loadJournals();
+4 -2
View File
@@ -157,6 +157,7 @@ from key_monitor_lib import (
KEY_DIRECTION_WATCH, KEY_DIRECTION_WATCH,
KEY_MONITOR_ALERT_ONLY_TYPES, KEY_MONITOR_ALERT_ONLY_TYPES,
KEY_MONITOR_AUTO_TYPES, KEY_MONITOR_AUTO_TYPES,
KEY_MONITOR_RS_TYPE,
KEY_MONITOR_RS_TYPES, KEY_MONITOR_RS_TYPES,
auto_amp_ok, auto_amp_ok,
auto_confirm_ok, auto_confirm_ok,
@@ -7684,6 +7685,7 @@ def add_key():
direction_sel = (d.get("direction") or "").strip().lower() direction_sel = (d.get("direction") or "").strip().lower()
if mt in KEY_MONITOR_RS_TYPES: if mt in KEY_MONITOR_RS_TYPES:
direction_sel = KEY_DIRECTION_WATCH direction_sel = KEY_DIRECTION_WATCH
mt = KEY_MONITOR_RS_TYPE
elif direction_sel not in ("long", "short"): elif direction_sel not in ("long", "short"):
flash("箱体/收敛突破请选择做多或做空") flash("箱体/收敛突破请选择做多或做空")
return redirect("/key_monitor") return redirect("/key_monitor")
@@ -7723,7 +7725,7 @@ def add_key():
conn = None conn = None
flash( flash(
f"当前持仓已达上限({occupied}/{MAX_ACTIVE_POSITIONS}):无法添加「箱体突破 / 收敛突破」。" f"当前持仓已达上限({occupied}/{MAX_ACTIVE_POSITIONS}):无法添加「箱体突破 / 收敛突破」。"
"请平仓后再试,或使用「关键阻力位/关键支撑位」(仅单次提醒)。" "请平仓后再试,或使用「关键支撑阻力」(仅提醒)。"
) )
return redirect("/key_monitor") return redirect("/key_monitor")
ex_sym_key = normalize_exchange_symbol(symbol) ex_sym_key = normalize_exchange_symbol(symbol)
@@ -7925,7 +7927,7 @@ def add_key():
extra += f"{time_close_label(tc_h)}" extra += f"{time_close_label(tc_h)}"
if mt in KEY_MONITOR_RS_TYPES: if mt in KEY_MONITOR_RS_TYPES:
flash( flash(
f"添加成功({symbol} 日成交量排名 {rank}/{total})|阻力/支撑:双向监控上/下沿," f"添加成功({symbol} 日成交量排名 {rank}/{total})|关键支撑阻力:双向监控上/下沿,"
f"5m 收盘突破后微信提醒 {KEY_ALERT_MAX_TIMES} 次(间隔 {KEY_ALERT_INTERVAL_MINUTES} 分钟)" f"5m 收盘突破后微信提醒 {KEY_ALERT_MAX_TIMES} 次(间隔 {KEY_ALERT_INTERVAL_MINUTES} 分钟)"
) )
else: else:
+1 -113
View File
@@ -596,8 +596,7 @@
<select name="type" required> <select name="type" required>
<option value="箱体突破">箱体突破</option> <option value="箱体突破">箱体突破</option>
<option value="收敛突破">收敛突破</option> <option value="收敛突破">收敛突破</option>
<option value="关键阻力">关键阻力</option> <option value="关键支撑阻力">关键支撑阻力</option>
<option value="关键支撑位">关键支撑位</option>
</select> </select>
<select name="direction" required> <select name="direction" required>
<option value="">方向</option><option value="long">做多</option><option value="short">做空</option> <option value="">方向</option><option value="long">做多</option><option value="short">做空</option>
@@ -1517,120 +1516,9 @@ if(journalForm){
} }
} }
function syncKeyMonitorFormFields(){
const typeEl = document.querySelector('#key-form [name="type"]');
const dirEl = document.getElementById("key-direction");
const modeEl = document.getElementById("key-sl-tp-mode");
const manualTp = document.getElementById("key-manual-tp");
const beWrap = document.getElementById("key-breakeven-wrap");
if(!typeEl) return;
const t = (typeEl.value || "").trim();
const autoTypes = new Set(["箱体突破","收敛突破"]);
const fibTypes = new Set(["斐波回调0.618","斐波回调0.786"]);
const fbTypes = new Set(["假突破"]);
const teTypes = new Set(["触价开仓"]);
const rsTypes = new Set(["关键阻力位","关键支撑位"]);
const showAuto = autoTypes.has(t);
const showFb = fbTypes.has(t);
const showTe = teTypes.has(t);
const showBe = showAuto || fibTypes.has(t) || showFb || showTe;
const showDir = !rsTypes.has(t);
const upperEl = document.getElementById("key-upper");
const lowerEl = document.getElementById("key-lower");
const fbPriceEl = document.getElementById("key-fb-price");
const teEntryEl = document.getElementById("key-trigger-entry");
const teSlEl = document.getElementById("key-trigger-sl");
const teTpEl = document.getElementById("key-trigger-tp");
if(dirEl){
dirEl.style.display = showDir ? "" : "none";
dirEl.required = showDir;
if(!showDir) dirEl.value = "";
}
if(modeEl) modeEl.style.display = showAuto ? "" : "none";
if(manualTp){
const trend = showAuto && modeEl && modeEl.value === "trend_manual";
manualTp.style.display = trend ? "" : "none";
manualTp.required = !!trend;
}
if(beWrap) beWrap.style.display = showBe ? "inline-flex" : "none";
if(window.TimeCloseUI) TimeCloseUI.syncKeyTimeCloseVisibility(showBe);
const hideBounds = showFb || showTe;
if(upperEl){
upperEl.style.display = hideBounds ? "none" : "";
upperEl.required = !hideBounds;
if(hideBounds) upperEl.value = "";
}
if(lowerEl){
lowerEl.style.display = hideBounds ? "none" : "";
lowerEl.required = !hideBounds;
if(hideBounds) lowerEl.value = "";
}
if(fbPriceEl){
fbPriceEl.style.display = showFb ? "" : "none";
fbPriceEl.required = showFb;
if(!showFb) fbPriceEl.value = "";
fbPriceEl.placeholder = (dirEl && dirEl.value === "short") ? "高点(阻力)" : ((dirEl && dirEl.value === "long") ? "低点(支撑)" : "做空填高点/做多填低点");
}
[teEntryEl, teSlEl, teTpEl].forEach((el)=>{
if(!el) return;
el.style.display = showTe ? "" : "none";
el.required = showTe;
if(!showTe) el.value = "";
});
}
const keyTypeSel = document.querySelector('#key-form [name="type"]');
const keyModeSel = document.getElementById("key-sl-tp-mode");
const keyDirSel = document.getElementById("key-direction");
if(keyTypeSel) keyTypeSel.addEventListener("change", syncKeyMonitorFormFields);
if(keyModeSel) keyModeSel.addEventListener("change", syncKeyMonitorFormFields);
if(keyDirSel) keyDirSel.addEventListener("change", syncKeyMonitorFormFields);
syncKeyMonitorFormFields();
if(window.TimeCloseUI){ if(window.TimeCloseUI){
TimeCloseUI.bindTimeCloseForm("key-time-close-cb", "key-time-close-hours", "key-time-close-wrap");
TimeCloseUI.bindTimeCloseForm("order-time-close-cb", "order-time-close-hours", "order-time-close-wrap"); TimeCloseUI.bindTimeCloseForm("order-time-close-cb", "order-time-close-hours", "order-time-close-wrap");
} }
const keyForm = document.getElementById("key-form");
if(keyForm){
keyForm.addEventListener("submit", (e)=>{
e.preventDefault();
if(window.FormSubmitGuard && FormSubmitGuard.isLocked(keyForm)) return;
const symbolEl = keyForm.querySelector('[name="symbol"]');
const symbol = (symbolEl ? symbolEl.value : "").trim();
if(!symbol){
alert("请先输入交易对");
return;
}
const typeVal = (keyForm.querySelector('[name="type"]') || {}).value || "";
if(typeVal === "假突破"){
if(window.FormSubmitGuard) FormSubmitGuard.nativeSubmitOnce(keyForm, "提交中…");
else keyForm.submit();
return;
}
if(window.FormSubmitGuard) FormSubmitGuard.lock(keyForm, "校验排名中…");
fetch(`/api/symbol_liquidity_rank?symbol=${encodeURIComponent(symbol)}`)
.then(r=>r.json().then(d=>({status:r.status, data:d})))
.then(({status,data})=>{
if(status >= 400 || !data.ok){
alert((data && data.msg) || "日成交量排名读取失败");
if(window.FormSubmitGuard) FormSubmitGuard.unlock(keyForm);
return;
}
const rankMax = data.rank_max || 30;
if(!data.in_top30){
alert(`${data.symbol} 当前日成交量排名 ${data.rank}/${data.total},不在前${rankMax},已拦截。`);
if(window.FormSubmitGuard) FormSubmitGuard.unlock(keyForm);
return;
}
if(window.FormSubmitGuard) FormSubmitGuard.nativeSubmitOnce(keyForm, "提交中…");
else keyForm.submit();
})
.catch(()=>{
alert("日成交量排名检查失败,请稍后重试");
if(window.FormSubmitGuard) FormSubmitGuard.unlock(keyForm);
});
});
}
// 复盘/AI列表:初次进入页面后再异步刷新一次,避免浏览器 bfcache/重定向后仍显示旧缓存 // 复盘/AI列表:初次进入页面后再异步刷新一次,避免浏览器 bfcache/重定向后仍显示旧缓存
setTimeout(() => { setTimeout(() => {
if(document.getElementById("journal-list")) loadJournals(); if(document.getElementById("journal-list")) loadJournals();
+4 -2
View File
@@ -156,6 +156,7 @@ from key_monitor_lib import (
KEY_DIRECTION_WATCH, KEY_DIRECTION_WATCH,
KEY_MONITOR_ALERT_ONLY_TYPES, KEY_MONITOR_ALERT_ONLY_TYPES,
KEY_MONITOR_AUTO_TYPES, KEY_MONITOR_AUTO_TYPES,
KEY_MONITOR_RS_TYPE,
KEY_MONITOR_RS_TYPES, KEY_MONITOR_RS_TYPES,
auto_amp_ok, auto_amp_ok,
auto_confirm_ok, auto_confirm_ok,
@@ -7307,6 +7308,7 @@ def add_key():
return redirect("/key_monitor") return redirect("/key_monitor")
if mt in KEY_MONITOR_RS_TYPES: if mt in KEY_MONITOR_RS_TYPES:
direction_sel = KEY_DIRECTION_WATCH direction_sel = KEY_DIRECTION_WATCH
mt = KEY_MONITOR_RS_TYPE
elif direction_sel not in ("long", "short"): elif direction_sel not in ("long", "short"):
flash("箱体/收敛突破请选择做多或做空") flash("箱体/收敛突破请选择做多或做空")
return redirect("/key_monitor") return redirect("/key_monitor")
@@ -7343,7 +7345,7 @@ def add_key():
conn.close() conn.close()
flash( flash(
f"当前持仓已达上限({occupied}/{MAX_ACTIVE_POSITIONS}):无法添加「箱体突破 / 收敛突破」。" f"当前持仓已达上限({occupied}/{MAX_ACTIVE_POSITIONS}):无法添加「箱体突破 / 收敛突破」。"
"请平仓后再试,或使用「关键阻力位/关键支撑位」(仅单次提醒)。" "请平仓后再试,或使用「关键支撑阻力」(仅提醒)。"
) )
return redirect("/key_monitor") return redirect("/key_monitor")
ex_sym_key = normalize_okx_symbol(symbol) ex_sym_key = normalize_okx_symbol(symbol)
@@ -7525,7 +7527,7 @@ def add_key():
pass pass
if mt in KEY_MONITOR_RS_TYPES: if mt in KEY_MONITOR_RS_TYPES:
flash( flash(
f"添加成功({symbol} 日成交量排名 {rank}/{total})|阻力/支撑:双向监控上/下沿," f"添加成功({symbol} 日成交量排名 {rank}/{total})|关键支撑阻力:双向监控上/下沿,"
f"5m 收盘突破后微信提醒 {KEY_ALERT_MAX_TIMES} 次(间隔 {KEY_ALERT_INTERVAL_MINUTES} 分钟)" f"5m 收盘突破后微信提醒 {KEY_ALERT_MAX_TIMES} 次(间隔 {KEY_ALERT_INTERVAL_MINUTES} 分钟)"
) )
else: else:
+1 -114
View File
@@ -625,8 +625,7 @@
<select name="type" required> <select name="type" required>
<option value="箱体突破">箱体突破</option> <option value="箱体突破">箱体突破</option>
<option value="收敛突破">收敛突破</option> <option value="收敛突破">收敛突破</option>
<option value="关键阻力">关键阻力</option> <option value="关键支撑阻力">关键支撑阻力</option>
<option value="关键支撑位">关键支撑位</option>
</select> </select>
<select name="direction" required> <select name="direction" required>
<option value="">方向</option><option value="long">做多</option><option value="short">做空</option> <option value="">方向</option><option value="long">做多</option><option value="short">做空</option>
@@ -1546,121 +1545,9 @@ if(journalForm){
} }
} }
function syncKeyMonitorFormFields(){
const typeEl = document.querySelector('#key-form [name="type"]');
const dirEl = document.getElementById("key-direction");
const modeEl = document.getElementById("key-sl-tp-mode");
const manualTp = document.getElementById("key-manual-tp");
const beWrap = document.getElementById("key-breakeven-wrap");
if(!typeEl) return;
const t = (typeEl.value || "").trim();
const autoTypes = new Set(["箱体突破","收敛突破"]);
const fibTypes = new Set(["斐波回调0.618","斐波回调0.786"]);
const fbTypes = new Set(["假突破"]);
const teTypes = new Set(["触价开仓"]);
const rsTypes = new Set(["关键阻力位","关键支撑位"]);
const showAuto = autoTypes.has(t);
const showFb = fbTypes.has(t);
const showTe = teTypes.has(t);
const showBe = showAuto || fibTypes.has(t) || showFb || showTe;
const showDir = !rsTypes.has(t);
const upperEl = document.getElementById("key-upper");
const lowerEl = document.getElementById("key-lower");
const fbPriceEl = document.getElementById("key-fb-price");
const teEntryEl = document.getElementById("key-trigger-entry");
const teSlEl = document.getElementById("key-trigger-sl");
const teTpEl = document.getElementById("key-trigger-tp");
if(dirEl){
dirEl.style.display = showDir ? "" : "none";
dirEl.required = showDir;
if(!showDir) dirEl.value = "";
}
if(modeEl) modeEl.style.display = showAuto ? "" : "none";
if(manualTp){
const trend = showAuto && modeEl && modeEl.value === "trend_manual";
manualTp.style.display = trend ? "" : "none";
manualTp.required = !!trend;
}
if(beWrap) beWrap.style.display = showBe ? "inline-flex" : "none";
if(window.TimeCloseUI) TimeCloseUI.syncKeyTimeCloseVisibility(showBe);
const hideBounds = showFb || showTe;
if(upperEl){
upperEl.style.display = hideBounds ? "none" : "";
upperEl.required = !hideBounds;
if(hideBounds) upperEl.value = "";
}
if(lowerEl){
lowerEl.style.display = hideBounds ? "none" : "";
lowerEl.required = !hideBounds;
if(hideBounds) lowerEl.value = "";
}
if(fbPriceEl){
fbPriceEl.style.display = showFb ? "" : "none";
fbPriceEl.required = showFb;
if(!showFb) fbPriceEl.value = "";
fbPriceEl.placeholder = (dirEl && dirEl.value === "short") ? "高点(阻力)" : ((dirEl && dirEl.value === "long") ? "低点(支撑)" : "做空填高点/做多填低点");
}
[teEntryEl, teSlEl, teTpEl].forEach((el)=>{
if(!el) return;
el.style.display = showTe ? "" : "none";
el.required = showTe;
if(!showTe) el.value = "";
});
}
const keyTypeSel = document.querySelector('#key-form [name="type"]');
const keyModeSel = document.getElementById("key-sl-tp-mode");
const keyDirSel = document.getElementById("key-direction");
if(keyTypeSel) keyTypeSel.addEventListener("change", syncKeyMonitorFormFields);
if(keyModeSel) keyModeSel.addEventListener("change", syncKeyMonitorFormFields);
if(keyDirSel) keyDirSel.addEventListener("change", syncKeyMonitorFormFields);
syncKeyMonitorFormFields();
if(window.TimeCloseUI){ if(window.TimeCloseUI){
TimeCloseUI.bindTimeCloseForm("key-time-close-cb", "key-time-close-hours", "key-time-close-wrap");
TimeCloseUI.bindTimeCloseForm("order-time-close-cb", "order-time-close-hours", "order-time-close-wrap"); TimeCloseUI.bindTimeCloseForm("order-time-close-cb", "order-time-close-hours", "order-time-close-wrap");
} }
const keyForm = document.getElementById("key-form");
if(keyForm){
keyForm.addEventListener("submit", (e)=>{
e.preventDefault();
if(window.FormSubmitGuard && FormSubmitGuard.isLocked(keyForm)) return;
const symbolEl = keyForm.querySelector('[name="symbol"]');
const symbol = (symbolEl ? symbolEl.value : "").trim();
if(!symbol){
alert("请先输入交易对");
return;
}
const typeVal = (keyForm.querySelector('[name="type"]') || {}).value || "";
if(typeVal === "假突破"){
if(window.FormSubmitGuard) FormSubmitGuard.nativeSubmitOnce(keyForm, "提交中…");
else keyForm.submit();
return;
}
if(window.FormSubmitGuard) FormSubmitGuard.lock(keyForm, "校验排名中…");
fetch(`/api/symbol_liquidity_rank?symbol=${encodeURIComponent(symbol)}`)
.then(r=>r.json().then(d=>({status:r.status, data:d})))
.then(({status,data})=>{
if(status >= 400 || !data.ok){
alert((data && data.msg) || "日成交量排名读取失败");
if(window.FormSubmitGuard) FormSubmitGuard.unlock(keyForm);
return;
}
const rankMax = data.rank_max || 30;
const inTop = data.in_top != null ? data.in_top : data.in_top30;
if(data.rank == null || !inTop){
alert(`${data.symbol} 当前24h成交额排名 ${data.rank == null ? "—" : data.rank}/${data.total},不在前${rankMax},已拦截。`);
if(window.FormSubmitGuard) FormSubmitGuard.unlock(keyForm);
return;
}
if(window.FormSubmitGuard) FormSubmitGuard.nativeSubmitOnce(keyForm, "提交中…");
else keyForm.submit();
})
.catch(()=>{
alert("日成交量排名检查失败,请稍后重试");
if(window.FormSubmitGuard) FormSubmitGuard.unlock(keyForm);
});
});
}
// 复盘/AI列表:初次进入页面后再异步刷新一次,避免浏览器 bfcache/重定向后仍显示旧缓存 // 复盘/AI列表:初次进入页面后再异步刷新一次,避免浏览器 bfcache/重定向后仍显示旧缓存
setTimeout(() => { setTimeout(() => {
if(document.getElementById("journal-list")) loadJournals(); if(document.getElementById("journal-list")) loadJournals();
+1
View File
@@ -56,6 +56,7 @@ def install_instance_theme_static(app) -> None:
"instance_ui.js": "application/javascript; charset=utf-8", "instance_ui.js": "application/javascript; charset=utf-8",
"ai_review_render.js": "application/javascript; charset=utf-8", "ai_review_render.js": "application/javascript; charset=utf-8",
"form_submit_guard.js": "application/javascript; charset=utf-8", "form_submit_guard.js": "application/javascript; charset=utf-8",
"key_monitor_form.js": "application/javascript; charset=utf-8",
"time_close_ui.js": "application/javascript; charset=utf-8", "time_close_ui.js": "application/javascript; charset=utf-8",
"focus_chart_page.js": "application/javascript; charset=utf-8", "focus_chart_page.js": "application/javascript; charset=utf-8",
"focus_chart_page.css": "text/css; charset=utf-8", "focus_chart_page.css": "text/css; charset=utf-8",
+21 -2
View File
@@ -7,11 +7,30 @@ from datetime import datetime
from typing import Any, Optional from typing import Any, Optional
KEY_MONITOR_AUTO_TYPES = frozenset({"箱体突破", "收敛突破"}) KEY_MONITOR_AUTO_TYPES = frozenset({"箱体突破", "收敛突破"})
KEY_MONITOR_RS_TYPES = frozenset({"关键阻力位", "关键支撑"}) KEY_MONITOR_RS_TYPE = "关键支撑阻力"
KEY_MONITOR_ALERT_ONLY_TYPES = KEY_MONITOR_RS_TYPES KEY_MONITOR_RS_LEGACY_TYPES = frozenset({"关键阻力位", "关键支撑位"})
KEY_MONITOR_RS_TYPES = frozenset({KEY_MONITOR_RS_TYPE}) | KEY_MONITOR_RS_LEGACY_TYPES
KEY_MONITOR_ALERT_ONLY_TYPES = frozenset({KEY_MONITOR_RS_TYPE}) | KEY_MONITOR_RS_LEGACY_TYPES
KEY_DIRECTION_WATCH = "watch" KEY_DIRECTION_WATCH = "watch"
def is_rs_key_monitor_type(monitor_type: str) -> bool:
return (monitor_type or "").strip() in KEY_MONITOR_RS_TYPES
def rs_monitor_type_label(monitor_type: str) -> str:
"""展示用:旧库里的阻力/支撑合并为「关键支撑阻力」。"""
if is_rs_key_monitor_type(monitor_type):
return KEY_MONITOR_RS_TYPE
return (monitor_type or "").strip()
def rs_monitor_type_for_storage(monitor_type: str) -> str:
if is_rs_key_monitor_type(monitor_type):
return KEY_MONITOR_RS_TYPE
return (monitor_type or "").strip()
def calc_breakout_breach_pct(direction: str, close: float, upper: float, lower: float) -> float: def calc_breakout_breach_pct(direction: str, close: float, upper: float, lower: float) -> float:
"""突破 K 收盘相对关键位的越过幅度(%)。未越过对应边界时返回 0。""" """突破 K 收盘相对关键位的越过幅度(%)。未越过对应边界时返回 0。"""
direction = (direction or "long").strip().lower() direction = (direction or "long").strip().lower()
+6 -1
View File
@@ -2940,6 +2940,7 @@
"关键位收敛结构", "关键位收敛结构",
]); ]);
const KEY_BUCKET_WATCH_TYPES = new Set([ const KEY_BUCKET_WATCH_TYPES = new Set([
"关键支撑阻力",
"关键阻力位", "关键阻力位",
"关键支撑位", "关键支撑位",
"关键位监控", "关键位监控",
@@ -3065,7 +3066,7 @@
</div> </div>
<div class="fs-head-actions"> <div class="fs-head-actions">
<button type="button" class="ghost btn-expand-back">返回监控</button> <button type="button" class="ghost btn-expand-back">返回监控</button>
${flaskOpen ? `<a class="btn-link btn-open-instance" href="#" data-ex-id="${esc(row.id)}" data-next="/">打开实例</a>` : ""} ${flaskOpen ? `<a class="btn-link btn-open-instance" href="#" data-ex-id="${esc(row.id)}" data-next="/key_monitor">关键位监控</a>` : ""}
${flaskOpen ? `<a class="btn-link btn-open-instance" href="#" data-ex-id="${esc(row.id)}" data-next="/strategy">策略交易</a>` : ""} ${flaskOpen ? `<a class="btn-link btn-open-instance" href="#" data-ex-id="${esc(row.id)}" data-next="/strategy">策略交易</a>` : ""}
<button type="button" class="danger btn-close-ex" data-id="${esc(row.id)}">全平</button> <button type="button" class="danger btn-close-ex" data-id="${esc(row.id)}">全平</button>
</div> </div>
@@ -3371,6 +3372,9 @@
const openFlask = flaskOpen const openFlask = flaskOpen
? `<a class="btn-link btn-open-instance" href="#" data-ex-id="${esc(row.id)}" data-next="/">实例</a>` ? `<a class="btn-link btn-open-instance" href="#" data-ex-id="${esc(row.id)}" data-next="/">实例</a>`
: ""; : "";
const openKey = flaskOpen
? `<a class="btn-link btn-open-instance" href="#" data-ex-id="${esc(row.id)}" data-next="/key_monitor">关键位</a>`
: "";
const openReview = flaskOpen const openReview = flaskOpen
? `<a class="btn-link btn-open-instance" href="#" data-ex-id="${esc(row.id)}" data-next="/records">复盘</a>` ? `<a class="btn-link btn-open-instance" href="#" data-ex-id="${esc(row.id)}" data-next="/records">复盘</a>`
: ""; : "";
@@ -3385,6 +3389,7 @@
</div> </div>
<div class="card-actions"> <div class="card-actions">
${openFlask} ${openFlask}
${openKey}
${openReview} ${openReview}
<button type="button" class="danger btn-close-ex" data-id="${esc(row.id)}">全平</button> <button type="button" class="danger btn-close-ex" data-id="${esc(row.id)}">全平</button>
</div> </div>
+148
View File
@@ -0,0 +1,148 @@
/**
* 关键位监控添加表单类型切换显隐成交量排名校验四所实例共用
*/
(function (global) {
const RS_TYPES = new Set([
"关键支撑阻力",
"关键阻力位",
"关键支撑位",
]);
function syncKeyMonitorFormFields() {
const typeEl = document.querySelector('#key-form [name="type"]');
const dirEl = document.getElementById("key-direction");
const modeEl = document.getElementById("key-sl-tp-mode");
const manualTp = document.getElementById("key-manual-tp");
const beWrap = document.getElementById("key-breakeven-wrap");
if (!typeEl) return;
const t = (typeEl.value || "").trim();
const autoTypes = new Set(["箱体突破", "收敛突破"]);
const fibTypes = new Set(["斐波回调0.618", "斐波回调0.786"]);
const fbTypes = new Set(["假突破"]);
const teTypes = new Set(["触价开仓"]);
const showAuto = autoTypes.has(t);
const showFb = fbTypes.has(t);
const showTe = teTypes.has(t);
const showBe = showAuto || fibTypes.has(t) || showFb || showTe;
const showDir = !RS_TYPES.has(t);
const upperEl = document.getElementById("key-upper");
const lowerEl = document.getElementById("key-lower");
const fbPriceEl = document.getElementById("key-fb-price");
const teEntryEl = document.getElementById("key-trigger-entry");
const teSlEl = document.getElementById("key-trigger-sl");
const teTpEl = document.getElementById("key-trigger-tp");
if (dirEl) {
dirEl.style.display = showDir ? "" : "none";
dirEl.required = showDir;
if (!showDir) dirEl.value = "";
}
if (modeEl) modeEl.style.display = showAuto ? "" : "none";
if (manualTp) {
const trend = showAuto && modeEl && modeEl.value === "trend_manual";
manualTp.style.display = trend ? "" : "none";
manualTp.required = !!trend;
}
if (beWrap) beWrap.style.display = showBe ? "inline-flex" : "none";
if (global.TimeCloseUI) global.TimeCloseUI.syncKeyTimeCloseVisibility(showBe);
const hideBounds = showFb || showTe;
if (upperEl) {
upperEl.style.display = hideBounds ? "none" : "";
upperEl.required = !hideBounds;
if (hideBounds) upperEl.value = "";
}
if (lowerEl) {
lowerEl.style.display = hideBounds ? "none" : "";
lowerEl.required = !hideBounds;
if (hideBounds) lowerEl.value = "";
}
if (fbPriceEl) {
fbPriceEl.style.display = showFb ? "" : "none";
fbPriceEl.required = showFb;
if (!showFb) fbPriceEl.value = "";
fbPriceEl.placeholder =
dirEl && dirEl.value === "short"
? "高点(阻力)"
: dirEl && dirEl.value === "long"
? "低点(支撑)"
: "做空填高点/做多填低点";
}
[teEntryEl, teSlEl, teTpEl].forEach((el) => {
if (!el) return;
el.style.display = showTe ? "" : "none";
el.required = showTe;
if (!showTe) el.value = "";
});
}
function bindKeyMonitorForm() {
const keyForm = document.getElementById("key-form");
const keyTypeSel = document.querySelector('#key-form [name="type"]');
const keyModeSel = document.getElementById("key-sl-tp-mode");
const keyDirSel = document.getElementById("key-direction");
if (keyTypeSel) keyTypeSel.addEventListener("change", syncKeyMonitorFormFields);
if (keyModeSel) keyModeSel.addEventListener("change", syncKeyMonitorFormFields);
if (keyDirSel) keyDirSel.addEventListener("change", syncKeyMonitorFormFields);
syncKeyMonitorFormFields();
if (global.TimeCloseUI) {
global.TimeCloseUI.bindTimeCloseForm(
"key-time-close-cb",
"key-time-close-hours",
"key-time-close-wrap"
);
}
if (!keyForm || keyForm.dataset.keyFormBound === "1") return;
keyForm.dataset.keyFormBound = "1";
keyForm.addEventListener("submit", (e) => {
e.preventDefault();
if (global.FormSubmitGuard && global.FormSubmitGuard.isLocked(keyForm)) return;
const symbolEl = keyForm.querySelector('[name="symbol"]');
const symbol = (symbolEl ? symbolEl.value : "").trim();
if (!symbol) {
alert("请先输入交易对");
return;
}
const typeVal = (keyForm.querySelector('[name="type"]') || {}).value || "";
if (typeVal === "假突破") {
if (global.FormSubmitGuard) global.FormSubmitGuard.nativeSubmitOnce(keyForm, "提交中…");
else keyForm.submit();
return;
}
if (global.FormSubmitGuard) global.FormSubmitGuard.lock(keyForm, "校验排名中…");
fetch(`/api/symbol_liquidity_rank?symbol=${encodeURIComponent(symbol)}`)
.then((r) => r.json().then((d) => ({ status: r.status, data: d })))
.then(({ status, data }) => {
if (status >= 400 || !data.ok) {
alert((data && data.msg) || "日成交量排名读取失败");
if (global.FormSubmitGuard) global.FormSubmitGuard.unlock(keyForm);
return;
}
const rankMax = data.rank_max || 30;
const inTop = data.in_top != null ? data.in_top : data.in_top30;
if (data.rank == null || !inTop) {
alert(
`${data.symbol} 当前日成交量排名 ${data.rank == null ? "—" : data.rank}/${data.total},不在前${rankMax},已拦截。`
);
if (global.FormSubmitGuard) global.FormSubmitGuard.unlock(keyForm);
return;
}
if (global.FormSubmitGuard) global.FormSubmitGuard.nativeSubmitOnce(keyForm, "提交中…");
else keyForm.submit();
})
.catch(() => {
alert("日成交量排名检查失败,请稍后重试");
if (global.FormSubmitGuard) global.FormSubmitGuard.unlock(keyForm);
});
});
}
global.KeyMonitorForm = {
syncFields: syncKeyMonitorFormFields,
init: bindKeyMonitorForm,
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", bindKeyMonitorForm);
} else {
bindKeyMonitorForm();
}
})(typeof window !== "undefined" ? window : globalThis);
+9 -5
View File
@@ -77,6 +77,10 @@
.key-rule-foot code{font-size:.54rem;color:#8fc8ff} .key-rule-foot code{font-size:.54rem;color:#8fc8ff}
</style> </style>
{% macro key_monitor_type_label(k) -%}
{%- if k.monitor_type in ['关键阻力位','关键支撑位','关键支撑阻力'] -%}关键支撑阻力{%- else -%}{{ k.monitor_type }}{%- endif -%}
{%- endmacro %}
{% macro key_direction_label(k) -%} {% macro key_direction_label(k) -%}
{% if k.direction == 'watch' %}双向{% elif k.direction == 'long' %}做多{% else %}做空{% endif %} {% if k.direction == 'watch' %}双向{% elif k.direction == 'long' %}做多{% else %}做空{% endif %}
{%- endmacro %} {%- endmacro %}
@@ -146,8 +150,7 @@
<option value="假突破">假突破(BTC/ETH</option> <option value="假突破">假突破(BTC/ETH</option>
{% endif %} {% endif %}
<option value="触价开仓">触价开仓</option> <option value="触价开仓">触价开仓</option>
<option value="关键阻力">关键阻力</option> <option value="关键支撑阻力">关键支撑阻力</option>
<option value="关键支撑位">关键支撑位</option>
</select> </select>
<select name="direction" id="key-direction" required> <select name="direction" id="key-direction" required>
<option value="">方向</option><option value="long">做多</option><option value="short">做空</option> <option value="">方向</option><option value="long">做多</option><option value="short">做空</option>
@@ -200,7 +203,7 @@
{% else %} {% else %}
<span class="pos-side-badge {{ 'pos-side-long' if k.direction == 'long' else 'pos-side-short' }}">{{ key_direction_label(k) }}</span> <span class="pos-side-badge {{ 'pos-side-long' if k.direction == 'long' else 'pos-side-short' }}">{{ key_direction_label(k) }}</span>
{% endif %} {% endif %}
<span class="badge direction">{{ k.monitor_type }}</span> <span class="badge direction">{{ key_monitor_type_label(k) }}</span>
</span> </span>
<span class="key-row-summary-live" id="key-summary-live-{{ k.id }}">现价 — · 门控 —</span> <span class="key-row-summary-live" id="key-summary-live-{{ k.id }}">现价 — · 门控 —</span>
</span> </span>
@@ -246,7 +249,7 @@
<span class="key-row-summary-title"> <span class="key-row-summary-title">
<strong>{{ h.symbol }}</strong> <strong>{{ h.symbol }}</strong>
<span class="pos-side-badge {{ 'pos-side-long' if h.direction == 'long' else 'pos-side-short' }}">{{ key_direction_label(h) }}</span> <span class="pos-side-badge {{ 'pos-side-long' if h.direction == 'long' else 'pos-side-short' }}">{{ key_direction_label(h) }}</span>
<span class="badge direction">{{ h.monitor_type }}</span> <span class="badge direction">{{ key_monitor_type_label(h) }}</span>
<span class="key-history-outcome-badge">{{ key_history_outcome_label(h) }}</span> <span class="key-history-outcome-badge">{{ key_history_outcome_label(h) }}</span>
</span> </span>
</span> </span>
@@ -257,7 +260,7 @@
<div class="key-row-collapse-body"> <div class="key-row-collapse-body">
<div class="key-row-summary-line key-history-brief">{{ key_history_brief(h) }}</div> <div class="key-row-summary-line key-history-brief">{{ key_history_brief(h) }}</div>
<div class="pos-meta"> <div class="pos-meta">
<span class="pos-meta-item">类型: {{ h.monitor_type }}</span> <span class="pos-meta-item">类型: {{ key_monitor_type_label(h) }}</span>
<span class="pos-meta-item">结案: {{ key_history_outcome_label(h) }}{% if h.close_reason %} ({{ h.close_reason }}){% endif %}</span> <span class="pos-meta-item">结案: {{ key_history_outcome_label(h) }}{% if h.close_reason %} ({{ h.close_reason }}){% endif %}</span>
<span class="pos-meta-item">时间: {{ h.closed_at or '—' }}</span> <span class="pos-meta-item">时间: {{ h.closed_at or '—' }}</span>
</div> </div>
@@ -318,3 +321,4 @@ document.querySelectorAll(".key-row-collapse").forEach((row)=>{
}); });
}); });
</script> </script>
<script src="/static/key_monitor_form.js?v=1"></script>
@@ -40,7 +40,7 @@
<td class="key-rule-cell">占当日开仓意图<br>全仓模式可用</td> <td class="key-rule-cell">占当日开仓意图<br>全仓模式可用</td>
</tr> </tr>
<tr> <tr>
<td class="key-rule-type">阻力 / 支撑</td> <td class="key-rule-type">关键支撑阻力</td>
<td class="key-rule-cell">双向;填上/下沿</td> <td class="key-rule-cell">双向;填上/下沿</td>
<td class="key-rule-cell">{{ r.tf }} 收盘破上沿或下沿<br>上沿优先</td> <td class="key-rule-cell">{{ r.tf }} 收盘破上沿或下沿<br>上沿优先</td>
<td class="key-rule-cell">无(仅提醒)</td> <td class="key-rule-cell">无(仅提醒)</td>
+27
View File
@@ -0,0 +1,27 @@
import unittest
from key_monitor_lib import (
KEY_MONITOR_RS_TYPE,
is_rs_key_monitor_type,
rs_monitor_type_for_storage,
rs_monitor_type_label,
)
class KeyMonitorRsTypeTests(unittest.TestCase):
def test_legacy_types_still_recognized(self):
self.assertTrue(is_rs_key_monitor_type("关键阻力位"))
self.assertTrue(is_rs_key_monitor_type("关键支撑位"))
def test_storage_normalizes_to_unified_type(self):
self.assertEqual(rs_monitor_type_for_storage("关键阻力位"), KEY_MONITOR_RS_TYPE)
self.assertEqual(rs_monitor_type_for_storage("关键支撑位"), KEY_MONITOR_RS_TYPE)
self.assertEqual(rs_monitor_type_for_storage(KEY_MONITOR_RS_TYPE), KEY_MONITOR_RS_TYPE)
def test_label_merges_legacy_display(self):
self.assertEqual(rs_monitor_type_label("关键阻力位"), KEY_MONITOR_RS_TYPE)
self.assertEqual(rs_monitor_type_label("箱体突破"), "箱体突破")
if __name__ == "__main__":
unittest.main()