增加复盘选择

This commit is contained in:
dekun
2026-05-11 08:45:17 +08:00
parent 76fae8b4bd
commit 02a4bb10ab
3 changed files with 88 additions and 9 deletions
Binary file not shown.
+24 -6
View File
@@ -827,13 +827,30 @@ ENTRY_REASON_OPTIONS = (
"趋势空头:小分歧高吸入场(左侧),确认条件:二次探顶",
"波段单:5m顺势突破,确认条件:2根k线+成交量放大+4h同向+日成交量前20",
)
# 复盘表单「其他」选项的 value(非入库值;自定义文本走 entry_reason_custom
ENTRY_REASON_OTHER = "__OTHER__"
def normalize_entry_reason(raw):
def normalize_entry_reason(raw, custom_text=None):
v = str(raw or "").strip()
if v == ENTRY_REASON_OTHER:
c = str(custom_text or "").strip()
return c[:2000] if c else ""
return v if v in ENTRY_REASON_OPTIONS else ""
def entry_reason_valid_for_storage(s):
"""允许五种固定整句、或自定义短文本(不含未解析的 __OTHER__ 占位)。"""
t = str(s or "").strip()
if not t:
return True
if t == ENTRY_REASON_OTHER:
return False
if t in ENTRY_REASON_OPTIONS:
return True
return 1 <= len(t) <= 2000
def normalize_early_exit_trigger(raw):
v = str(raw or "").strip()
return v if v in EARLY_EXIT_TRIGGERS else ""
@@ -865,7 +882,7 @@ def ai_extract_journal_from_image(image_b64):
2) 时间输出为 YYYY-MM-DDTHH:MM(用于 HTML datetime-local),无法识别填空字符串。
3) 不要猜测主观原因;early_exit_note(仅手工平仓)、note 默认留空,除非图中明确写出。
4) 允许字段为空。
5) entry_reason 只能从下列完整字符串中选一个(一字不差;截图无法归类则填空字符串):
5) entry_reason:优先从下列完整字符串中选一个(一字不差);若无法归类则可将简述写入 entry_reason(保存时也可选表单「其他」手写):
- 趋势多头:4h大结构突破前进场,确认条件:三次探顶,5m收敛不创新低
- 趋势空头:4h大结构突破前进场,确认条件:三次探底,5m收敛不创新高
- 趋势多头:小分歧低吸入场(左侧),确认条件:二次探底
@@ -3900,6 +3917,7 @@ def render_main_page(page="trade"):
occupied_miss_total=occupied_miss_total,
price_fmt=format_price_for_symbol,
entry_reason_options=list(ENTRY_REASON_OPTIONS),
entry_reason_other_value=ENTRY_REASON_OTHER,
exchange_display=EXCHANGE_DISPLAY_NAME,
)
@@ -5031,9 +5049,9 @@ def add_miss():
@login_required
def add_journal():
d = request.form
entry_reason_norm = normalize_entry_reason(d.get("entry_reason"))
entry_reason_norm = normalize_entry_reason(d.get("entry_reason"), d.get("entry_reason_custom"))
if not entry_reason_norm:
flash("请选择开仓类型(五种之一)")
flash("请选择开仓类型;若选「其他」请在下方填写自定义说明")
return redirect("/records")
early_exit_trigger = normalize_early_exit_trigger(d.get("early_exit_trigger"))
early_exit_note = str(d.get("early_exit_note") or "").strip()
@@ -5356,8 +5374,8 @@ def api_trade_record_review_update():
reviewed_entry_reason_update = _MISSING_ER
if "reviewed_entry_reason" in payload:
s = str(payload.get("reviewed_entry_reason") or "").strip()
if s and not normalize_entry_reason(s):
return jsonify({"ok": False, "msg": "开仓类型须为五种固定枚举整句之一或留空"}), 400
if s and not entry_reason_valid_for_storage(s):
return jsonify({"ok": False, "msg": "开仓类型须为五种固定整句之一、自定义说明(2000字内)或留空"}), 400
reviewed_entry_reason_update = s or None
conn = get_db()
+64 -3
View File
@@ -36,6 +36,10 @@
font-size:.8rem;
line-height:1.35;
}
.journal-card .form-grid input[name="entry_reason_custom"]{
grid-column:1/-1;
font-size:.8rem;
}
input,select,button,textarea{padding:8px 10px;border-radius:8px;border:1px solid #2e2e45;background:#1a1a29;color:#fff;font-size:.88rem;outline:none}
button{background:linear-gradient(90deg,#4285f4,#7b42ff);border:none;cursor:pointer}
.list{display:flex;flex-direction:column;gap:8px;margin-top:8px;max-height:240px;overflow:auto}
@@ -427,12 +431,14 @@
<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="平仓如何触发">
@@ -535,6 +541,42 @@
<script>
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }};
const JOURNAL_ENTRY_REASON_OTHER = {{ entry_reason_other_value | tojson }};
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 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 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";}
@@ -648,7 +690,7 @@ function editTradeRecordReview(t){
const result = prompt("结果(止盈/止损/保本止盈/移动止盈/手动平仓)", String(t.result || ""));
if(result === null) return;
const note = prompt("备注(可空)", String(t.miss_reason || "")) ?? "";
const entryHint = "开仓类型(五种固定枚举整句,与复盘表单一致;留空=本次不改该项)";
const entryHint = "开仓类型五种固定整句、或自定义说明(2000字内;与复盘表单一致;留空=本次不改该项)";
const entryIn = prompt(entryHint, String(t.effective_entry_reason || ""));
if(entryIn === null) return;
const payload = {
@@ -887,6 +929,8 @@ function fillJournalFromTrade(t){
setJournalField("early_exit_trigger", "");
setJournalField("early_exit_note", "");
setJournalField("entry_reason", "");
setJournalField("entry_reason_custom", "");
syncJournalEntryReasonOtherUi();
const er = String(t.result || "").trim();
const exitTrigMap = { 保本止盈: "保本止盈", 移动止盈: "移动止盈", 手动平仓: "手动平仓", 止损: "止损" };
if(exitTrigMap[er]) setJournalField("early_exit_trigger", exitTrigMap[er]);
@@ -925,10 +969,18 @@ function prefillJournalByImage(){
setJournalField("expect_rr", d.expect_rr || "");
setJournalField("real_rr", d.real_rr || "");
let entryReason = String(d.entry_reason || "").trim();
if(!JOURNAL_ENTRY_REASON_OPTIONS || !JOURNAL_ENTRY_REASON_OPTIONS.includes(entryReason)){
let customEr = "";
if(JOURNAL_ENTRY_REASON_OPTIONS && JOURNAL_ENTRY_REASON_OPTIONS.includes(entryReason)){
// keep
} else if(entryReason){
customEr = entryReason;
entryReason = JOURNAL_ENTRY_REASON_OTHER;
} else {
entryReason = "";
}
setJournalField("entry_reason", entryReason);
setJournalField("entry_reason_custom", customEr);
syncJournalEntryReasonOtherUi();
const trig = (d.early_exit_trigger || "").trim();
let noteEx = (d.early_exit_note || "").trim();
const legacy = (d.early_exit_reason || "").trim();
@@ -1213,6 +1265,15 @@ if(sltpModeEl){
}
refreshAccountSnapshot();
const _journalFormEl = document.getElementById("journal-form");
if(_journalFormEl){
_journalFormEl.addEventListener("submit", function(ev){
if(!validateJournalEntryReason()) ev.preventDefault();
});
const _jErSel = _journalFormEl.querySelector('[name="entry_reason"]');
if(_jErSel) _jErSel.addEventListener("change", syncJournalEntryReasonOtherUi);
syncJournalEntryReasonOtherUi();
}
refreshOrderDefaults();
refreshPriceSnapshot();
setInterval(refreshAccountSnapshot, {{ balance_refresh_seconds * 1000 }});