fix: AI daily review sqlite3.Row crash and error feedback across four exchanges
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+41
-21
@@ -21,46 +21,66 @@ def _journal_nz(v: Any, default: str = "无") -> str:
|
|||||||
return s if s else default
|
return s if s else default
|
||||||
|
|
||||||
|
|
||||||
|
def _row_get(row: Any, key: str, default: Any = None) -> Any:
|
||||||
|
"""兼容 dict 与 sqlite3.Row(Row 无 .get 方法)。"""
|
||||||
|
if row is None:
|
||||||
|
return default
|
||||||
|
getter = getattr(row, "get", None)
|
||||||
|
if callable(getter):
|
||||||
|
return getter(key, default)
|
||||||
|
try:
|
||||||
|
keys = row.keys() if hasattr(row, "keys") else ()
|
||||||
|
if key in keys:
|
||||||
|
return row[key]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
return row[key]
|
||||||
|
except (KeyError, TypeError, IndexError):
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
def journal_row_lines_for_ai(
|
def journal_row_lines_for_ai(
|
||||||
idx: int,
|
idx: int,
|
||||||
row: Mapping[str, Any],
|
row: Any,
|
||||||
*,
|
*,
|
||||||
include_hold_duration: bool = True,
|
include_hold_duration: bool = True,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""把 journal 字段拼成给 AI 的文本;四所日复盘/周复盘共用。"""
|
"""把 journal 字段拼成给 AI 的文本;四所日复盘/周复盘共用。"""
|
||||||
lines = [
|
lines = [
|
||||||
(
|
(
|
||||||
f"{idx}. {_journal_nz(row.get('coin'))} {_journal_nz(row.get('tf'))} "
|
f"{idx}. {_journal_nz(_row_get(row, 'coin'))} {_journal_nz(_row_get(row, 'tf'))} "
|
||||||
f"| 盈亏:{_journal_nz(row.get('pnl'))}U "
|
f"| 盈亏:{_journal_nz(_row_get(row, 'pnl'))}U "
|
||||||
f"| 实际RR:{_journal_nz(row.get('real_rr'))} "
|
f"| 实际RR:{_journal_nz(_row_get(row, 'real_rr'))} "
|
||||||
f"| 预期RR:{_journal_nz(row.get('expect_rr'))}"
|
f"| 预期RR:{_journal_nz(_row_get(row, 'expect_rr'))}"
|
||||||
),
|
),
|
||||||
f" 开仓逻辑:{_journal_nz(row.get('entry_reason'))}",
|
f" 开仓逻辑:{_journal_nz(_row_get(row, 'entry_reason'))}",
|
||||||
f" 平仓/离场(交易员自述):{_journal_nz(row.get('exit_reason'))}",
|
f" 平仓/离场(交易员自述):{_journal_nz(_row_get(row, 'exit_reason'))}",
|
||||||
]
|
]
|
||||||
if include_hold_duration:
|
if include_hold_duration:
|
||||||
lines.append(f" 持仓时长:{_journal_nz(row.get('hold_duration'))}")
|
lines.append(f" 持仓时长:{_journal_nz(_row_get(row, 'hold_duration'))}")
|
||||||
ee_bits = [
|
ee_bits = [
|
||||||
_journal_nz(row.get("early_exit")),
|
_journal_nz(_row_get(row, "early_exit")),
|
||||||
_journal_nz(row.get("early_exit_reason")),
|
_journal_nz(_row_get(row, "early_exit_reason")),
|
||||||
_journal_nz(row.get("early_exit_trigger")),
|
_journal_nz(_row_get(row, "early_exit_trigger")),
|
||||||
_journal_nz(row.get("early_exit_note")),
|
_journal_nz(_row_get(row, "early_exit_note")),
|
||||||
]
|
]
|
||||||
if any(x != "无" for x in ee_bits):
|
if any(x != "无" for x in ee_bits):
|
||||||
lines.append(
|
lines.append(
|
||||||
" 提前离场记录:"
|
" 提前离场记录:"
|
||||||
f"{ee_bits[0]} | 原因:{ee_bits[1]} | 触发:{ee_bits[2]} | 备注:{ee_bits[3]}"
|
f"{ee_bits[0]} | 原因:{ee_bits[1]} | 触发:{ee_bits[2]} | 备注:{ee_bits[3]}"
|
||||||
)
|
)
|
||||||
mood_bits = f"心态标签:{_journal_nz(row.get('mood_issues'))}"
|
mood_bits = f"心态标签:{_journal_nz(_row_get(row, 'mood_issues'))}"
|
||||||
if row.get("mood_score") is not None:
|
mood_score = _row_get(row, "mood_score")
|
||||||
mood_bits += f" | 自评心态分:{row.get('mood_score')}"
|
if mood_score is not None:
|
||||||
|
mood_bits += f" | 自评心态分:{mood_score}"
|
||||||
lines.append(f" {mood_bits}")
|
lines.append(f" {mood_bits}")
|
||||||
if _journal_nz(row.get("post_breakeven_stare")) != "无":
|
if _journal_nz(_row_get(row, "post_breakeven_stare")) != "无":
|
||||||
lines.append(f" 保本后盯盘:{_journal_nz(row.get('post_breakeven_stare'))}")
|
lines.append(f" 保本后盯盘:{_journal_nz(_row_get(row, 'post_breakeven_stare'))}")
|
||||||
if _journal_nz(row.get("new_trade_while_occupied")) != "无":
|
if _journal_nz(_row_get(row, "new_trade_while_occupied")) != "无":
|
||||||
lines.append(f" 占用时新开仓:{_journal_nz(row.get('new_trade_while_occupied'))}")
|
lines.append(f" 占用时新开仓:{_journal_nz(_row_get(row, 'new_trade_while_occupied'))}")
|
||||||
if _journal_nz(row.get("note")) != "无":
|
if _journal_nz(_row_get(row, "note")) != "无":
|
||||||
lines.append(f" 备注:{_journal_nz(row.get('note'))}")
|
lines.append(f" 备注:{_journal_nz(_row_get(row, 'note'))}")
|
||||||
return "\n".join(lines) + "\n"
|
return "\n".join(lines) + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1236,14 +1236,16 @@ function genDaily(){
|
|||||||
const d = document.getElementById("day_date").value;
|
const d = document.getElementById("day_date").value;
|
||||||
if(!d){alert("请选择日期");return;}
|
if(!d){alert("请选择日期");return;}
|
||||||
fetch("/ai_daily_review",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`date=${encodeURIComponent(d)}`})
|
fetch("/ai_daily_review",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`date=${encodeURIComponent(d)}`})
|
||||||
.then(r=>r.json()).then(data=>{
|
.then(r=>{ if(!r.ok) throw new Error("HTTP "+r.status); return r.json(); })
|
||||||
|
.then(data=>{
|
||||||
const el=document.getElementById("daily_result");
|
const el=document.getElementById("daily_result");
|
||||||
const wrap=document.getElementById("daily_result_wrap");
|
const wrap=document.getElementById("daily_result_wrap");
|
||||||
setAiReviewMarkdown(el, data.result);
|
setAiReviewMarkdown(el, data.result);
|
||||||
if(wrap){ wrap.style.display="block"; }
|
if(wrap){ wrap.style.display="block"; }
|
||||||
else { el.style.display="block"; }
|
else { el.style.display="block"; }
|
||||||
loadReviews();
|
loadReviews();
|
||||||
});
|
})
|
||||||
|
.catch(e=>alert("生成日复盘失败:"+(e.message||e)));
|
||||||
}
|
}
|
||||||
|
|
||||||
function genWeekly(){
|
function genWeekly(){
|
||||||
@@ -1251,14 +1253,16 @@ function genWeekly(){
|
|||||||
const e=document.getElementById("week_end").value;
|
const e=document.getElementById("week_end").value;
|
||||||
if(!s || !e){alert("请选择起止日期");return;}
|
if(!s || !e){alert("请选择起止日期");return;}
|
||||||
fetch("/ai_weekly_review",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`start_date=${encodeURIComponent(s)}&end_date=${encodeURIComponent(e)}`})
|
fetch("/ai_weekly_review",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`start_date=${encodeURIComponent(s)}&end_date=${encodeURIComponent(e)}`})
|
||||||
.then(r=>r.json()).then(data=>{
|
.then(r=>{ if(!r.ok) throw new Error("HTTP "+r.status); return r.json(); })
|
||||||
|
.then(data=>{
|
||||||
const el=document.getElementById("weekly_result");
|
const el=document.getElementById("weekly_result");
|
||||||
const wrap=document.getElementById("weekly_result_wrap");
|
const wrap=document.getElementById("weekly_result_wrap");
|
||||||
setAiReviewMarkdown(el, data.result);
|
setAiReviewMarkdown(el, data.result);
|
||||||
if(wrap){ wrap.style.display="block"; }
|
if(wrap){ wrap.style.display="block"; }
|
||||||
else { el.style.display="block"; }
|
else { el.style.display="block"; }
|
||||||
loadReviews();
|
loadReviews();
|
||||||
});
|
})
|
||||||
|
.catch(e=>alert("生成周复盘失败:"+(e.message||e)));
|
||||||
}
|
}
|
||||||
|
|
||||||
function exportDailyBundleMd(){
|
function exportDailyBundleMd(){
|
||||||
|
|||||||
@@ -1236,14 +1236,16 @@ function genDaily(){
|
|||||||
const d = document.getElementById("day_date").value;
|
const d = document.getElementById("day_date").value;
|
||||||
if(!d){alert("请选择日期");return;}
|
if(!d){alert("请选择日期");return;}
|
||||||
fetch("/ai_daily_review",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`date=${encodeURIComponent(d)}`})
|
fetch("/ai_daily_review",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`date=${encodeURIComponent(d)}`})
|
||||||
.then(r=>r.json()).then(data=>{
|
.then(r=>{ if(!r.ok) throw new Error("HTTP "+r.status); return r.json(); })
|
||||||
|
.then(data=>{
|
||||||
const el=document.getElementById("daily_result");
|
const el=document.getElementById("daily_result");
|
||||||
const wrap=document.getElementById("daily_result_wrap");
|
const wrap=document.getElementById("daily_result_wrap");
|
||||||
setAiReviewMarkdown(el, data.result);
|
setAiReviewMarkdown(el, data.result);
|
||||||
if(wrap){ wrap.style.display="block"; }
|
if(wrap){ wrap.style.display="block"; }
|
||||||
else { el.style.display="block"; }
|
else { el.style.display="block"; }
|
||||||
loadReviews();
|
loadReviews();
|
||||||
});
|
})
|
||||||
|
.catch(e=>alert("生成日复盘失败:"+(e.message||e)));
|
||||||
}
|
}
|
||||||
|
|
||||||
function genWeekly(){
|
function genWeekly(){
|
||||||
@@ -1251,14 +1253,16 @@ function genWeekly(){
|
|||||||
const e=document.getElementById("week_end").value;
|
const e=document.getElementById("week_end").value;
|
||||||
if(!s || !e){alert("请选择起止日期");return;}
|
if(!s || !e){alert("请选择起止日期");return;}
|
||||||
fetch("/ai_weekly_review",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`start_date=${encodeURIComponent(s)}&end_date=${encodeURIComponent(e)}`})
|
fetch("/ai_weekly_review",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`start_date=${encodeURIComponent(s)}&end_date=${encodeURIComponent(e)}`})
|
||||||
.then(r=>r.json()).then(data=>{
|
.then(r=>{ if(!r.ok) throw new Error("HTTP "+r.status); return r.json(); })
|
||||||
|
.then(data=>{
|
||||||
const el=document.getElementById("weekly_result");
|
const el=document.getElementById("weekly_result");
|
||||||
const wrap=document.getElementById("weekly_result_wrap");
|
const wrap=document.getElementById("weekly_result_wrap");
|
||||||
setAiReviewMarkdown(el, data.result);
|
setAiReviewMarkdown(el, data.result);
|
||||||
if(wrap){ wrap.style.display="block"; }
|
if(wrap){ wrap.style.display="block"; }
|
||||||
else { el.style.display="block"; }
|
else { el.style.display="block"; }
|
||||||
loadReviews();
|
loadReviews();
|
||||||
});
|
})
|
||||||
|
.catch(e=>alert("生成周复盘失败:"+(e.message||e)));
|
||||||
}
|
}
|
||||||
|
|
||||||
function exportDailyBundleMd(){
|
function exportDailyBundleMd(){
|
||||||
|
|||||||
@@ -1199,14 +1199,16 @@ function genDaily(){
|
|||||||
const d = document.getElementById("day_date").value;
|
const d = document.getElementById("day_date").value;
|
||||||
if(!d){alert("请选择日期");return;}
|
if(!d){alert("请选择日期");return;}
|
||||||
fetch("/ai_daily_review",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`date=${encodeURIComponent(d)}`})
|
fetch("/ai_daily_review",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`date=${encodeURIComponent(d)}`})
|
||||||
.then(r=>r.json()).then(data=>{
|
.then(r=>{ if(!r.ok) throw new Error("HTTP "+r.status); return r.json(); })
|
||||||
|
.then(data=>{
|
||||||
const el=document.getElementById("daily_result");
|
const el=document.getElementById("daily_result");
|
||||||
const wrap=document.getElementById("daily_result_wrap");
|
const wrap=document.getElementById("daily_result_wrap");
|
||||||
setAiReviewMarkdown(el, data.result);
|
setAiReviewMarkdown(el, data.result);
|
||||||
if(wrap){ wrap.style.display="block"; }
|
if(wrap){ wrap.style.display="block"; }
|
||||||
else { el.style.display="block"; }
|
else { el.style.display="block"; }
|
||||||
loadReviews();
|
loadReviews();
|
||||||
});
|
})
|
||||||
|
.catch(e=>alert("生成日复盘失败:"+(e.message||e)));
|
||||||
}
|
}
|
||||||
|
|
||||||
function genWeekly(){
|
function genWeekly(){
|
||||||
@@ -1214,14 +1216,16 @@ function genWeekly(){
|
|||||||
const e=document.getElementById("week_end").value;
|
const e=document.getElementById("week_end").value;
|
||||||
if(!s || !e){alert("请选择起止日期");return;}
|
if(!s || !e){alert("请选择起止日期");return;}
|
||||||
fetch("/ai_weekly_review",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`start_date=${encodeURIComponent(s)}&end_date=${encodeURIComponent(e)}`})
|
fetch("/ai_weekly_review",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`start_date=${encodeURIComponent(s)}&end_date=${encodeURIComponent(e)}`})
|
||||||
.then(r=>r.json()).then(data=>{
|
.then(r=>{ if(!r.ok) throw new Error("HTTP "+r.status); return r.json(); })
|
||||||
|
.then(data=>{
|
||||||
const el=document.getElementById("weekly_result");
|
const el=document.getElementById("weekly_result");
|
||||||
const wrap=document.getElementById("weekly_result_wrap");
|
const wrap=document.getElementById("weekly_result_wrap");
|
||||||
setAiReviewMarkdown(el, data.result);
|
setAiReviewMarkdown(el, data.result);
|
||||||
if(wrap){ wrap.style.display="block"; }
|
if(wrap){ wrap.style.display="block"; }
|
||||||
else { el.style.display="block"; }
|
else { el.style.display="block"; }
|
||||||
loadReviews();
|
loadReviews();
|
||||||
});
|
})
|
||||||
|
.catch(e=>alert("生成周复盘失败:"+(e.message||e)));
|
||||||
}
|
}
|
||||||
|
|
||||||
function exportDailyBundleMd(){
|
function exportDailyBundleMd(){
|
||||||
|
|||||||
@@ -7757,6 +7757,7 @@ def ai_daily_review():
|
|||||||
text = f"【每日交易记录】{date}\n总笔数:{len(rows)}\n\n"
|
text = f"【每日交易记录】{date}\n总笔数:{len(rows)}\n\n"
|
||||||
for idx, row in enumerate(rows, 1):
|
for idx, row in enumerate(rows, 1):
|
||||||
text += journal_row_lines_for_ai(idx, row)
|
text += journal_row_lines_for_ai(idx, row)
|
||||||
|
text += "\n"
|
||||||
|
|
||||||
image_paths = collect_images_for_ai_review(
|
image_paths = collect_images_for_ai_review(
|
||||||
rows,
|
rows,
|
||||||
@@ -7792,6 +7793,7 @@ def ai_weekly_review():
|
|||||||
text = f"【周交易记录】{start_date}~{end_date}\n总笔数:{len(rows)}\n\n"
|
text = f"【周交易记录】{start_date}~{end_date}\n总笔数:{len(rows)}\n\n"
|
||||||
for idx, row in enumerate(rows, 1):
|
for idx, row in enumerate(rows, 1):
|
||||||
text += journal_row_lines_for_ai(idx, row)
|
text += journal_row_lines_for_ai(idx, row)
|
||||||
|
text += "\n"
|
||||||
|
|
||||||
image_paths = collect_images_for_ai_review(
|
image_paths = collect_images_for_ai_review(
|
||||||
rows,
|
rows,
|
||||||
|
|||||||
@@ -1245,14 +1245,16 @@ function genDaily(){
|
|||||||
const d = document.getElementById("day_date").value;
|
const d = document.getElementById("day_date").value;
|
||||||
if(!d){alert("请选择日期");return;}
|
if(!d){alert("请选择日期");return;}
|
||||||
fetch("/ai_daily_review",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`date=${encodeURIComponent(d)}`})
|
fetch("/ai_daily_review",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`date=${encodeURIComponent(d)}`})
|
||||||
.then(r=>r.json()).then(data=>{
|
.then(r=>{ if(!r.ok) throw new Error("HTTP "+r.status); return r.json(); })
|
||||||
|
.then(data=>{
|
||||||
const el=document.getElementById("daily_result");
|
const el=document.getElementById("daily_result");
|
||||||
const wrap=document.getElementById("daily_result_wrap");
|
const wrap=document.getElementById("daily_result_wrap");
|
||||||
setAiReviewMarkdown(el, data.result);
|
setAiReviewMarkdown(el, data.result);
|
||||||
if(wrap){ wrap.style.display="block"; }
|
if(wrap){ wrap.style.display="block"; }
|
||||||
else { el.style.display="block"; }
|
else { el.style.display="block"; }
|
||||||
loadReviews();
|
loadReviews();
|
||||||
});
|
})
|
||||||
|
.catch(e=>alert("生成日复盘失败:"+(e.message||e)));
|
||||||
}
|
}
|
||||||
|
|
||||||
function genWeekly(){
|
function genWeekly(){
|
||||||
@@ -1260,14 +1262,16 @@ function genWeekly(){
|
|||||||
const e=document.getElementById("week_end").value;
|
const e=document.getElementById("week_end").value;
|
||||||
if(!s || !e){alert("请选择起止日期");return;}
|
if(!s || !e){alert("请选择起止日期");return;}
|
||||||
fetch("/ai_weekly_review",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`start_date=${encodeURIComponent(s)}&end_date=${encodeURIComponent(e)}`})
|
fetch("/ai_weekly_review",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`start_date=${encodeURIComponent(s)}&end_date=${encodeURIComponent(e)}`})
|
||||||
.then(r=>r.json()).then(data=>{
|
.then(r=>{ if(!r.ok) throw new Error("HTTP "+r.status); return r.json(); })
|
||||||
|
.then(data=>{
|
||||||
const el=document.getElementById("weekly_result");
|
const el=document.getElementById("weekly_result");
|
||||||
const wrap=document.getElementById("weekly_result_wrap");
|
const wrap=document.getElementById("weekly_result_wrap");
|
||||||
setAiReviewMarkdown(el, data.result);
|
setAiReviewMarkdown(el, data.result);
|
||||||
if(wrap){ wrap.style.display="block"; }
|
if(wrap){ wrap.style.display="block"; }
|
||||||
else { el.style.display="block"; }
|
else { el.style.display="block"; }
|
||||||
loadReviews();
|
loadReviews();
|
||||||
});
|
})
|
||||||
|
.catch(e=>alert("生成周复盘失败:"+(e.message||e)));
|
||||||
}
|
}
|
||||||
|
|
||||||
function exportDailyBundleMd(){
|
function exportDailyBundleMd(){
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""AI 复盘 journal 文本格式化(四所共用)。"""
|
"""AI 复盘 journal 文本格式化(四所共用)。"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
import sys
|
import sys
|
||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -36,6 +37,27 @@ class TestAiReviewLib(unittest.TestCase):
|
|||||||
self.assertIn("备注:测试备注", text)
|
self.assertIn("备注:测试备注", text)
|
||||||
self.assertNotIn("开仓类型", text)
|
self.assertNotIn("开仓类型", text)
|
||||||
|
|
||||||
|
def test_journal_row_accepts_sqlite_row(self):
|
||||||
|
conn = sqlite3.connect(":memory:")
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
conn.execute(
|
||||||
|
"""CREATE TABLE journal_entries (
|
||||||
|
coin TEXT, tf TEXT, pnl TEXT, real_rr TEXT, expect_rr TEXT,
|
||||||
|
entry_reason TEXT, exit_reason TEXT, hold_duration TEXT,
|
||||||
|
mood_issues TEXT, mood_score INTEGER, note TEXT
|
||||||
|
)"""
|
||||||
|
)
|
||||||
|
conn.execute(
|
||||||
|
"""INSERT INTO journal_entries VALUES (?,?,?,?,?,?,?,?,?,?,?)""",
|
||||||
|
("BTC", "15m", "5", "1.2", "2.0", "突破", "止盈", "2小时", "", None, ""),
|
||||||
|
)
|
||||||
|
row = conn.execute("SELECT * FROM journal_entries").fetchone()
|
||||||
|
conn.close()
|
||||||
|
text = journal_row_lines_for_ai(1, row)
|
||||||
|
self.assertIn("BTC 15m", text)
|
||||||
|
self.assertIn("实际RR:1.2", text)
|
||||||
|
self.assertIn("开仓逻辑:突破", text)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user