Streamlit+SQLite实战:构建个人投资日志与决策复盘系统

发布时间:2026/10/2 3:14:17
Streamlit+SQLite实战:构建个人投资日志与决策复盘系统
做投资日志管理系统这个项目纯属是被自己逼出来的。2023年我翻了翻当年上半年的交割单几十笔交易摆在那里但当我认真想复盘的时候发现至少三分之一的买入操作完全想不起当初的理由。有的票连为什么卖出都说不清楚——是止损了是换股了还是单纯拿不住了账户里的数字是真实的但我的决策过程一片空白。这种“投资失忆症”让我意识到问题不在于我做了多少研究而在于我从来没有一套系统把决策、执行、结果串起来。市面上的记账软件和券商App其实都提供了流水记录但它们的本质是“账本”记录的是钱怎么流动而不是“决策为什么发生”。我需要的是一个能回答“我当时为什么买”“我预期的逻辑是否兑现”“这次操作让我学到了什么”的工具。于是就有了这个投资日志管理系统——一套面向个人投资者的决策记录与复盘分析工具。它适合所有认真对待自己交易的人不管是做长线价值投资、波段操作还是纯粹想改掉追涨杀跌毛病的新手都能从中得到一套可落地的决策留痕方案。这篇文章不聊宏大理论直接分享我如何设计这套系统的功能、数据结构以及用了三个月后它给我带来的真实变化。1. 我的投资失忆症为什么要专门做一套日志系统1.1 想不起理由的持仓和无法复盘的交易先说说最扎心的场景。有一次和朋友聊起一只我持有过的股票我信誓旦旦地说当时看好它的逻辑是行业景气度回升。结果翻出交易记录一看买入时间和那个逻辑对不上——我是早在行业景气度起来之前就买了中间还补过两次仓。真正驱动我买入的其实是当时看了一篇研报加上涨得不错就顺手上了车。这种“事后给自己编理由”的行为如果不记录下来永远发现不了。很多人的交易行为在事后回忆时都会被大脑自动美化。你会倾向于认为自己当时是理性分析的但实际上可能只是跟风、情绪冲动、或是被涨跌幅吓到了。投资日志管理系统要做的第一件事就是打破这种善意遗忘。1.2 复盘缺失的代价同样的错误反复犯不做复盘的代价是什么是同样的错误会换个外壳反复出现。我统计过自己2022年的交易追高买入的比例高得吓人但当时完全没有意识因为每笔交易都在不同的股票上。买入点不同、题材不同、时间不同大脑不会自动把这些行为归类为“同一个错误模式”。只有把几十笔交易放到同一个维度下看规律才会浮现。那段时间我试过用Excel记录。列了日期、股票、方向、价格、盈亏坚持了两周就放弃了。原因很简单Excel就是一个电子白板它不会在你漏填理由的时候提醒你不会在你频繁交易的时候警告你更不会到周末自动生成一份“本周你做了哪些决策”的清单。工具本身不提供流程全凭自律而自律恰恰是最不可靠的东西。1.3 从Excel到系统表格解决不了流程问题所以我想做一个真正意义上的“系统”而不是一张表。它的核心逻辑是把投资行为拆成三个层次决策我打算怎么做、交易我实际做了什么、复盘结果验证了什么。这三者之间是树状关联的——一个决策可能触发多笔交易一次复盘对应一个决策。系统要做的就是强制你在录入交易时关联到某个决策并在交易之后安排复盘任务。只有把流程固化到工具里复盘这件事才不会依赖“某天忽然勤快了”的偶然状态。这是我做这个系统的基本出发点后面的所有功能设计都沿着这条线展开。2. 系统功能设计一个日志工具到底该记什么2.1 交易记录不是流水账而是决策快照传统的股票记账软件记录的是“成交明细”时间、代码、方向、价格、数量、手续费。这些信息券商都有没必要自己再维护一套。投资日志系统里的“交易记录”必须比这多一层——它要记录这笔操作发生时的思考状态和触发条件。我设计的交易字段包括基础成交信息代码、名称、方向、价格、数量、手续费、时间决策关联关联到某个决策编号必填项操作类型建仓、加仓、减仓、清仓、止盈、止损、换仓交易理由选填但系统会给出高频选项突破买入、回调低吸、业绩兑现、止损纪律等情绪指数交易时的情绪状态打分1~5其中“情绪指数”和“决策关联”是我认为最有价值的两个字段。前者帮你事后发现“我是不是又在情绪激动时做决定”后者则把交易挂到了决策树上保证复盘的时候能回溯。2.2 决策日志把“我觉得”变成可检验的假设决策日志是整个系统的最上层。它记录的不是“我买了XX股票”而是“我为什么认为XX股票值得买”。这是我参考了专业投资机构的研究报告格式后设计的不需要像券商研报那样复杂但我要求自己至少写清五个要素决策要素必填内容说明投资假设核心逻辑是什么用一句话讲清楚买入理由预期验证指标什么信号证明逻辑是对的比如季报营收增速超过30%失败信号什么情况下承认判断错误比如跌破关键支撑位仓位上限最多投入多少控制单票风险持有周期与预期收益打算拿多久预期涨多少给后面的复盘提供锚点填完这五项一个模糊的“我觉得它会涨”就变成了一条可证伪的假设。这是投资日志系统最核心的价值它不保证你做出正确的决策但保证你做出的决策可以被检验。2.3 定期复盘让每一笔买卖都有后续跟踪复盘模块是整个系统的动力来源。我设置了两层复盘定期例行复盘和决策到期复盘。定期例行复盘按周触发系统自动汇总本周所有新交易、平仓交易和到期的决策要求你逐条回答“这笔操作是否符合当时设定的逻辑”。决策到期复盘则针对单个决策在预定的持有周期结束后提醒你回来检验结果。这里有一个细节值得单独说说复盘不是看赚了还是亏了而是看逻辑是否兑现。很多决策结果是“逻辑对了但亏了钱”比如买早了或者“逻辑错了但赚了钱”比如纯赌运气。这两者在结果上盈亏不同但质量上天差地别。日志系统在复盘时会把“结果盈亏”和“逻辑兑现”分开记录这样统计出来的胜率才是有意义的。2.4 数据看板用数字逼你面对现实看板是让我最难受也最受益的部分。系统自动汇总几类指标累计收益曲线、月度交易次数、胜率分布、持仓集中度、情绪与交易频率的相关性。其中“情绪与交易频率”这张图很扎心。它把每笔交易的情绪指数和当时的交易频率画在一起很容易就能看到规律——情绪高涨到4分或5分的时候往往是交易频率的峰值期而那段时间的胜率明显偏低。人在亢奋状态下做决策的命中率就是这么直观地被数据钉在了屏幕上。这一套看板下来赚钱亏钱反而成了次要信息真正有价值的是这些行为数据。毕竟收益是结果行为是原因盯住原因去改进结果自然会变化。3. 技术选型与数据结构轻量、私有、可迁移3.1 为什么选 Streamlit SQLite技术选型这块我一开始就确定了三个原则轻量、完全私有、数据可迁移。投资记录是非常敏感的个人数据我不希望任何第三方服务商碰到它所以PaaS类方案直接排除。最终选择的是Python Streamlit SQLite Pandas这套组合。Streamlit的优势是开发效率极高用纯Python就能写出带表单、表格、图表的完整Web应用不需要前端基础。SQLite是单文件数据库整个库就是一个.db文件备份等于复制文件加密的话用7-Zip再压一层就行。Pandas则负责持仓计算、收益曲线聚合、胜率统计这些数据处理逻辑。对于个人项目来说这套选型足够了。如果你需要多人协作或移动端访问可以后续把SQLite换成PostgreSQL但初期完全没必要给自己增加运维负担。我的建议是能用本地单机解决的问题不要急着上服务端架构。3.2 三张核心表trades、decisions、reviews数据库设计经过了两次大改版。第一版只有一张trades表把什么都往里面塞结果复盘时发现信息太碎构不成完整故事。第二版才拆成了三张表以决策为根节点-- 决策表 CREATE TABLE decisions ( id INTEGER PRIMARY KEY AUTOINCREMENT, symbol TEXT NOT NULL, name TEXT NOT NULL, logic TEXT NOT NULL, -- 投资假设 verify_signal TEXT, -- 预期验证指标 fail_signal TEXT, -- 失败信号 position_limit REAL, -- 仓位上限 holding_period TEXT, -- 持有周期 expected_return REAL, -- 预期收益 confidence INTEGER, -- 信心指数 1-5 status TEXT DEFAULT open, -- open/closed created_at TEXT DEFAULT (datetime(now, localtime)) ); -- 交易表 CREATE TABLE trades ( id INTEGER PRIMARY KEY AUTOINCREMENT, decision_id INTEGER NOT NULL, symbol TEXT NOT NULL, name TEXT NOT NULL, direction TEXT NOT NULL, -- buy/sell trade_type TEXT NOT NULL, -- 建仓/加仓/减仓/清仓/止盈/止损/换仓 price REAL NOT NULL, quantity INTEGER NOT NULL, fee REAL DEFAULT 0, reason TEXT, -- 高频理由 emotion_level INTEGER DEFAULT 3, -- 情绪指数 1-5 trade_date TEXT NOT NULL, created_at TEXT DEFAULT (datetime(now, localtime)), FOREIGN KEY (decision_id) REFERENCES decisions(id) ); -- 复盘表 CREATE TABLE reviews ( id INTEGER PRIMARY KEY AUTOINCREMENT, decision_id INTEGER NOT NULL, review_date TEXT NOT NULL, result_touched INTEGER DEFAULT 0, -- 盈亏结果 1盈利 0亏损 logic_verified INTEGER DEFAULT 0, -- 逻辑是否兑现 1是 0否 deviation TEXT, -- 偏差描述 lesson TEXT, -- 经验教训 rating INTEGER DEFAULT 3, -- 操作评分 1-5 FOREIGN KEY (decision_id) REFERENCES decisions(id) );这三张表形成了完整的链路决策产生交易交易被执行复盘回到决策。你从任何一张表都能查到完整的上下文不会出现“这笔交易挂了但不知道对应哪个决策”的情况。3.3 表间关系与数据约束细节表结构定下来之后有一个细节我反复打磨过就是交易与决策之间的约束关系。在界面上交易录入时只能选择状态为open的决策——如果某个决策已经被判定为closed对应的交易就必须关联到新决策上。这个约束防止了一种偷懒行为一个决策被止损打脸后过几天同一只股票换个理由重新买入却被挂到旧决策下面假装是连续持仓。另一个细节是把trade_type做成业务枚举而不是用自由文本。一开始我用的是“买入原因”文本框结果写出来的原因五花八门统计时根本无法归类。改成固定枚举再加一个自由补充的备注字段之后数据质量立刻好起来了看板上的“操作类型分布”图表也终于能看了。4. 核心功能实现交易录入、复盘提醒与收益统计4.1 强制理由的交易录入表单Streamlit里做表单很直接但我在录入口加了两个强制逻辑。第一选择“卖出”方向时系统会先让你选trade_type止盈、止损、减仓还是换仓然后弹出一句提示“请确认本次卖出是否在当初设定的失败信号触发范围内。”这不是为了阻塞操作而是强迫你在点击卖出按钮的那一刻直面自己的决策依据。第二买入交易必须关联一条还在open状态下的决策。如果没有现成决策系统会引导你先去新建决策再回来录交易。这样做的效果立竿见影——以前我可能会临时起意买一只票现在从临时起意到真正下单之间被迫多走了一步“写投资假设”的流程。就这一步确实过滤掉了不少冲动交易。代码实现改造如下import streamlit as st import sqlite3 from datetime import datetime def log_trade(conn, decision_id, symbol, name, direction, trade_type, price, quantity, fee, reason, emotion_level): if direction sell: st.warning(卖出操作已记录请确认是否对应决策中的失败信号场景) cur conn.cursor() cur.execute( INSERT INTO trades (decision_id, symbol, name, direction, trade_type, price, quantity, fee, reason, emotion_level, trade_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) , (decision_id, symbol, name, direction, trade_type, price, quantity, fee, reason, emotion_level, datetime.now().strftime(%Y-%m-%d %H:%M))) conn.commit()4.2 持仓成本与实时市值计算持仓计算这块我一开始就决定不引入股票现价的数据源。原因有两个一是对接实时行情API涉及token申请和请求频率限制麻烦二是这个系统的重心在决策复盘而非盯盘现价只在打开看板时看一眼就够了。所以我采用了一个取巧但完全够用的方案——在录入和复盘时手动录入当前价格用Pandas统一计算。def calc_positions(conn, current_prices): df pd.read_sql_query( SELECT symbol, name, direction, price, quantity, fee, trade_date FROM trades ORDER BY trade_date , conn) # 按 symbol 聚合买入为 卖出为 - df[amount] df.apply( lambda r: r[price] * r[quantity] r[fee], axis1 ) buys df[df[direction] buy].groupby(symbol)[amount].sum() sells df[df[direction] sell].groupby(symbol)[amount].sum() buy_qty df[df[direction] buy].groupby(symbol)[quantity].sum() sell_qty df[df[direction] sell].groupby(symbol)[quantity].sum() remaining_qty buy_qty - sell_qty avg_cost (buys - sells) / remaining_qty.replace(0, 1) # 合并当前价 result pd.DataFrame({ remaining_qty: remaining_qty, avg_cost: avg_cost, current_price: pd.Series(current_prices) }) result[market_value] result[remaining_qty] * result[current_price] result[unrealized_pnl] (result[current_price] - result[avg_cost]) * result[remaining_qty] return result[result[remaining_qty] 0]这里有个细节值得留意卖出时确认的盈亏应该计入对应决策的总盈亏不能简单地按单笔交易计算。系统在计算已实现盈亏时我把同一决策下所有卖出交易的收益汇总和当初建仓时的总成本算比值这样的口径才能真实反映“这个决策赚不赚钱”。4.3 复盘工作台预期检验与偏差分析复盘工作台是所有功能里最容易被低估的。很多人以为复盘就是看看截图和收益然后感叹两句。我这里的复盘要求是结构化的每个待复盘决策页面都会展示当初填写的logic、verify_signal、fail_signal并列出关联交易的完整时间线然后要求回答两个问题——逻辑是否兑现、结果是否盈利。回答完这两个问题后系统会生成一条复盘记录并在看板上追加一个“决策质量分”逻辑兑现结果盈利判定说明是是正确决策值得重复是否时机/仓位问题逻辑好但执行差否是运气成分不要归因于实力否否错误决策重点分析这套四象限判定花了我不少心思。因为在真实交易里结果和逻辑不同步的情况远远多于两者一致的情况如果用盈亏去评价决策质量等于被市场情绪牵着走。系统化之后我才慢慢学会了区分“好决策坏结果”和“坏决策好结果”。4.4 收益曲线和胜率统计的坑收益曲线的计算比预想的要麻烦。最初版本用简单持仓成本差算总收益但多次加仓减仓后成本计算容易乱。后来我改为按已实现盈亏逐笔累加再叠加未实现盈亏的方式。已实现盈亏从卖出交易里算未实现盈亏用当前持仓市值减去总买入成本。这两部分加起来才是当日的总盈亏口径。胜率统计也有讲究。我用了两个胜率指标按交易笔数统计赚钱的交易笔数除以总交易笔数和按决策统计盈利决策数除以总决策数。两者差异很大一个频繁做小止盈的人交易笔数胜率可能很高但决策层面总盈利可能一般反之一个较少出手但决策质量高的人决策胜率可能更高。两者同时展示能避免被单一指标误导。5. 实测下来的变化三个月使用记录5.1 交易频率的真实下降系统上线三个月后我在看板上看到了一个令人意外的数据变化月均交易笔数从上线的11笔降到了5笔左右但决策数量并没有明显下降变化的是每个决策匹配的交易笔数更少了。以前一个“感觉可以买”的决策会引发建仓、补仓、补仓再补仓最后演变成重仓套牢才不得不止损。现在每个决策最多触发两笔交易建仓和清仓中间加仓需要新建决策或补充逻辑验证。这个变化完全是强制决策关联带来的副作用——当你知道每次加仓都要归结到一条逻辑假设上去那种“跌了就补平摊成本”的廉价操作就很难再自然发生了因为你会发现根本写不出像样的加仓理由。5.2 一次让我羞愧的“记忆失真”系统上线第二个月我做了一次典型的记忆校正。某只票我因为业绩雷止损掉了亏损约8%。我一直认为自己是“及时止损”印象里从买入到卖出没有拖太久。但系统调出的时间线显示首次买入到最终清仓中间隔了整整47天这期间股价已经连续低迷了三周而我加了两次仓。我的“及时止损”实际是“套牢数月后的割肉”。这一单在决策日志里清晰记录着当初的fail_signal早就触发了但执行时根本没有对照。这件事让我意识到大脑对痛苦经历的时长感知会严重压缩48天的煎熬回忆起来可能只有两周的感觉。没有日志系统这类校正永远无法发生。5.3 一个完整复盘案例拆解以某次科技股波段操作为例。决策日志里的记录是这样的投资假设公司Q3财报毛利率提升叠加新一轮产品周期启动估值有向上修复空间验证指标财报发布后毛利率环比提升超过2个百分点失败信号股价跌破年线且伴随成交量放大仓位上限总资金10%预期周期3个月预期收益15%实际结果是买入后2周内涨了6%当时很得意加了一次仓仓位推到了将近15%。随后一个月股价开始回落跌破了年线同时放量。按失败信号应该清仓但因为仓位已经推高、不想认错又扛了一周。最终亏损9%止损离场。复盘时系统显示逻辑中“毛利率提升”财报其实兑现了但周期判断错了——产品周期启动被财报中的指引下调证伪。四象限判定为逻辑部分兑现、结果亏损问题出在加仓时机和未执行失败信号上。这次复盘让一个本来只值一条“亏钱了”的交易变成了一份完整的操作行为修正清单加仓必须在逻辑得到验证后执行不能靠浮盈感觉失败信号触发后无条件执行止损不允许加仓或延迟。这些都是写在纸上的常识但如果不是因为系统逼着我回答那几个问题我大概率会以“市场不好”作为总结草草了事。6. 从0.1到0.2踩坑与后续迭代方向6.1 数据迁移和备份的教训第一个教训来自一次重启事故。Streamlit开发调试时经常要重启服务我有一次误删了项目目录里的.db文件当时因为还没建立备份习惯损失了两周的记录。从那以后我写了一个简单的备份脚本每次启动时自动复制一份带时间戳的数据库备份。import shutil from datetime import datetime def backup_db(srclogs.db, backup_dir./backups): ts datetime.now().strftime(%Y%m%d_%H%M%S) shutil.copy2(src, f{backup_dir}/logs_{ts}.db) # 只保留最近30份 backups sorted(Path(backup_dir).glob(logs_*.db), reverseTrue) for old in backups[30:]: old.unlink()这件事提醒我个人项目再轻量数据安全策略也应该从第一天开始就考虑等数据丢了再想办法属于难度地狱模式。6.2 情绪字段比想象中更有用我在第四节提到情绪指数当初设计时其实是半信半疑的。但数据统计出来后这个字段的价值超过其他所有字段的总和。图表显示情绪指数4分以上的交易胜率只有2分以下交易的一半左右。而情绪指数和交易频率之间存在清晰的时序关系——往往是连续亏损后情绪高涨想翻本或者连续盈利后情绪高涨自信心暴涨这两种状态交易频率都会飙升。我后来给这个字段加了一个实时提示功能如果当天情绪指数打到了4分以上系统会弹出一张警示卡片并用红色字体提醒“高情绪状态下的交易胜率显著低于正常状态请确认是否重写决策假设后再执行”。这个提示很粗暴但很有效。6.3 后续可以扩展的方向目前系统已经跑通了决策-交易-复盘的主链路下一步我在规划几个提升方向接入历史行情API让系统每分钟自动更新持仓市值去掉手动录入现价增加“交易计划单”功能在决策日志里填好后先不执行等市场走到验证信号再触发提醒对平仓交易自动生成周报并邮件推送强制每周做一次轻量例行复盘增加组合层面的相关性分析识别持仓中是否存在同涨同跌的隐患这些扩展不会改变系统的核心设计理念——它仍然不是一个预测工具而是一个让人诚实地面对自己决策过程的记录器。最后说一点整套系统使用中的个人体会做投资日志最大的敌人不是工具不好用而是自己抗拒记录。因为记录意味着你要承认很多操作经不起审视。系统能做到的是把审视变成流程把流程变成习惯把习惯变成行为约束。如果你也经常看着交割单想不起当时为什么操作不妨从小处着手——先记录一周不需要做任何分析等积累到几十条交易记录后再回头翻看那种感觉会让你立刻明白这个系统的价值在哪里。