修复一些bug
This commit is contained in:
@@ -39,6 +39,8 @@ from history_window_lib import (
|
||||
PRESET_UTC_LAST24H,
|
||||
PRESET_UTC_LAST7D,
|
||||
PRESET_UTC_TODAY,
|
||||
list_window_redirect_query,
|
||||
resolve_list_window,
|
||||
resolve_window,
|
||||
utc_window_to_bj_sql_strings,
|
||||
)
|
||||
@@ -884,6 +886,7 @@ ENTRY_REASON_OPTIONS = (
|
||||
"趋势多头:小分歧低吸入场(左侧),确认条件:二次探底",
|
||||
"趋势空头:小分歧高吸入场(左侧),确认条件:二次探顶",
|
||||
"波段单:5m顺势突破,确认条件:2根k线+成交量放大+4h同向+日成交量前20",
|
||||
"趋势回调",
|
||||
)
|
||||
# 复盘表单「其他」选项的 value(非入库值;自定义文本走 entry_reason_custom)
|
||||
ENTRY_REASON_OTHER = "__OTHER__"
|
||||
@@ -898,7 +901,7 @@ def normalize_entry_reason(raw, custom_text=None):
|
||||
|
||||
|
||||
def entry_reason_valid_for_storage(s):
|
||||
"""允许五种固定整句、或自定义短文本(不含未解析的 __OTHER__ 占位)。"""
|
||||
"""允许固定开仓类型选项、或自定义短文本(不含未解析的 __OTHER__ 占位)。"""
|
||||
t = str(s or "").strip()
|
||||
if not t:
|
||||
return True
|
||||
@@ -946,6 +949,7 @@ def ai_extract_journal_from_image(image_b64):
|
||||
- 趋势多头:小分歧低吸入场(左侧),确认条件:二次探底
|
||||
- 趋势空头:小分歧高吸入场(左侧),确认条件:二次探顶
|
||||
- 波段单:5m顺势突破,确认条件:2根k线+成交量放大+4h同向+日成交量前20
|
||||
- 趋势回调
|
||||
6) early_exit_trigger 只能从下列取值中选一个(无法识别则填空字符串):保本止盈、移动止盈、手动平仓、止损、其他。
|
||||
7) 若触发为「手动平仓」,early_exit_note 必须写出图中可见的补充说明;其他触发类型 early_exit_note 留空。
|
||||
8) 若图中有无法归类的离场说明原文,可放进 early_exit_note,early_exit_trigger 填「其他」或留空。
|
||||
@@ -3567,7 +3571,12 @@ def trend_plan_history_status_label(status):
|
||||
|
||||
|
||||
def _list_window_from_request():
|
||||
return resolve_window(request.args, default_preset=PRESET_UTC_TODAY)
|
||||
return resolve_list_window(request.args, session, default_preset=PRESET_UTC_TODAY)
|
||||
|
||||
|
||||
def _redirect_records():
|
||||
qs = list_window_redirect_query(session)
|
||||
return redirect(f"/records?{qs}" if qs else "/records")
|
||||
|
||||
|
||||
def calc_trend_manual_breakeven_stop(direction, entry_price, offset_pct=None):
|
||||
@@ -6700,7 +6709,7 @@ def add_miss():
|
||||
conn.commit()
|
||||
conn.close()
|
||||
flash("已记录错过机会")
|
||||
return redirect("/records")
|
||||
return _redirect_records()
|
||||
|
||||
|
||||
@app.route("/add_journal", methods=["POST"])
|
||||
@@ -6710,15 +6719,15 @@ def add_journal():
|
||||
entry_reason_norm = normalize_entry_reason(d.get("entry_reason"), d.get("entry_reason_custom"))
|
||||
if not entry_reason_norm:
|
||||
flash("请选择开仓类型;若选「其他」请在下方填写自定义说明")
|
||||
return redirect("/records")
|
||||
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()
|
||||
if not early_exit_trigger:
|
||||
flash("请选择离场触发")
|
||||
return redirect("/records")
|
||||
return _redirect_records()
|
||||
if early_exit_trigger == "手动平仓" and not early_exit_note:
|
||||
flash("手工平仓必须填写补充说明")
|
||||
return redirect("/records")
|
||||
return _redirect_records()
|
||||
if early_exit_trigger != "手动平仓":
|
||||
early_exit_note = ""
|
||||
# 兼容字段:仅「手工平仓」记为「主观提前」语义下的「是」
|
||||
@@ -6816,14 +6825,20 @@ def add_journal():
|
||||
flash(f"交易复盘记录已保存。{chart_msg}")
|
||||
else:
|
||||
flash("交易复盘记录已保存")
|
||||
return redirect("/records")
|
||||
return _redirect_records()
|
||||
|
||||
|
||||
@app.route("/api/journals")
|
||||
@login_required
|
||||
def api_journals():
|
||||
win = _list_window_from_request()
|
||||
start_bj, end_bj = utc_window_to_bj_sql_strings(win["start_utc"], win["end_utc"], APP_TZ)
|
||||
conn = get_db()
|
||||
rows = conn.execute("SELECT * FROM journal_entries ORDER BY created_at DESC").fetchall()
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM journal_entries WHERE COALESCE(close_datetime, created_at, open_datetime) >= ? "
|
||||
"AND COALESCE(close_datetime, created_at, open_datetime) <= ? ORDER BY created_at DESC LIMIT 500",
|
||||
(start_bj, end_bj),
|
||||
).fetchall()
|
||||
conn.close()
|
||||
result = []
|
||||
for r in rows:
|
||||
@@ -6871,8 +6886,13 @@ def delete_journal(jid):
|
||||
@app.route("/api/reviews")
|
||||
@login_required
|
||||
def api_reviews():
|
||||
win = _list_window_from_request()
|
||||
start_bj, end_bj = utc_window_to_bj_sql_strings(win["start_utc"], win["end_utc"], APP_TZ)
|
||||
conn = get_db()
|
||||
rows = conn.execute("SELECT * FROM ai_reviews ORDER BY created_at DESC").fetchall()
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM ai_reviews WHERE created_at >= ? AND created_at <= ? ORDER BY created_at DESC LIMIT 200",
|
||||
(start_bj, end_bj),
|
||||
).fetchall()
|
||||
conn.close()
|
||||
return jsonify([row_to_dict(r) for r in rows])
|
||||
|
||||
|
||||
@@ -380,45 +380,6 @@
|
||||
<button type="submit" {% if not can_trade %}disabled style="opacity:.5;cursor:not-allowed"{% endif %}>生成预览</button>
|
||||
</form>
|
||||
<script>
|
||||
|
||||
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 || "/records";
|
||||
window.location.href = qs ? (path + "?" + qs) : path;
|
||||
}
|
||||
function attachListWindowToExports(){
|
||||
const qs = listWindowQueryString();
|
||||
if(!qs) return;
|
||||
document.querySelectorAll('.export-bar a[href^="/export/trade_records"]').forEach(a=>{
|
||||
const base = a.getAttribute("href").split("?")[0];
|
||||
a.setAttribute("href", base + "?" + qs);
|
||||
});
|
||||
}
|
||||
|
||||
(function(){
|
||||
const dirSel = document.getElementById("trend-direction");
|
||||
const addInp = document.getElementById("trend-add-upper");
|
||||
@@ -584,7 +545,13 @@ function attachListWindowToExports(){
|
||||
|
||||
{% if page == 'records' %}
|
||||
<div class="card full records-card">
|
||||
<h2>交易记录</h2>
|
||||
<h2>交易记录 & 错过机会</h2>
|
||||
<div class="form-row" style="margin-bottom:10px;gap:8px">
|
||||
<label style="display:flex;align-items:center;gap:6px;font-size:.82rem;color:#cfd3ef">
|
||||
<input id="review-mode-toggle" type="checkbox">
|
||||
修改/核对开关(开启后可编辑关键字段)
|
||||
</label>
|
||||
</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>
|
||||
@@ -612,6 +579,41 @@ function attachListWindowToExports(){
|
||||
{% else %}<span class="badge miss">{{ effective_result }}</span>{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
class="table-del"
|
||||
style="background:#1f3a5a;color:#8fc8ff;margin-right:6px"
|
||||
onclick='fillJournalFromTrade({{ {
|
||||
"symbol": r.symbol,
|
||||
"monitor_type": r.monitor_type,
|
||||
"direction": r.direction,
|
||||
"trigger_price": r.trigger_price,
|
||||
"stop_loss": stop_show,
|
||||
"take_profit": tp_show,
|
||||
"opened_at": r.effective_opened_at,
|
||||
"closed_at": r.effective_closed_at,
|
||||
"pnl_amount": r.effective_pnl_amount,
|
||||
"result": r.effective_result,
|
||||
"risk_amount": r.risk_amount
|
||||
}|tojson|safe }})'
|
||||
>填入复盘</button>
|
||||
<button
|
||||
type="button"
|
||||
class="table-del review-edit-btn"
|
||||
style="background:#1f3a5a;color:#8fc8ff;margin-right:6px"
|
||||
onclick='editTradeRecordReview({{ {
|
||||
"id": r.id,
|
||||
"opened_at": r.effective_opened_at,
|
||||
"closed_at": r.effective_closed_at,
|
||||
"stop_loss": stop_show,
|
||||
"take_profit": tp_show,
|
||||
"pnl_amount": r.effective_pnl_amount,
|
||||
"result": r.effective_result,
|
||||
"miss_reason": r.effective_miss_reason,
|
||||
"effective_entry_reason": r.effective_entry_reason or ""
|
||||
}|tojson|safe }})'
|
||||
disabled
|
||||
>核对修改</button>
|
||||
<button type="button" class="table-del" onclick="deleteTradeRecord({{ r.id }})">删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -641,6 +643,90 @@ function attachListWindowToExports(){
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card journal-card">
|
||||
<h2>交易复盘记录上传(含截图)</h2>
|
||||
<form id="journal-form" action="/add_journal" method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="risk_amount_hint" id="risk-amount-hint">
|
||||
<input type="hidden" name="entry_price_hint" id="entry-price-hint">
|
||||
<input type="hidden" name="stop_loss_hint" id="stop-loss-hint">
|
||||
<input type="hidden" name="direction_hint" id="direction-hint">
|
||||
<div class="form-grid">
|
||||
<input type="datetime-local" name="open_datetime" required>
|
||||
<input type="datetime-local" name="close_datetime" required>
|
||||
<input name="coin" placeholder="BTC" required>
|
||||
<input name="tf" placeholder="5m" required>
|
||||
<input name="pnl" placeholder="盈亏(U)" required>
|
||||
<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>
|
||||
<option value="止损">止损</option>
|
||||
<option value="其他">其他</option>
|
||||
</select>
|
||||
<input name="early_exit_note" id="early-exit-note" placeholder="离场补充(仅手工平仓必填)">
|
||||
<select name="post_breakeven_stare"><option value="否">保本后盯盘:否</option><option value="是">保本后盯盘:是</option></select>
|
||||
<select name="new_trade_while_occupied"><option value="否">占用时新开仓:否</option><option value="是">占用时新开仓:是</option></select>
|
||||
<input id="journal-screenshot" type="file" name="screenshot" accept="image/*">
|
||||
</div>
|
||||
<div class="form-row" style="margin-top:8px">
|
||||
<label style="display:flex;align-items:center;gap:6px;font-size:.82rem;color:#cfd3ef">
|
||||
<input type="checkbox" name="journal_exchange_chart" value="true" checked>
|
||||
保存时自动生成多周期K线图(4h/1h/15m/5m 各100)并作为截图
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-row" style="margin-top:8px">
|
||||
<button type="button" style="background:#1f3a5a" onclick="prefillJournalByImage()">AI识别预填(你再手动改原因)</button>
|
||||
</div>
|
||||
<div class="mood-grid" style="margin-top:8px">
|
||||
<label><input type="checkbox" name="mood_issues" value="怕踏空">怕踏空</label>
|
||||
<label><input type="checkbox" name="mood_issues" value="报复开仓">报复开仓</label>
|
||||
<label><input type="checkbox" name="mood_issues" value="盈利飘了">盈利飘了</label>
|
||||
<label><input type="checkbox" name="mood_issues" value="拿不住单">拿不住单</label>
|
||||
<label><input type="checkbox" name="mood_issues" value="扛单">扛单</label>
|
||||
<label><input type="checkbox" name="mood_issues" value="重仓违规">重仓违规</label>
|
||||
</div>
|
||||
<textarea name="note" rows="2" style="width:100%;margin-top:8px" placeholder="备注"></textarea>
|
||||
<button type="submit" style="margin-top:8px">保存复盘记录</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card full review-card">
|
||||
<h2>AI复盘(按交易记录)</h2>
|
||||
<div class="form-row">
|
||||
<input type="date" id="day_date">
|
||||
<button type="button" onclick="genDaily()">生成日复盘</button>
|
||||
<button type="button" onclick="exportDailyBundleMd()" style="background:#1f3a5a">导出当日日复盘MD</button>
|
||||
<input type="date" id="week_start">
|
||||
<input type="date" id="week_end">
|
||||
<button type="button" onclick="genWeekly()">生成周复盘</button>
|
||||
<button type="button" onclick="exportWeeklyBundleMd()" style="background:#1f3a5a">导出当周复盘MD</button>
|
||||
</div>
|
||||
<div id="daily_result" class="ai-result" style="display:none"></div>
|
||||
<div id="weekly_result" class="ai-result" style="display:none"></div>
|
||||
<div class="panel-list" style="margin-top:10px">
|
||||
<div class="panel-item">
|
||||
<strong>交易复盘记录</strong>
|
||||
<div id="journal-list"></div>
|
||||
</div>
|
||||
<div class="panel-item">
|
||||
<strong>AI历史复盘</strong>
|
||||
<div id="review-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -926,7 +1012,7 @@ function editTradeRecordReview(t){
|
||||
const result = prompt("结果(止盈/止损/保本止盈/移动止盈/手动平仓)", String(t.result || ""));
|
||||
if(result === null) return;
|
||||
const note = prompt("备注(可空)", String(t.miss_reason || "")) ?? "";
|
||||
const entryHint = "开仓类型:五种固定整句、或自定义说明(2000字内;与复盘表单一致;留空=本次不改该项)";
|
||||
const entryHint = "开仓类型:固定选项整句、或自定义说明(2000字内;与复盘表单一致;留空=本次不改该项)";
|
||||
const entryIn = prompt(entryHint, String(t.effective_entry_reason || ""));
|
||||
if(entryIn === null) return;
|
||||
const payload = {
|
||||
@@ -980,7 +1066,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=>{
|
||||
@@ -1002,7 +1089,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=>{
|
||||
@@ -1167,6 +1255,10 @@ function fillJournalFromTrade(t){
|
||||
setJournalField("entry_reason", "");
|
||||
setJournalField("entry_reason_custom", "");
|
||||
syncJournalEntryReasonOtherUi();
|
||||
if(String(t.monitor_type || "").trim() === "趋势回调" && JOURNAL_ENTRY_REASON_OPTIONS.includes("趋势回调")){
|
||||
setJournalField("entry_reason", "趋势回调");
|
||||
syncJournalEntryReasonOtherUi();
|
||||
}
|
||||
const er = String(t.result || "").trim();
|
||||
const exitTrigMap = { 保本止盈: "保本止盈", 移动止盈: "移动止盈", 手动平仓: "手动平仓", 止损: "止损" };
|
||||
if(exitTrigMap[er]) setJournalField("early_exit_trigger", exitTrigMap[er]);
|
||||
@@ -1259,6 +1351,44 @@ function toggleStatsCard(){
|
||||
btn.innerText = collapsed ? "展开" : "折叠";
|
||||
}
|
||||
|
||||
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 || "/records";
|
||||
window.location.href = qs ? (path + "?" + qs) : path;
|
||||
}
|
||||
function attachListWindowToExports(){
|
||||
const qs = listWindowQueryString();
|
||||
if(!qs) return;
|
||||
document.querySelectorAll('.export-bar a[href^="/export/trade_records"]').forEach(a=>{
|
||||
const base = a.getAttribute("href").split("?")[0];
|
||||
a.setAttribute("href", base + "?" + qs);
|
||||
});
|
||||
}
|
||||
|
||||
attachListWindowToExports();
|
||||
toggleListWindowCustom();
|
||||
if(document.getElementById("journal-list")) loadJournals();
|
||||
|
||||
Reference in New Issue
Block a user