Move entry scheme to active plans only, required on archive.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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["direction_label"] = DIRECTIONS.get(d.get("direction") or "", d.get("direction") or "")
|
||||||
d["entry_scheme_label"] = ENTRY_SCHEMES.get(
|
d["entry_scheme_label"] = ENTRY_SCHEMES.get(
|
||||||
d.get("entry_scheme") or "", d.get("entry_scheme") or ""
|
d.get("entry_scheme") or "", d.get("entry_scheme") or ""
|
||||||
)
|
) or "待填写"
|
||||||
res = d.get("result")
|
res = d.get("result")
|
||||||
d["result_label"] = RESULTS.get(res, "") if res else ""
|
d["result_label"] = RESULTS.get(res, "") if res else ""
|
||||||
return d
|
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, "趋势周期")
|
trend_tf = _validate_choice(payload.get("trend_timeframe"), TREND_TIMEFRAMES, "趋势周期")
|
||||||
entry_tf = _validate_choice(payload.get("entry_timeframe"), ENTRY_TIMEFRAMES, "入场周期")
|
entry_tf = _validate_choice(payload.get("entry_timeframe"), ENTRY_TIMEFRAMES, "入场周期")
|
||||||
direction = _validate_choice(payload.get("direction"), DIRECTIONS, "方向")
|
direction = _validate_choice(payload.get("direction"), DIRECTIONS, "方向")
|
||||||
|
entry_scheme = ""
|
||||||
|
if payload.get("entry_scheme"):
|
||||||
entry_scheme = _validate_choice(payload.get("entry_scheme"), ENTRY_SCHEMES, "入场方案")
|
entry_scheme = _validate_choice(payload.get("entry_scheme"), ENTRY_SCHEMES, "入场方案")
|
||||||
target_level = str(payload.get("target_level") or "").strip()
|
target_level = str(payload.get("target_level") or "").strip()
|
||||||
current_range = str(payload.get("current_range") or "").strip()
|
current_range = str(payload.get("current_range") or "").strip()
|
||||||
@@ -295,6 +297,9 @@ def update_entry_plan(
|
|||||||
now = _now_ms()
|
now = _now_ms()
|
||||||
fields["updated_at"] = now
|
fields["updated_at"] = now
|
||||||
if archive_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["status"] = "archived"
|
||||||
fields["archived_at"] = now
|
fields["archived_at"] = now
|
||||||
sets = ", ".join(f"{k}=?" for k in fields)
|
sets = ", ".join(f"{k}=?" for k in fields)
|
||||||
|
|||||||
@@ -6599,6 +6599,12 @@ body.funds-fullscreen-open {
|
|||||||
color: var(--text);
|
color: var(--text);
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
.plan-scheme-row {
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
.plan-field-scheme select {
|
||||||
|
min-width: 160px;
|
||||||
|
}
|
||||||
.plan-close-row {
|
.plan-close-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|||||||
@@ -109,10 +109,6 @@
|
|||||||
<span>当前区间</span>
|
<span>当前区间</span>
|
||||||
<input id="plan-create-range" type="text" placeholder="如 67000-68000" />
|
<input id="plan-create-range" type="text" placeholder="如 67000-68000" />
|
||||||
</label>
|
</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">
|
<label class="plan-field plan-field-full">
|
||||||
<span>备注</span>
|
<span>备注</span>
|
||||||
<textarea id="plan-create-note" rows="2" placeholder="计划说明…"></textarea>
|
<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="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_draw.js?v=20260609-market-day-split"></script>
|
||||||
<script src="/assets/chart.js?v=20260609-prev-day-lines"></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/archive.js?v=20260612-archive-ai-chat"></script>
|
||||||
<script src="/assets/funds.js?v=20260609-hub-funds-fold"></script>
|
<script src="/assets/funds.js?v=20260609-hub-funds-fold"></script>
|
||||||
<script src="/assets/dashboard.js?v=20260612-dash-monitor-count"></script>
|
<script src="/assets/dashboard.js?v=20260612-dash-monitor-count"></script>
|
||||||
|
|||||||
@@ -115,7 +115,6 @@
|
|||||||
fillSelect($("plan-create-type"), meta.plan_types, "value", "label");
|
fillSelect($("plan-create-type"), meta.plan_types, "value", "label");
|
||||||
fillSelect($("plan-create-trend-tf"), meta.trend_timeframes);
|
fillSelect($("plan-create-trend-tf"), meta.trend_timeframes);
|
||||||
fillSelect($("plan-create-entry-tf"), meta.entry_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");
|
renderDirectionRadios($("plan-create-direction"), "plan-direction", "long");
|
||||||
const dateEl = $("plan-create-date");
|
const dateEl = $("plan-create-date");
|
||||||
if (dateEl && !dateEl.value) dateEl.value = todayIso();
|
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() {
|
function renderActiveList() {
|
||||||
const host = $("plan-active-list");
|
const host = $("plan-active-list");
|
||||||
const cnt = $("plan-active-count");
|
const cnt = $("plan-active-count");
|
||||||
@@ -166,8 +180,6 @@
|
|||||||
esc(p.trend_timeframe) +
|
esc(p.trend_timeframe) +
|
||||||
" / 入场 " +
|
" / 入场 " +
|
||||||
esc(p.entry_timeframe) +
|
esc(p.entry_timeframe) +
|
||||||
" · " +
|
|
||||||
esc(p.entry_scheme_label || p.entry_scheme) +
|
|
||||||
"</div>" +
|
"</div>" +
|
||||||
'<div class="plan-active-levels">目标 ' +
|
'<div class="plan-active-levels">目标 ' +
|
||||||
esc(p.target_level || "—") +
|
esc(p.target_level || "—") +
|
||||||
@@ -175,6 +187,14 @@
|
|||||||
esc(p.current_range || "—") +
|
esc(p.current_range || "—") +
|
||||||
"</div>" +
|
"</div>" +
|
||||||
(p.note ? '<div class="plan-active-note">' + esc(p.note) + "</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">' +
|
'<div class="plan-close-row">' +
|
||||||
'<label class="plan-field plan-field-inline"><span>结果</span>' +
|
'<label class="plan-field plan-field-inline"><span>结果</span>' +
|
||||||
'<select class="plan-close-result" data-id="' +
|
'<select class="plan-close-result" data-id="' +
|
||||||
@@ -341,7 +361,6 @@
|
|||||||
direction: (dir && dir.value) || "",
|
direction: (dir && dir.value) || "",
|
||||||
target_level: ($("plan-create-target") && $("plan-create-target").value) || "",
|
target_level: ($("plan-create-target") && $("plan-create-target").value) || "",
|
||||||
current_range: ($("plan-create-range") && $("plan-create-range").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) || "",
|
note: ($("plan-create-note") && $("plan-create-note").value) || "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -555,12 +574,18 @@
|
|||||||
const card = t.closest(".plan-active-card");
|
const card = t.closest(".plan-active-card");
|
||||||
const resultEl = card && card.querySelector('.plan-close-result[data-id="' + id + '"]');
|
const resultEl = card && card.querySelector('.plan-close-result[data-id="' + id + '"]');
|
||||||
const pnlEl = card && card.querySelector('.plan-close-pnl[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;
|
const result = resultEl && resultEl.value;
|
||||||
if (!result) {
|
if (!result) {
|
||||||
toast("请先选择结果(盈/亏)", true);
|
toast("请先选择结果(盈/亏)", true);
|
||||||
return;
|
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;
|
const pnlRaw = pnlEl && pnlEl.value;
|
||||||
if (pnlRaw !== "" && pnlRaw != null) payload.pnl_amount = Number(pnlRaw);
|
if (pnlRaw !== "" && pnlRaw != null) payload.pnl_amount = Number(pnlRaw);
|
||||||
api("/api/entry-plans/" + id, {
|
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");
|
const historyList = $("plan-history-list");
|
||||||
|
|||||||
@@ -29,15 +29,15 @@
|
|||||||
| 方向 | 多 / 空 |
|
| 方向 | 多 / 空 |
|
||||||
| 目标位 | 文本 |
|
| 目标位 | 文本 |
|
||||||
| 当前区间 | 文本 |
|
| 当前区间 | 文本 |
|
||||||
| 入场方案 | 突破方案 / 假突破突破方案 / 箱体拐点方案 |
|
| 入场方案 | **仅进行中**填写:突破 / 假突破 / 箱体拐点(根据实际进场选择;归档前必选) |
|
||||||
| 结果 | **仅进行中**可填:盈 / 亏;**必选其一才归档** |
|
| 结果 | **仅进行中**可填:盈 / 亏;**必选其一才归档** |
|
||||||
| 盈亏 | **可选**数字(U),不参与是否归档 |
|
| 盈亏 | **可选**数字(U),不参与是否归档 |
|
||||||
| 备注 | 文本 |
|
| 备注 | 文本 |
|
||||||
|
|
||||||
## 业务流程
|
## 业务流程
|
||||||
|
|
||||||
1. **新建** → 状态 `active`(进行中)
|
1. **新建** → 状态 `active`(进行中),**不含入场方案**
|
||||||
2. **修改** → 可改入场方案、备注、价位等(弹窗)
|
2. **进行中** → 选择/修改 **入场方案**(根据实际进场填写),可改备注、价位等
|
||||||
3. **删除** → 仅 **未填结果** 的进行中计划可删
|
3. **删除** → 仅 **未填结果** 的进行中计划可删
|
||||||
4. **归档** → 在进行中选择 **盈/亏** 并点「填写结果并归档」→ 状态 `archived`,移入计划历史
|
4. **归档** → 在进行中选择 **盈/亏** 并点「填写结果并归档」→ 状态 `archived`,移入计划历史
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,35 @@ def test_normalize_plan_symbol():
|
|||||||
assert normalize_plan_symbol("ETH/USDT") == "ETH/USDT"
|
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():
|
def test_create_list_delete_active_plan():
|
||||||
with tempfile.TemporaryDirectory() as td:
|
with tempfile.TemporaryDirectory() as td:
|
||||||
db = Path(td) / "plans.db"
|
db = Path(td) / "plans.db"
|
||||||
|
|||||||
Reference in New Issue
Block a user