Move entry scheme to active plans only, required on archive.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-22 16:56:34 +08:00
parent a837cfd14c
commit ed3709dddf
6 changed files with 96 additions and 15 deletions
+6 -1
View File
@@ -129,7 +129,7 @@ def _row_to_dict(row: sqlite3.Row | None) -> dict[str, Any] | None:
d["direction_label"] = DIRECTIONS.get(d.get("direction") or "", d.get("direction") or "")
d["entry_scheme_label"] = ENTRY_SCHEMES.get(
d.get("entry_scheme") or "", d.get("entry_scheme") or ""
)
) or "待填写"
res = d.get("result")
d["result_label"] = RESULTS.get(res, "") if res else ""
return d
@@ -157,6 +157,8 @@ def create_entry_plan(payload: dict[str, Any], *, db_path: Path | None = None) -
trend_tf = _validate_choice(payload.get("trend_timeframe"), TREND_TIMEFRAMES, "趋势周期")
entry_tf = _validate_choice(payload.get("entry_timeframe"), ENTRY_TIMEFRAMES, "入场周期")
direction = _validate_choice(payload.get("direction"), DIRECTIONS, "方向")
entry_scheme = ""
if payload.get("entry_scheme"):
entry_scheme = _validate_choice(payload.get("entry_scheme"), ENTRY_SCHEMES, "入场方案")
target_level = str(payload.get("target_level") or "").strip()
current_range = str(payload.get("current_range") or "").strip()
@@ -295,6 +297,9 @@ def update_entry_plan(
now = _now_ms()
fields["updated_at"] = now
if archive_now:
scheme_val = fields.get("entry_scheme", row["entry_scheme"])
if not str(scheme_val or "").strip():
raise ValueError("归档前请在进行中计划里选择入场方案")
fields["status"] = "archived"
fields["archived_at"] = now
sets = ", ".join(f"{k}=?" for k in fields)
+6
View File
@@ -6599,6 +6599,12 @@ body.funds-fullscreen-open {
color: var(--text);
opacity: 0.9;
}
.plan-scheme-row {
margin-top: 6px;
}
.plan-field-scheme select {
min-width: 160px;
}
.plan-close-row {
display: flex;
flex-wrap: wrap;
+1 -5
View File
@@ -109,10 +109,6 @@
<span>当前区间</span>
<input id="plan-create-range" type="text" placeholder="如 67000-68000" />
</label>
<label class="plan-field plan-field-full">
<span>入场方案</span>
<select id="plan-create-scheme" required></select>
</label>
<label class="plan-field plan-field-full">
<span>备注</span>
<textarea id="plan-create-note" rows="2" placeholder="计划说明…"></textarea>
@@ -814,7 +810,7 @@
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
<script src="/assets/chart_draw.js?v=20260609-market-day-split"></script>
<script src="/assets/chart.js?v=20260609-prev-day-lines"></script>
<script src="/assets/plan.js?v=20260614-entry-plan"></script>
<script src="/assets/plan.js?v=20260614-entry-plan-scheme"></script>
<script src="/assets/archive.js?v=20260612-archive-ai-chat"></script>
<script src="/assets/funds.js?v=20260609-hub-funds-fold"></script>
<script src="/assets/dashboard.js?v=20260612-dash-monitor-count"></script>
+50 -5
View File
@@ -115,7 +115,6 @@
fillSelect($("plan-create-type"), meta.plan_types, "value", "label");
fillSelect($("plan-create-trend-tf"), meta.trend_timeframes);
fillSelect($("plan-create-entry-tf"), meta.entry_timeframes);
fillSelect($("plan-create-scheme"), meta.entry_schemes, "value", "label");
renderDirectionRadios($("plan-create-direction"), "plan-direction", "long");
const dateEl = $("plan-create-date");
if (dateEl && !dateEl.value) dateEl.value = todayIso();
@@ -133,6 +132,21 @@
);
}
function schemeOptionsHtml(selected) {
let html = '<option value="">请选择</option>';
(meta.entry_schemes || []).forEach(function (s) {
html +=
'<option value="' +
esc(s.value) +
'"' +
(selected === s.value ? " selected" : "") +
">" +
esc(s.label) +
"</option>";
});
return html;
}
function renderActiveList() {
const host = $("plan-active-list");
const cnt = $("plan-active-count");
@@ -166,8 +180,6 @@
esc(p.trend_timeframe) +
" / 入场 " +
esc(p.entry_timeframe) +
" · " +
esc(p.entry_scheme_label || p.entry_scheme) +
"</div>" +
'<div class="plan-active-levels">目标 ' +
esc(p.target_level || "—") +
@@ -175,6 +187,14 @@
esc(p.current_range || "—") +
"</div>" +
(p.note ? '<div class="plan-active-note">' + esc(p.note) + "</div>" : "") +
'<div class="plan-scheme-row">' +
'<label class="plan-field plan-field-inline plan-field-scheme"><span>入场方案</span>' +
'<select class="plan-active-scheme" data-id="' +
esc(p.id) +
'">' +
schemeOptionsHtml(p.entry_scheme || "") +
"</select></label>" +
"</div>" +
'<div class="plan-close-row">' +
'<label class="plan-field plan-field-inline"><span>结果</span>' +
'<select class="plan-close-result" data-id="' +
@@ -341,7 +361,6 @@
direction: (dir && dir.value) || "",
target_level: ($("plan-create-target") && $("plan-create-target").value) || "",
current_range: ($("plan-create-range") && $("plan-create-range").value) || "",
entry_scheme: ($("plan-create-scheme") && $("plan-create-scheme").value) || "",
note: ($("plan-create-note") && $("plan-create-note").value) || "",
};
}
@@ -555,12 +574,18 @@
const card = t.closest(".plan-active-card");
const resultEl = card && card.querySelector('.plan-close-result[data-id="' + id + '"]');
const pnlEl = card && card.querySelector('.plan-close-pnl[data-id="' + id + '"]');
const schemeEl = card && card.querySelector('.plan-active-scheme[data-id="' + id + '"]');
const result = resultEl && resultEl.value;
if (!result) {
toast("请先选择结果(盈/亏)", true);
return;
}
const payload = { result: result };
const scheme = schemeEl && schemeEl.value;
if (!scheme) {
toast("请先选择入场方案(根据实际进场填写)", true);
return;
}
const payload = { result: result, entry_scheme: scheme };
const pnlRaw = pnlEl && pnlEl.value;
if (pnlRaw !== "" && pnlRaw != null) payload.pnl_amount = Number(pnlRaw);
api("/api/entry-plans/" + id, {
@@ -577,6 +602,26 @@
});
}
});
activeList.addEventListener("change", function (ev) {
const t = ev.target;
if (!(t instanceof HTMLElement) || !t.classList.contains("plan-active-scheme")) return;
const id = t.getAttribute("data-id");
const scheme = t.value;
if (!id || !scheme) return;
api("/api/entry-plans/" + id, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ entry_scheme: scheme }),
})
.then(function () {
toast("入场方案已保存");
return loadActive();
})
.catch(function (e) {
toast(e.message || "保存失败", true);
});
});
}
const historyList = $("plan-history-list");
+3 -3
View File
@@ -29,15 +29,15 @@
| 方向 | 多 / 空 |
| 目标位 | 文本 |
| 当前区间 | 文本 |
| 入场方案 | 突破方案 / 假突破突破方案 / 箱体拐点方案 |
| 入场方案 | **仅进行中**填写:突破 / 假突破 / 箱体拐点(根据实际进场选择;归档前必选) |
| 结果 | **仅进行中**可填:盈 / 亏;**必选其一才归档** |
| 盈亏 | **可选**数字(U),不参与是否归档 |
| 备注 | 文本 |
## 业务流程
1. **新建** → 状态 `active`(进行中)
2. **修改** → 可改入场方案、备注、价位等(弹窗)
1. **新建** → 状态 `active`(进行中)**不含入场方案**
2. **进行中**选择/修改 **入场方案**(根据实际进场填写),可改备注、价位等
3. **删除** → 仅 **未填结果** 的进行中计划可删
4. **归档** → 在进行中选择 **盈/亏** 并点「填写结果并归档」→ 状态 `archived`,移入计划历史
+29
View File
@@ -40,6 +40,35 @@ def test_normalize_plan_symbol():
assert normalize_plan_symbol("ETH/USDT") == "ETH/USDT"
def test_create_without_entry_scheme():
with tempfile.TemporaryDirectory() as td:
db = Path(td) / "plans.db"
payload = _base_payload()
del payload["entry_scheme"]
row = create_entry_plan(payload, db_path=db)
assert row["entry_scheme"] == ""
assert row["entry_scheme_label"] == "待填写"
def test_archive_requires_entry_scheme():
with tempfile.TemporaryDirectory() as td:
db = Path(td) / "plans.db"
payload = _base_payload()
del payload["entry_scheme"]
row = create_entry_plan(payload, db_path=db)
try:
update_entry_plan(int(row["id"]), {"result": "win"}, db_path=db)
assert False, "expected ValueError"
except ValueError as e:
assert "入场方案" in str(e)
updated = update_entry_plan(
int(row["id"]),
{"entry_scheme": "breakout", "result": "win"},
db_path=db,
)
assert updated["status"] == "archived"
def test_create_list_delete_active_plan():
with tempfile.TemporaryDirectory() as td:
db = Path(td) / "plans.db"