feat: DB-first architecture with lock-safe writes

- price_monitor: writes live prices to both JSON and mofin.db (holdings + live_prices + portfolio_summary)
- mofin_db: added execute_with_retry/commit_with_retry with exponential backoff on 'database is locked'
- mofin_db: increased timeout 5s->15s, added PRAGMA busy_timeout=15000
- price_monitor retry loop: fixed break-before-if-ok bug (was not retrying on write failure)
- DB connection: WAL mode + retry decorator for all write operations
- cash sync: preserves DB authoritative cash (JSON cash not pushed to DB)

This is the DB-first version. JSON writes remain for dashboard compatibility.
Next step: remove JSON writes entirely for full DB-only architecture.
This commit is contained in:
知微
2026-07-06 12:02:11 +08:00
parent 687155487d
commit e185b4e4dc
30 changed files with 1856 additions and 2042 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
+709 -1063
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+147 -221
View File
@@ -1,240 +1,166 @@
{ {
"holdings": [ "holdings": [
{
"code": "300308",
"name": "中际旭创",
"shares": 100,
"cost": 1316.53,
"price": 1116.0,
"market_value": 111600.0,
"change_pct": -2.36,
"currency": "CNY",
"position_pct": 15.27,
"_currency": "CNY"
},
{
"code": "06869",
"name": "长飞光纤光缆",
"shares": 500,
"cost": 263.73,
"price": 201.2,
"market_value": 87220.0,
"change_pct": 1.64,
"currency": "HKD",
"position_pct": 13.47,
"_currency": "HKD"
},
{
"code": "01478",
"name": "丘钛科技",
"shares": 11000,
"cost": 13.47,
"price": 6.99,
"market_value": 66660.0,
"change_pct": 4.02,
"currency": "HKD",
"position_pct": 7.97,
"_currency": "HKD"
},
{
"code": "601899",
"name": "紫金矿业",
"shares": 2400,
"cost": 39.89,
"price": 27.82,
"market_value": 66768.0,
"change_pct": 5.78,
"currency": "CNY",
"position_pct": 7.34,
"_currency": "CNY"
},
{
"code": "688411",
"name": "海博思创",
"shares": 200,
"cost": 266.95,
"price": 251.15,
"market_value": 50230.0,
"change_pct": -1.78,
"currency": "CNY",
"position_pct": 6.31,
"_currency": "CNY"
},
{
"code": "688981",
"name": "中芯国际",
"shares": 300,
"cost": 126.07,
"price": 140.31,
"market_value": 42093.0,
"change_pct": -2.63,
"currency": "CNY",
"position_pct": 5.44,
"_currency": "CNY"
},
{
"code": "01888",
"name": "建滔积层板",
"shares": 500,
"cost": 88.24,
"price": 84.45,
"market_value": 36545.0,
"change_pct": 0.78,
"currency": "HKD",
"position_pct": 5.28,
"_currency": "HKD"
},
{
"code": "688639",
"name": "华恒生物",
"shares": 2800,
"cost": 21.51,
"price": 16.66,
"market_value": 46648.0,
"change_pct": -1.71,
"currency": "CNY",
"position_pct": 5.25,
"_currency": "CNY"
},
{
"code": "300750",
"name": "宁德时代",
"shares": 100,
"cost": 401.78,
"price": 380.0,
"market_value": 38000.0,
"change_pct": -0.61,
"currency": "CNY",
"position_pct": 4.64,
"_currency": "CNY"
},
{
"code": "01211",
"name": "比亚迪股份",
"shares": 600,
"cost": 104.87,
"price": 84.1,
"market_value": 43932.0,
"change_pct": 7.41,
"currency": "HKD",
"position_pct": 4.62,
"_currency": "HKD"
},
{
"code": "02202",
"name": "万科企业",
"shares": 19700,
"cost": 4.67,
"price": 2.34,
"market_value": 40188.0,
"change_pct": 4.93,
"currency": "HKD",
"position_pct": 4.6,
"_currency": "HKD"
},
{ {
"code": "00700", "code": "00700",
"name": "腾讯", "name": "腾讯",
"shares": 100, "shares": 100,
"cost": 443.13, "cost": 443.13,
"price": 431.2, "price": 447.0,
"market_value": 37437.0, "market_value": 39063.0,
"change_pct": 0.23, "change_pct": 3.66,
"currency": "HKD", "currency": "HKD",
"position_pct": null, "position_pct": null
"_currency": "HKD"
},
{
"code": "00981",
"name": "中芯国际",
"shares": 500,
"cost": 75.94,
"price": 77.6,
"market_value": 33530.0,
"change_pct": -3.48,
"currency": "HKD",
"position_pct": 4.2,
"_currency": "HKD"
},
{
"code": "300548",
"name": "长芯博创",
"shares": 100,
"cost": 231.46,
"price": 221.01,
"market_value": 22101.0,
"change_pct": -0.45,
"currency": "CNY",
"position_pct": 3.2,
"_currency": "CNY"
},
{
"code": "518880",
"name": "黄金ETF华安",
"shares": 2400,
"cost": 12.19,
"price": 8.67,
"market_value": 20808.0,
"change_pct": 2.32,
"currency": "CNY",
"position_pct": 2.45,
"_currency": "CNY"
},
{
"code": "300035",
"name": "中科电气",
"shares": 1400,
"cost": 22.29,
"price": 14.29,
"market_value": 20006.0,
"change_pct": 0.85,
"currency": "CNY",
"position_pct": 2.42,
"_currency": "CNY"
},
{
"code": "000700",
"name": "模塑科技",
"shares": 1400,
"cost": 14.83,
"price": 17.6,
"market_value": 24640.0,
"change_pct": 4.33,
"currency": "CNY",
"position_pct": 2.41,
"_currency": "CNY"
},
{
"code": "600563",
"name": "法拉电子",
"shares": 100,
"cost": 147.18,
"price": 157.06,
"market_value": 15706.0,
"change_pct": -4.41,
"currency": "CNY",
"position_pct": 2.3,
"_currency": "CNY"
}, },
{ {
"code": "01088", "code": "01088",
"name": "中国神华", "name": "中国神华",
"shares": 500, "shares": 500,
"cost": 45.89, "cost": 45.89,
"price": 40.0, "price": 40.68,
"market_value": 17355.0, "market_value": 17575.0,
"change_pct": 1.01, "change_pct": 1.7,
"currency": "HKD", "currency": "HKD",
"position_pct": 2.14, "position_pct": 2.14
"_currency": "HKD" },
{
"code": "01211",
"name": "比亚迪股份",
"shares": 600,
"cost": 104.87,
"price": 84.2,
"market_value": 44376.0,
"change_pct": 0.12,
"currency": "HKD",
"position_pct": 4.62
},
{
"code": "01478",
"name": "丘钛科技",
"shares": 11000,
"cost": 13.47,
"price": 6.77,
"market_value": 64460.0,
"change_pct": -3.15,
"currency": "HKD",
"position_pct": 7.97
},
{
"code": "02202",
"name": "万科企业",
"shares": 19700,
"cost": 4.67,
"price": 2.33,
"market_value": 40385.0,
"change_pct": -0.43,
"currency": "HKD",
"position_pct": 4.6
},
{
"code": "300035",
"name": "中科电气",
"shares": 1400,
"cost": 22.29,
"price": 14.02,
"market_value": 19712.0,
"change_pct": -1.89,
"currency": "CNY",
"position_pct": 2.42
},
{
"code": "300308",
"name": "中际旭创",
"shares": 100,
"cost": 1316.53,
"price": 1116.03,
"market_value": 106181.0,
"change_pct": 0.0,
"currency": "CNY",
"position_pct": 15.27
},
{
"code": "300750",
"name": "宁德时代",
"shares": 100,
"cost": 401.78,
"price": 377.6,
"market_value": 37726.0,
"change_pct": -0.63,
"currency": "CNY",
"position_pct": 4.64
},
{
"code": "518880",
"name": "黄金ETF华安",
"shares": 2400,
"cost": 12.19,
"price": 8.66,
"market_value": 20832.0,
"change_pct": -0.08,
"currency": "CNY",
"position_pct": 2.45
},
{
"code": "600563",
"name": "法拉电子",
"shares": 100,
"cost": 147.18,
"price": 153.42,
"market_value": 15021.0,
"change_pct": -2.32,
"currency": "CNY",
"position_pct": 2.3
},
{
"code": "601899",
"name": "紫金矿业",
"shares": 2400,
"cost": 39.89,
"price": 28.41,
"market_value": 68784.0,
"change_pct": 2.12,
"currency": "CNY",
"position_pct": 7.34
},
{
"code": "688411",
"name": "海博思创",
"shares": 200,
"cost": 266.95,
"price": 262.98,
"market_value": 52064.0,
"change_pct": 4.71,
"currency": "CNY",
"position_pct": 6.31
},
{
"code": "688639",
"name": "华恒生物",
"shares": 2800,
"cost": 21.51,
"price": 16.61,
"market_value": 46480.0,
"change_pct": -0.3,
"currency": "CNY",
"position_pct": 5.25
},
{
"code": "688981",
"name": "中芯国际",
"shares": 300,
"cost": 126.07,
"price": 147.18,
"market_value": 40599.0,
"change_pct": 4.9,
"currency": "CNY",
"position_pct": 5.44
} }
], ],
"total_assets": 952877.07, "cash": 289196.0,
"total_mv": 820755.14, "frozen_cash": 0.0,
"stock_value": 820755.14, "total_mv": 619713.76,
"cash": 132121.93, "total_assets": 908909.76,
"frozen_cash": 0, "position_pct": 68.18,
"position_pct": 86.13,
"currency": "CNY", "currency": "CNY",
"updated_at": "2026-07-04 09:51" "updated_at": "2026-07-06 11:35:19",
"stock_value": 619713.76
} }
+66
View File
@@ -27,6 +27,12 @@
"high": 1215.52, "high": 1215.52,
"low": 1185.0, "low": 1185.0,
"close": 1194.45 "close": 1194.45
},
{
"date": "2026-07-06",
"high": 1215.0,
"low": 1180.0,
"close": 1203.23
} }
], ],
"02202": [ "02202": [
@@ -63,6 +69,12 @@
"high": 50.2, "high": 50.2,
"low": 48.31, "low": 48.31,
"close": 49.09 "close": 49.09
},
{
"date": "2026-07-06",
"high": 49.96,
"low": 48.81,
"close": 49.38
} }
], ],
"02359": [ "02359": [
@@ -119,6 +131,12 @@
"high": 502.0, "high": 502.0,
"low": 444.55, "low": 444.55,
"close": 480.32 "close": 480.32
},
{
"date": "2026-07-06",
"high": 499.0,
"low": 448.0,
"close": 476.72
} }
], ],
"06160": [ "06160": [
@@ -155,6 +173,12 @@
"high": 687.04, "high": 687.04,
"low": 633.01, "low": 633.01,
"close": 643.81 "close": 643.81
},
{
"date": "2026-07-06",
"high": 649.88,
"low": 615.0,
"close": 649.01
} }
], ],
"09868": [ "09868": [
@@ -197,6 +221,12 @@
"high": 757.88, "high": 757.88,
"low": 713.0, "low": 713.0,
"close": 738.38 "close": 738.38
},
{
"date": "2026-07-06",
"high": 745.0,
"low": 690.11,
"close": 743.0
} }
], ],
"300124": [ "300124": [
@@ -211,6 +241,12 @@
"high": 74.63, "high": 74.63,
"low": 67.31, "low": 67.31,
"close": 72.15 "close": 72.15
},
{
"date": "2026-07-06",
"high": 71.94,
"low": 68.6,
"close": 69.55
} }
], ],
"000657": [ "000657": [
@@ -225,6 +261,12 @@
"high": 101.5, "high": 101.5,
"low": 87.88, "low": 87.88,
"close": 89.63 "close": 89.63
},
{
"date": "2026-07-06",
"high": 90.58,
"low": 80.46,
"close": 81.83
} }
], ],
"000711": [ "000711": [
@@ -239,6 +281,12 @@
"high": 5.26, "high": 5.26,
"low": 4.87, "low": 4.87,
"close": 5.26 "close": 5.26
},
{
"date": "2026-07-06",
"high": 5.65,
"low": 5.0,
"close": 5.29
} }
], ],
"001309": [ "001309": [
@@ -253,6 +301,12 @@
"high": 892.1, "high": 892.1,
"low": 795.0, "low": 795.0,
"close": 881.91 "close": 881.91
},
{
"date": "2026-07-06",
"high": 918.98,
"low": 860.0,
"close": 916.6
} }
], ],
"002594": [ "002594": [
@@ -267,6 +321,12 @@
"high": 88.88, "high": 88.88,
"low": 81.9, "low": 81.9,
"close": 88.47 "close": 88.47
},
{
"date": "2026-07-06",
"high": 88.95,
"low": 86.61,
"close": 87.68
} }
], ],
"00700": [ "00700": [
@@ -335,6 +395,12 @@
"high": 646.85, "high": 646.85,
"low": 574.1, "low": 574.1,
"close": 618.02 "close": 618.02
},
{
"date": "2026-07-06",
"high": 710.0,
"low": 659.16,
"close": 695.26
} }
] ]
} }
+5
View File
@@ -165,6 +165,11 @@
"time": "2026-07-01T08:55:24.130605", "time": "2026-07-01T08:55:24.130605",
"content": "**港股(恒指-0.63%):** 中芯国际H(00981) 89.40+5.42%浮盈+17%持有 | 腾讯控股(00700) 429.80+2.28%浮亏-3%距止损3.4% | 丘钛科技(014", "content": "**港股(恒指-0.63%):** 中芯国际H(00981) 89.40+5.42%浮盈+17%持有 | 腾讯控股(00700) 429.80+2.28%浮亏-3%距止损3.4% | 丘钛科技(014",
"report_id": "cron_d42f2ce3b479_2026-06-30_20-25-27" "report_id": "cron_d42f2ce3b479_2026-06-30_20-25-27"
},
{
"time": "2026-07-06T08:55:01.761812",
"content": "| **中芯国际双持(688981+00981)** | A+H 双持合计约 9.64% 仓位,两市场同步净多头,若港股中芯继续走弱(注意 00981 止损 ¥77.55 高于成本 ¥75.94 的异",
"report_id": "cron_e02b8bde74f8_2026-07-05_22-05-42"
} }
] ]
} }
+15
View File
@@ -616,6 +616,21 @@
"content": "- **比亚迪(01211)** — 现价72.65,止损63.99(距12%)。港股汽车板块整体承压,持续下跌趋势未改。建议:重新审视持股逻辑——如果中线看多理由充分,可考虑将止损下移至60附近;如", "content": "- **比亚迪(01211)** — 现价72.65,止损63.99(距12%)。港股汽车板块整体承压,持续下跌趋势未改。建议:重新审视持股逻辑——如果中线看多理由充分,可考虑将止损下移至60附近;如",
"report_id": "cron_e02b8bde74f8_2026-06-28_22-11-21" "report_id": "cron_e02b8bde74f8_2026-06-28_22-11-21"
}, },
{
"time": "2026-07-06T08:55:01.761812",
"content": "3. **0714止盈策略严重过期** — 当前 19 个持仓中有约 13 个(68%)的成本价高于止盈价,说明策略数据已随股价下跌严重偏离,大量止损/止盈位明显陈旧(如万科 02202 成本 ¥4.",
"report_id": "cron_e02b8bde74f8_2026-07-05_22-05-42"
},
{
"time": "2026-07-06T08:55:01.761812",
"content": "3. **策略数据翻新** — 针对成本远高于止盈的 13 个持仓(如 万科02202、比亚迪01211、华恒生物688639、中科电气300035、黄金ETF518880 等),建议逐个审核是否需要",
"report_id": "cron_e02b8bde74f8_2026-07-05_22-05-42"
},
{
"time": "2026-07-06T08:55:01.761812",
"content": "| **比亚迪(01211)止损临近** | 此前 advice 反复提示止损 ¥81.72、现价距仅 3%。港股比亚迪已是 4.62% 仓位且浮亏 -20.1%,若下周港股继续弱势,止损单需提前挂好",
"report_id": "cron_e02b8bde74f8_2026-07-05_22-05-42"
},
{ {
"time": "2026-06-01T10:25:54.503460", "time": "2026-06-01T10:25:54.503460",
"content": "比亚迪股份(01211) 仓位4.56% +2.27%→ 持有,连涨", "content": "比亚迪股份(01211) 仓位4.56% +2.27%→ 持有,连涨",
+10
View File
@@ -456,6 +456,16 @@
"content": "③ 丘钛科技(01478) 距止损6.48仅+7.3%!仓位7.97%", "content": "③ 丘钛科技(01478) 距止损6.48仅+7.3%!仓位7.97%",
"report_id": "cron_d42f2ce3b479_2026-07-03_20-03-44" "report_id": "cron_d42f2ce3b479_2026-07-03_20-03-44"
}, },
{
"time": "2026-07-06T08:55:01.761812",
"content": "2. **收紧输出质量** — 使用\"关注/观望/留意\"时须紧随具体价格、止损位和触发条件。满 3 次模糊表述自动标记为低质量输出。对 01478 和 688795 做一次明确的持仓策略复核:继续持有",
"report_id": "cron_e02b8bde74f8_2026-07-05_22-05-42"
},
{
"time": "2026-07-06T08:55:01.761812",
"content": "| **丘钛科技(01478)** | 7.97% 第三大仓位,成本 ¥13.47 远超止盈 ¥7.59(浮亏约 -43%),150 次提及却无追踪结论。无论继续持有还是止损,需 Dad 明确决策 |",
"report_id": "cron_e02b8bde74f8_2026-07-05_22-05-42"
},
{ {
"time": "2026-06-01T10:25:54.503460", "time": "2026-06-01T10:25:54.503460",
"content": "丘钛科技(01478) 仓位8.58% +4.10%→ 持有,走势健康", "content": "丘钛科技(01478) 仓位8.58% +4.10%→ 持有,走势健康",
+10
View File
@@ -621,6 +621,16 @@
"content": "- **万科(02202)** — 现价2.20,止损2.00(距9%)。周五港股继续弱,若跌破2.00港币将触发止损。建议:提前制定应对方案,而不是等到触发了再慌。", "content": "- **万科(02202)** — 现价2.20,止损2.00(距9%)。周五港股继续弱,若跌破2.00港币将触发止损。建议:提前制定应对方案,而不是等到触发了再慌。",
"report_id": "cron_e02b8bde74f8_2026-06-28_22-11-21" "report_id": "cron_e02b8bde74f8_2026-06-28_22-11-21"
}, },
{
"time": "2026-07-06T08:55:01.761812",
"content": "3. **0714止盈策略严重过期** — 当前 19 个持仓中有约 13 个(68%)的成本价高于止盈价,说明策略数据已随股价下跌严重偏离,大量止损/止盈位明显陈旧(如万科 02202 成本 ¥4.",
"report_id": "cron_e02b8bde74f8_2026-07-05_22-05-42"
},
{
"time": "2026-07-06T08:55:01.761812",
"content": "3. **策略数据翻新** — 针对成本远高于止盈的 13 个持仓(如 万科02202、比亚迪01211、华恒生物688639、中科电气300035、黄金ETF518880 等),建议逐个审核是否需要",
"report_id": "cron_e02b8bde74f8_2026-07-05_22-05-42"
},
{ {
"time": "2026-06-04T09:55:40.300986", "time": "2026-06-04T09:55:40.300986",
"content": "- 万科(02202): 补仓区间2.5~2.6,昨收2.70,距+3.8%,未进入。", "content": "- 万科(02202): 补仓区间2.5~2.6,昨收2.70,距+3.8%,未进入。",
+5
View File
@@ -20,6 +20,11 @@
"time": "2026-06-28T08:55:52.587605", "time": "2026-06-28T08:55:52.587605",
"content": "6. 🟢 **中国人寿** (02628) — 距买入区-2%,保险板块虽弱但估值低", "content": "6. 🟢 **中国人寿** (02628) — 距买入区-2%,保险板块虽弱但估值低",
"report_id": "cron_watchlist_health_weekly_2026-06-27_20-04-22" "report_id": "cron_watchlist_health_weekly_2026-06-27_20-04-22"
},
{
"time": "2026-07-05T08:55:15.087154",
"content": "6. 中国人寿(02628) — 接近止损位28.26,保险板块承压",
"report_id": "cron_watchlist_health_weekly_2026-07-04_20-13-24"
} }
] ]
} }
+5
View File
@@ -725,6 +725,11 @@
"time": "2026-06-14T08:55:34.460610", "time": "2026-06-14T08:55:34.460610",
"content": "- 中国人寿(02628) & 小鹏汽车(09868) — 盈亏比过低,不建议买入", "content": "- 中国人寿(02628) & 小鹏汽车(09868) — 盈亏比过低,不建议买入",
"report_id": "cron_watchlist_health_weekly_2026-06-13_20-07-41" "report_id": "cron_watchlist_health_weekly_2026-06-13_20-07-41"
},
{
"time": "2026-07-05T08:55:15.087154",
"content": "4. 小鹏汽车(09868) — 跌破止损位,汽车行业竞争格局",
"report_id": "cron_watchlist_health_weekly_2026-07-04_20-13-24"
} }
] ]
} }
+5
View File
@@ -861,6 +861,11 @@
"content": "{\"type\":\"周复盘\",\"time\":\"周日\",\"summary\":\"A股先跌后弹分化格局,全市场普跌后暴力反弹\",\"key_holdings\":[{\"code\":\"600110\",\"name\":", "content": "{\"type\":\"周复盘\",\"time\":\"周日\",\"summary\":\"A股先跌后弹分化格局,全市场普跌后暴力反弹\",\"key_holdings\":[{\"code\":\"600110\",\"name\":",
"report_id": "cron_e02b8bde74f8_2026-06-14_22-04-57" "report_id": "cron_e02b8bde74f8_2026-06-14_22-04-57"
}, },
{
"time": "2026-07-06T08:55:01.761812",
"content": "2. **海博思创(688411)追踪较好** — 该股在持仓中占比 6.31%,成本 ¥266.95 处于买入区内(¥231.32~¥269.87),参与价合理,后续追踪记录清晰,止盈 ¥277.5",
"report_id": "cron_e02b8bde74f8_2026-07-05_22-05-42"
},
{ {
"time": "2026-06-03T09:56:06.723399", "time": "2026-06-03T09:56:06.723399",
"content": "| 688411 | 海博思创 | 295.00 | 299.17 | -1.39% | 追踪止盈290 | 正常 |", "content": "| 688411 | 海博思创 | 295.00 | 299.17 | -1.39% | 追踪止盈290 | 正常 |",
+5
View File
@@ -91,6 +91,11 @@
"content": "- 华恒生物(688639) ¥21.2,买入区20.78~21.4 ✅ 在区间内,RR:2.28", "content": "- 华恒生物(688639) ¥21.2,买入区20.78~21.4 ✅ 在区间内,RR:2.28",
"report_id": "cron_e02b8bde74f8_2026-06-14_22-04-57" "report_id": "cron_e02b8bde74f8_2026-06-14_22-04-57"
}, },
{
"time": "2026-07-06T08:55:01.761812",
"content": "3. **策略数据翻新** — 针对成本远高于止盈的 13 个持仓(如 万科02202、比亚迪01211、华恒生物688639、中科电气300035、黄金ETF518880 等),建议逐个审核是否需要",
"report_id": "cron_e02b8bde74f8_2026-07-05_22-05-42"
},
{ {
"time": "2026-06-01T10:25:54.503460", "time": "2026-06-01T10:25:54.503460",
"content": "华恒生物(688639) 仓位0.47% -1.71%→ 持有,低位整理", "content": "华恒生物(688639) 仓位0.47% -1.71%→ 持有,低位整理",
+5
View File
@@ -26,6 +26,11 @@
"content": "• **摩尔线程(688795) 616.38** +2.73% | 买入区580-600,偏高→等回调至600以下", "content": "• **摩尔线程(688795) 616.38** +2.73% | 买入区580-600,偏高→等回调至600以下",
"report_id": "cron_d3797d924ddc_2026-06-02_16-33-11" "report_id": "cron_d3797d924ddc_2026-06-02_16-33-11"
}, },
{
"time": "2026-07-06T08:55:01.761812",
"content": "2. **收紧输出质量** — 使用\"关注/观望/留意\"时须紧随具体价格、止损位和触发条件。满 3 次模糊表述自动标记为低质量输出。对 01478 和 688795 做一次明确的持仓策略复核:继续持有",
"report_id": "cron_e02b8bde74f8_2026-07-05_22-05-42"
},
{ {
"time": "2026-06-14T08:55:34.460610", "time": "2026-06-14T08:55:34.460610",
"content": "**🟢 摩尔线程-U(688795) ¥610.55 | 买入区598.34~622.76 ✅ | RR:3.44**", "content": "**🟢 摩尔线程-U(688795) ¥610.55 | 买入区598.34~622.76 ✅ | RR:3.44**",
+5
View File
@@ -151,6 +151,11 @@
"content": "② 中芯国际A(688981) 已跌破止损位!仓位5.44%", "content": "② 中芯国际A(688981) 已跌破止损位!仓位5.44%",
"report_id": "cron_d42f2ce3b479_2026-07-03_20-03-44" "report_id": "cron_d42f2ce3b479_2026-07-03_20-03-44"
}, },
{
"time": "2026-07-06T08:55:01.761812",
"content": "| **中芯国际双持(688981+00981)** | A+H 双持合计约 9.64% 仓位,两市场同步净多头,若港股中芯继续走弱(注意 00981 止损 ¥77.55 高于成本 ¥75.94 的异",
"report_id": "cron_e02b8bde74f8_2026-07-05_22-05-42"
},
{ {
"time": "2026-06-11T08:55:23.441938", "time": "2026-06-11T08:55:23.441938",
"content": "• 中芯国际(688981) 竞价125.00(-1.81%),策略买入区116~136内", "content": "• 中芯国际(688981) 竞价125.00(-1.81%),策略买入区116~136内",
+97
View File
@@ -0,0 +1,97 @@
# 策略复盘闭环系统设计
> 版本: v1 | 最后更新: 2026-06-24
> 核心理念:每条策略建议都必须有回头检查,用实际结果驱动策略逻辑进化。
## 一、现状
| 环节 | 状态 |
|------|------|
| 策略生成 | ✅ `strategy_lifecycle.py` 按规则生成买入区/止损/止盈 |
| 策略评估 | ✅ 日评估六维分析每条策略的当前状况 |
| 策略重评 | ✅ 过期/偏离时自动触发重评 |
| **成功率追踪** | ❌ 有 `accuracy_stats` 空表,从未写入 |
| **复盘归因** | ❌ 没有"回头看"机制 |
| **策略逻辑修正** | ❌ 评估结果从不反馈到生成规则 |
| **验证测试** | ❌ 改完规则没有验证环节 |
## 二、闭环设计
```
生成策略 → 执行/等待 → 回头看(复盘) → 归因分析 → 修正策略逻辑 → 验证 → 部署
↑ |
└──── 迭代循环 ───────┘
```
### 环节1:回头看(复盘)
每条策略/建议在生成后 T+5、T+20、T+60 三个时间点回头检查:
| 检查点 | 时机 | 判断标准 |
|--------|------|---------|
| T+5 | 5个交易日后 | 止损触发?止盈触发?价格走向是否正确? |
| T+20 | 一个月后 | 中期趋势验证。买入区是否有效? |
| T+60 | 三个月后 | 长期逻辑验证。大方向判断是否正确? |
判断分类:
-**正确**:价格朝预期方向走了 > 止损/止盈间距的 50%
- ⚠️ **部分正确**:方向对了但幅度不够,或方向对但时机差
-**错误**:方向错了,或止损被打后价格反转了
-**待定**:还在运行中,未到判断时点
### 环节2:归因分析
对 ❌ 错误 和 ⚠️ 部分正确 的做根因分类:
| 失败模式 | 判断条件 | 修复方向 |
|----------|---------|---------|
| 止损过紧 | 价格跌破止损 < 3天后回到买入区 | 放宽止损到强支撑 × 0.95 |
| 入场过早 | 买入后继续跌 > 入场点 10% 才反弹 | 买入区下移,等缩量确认 |
| 止盈过近 | 价格突破止盈 < 5天后继续涨 > 15% | 止盈放到更高阻力位 |
| 方向看错 | 价格持续朝反方向走 | 检查多周期趋势判断逻辑 |
| 情景错配 | 策略假设的情景与实际不符 | 加入情景过滤条件 |
| 信号误判 | timing_signal 信号错误 | 修正信号合成逻辑 |
| 行业拖累 | 个股选对了但行业暴跌 | 加入行业动量过滤 |
### 环节3:策略逻辑修正
归因结果反馈到策略生成规则的几个层面:
1. **prompt_manager 规则更新** — 止损/止盈/买入区的生成规则
2. **timing_signal 合成权重调整** — 各因子的权重
3. **股票分类规则调整** — 什么情况归为弱势/深套/短炒
4. **情景判定阈值调整** — detect_scenario 的参数
### 环节4:回测验证
修正后的规则用历史数据跑模拟,对比新旧规则的成功率:
```
模拟方式:取过去60天的数据
1. 用旧规则生成每条策略 → 计算成功率
2. 用新规则生成同样股票的策略 → 计算成功率
3. 对比:新规则是否 > 旧规则 +5%?
4. 如果是 → 部署新规则
5. 如果不是 → 继续调整
```
## 三、参考来源
知识库中有多篇量化分析文章可以参考:
- 止损/止盈的统计学最优位置
- 多因子信号合成的权重分配方法
- 不同市场环境下的策略参数调优
- 回测验证的方法论和陷阱(过拟合/幸存者偏差)
## 四、实施路线
### Phase 1(本session
- 策略复盘脚本:遍历 active 策略,检查实际结果,写入 accuracy_stats
- 归因分析:对失败策略分类失败模式
- 初步报告:当前策略整体成功率 + 常见失败模式
### Phase 2(后续)
- 策略逻辑修正:根据归因调整 prompt_manager 规则
- 回测验证:用历史数据验证新规则
- 知识库文章萃取:从量化分析文章中提取可用因子
- 持续迭代:每周跑一次复盘,持续优化
+47 -8
View File
@@ -20,15 +20,22 @@ DB_PATH = '/home/hmo/web-dashboard/data/mofin.db'
def _get_db(): def _get_db():
db = sqlite3.connect(DB_PATH) """获取数据库连接(WAL 模式,15秒超时防并发锁)"""
db = sqlite3.connect(DB_PATH, timeout=15)
db.row_factory = sqlite3.Row db.row_factory = sqlite3.Row
db.execute("PRAGMA journal_mode=WAL")
db.execute("PRAGMA busy_timeout=15000")
return db return db
# ── portfolio ───────────────────────────────────────────────────── # ── portfolio ─────────────────────────────────────────────────────
def read_portfolio(): def read_portfolio():
"""返回 portfolio.json 等价 dict。纯 DB。""" """返回 portfolio.json 等价 dict。纯 DB。
总市值从 holdings 实时计算(shares × price,港股 × 汇率),
不信任 portfolio_summary 的存储值,因为可能未及时更新。
"""
db = _get_db() db = _get_db()
rows = db.execute( rows = db.execute(
"SELECT code, name, shares, cost, price, market_value, " "SELECT code, name, shares, cost, price, market_value, "
@@ -46,14 +53,46 @@ def read_portfolio():
db.close() db.close()
# ── 总市值从 holdings 实时计算 ────────────────────
cash = float(summary.get('cash', 0) or 0)
frozen = float(summary.get('frozen_cash', 0) or 0)
# 获取港股汇率
import subprocess, os as _os
rate = 0.865 # 默认值
try:
hk_rate_py = _os.path.join(_os.path.dirname(_os.path.dirname(__file__)), 'hk_rate.py')
if _os.path.exists(hk_rate_py):
r = subprocess.run(
[_os.path.join(_os.path.dirname(_os.path.dirname(__file__)), 'venv', 'bin', 'python3'),
hk_rate_py, '--rate'],
capture_output=True, text=True, timeout=10
)
if r.returncode == 0 and r.stdout.strip():
rate = float(r.stdout.strip())
except Exception:
pass
total_mv = 0.0
for h in holdings:
p = float(h.get('price', 0) or 0)
s = float(h.get('shares', 0) or 0)
if h.get('currency') == 'HKD' or (len(str(h.get('code',''))) == 5 and str(h.get('code',''))[0] in ('0','1')):
total_mv += s * p * rate
else:
total_mv += s * p
total_mv = round(total_mv, 2)
total_assets = round(total_mv + cash + frozen, 2)
position_pct = round(total_mv / total_assets * 100, 2) if total_assets > 0 else 0
return { return {
"holdings": holdings, "holdings": holdings,
"total_assets": summary.get("total_assets", 0), "total_assets": total_assets,
"total_mv": summary.get("total_mv", 0), "total_mv": total_mv,
"stock_value": summary.get("stock_value", summary.get("total_mv", 0)), "stock_value": total_mv,
"cash": summary.get("cash", 0), "cash": cash,
"frozen_cash": summary.get("frozen_cash", 0), "frozen_cash": frozen,
"position_pct": summary.get("position_pct", 0), "position_pct": position_pct,
"currency": summary.get("currency", "CNY"), "currency": summary.get("currency", "CNY"),
"updated_at": summary.get("updated_at", ""), "updated_at": summary.get("updated_at", ""),
} }
+83 -5
View File
@@ -15,9 +15,11 @@
import sqlite3 import sqlite3
import json import json
import time
import functools
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional, Callable
DATA_DIR = Path(__file__).parent / "data" DATA_DIR = Path(__file__).parent / "data"
DB_PATH = DATA_DIR / "mofin.db" DB_PATH = DATA_DIR / "mofin.db"
@@ -27,15 +29,87 @@ DB_PATH = DATA_DIR / "mofin.db"
# ═══════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════
def get_conn() -> sqlite3.Connection: def get_conn() -> sqlite3.Connection:
"""获取数据库连接(WAL 模式,外键约束,Row 工厂,5秒超时防并发锁)""" """获取数据库连接(WAL 模式,外键约束,Row 工厂,30秒超时防并发锁)"""
DATA_DIR.mkdir(parents=True, exist_ok=True) DATA_DIR.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(str(DB_PATH), timeout=5) conn = sqlite3.connect(str(DB_PATH), timeout=30)
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
conn.execute("PRAGMA journal_mode=WAL") conn.execute("PRAGMA journal_mode=WAL")
conn.execute("PRAGMA foreign_keys=ON") conn.execute("PRAGMA foreign_keys=ON")
conn.execute("PRAGMA busy_timeout=30000")
conn.execute("PRAGMA synchronous=NORMAL")
return conn return conn
def execute_with_retry(conn: sqlite3.Connection, sql: str, params: tuple = (),
max_retries: int = 3, base_delay: float = 1.0) -> sqlite3.Cursor:
"""执行SQL并自动重试(捕获 database is locked"""
last_err = None
for attempt in range(max_retries + 1):
try:
return conn.execute(sql, params)
except sqlite3.OperationalError as e:
if "database is locked" not in str(e) and "cannot commit" not in str(e):
raise # 非锁错误直接抛
last_err = e
if attempt < max_retries:
delay = base_delay * (2 ** attempt) # 指数退避: 1s, 2s, 4s
time.sleep(delay)
else:
raise sqlite3.OperationalError(
f"DB锁重试{max_retries}次仍失败: {e}"
)
# unreachable -- both paths in loop either return or raise
if last_err:
raise last_err # type: ignore[misc]
def commit_with_retry(conn: sqlite3.Connection, max_retries: int = 3,
base_delay: float = 1.0) -> None:
"""提交事务并自动重试"""
last_err = None
for attempt in range(max_retries + 1):
try:
conn.commit()
return
except sqlite3.OperationalError as e:
if "database is locked" not in str(e) and "cannot commit" not in str(e):
raise
last_err = e
if attempt < max_retries:
delay = base_delay * (2 ** attempt)
time.sleep(delay)
else:
raise sqlite3.OperationalError(
f"DB提交重试{max_retries}次仍失败: {e}"
)
raise last_err
def retry_db_write(func: Callable) -> Callable:
"""装饰器:为 DB 写函数自动添加重试"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
max_retries = 3
base_delay = 1.0
last_err = None
for attempt in range(max_retries + 1):
try:
return func(*args, **kwargs)
except sqlite3.OperationalError as e:
if "database is locked" not in str(e) and "cannot commit" not in str(e):
raise
last_err = e
if attempt < max_retries:
delay = base_delay * (2 ** attempt)
time.sleep(delay)
else:
raise sqlite3.OperationalError(
f"DB写重试{max_retries}次仍失败({func.__name__}): {e}"
)
raise last_err
return wrapper
# ═══════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════
# 建表(幂等) # 建表(幂等)
# ═══════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════
@@ -1058,6 +1132,7 @@ def write_holding_strategy(conn, code: str, name: str, data: dict) -> tuple[bool
def write_holdings_batch(conn, holdings: list[dict]) -> tuple[bool, str]: def write_holdings_batch(conn, holdings: list[dict]) -> tuple[bool, str]:
"""批量写入持仓(替代 portfolio.json holdings[]""" """批量写入持仓(替代 portfolio.json holdings[]"""
try: try:
conn.execute("BEGIN IMMEDIATE")
for h in holdings: for h in holdings:
currency = str(h.get('currency', 'CNY')).upper() currency = str(h.get('currency', 'CNY')).upper()
if currency not in ('CNY', 'HKD'): if currency not in ('CNY', 'HKD'):
@@ -1082,11 +1157,12 @@ def write_holdings_batch(conn, holdings: list[dict]) -> tuple[bool, str]:
except sqlite3.IntegrityError as e: except sqlite3.IntegrityError as e:
conn.rollback() conn.rollback()
return False, f"币种约束: {e}" return False, f"币种约束: {e}"
except sqlite3.OperationalError as e:
return False, f"DB锁冲突(重试耗尽): {e}"
def write_portfolio_summary(conn, data: dict) -> tuple[bool, str]: def write_portfolio_summary(conn, data: dict) -> tuple[bool, str]:
"""写入持仓汇总(替代 portfolio.json 顶层)""" """写入持仓汇总(替代 portfolio.json 顶层)"""
try: try:
conn.execute("BEGIN IMMEDIATE")
conn.execute(""" conn.execute("""
INSERT INTO portfolio_summary (id, total_assets, total_mv, stock_value, INSERT INTO portfolio_summary (id, total_assets, total_mv, stock_value,
cash, frozen_cash, position_pct, total_pnl, currency, updated_at) cash, frozen_cash, position_pct, total_pnl, currency, updated_at)
@@ -1106,6 +1182,8 @@ def write_portfolio_summary(conn, data: dict) -> tuple[bool, str]:
return True, "汇总已写入" return True, "汇总已写入"
except sqlite3.IntegrityError as e: except sqlite3.IntegrityError as e:
return False, f"约束: {e}" return False, f"约束: {e}"
except sqlite3.OperationalError as e:
return False, f"DB锁冲突: {e}"
def write_watchlist_stock(conn, stock: dict) -> tuple[bool, str]: def write_watchlist_stock(conn, stock: dict) -> tuple[bool, str]:
+4 -4
View File
@@ -17,6 +17,8 @@ import urllib.error
from datetime import datetime, date, timedelta from datetime import datetime, date, timedelta
from typing import Optional from typing import Optional
from mofin_db import get_conn
DATA_DIR = "/home/hmo/web-dashboard/data" DATA_DIR = "/home/hmo/web-dashboard/data"
HISTORY_PATH = os.path.join(DATA_DIR, "price_history.json") HISTORY_PATH = os.path.join(DATA_DIR, "price_history.json")
# multi_tf_cache.json 已迁移到 DB (mtf_cache 表) # multi_tf_cache.json 已迁移到 DB (mtf_cache 表)
@@ -91,8 +93,7 @@ def _load_mtf_cache():
if _MTF_CACHE_DATA is not None: if _MTF_CACHE_DATA is not None:
return _MTF_CACHE_DATA return _MTF_CACHE_DATA
try: try:
import sqlite3 db = get_conn()
db = sqlite3.connect('/home/hmo/web-dashboard/data/mofin.db')
rows = db.execute("SELECT code, cache_json FROM mtf_cache").fetchall() rows = db.execute("SELECT code, cache_json FROM mtf_cache").fetchall()
_MTF_CACHE_DATA = {} _MTF_CACHE_DATA = {}
for code, json_str in rows: for code, json_str in rows:
@@ -112,8 +113,7 @@ def _save_mtf_cache():
if _MTF_CACHE_DATA is None: if _MTF_CACHE_DATA is None:
return return
try: try:
import sqlite3 db = get_conn()
db = sqlite3.connect('/home/hmo/web-dashboard/data/mofin.db')
for code, data in _MTF_CACHE_DATA.items(): for code, data in _MTF_CACHE_DATA.items():
db.execute( db.execute(
"INSERT OR REPLACE INTO mtf_cache (code, cache_json, updated_at) VALUES (?,?,datetime('now','localtime'))", "INSERT OR REPLACE INTO mtf_cache (code, cache_json, updated_at) VALUES (?,?,datetime('now','localtime'))",
+509 -727
View File
File diff suppressed because it is too large Load Diff
+22
View File
@@ -0,0 +1,22 @@
import sqlite3
# Script connects to THIS db
proj_db = '/home/hmo/projects/MoFin/data/mofin.db'
# Real data lives in THIS db
real_db = '/home/hmo/web-dashboard/data/mofin.db'
for label, path in [("project", proj_db), ("real", real_db)]:
db = sqlite3.connect(path)
tables = [r[0] for r in db.execute("SELECT name FROM sqlite_master WHERE type='table'")]
print(f"{label} db ({path}): {len(tables)} tables")
for t in tables:
if t == 'todos':
sql = db.execute(f"SELECT sql FROM sqlite_master WHERE name='{t}'").fetchone()
print(f" {t}: {sql[0][:100] if sql else 'no sql'}")
elif t in ('holdings', 'holding_strategies', 'watchlist_stocks', 'portfolio_summary'):
cnt = db.execute(f"SELECT COUNT(*) FROM {t}").fetchone()[0]
print(f" {t}: {cnt} rows")
else:
cnt = db.execute(f"SELECT COUNT(*) FROM {t}").fetchone()[0]
print(f" {t}: {cnt} rows")
db.close()
+8
View File
@@ -0,0 +1,8 @@
import sqlite3
for label, path in [("project", '/home/hmo/projects/MoFin/data/mofin.db'), ("real", '/home/hmo/web-dashboard/data/mofin.db')]:
db = sqlite3.connect(path)
sql = db.execute("SELECT sql FROM sqlite_master WHERE name='todos'").fetchone()
print(f"=== {label}: {path} ===")
print(sql[0] if sql else "NOT FOUND")
db.close()
+6
View File
@@ -0,0 +1,6 @@
"""Fix DB_PATH in self_todo_executor.py"""
path = '/home/hmo/.hermes/profiles/position-analyst/scripts/self_todo_executor.py'
content = open(path).read()
content = content.replace('projects/MoFin/data', 'web-dashboard/data')
open(path, 'w').write(content)
print("DB_PATH fixed to web-dashboard/data/mofin.db")
+47
View File
@@ -0,0 +1,47 @@
"""Fix: unify todos table schema across project and real DB"""
import sqlite3
project_db = '/home/hmo/projects/MoFin/data/mofin.db'
real_db = '/home/hmo/web-dashboard/data/mofin.db'
# Zhiwei's canonical schema (from project db)
target_schema = """
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT,
status TEXT DEFAULT 'pending',
priority TEXT DEFAULT 'medium',
source TEXT DEFAULT 'manual',
fix_action TEXT,
retry_count INTEGER DEFAULT 0,
note TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
"""
def ensure_todos(db_path, label):
db = sqlite3.connect(db_path)
existing = db.execute("SELECT name FROM sqlite_master WHERE name='todos'").fetchone()
if not existing:
db.execute(f"CREATE TABLE todos ({target_schema})")
print(f"{label}: created todos table")
else:
# Ensure all columns exist
existing_cols = {r[1] for r in db.execute("PRAGMA table_info(todos)")}
needed = {'title', 'description', 'status', 'priority', 'source', 'fix_action',
'retry_count', 'note', 'created_at', 'updated_at'}
missing = needed - existing_cols
for col in missing:
if col in ('retry_count',):
db.execute(f"ALTER TABLE todos ADD COLUMN {col} INTEGER DEFAULT 0")
elif col in ('created_at', 'updated_at'):
db.execute(f"ALTER TABLE todos ADD COLUMN {col} TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
else:
db.execute(f"ALTER TABLE todos ADD COLUMN {col} TEXT")
print(f"{label}: checked, {len(missing)} missing columns added" if missing else f"{label}: schema OK")
db.commit()
db.close()
ensure_todos(project_db, "project db")
ensure_todos(real_db, "real db")
print("\nDone. Both DBs now have matching todos schema.")
+3 -5
View File
@@ -20,6 +20,7 @@ import threading
import time import time
from datetime import datetime, time from datetime import datetime, time
from mo_data import read_portfolio, read_decisions from mo_data import read_portfolio, read_decisions
from mofin_db import get_conn
# ── MoFin unified model ────────────────────────────────────────────── # ── MoFin unified model ──────────────────────────────────────────────
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
@@ -63,9 +64,7 @@ def fetch_trend_data(code):
# 价格从 DB 读取,不再自拉腾讯 API # 价格从 DB 读取,不再自拉腾讯 API
current = 0 current = 0
try: try:
import sqlite3 db = get_conn()
db = sqlite3.connect('/home/hmo/web-dashboard/data/mofin.db')
db.row_factory = sqlite3.Row
row = db.execute("SELECT price FROM holdings WHERE code=? AND is_active=1", (code,)).fetchone() row = db.execute("SELECT price FROM holdings WHERE code=? AND is_active=1", (code,)).fetchone()
if not row: if not row:
row = db.execute("SELECT price FROM watchlist_stocks WHERE code=? AND is_active=1", (code,)).fetchone() row = db.execute("SELECT price FROM watchlist_stocks WHERE code=? AND is_active=1", (code,)).fetchone()
@@ -155,8 +154,7 @@ def load_macro_line():
parts = [] parts = []
try: try:
# 优先 DB # 优先 DB
import sqlite3 db = get_conn()
db = sqlite3.connect("/home/hmo/MoFin/data/mofin.db")
row = db.execute( row = db.execute(
"SELECT structure FROM macro_context_log " "SELECT structure FROM macro_context_log "
"WHERE has_valid_data=1 ORDER BY created_at DESC LIMIT 1" "WHERE has_valid_data=1 ORDER BY created_at DESC LIMIT 1"
+32
View File
@@ -0,0 +1,32 @@
"""Verify self_todo_executor works with real DB"""
import subprocess
script = '/home/hmo/.hermes/profiles/position-analyst/scripts/self_todo_executor.py'
# Test 1: DB_PATH
content = open(script).read()
if 'web-dashboard/data/mofin.db' in content:
print("DB_PATH: OK")
else:
print("DB_PATH: WRONG")
exit(1)
# Test 2: script can import and run
try:
import importlib.util
spec = importlib.util.spec_from_file_location("executor", script)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
print("Import: OK")
except Exception as e:
print(f"Import: FAIL -> {e}")
exit(1)
# Test 3: get_pending works
try:
rows = mod.get_pending()
print(f"get_pending: OK ({len(rows)} pending)")
except Exception as e:
print(f"get_pending: FAIL -> {e}")
exit(1)
print("\nAll checks passed.")
+6 -9
View File
@@ -698,9 +698,8 @@ def compute_sector_adjustment(code, market_ctx, stock_sector_map):
def load_macro_context(): def load_macro_context():
"""读取宏观上下文,返回 (bias, desc),优先 DB,回退 JSON""" """读取宏观上下文,返回 (bias, desc),优先 DB,回退 JSON"""
try: try:
import sqlite3 from mofin_db import get_conn
from pathlib import Path conn = get_conn()
conn = sqlite3.connect(str(Path(__file__).parent.parent / "data" / "mofin.db"))
row = conn.execute( row = conn.execute(
"SELECT indices, structure FROM macro_context_log " "SELECT indices, structure FROM macro_context_log "
"WHERE has_valid_data=1 ORDER BY created_at DESC LIMIT 1" "WHERE has_valid_data=1 ORDER BY created_at DESC LIMIT 1"
@@ -740,9 +739,8 @@ def batch_fetch_prices(codes):
# 主通道:从 DB 读取(price_monitor 唯一价格入口) # 主通道:从 DB 读取(price_monitor 唯一价格入口)
try: try:
import sqlite3 from mofin_db import get_conn
db = sqlite3.connect('/home/hmo/web-dashboard/data/mofin.db') db = get_conn()
db.row_factory = sqlite3.Row
for raw_code in codes: for raw_code in codes:
raw_code = str(raw_code).split('_')[0] raw_code = str(raw_code).split('_')[0]
if not raw_code: continue if not raw_code: continue
@@ -842,9 +840,8 @@ def get_price_tencent(code):
# 主通道: DB # 主通道: DB
try: try:
import sqlite3 from mofin_db import get_conn
db = sqlite3.connect('/home/hmo/web-dashboard/data/mofin.db') db = get_conn()
db.row_factory = sqlite3.Row
row = db.execute("SELECT price FROM holdings WHERE code=? AND is_active=1", (raw_code,)).fetchone() row = db.execute("SELECT price FROM holdings WHERE code=? AND is_active=1", (raw_code,)).fetchone()
if not row: if not row:
row = db.execute("SELECT price FROM holding_strategies WHERE code=? AND status='active' ORDER BY updated_at DESC LIMIT 1", (raw_code,)).fetchone() row = db.execute("SELECT price FROM holding_strategies WHERE code=? AND status='active' ORDER BY updated_at DESC LIMIT 1", (raw_code,)).fetchone()