migrate: remove JSON, DB-only — mo_data, server, scripts, prompts (27 files)

This commit is contained in:
知微
2026-07-03 12:12:05 +08:00
parent b1a79d962c
commit b3bedc8024
43 changed files with 8272 additions and 7449 deletions
Binary file not shown.
+66 -2
View File
@@ -1,6 +1,6 @@
{
"last_updated": "2026-06-30 11:11",
"total_candidates": 3,
"last_updated": "2026-07-03 11:32",
"total_candidates": 5,
"sectors_analyzed_today": [
"半导体",
"金属新材料",
@@ -439,6 +439,70 @@
"drop_reason": null,
"trend_warning": false,
"trend_note": ""
},
{
"code": "300699",
"name": "光威复材",
"sector": "军工装备",
"xiaoguo_score": 8.5,
"xiaoguo_reason": "碳纤维核心供应商,与中简科技同属新材料主线,业绩确定性强,机构持仓集中。技术面放量突破震荡平台,量价配合健康。",
"xiaoguo_strategy": {
"entry_range": "38.5-40.2元",
"stop_loss": "36.8元",
"target": "45.0-48.0元"
},
"verified_price": 30.28,
"verified_change": 6.62,
"added_at": "2026-07-03 11:10",
"last_updated": "2026-07-03 11:10",
"num_observations": 1,
"score_history": [
{
"date": "2026-07-03 11:10",
"score": 8.5
}
],
"zhiwei_star": null,
"zhiwei_reviewed": false,
"zhiwei_reviewed_at": null,
"promoted": false,
"promoted_at": null,
"dropped": false,
"drop_reason": null,
"trend_warning": false,
"trend_note": ""
},
{
"code": "600760",
"name": "中航沈飞",
"sector": "军工装备",
"xiaoguo_score": 8.0,
"xiaoguo_reason": "歼击机核心总装平台,订单饱满且估值处于历史中低位。板块情绪升温时具备高弹性,适合作为稳健底仓。",
"xiaoguo_strategy": {
"entry_range": "32.0-33.5元",
"stop_loss": "30.5元",
"target": "36.5-38.0元"
},
"verified_price": 41.39,
"verified_change": 1.5,
"added_at": "2026-07-03 11:10",
"last_updated": "2026-07-03 11:10",
"num_observations": 1,
"score_history": [
{
"date": "2026-07-03 11:10",
"score": 8.0
}
],
"zhiwei_star": null,
"zhiwei_reviewed": false,
"zhiwei_reviewed_at": null,
"promoted": false,
"promoted_at": null,
"dropped": false,
"drop_reason": null,
"trend_warning": false,
"trend_note": ""
}
]
}
+506 -844
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.
+39 -39
View File
@@ -1254,7 +1254,7 @@
"volume": 147766189.0
}
],
"updated_at": 1783045022.0597436
"updated_at": 1783051747.536662
},
"688795": {
"daily": [
@@ -2479,7 +2479,7 @@
"volume": 4788252.0
}
],
"updated_at": 1783045017.2558444
"updated_at": 1783051740.058768
},
"000657": {
"daily": [
@@ -3736,7 +3736,7 @@
"volume": 1051508.0
}
],
"updated_at": 1783044767.899178
"updated_at": 1783051278.4453125
},
"000700": {
"daily": [
@@ -4993,7 +4993,7 @@
"volume": 1265397.0
}
],
"updated_at": 1783044853.6724539
"updated_at": 1783051627.982171
},
"000711": {
"daily": [
@@ -6250,7 +6250,7 @@
"volume": 496248.0
}
],
"updated_at": 1783044770.290931
"updated_at": 1783051282.8847363
},
"001309": {
"daily": [
@@ -7507,7 +7507,7 @@
"volume": 216663.0
}
],
"updated_at": 1783044771.251316
"updated_at": 1783051286.5401492
},
"002594": {
"daily": [
@@ -8764,7 +8764,7 @@
"volume": 934285.0
}
],
"updated_at": 1783044773.5265088
"updated_at": 1783051286.9981902
},
"00700": {
"daily": [
@@ -10029,7 +10029,7 @@
"volume": 13032847.0
}
],
"updated_at": 1783044900.4552383
"updated_at": 1783051633.4890287
},
"00968": {
"daily": [
@@ -11294,7 +11294,7 @@
"volume": 19422000.0
}
],
"updated_at": 1783044902.281637
"updated_at": 1783045247.978355
},
"00981": {
"daily": [
@@ -12559,7 +12559,7 @@
"volume": 60114819.0
}
],
"updated_at": 1783044904.170763
"updated_at": 1783051638.5061805
},
"01070": {
"daily": [
@@ -13824,7 +13824,7 @@
"volume": 690000.0
}
],
"updated_at": 1783044904.831619
"updated_at": 1783045568.5510702
},
"01088": {
"daily": [
@@ -15089,7 +15089,7 @@
"volume": 2870057.0
}
],
"updated_at": 1783044905.7026997
"updated_at": 1783051643.521601
},
"01211": {
"daily": [
@@ -16354,7 +16354,7 @@
"volume": 13286402.0
}
],
"updated_at": 1783044907.9523368
"updated_at": 1783051648.5394585
},
"01478": {
"daily": [
@@ -17619,7 +17619,7 @@
"volume": 1618000.0
}
],
"updated_at": 1783044914.9662342
"updated_at": 1783051653.5516405
},
"01888": {
"daily": [
@@ -18884,7 +18884,7 @@
"volume": 29941901.0
}
],
"updated_at": 1783044916.768711
"updated_at": 1783051658.5684023
},
"02202": {
"daily": [
@@ -20149,7 +20149,7 @@
"volume": 19786580.0
}
],
"updated_at": 1783044922.7405882
"updated_at": 1783051663.581557
},
"02318": {
"daily": [
@@ -21414,7 +21414,7 @@
"volume": 11523457.0
}
],
"updated_at": 1783044925.0042746
"updated_at": 1783045619.0956357
},
"02359": {
"daily": [
@@ -22679,7 +22679,7 @@
"volume": 1730975.0
}
],
"updated_at": 1783044933.7568398
"updated_at": 1783045626.3903263
},
"02388": {
"daily": [
@@ -23944,7 +23944,7 @@
"volume": 3823095.0
}
],
"updated_at": 1783044937.2679691
"updated_at": 1783045637.5225153
},
"02628": {
"daily": [
@@ -25209,7 +25209,7 @@
"volume": 23110112.0
}
],
"updated_at": 1783044942.9842496
"updated_at": 1783045647.7615216
},
"06160": {
"daily": [
@@ -26474,7 +26474,7 @@
"volume": 2575221.0
}
],
"updated_at": 1783044948.6934419
"updated_at": 1783045655.0431929
},
"06869": {
"daily": [
@@ -27739,7 +27739,7 @@
"volume": 15066251.0
}
],
"updated_at": 1783044950.480943
"updated_at": 1783051668.878092
},
"09868": {
"daily": [
@@ -29004,7 +29004,7 @@
"volume": 10168056.0
}
],
"updated_at": 1783044951.3377807
"updated_at": 1783045674.3812273
},
"09988": {
"daily": [
@@ -30269,7 +30269,7 @@
"volume": 35148396.0
}
],
"updated_at": 1783044956.074646
"updated_at": 1783045677.2402296
},
"300035": {
"daily": [
@@ -31526,7 +31526,7 @@
"volume": 230937.0
}
],
"updated_at": 1783044957.8285887
"updated_at": 1783051674.5847087
},
"300124": {
"daily": [
@@ -32783,7 +32783,7 @@
"volume": 722493.0
}
],
"updated_at": 1783044958.4761739
"updated_at": 1783051344.573262
},
"300308": {
"daily": [
@@ -34040,7 +34040,7 @@
"volume": 389058.0
}
],
"updated_at": 1783044962.1302354
"updated_at": 1783051680.1109805
},
"300548": {
"daily": [
@@ -35297,7 +35297,7 @@
"volume": 242727.0
}
],
"updated_at": 1783044974.5997705
"updated_at": 1783051685.6375031
},
"300750": {
"daily": [
@@ -36554,7 +36554,7 @@
"volume": 551212.0
}
],
"updated_at": 1783044988.1779277
"updated_at": 1783051690.893728
},
"301308": {
"daily": [
@@ -37811,7 +37811,7 @@
"volume": 296230.0
}
],
"updated_at": 1783044993.181729
"updated_at": 1783051380.6922731
},
"518880": {
"daily": [
@@ -39068,7 +39068,7 @@
"volume": 3915247.0
}
],
"updated_at": 1783045000.125372
"updated_at": 1783051695.915503
},
"600519": {
"daily": [
@@ -40325,7 +40325,7 @@
"volume": 64803.0
}
],
"updated_at": 1783045000.7318592
"updated_at": 1783051561.7579694
},
"600563": {
"daily": [
@@ -41582,7 +41582,7 @@
"volume": 180947.0
}
],
"updated_at": 1783045002.30473
"updated_at": 1783051700.9725504
},
"601318": {
"daily": [
@@ -42839,7 +42839,7 @@
"volume": 1746202.0
}
],
"updated_at": 1783045007.7435973
"updated_at": 1783051575.149798
},
"601899": {
"daily": [
@@ -44096,7 +44096,7 @@
"volume": 4780454.0
}
],
"updated_at": 1783045008.845117
"updated_at": 1783051705.9927075
},
"688411": {
"daily": [
@@ -45353,7 +45353,7 @@
"volume": 13672788.0
}
],
"updated_at": 1783045009.6267552
"updated_at": 1783051711.208435
},
"688630": {
"daily": [
@@ -46610,7 +46610,7 @@
"volume": 9660790.0
}
],
"updated_at": 1783045010.4224615
"updated_at": 1783051587.7339969
},
"688639": {
"daily": [
@@ -47867,7 +47867,7 @@
"volume": 13996588.0
}
],
"updated_at": 1783045016.168726
"updated_at": 1783051733.2346125
},
"688802": {
"daily": [
@@ -49092,6 +49092,6 @@
"volume": 3202146.0
}
],
"updated_at": 1783045018.4521751
"updated_at": 1783051743.6900556
}
}
+67 -67
View File
@@ -5,9 +5,9 @@
"name": "中际旭创",
"shares": 100,
"cost": 1316.53,
"price": 1145.31,
"market_value": 115899.0,
"change_pct": 0.2,
"price": 1157.97,
"market_value": 115797.0,
"change_pct": 1.31,
"currency": "CNY",
"position_pct": 15.27,
"_currency": "CNY"
@@ -16,10 +16,10 @@
"code": "06869",
"name": "长飞光纤光缆",
"shares": 500,
"cost": 263.72,
"price": 202.6,
"market_value": 87825.0,
"change_pct": 2.34,
"cost": 228.65,
"price": 178.26,
"market_value": 102700.0,
"change_pct": 3.859,
"currency": "HKD",
"position_pct": 13.47,
"_currency": "HKD"
@@ -28,10 +28,10 @@
"code": "01478",
"name": "丘钛科技",
"shares": 11000,
"cost": 13.47,
"price": 6.94,
"market_value": 66110.0,
"change_pct": 3.27,
"cost": 11.68,
"price": 6.11,
"market_value": 77550.0,
"change_pct": 4.911,
"currency": "HKD",
"position_pct": 7.97,
"_currency": "HKD"
@@ -41,9 +41,9 @@
"name": "紫金矿业",
"shares": 2400,
"cost": 39.89,
"price": 28.05,
"market_value": 66528.0,
"change_pct": 6.65,
"price": 28.04,
"market_value": 67296.0,
"change_pct": 6.62,
"currency": "CNY",
"position_pct": 7.34,
"_currency": "CNY"
@@ -53,9 +53,9 @@
"name": "海博思创",
"shares": 200,
"cost": 266.95,
"price": 257.0,
"market_value": 51770.00000000001,
"change_pct": 0.5,
"price": 257.52,
"market_value": 51504.0,
"change_pct": 0.71,
"currency": "CNY",
"position_pct": 6.31,
"_currency": "CNY"
@@ -65,9 +65,9 @@
"name": "中芯国际",
"shares": 300,
"cost": 126.07,
"price": 140.66,
"market_value": 43011.0,
"change_pct": -2.39,
"price": 143.65,
"market_value": 43095.0,
"change_pct": -0.31,
"currency": "CNY",
"position_pct": 5.44,
"_currency": "CNY"
@@ -76,10 +76,10 @@
"code": "01888",
"name": "建滔积层板",
"shares": 500,
"cost": 88.23,
"price": 86.45,
"market_value": 42450.0,
"change_pct": 3.16,
"cost": 76.5,
"price": 74.17,
"market_value": 42600.0,
"change_pct": 2.088,
"currency": "HKD",
"position_pct": 5.28,
"_currency": "HKD"
@@ -90,7 +90,7 @@
"shares": 2800,
"cost": 21.51,
"price": 16.69,
"market_value": 46871.99999999999,
"market_value": 46732.0,
"change_pct": -1.53,
"currency": "CNY",
"position_pct": 5.25,
@@ -101,9 +101,9 @@
"name": "宁德时代",
"shares": 100,
"cost": 401.78,
"price": 386.2,
"market_value": 38427.0,
"change_pct": 1.01,
"price": 384.16,
"market_value": 38416.0,
"change_pct": 0.47,
"currency": "CNY",
"position_pct": 4.64,
"_currency": "CNY"
@@ -112,10 +112,10 @@
"code": "01211",
"name": "比亚迪股份",
"shares": 600,
"cost": 104.87,
"price": 82.5,
"market_value": 42918.0,
"change_pct": 5.36,
"cost": 90.92,
"price": 71.66,
"market_value": 49560.0,
"change_pct": 5.556,
"currency": "HKD",
"position_pct": 4.62,
"_currency": "HKD"
@@ -124,10 +124,10 @@
"code": "02202",
"name": "万科企业",
"shares": 19700,
"cost": 4.67,
"price": 2.34,
"market_value": 39794.0,
"change_pct": 4.93,
"cost": 4.05,
"price": 2.02,
"market_value": 45704.0,
"change_pct": 4.484,
"currency": "HKD",
"position_pct": 4.6,
"_currency": "HKD"
@@ -137,9 +137,9 @@
"name": "腾讯",
"shares": 100,
"cost": null,
"price": 442.2,
"market_value": 38269.0,
"change_pct": 2.79,
"price": 379.05,
"market_value": 43700.0,
"change_pct": 1.627,
"currency": "HKD",
"position_pct": null,
"_currency": "HKD"
@@ -148,10 +148,10 @@
"code": "00981",
"name": "中芯国际",
"shares": 500,
"cost": 75.94,
"price": 79.8,
"market_value": 34570.0,
"change_pct": -0.75,
"cost": 65.84,
"price": 69.1,
"market_value": 39825.0,
"change_pct": -0.871,
"currency": "HKD",
"position_pct": 4.2,
"_currency": "HKD"
@@ -161,9 +161,9 @@
"name": "长芯博创",
"shares": 100,
"cost": 231.46,
"price": 224.15,
"market_value": 22623.0,
"change_pct": 0.97,
"price": 227.0,
"market_value": 22700.0,
"change_pct": 2.25,
"currency": "CNY",
"position_pct": 3.2,
"_currency": "CNY"
@@ -173,9 +173,9 @@
"name": "黄金ETF华安",
"shares": 2400,
"cost": 12.19,
"price": 8.68,
"market_value": 20832.0,
"change_pct": 2.4,
"price": 8.69,
"market_value": 20856.0,
"change_pct": 2.49,
"currency": "CNY",
"position_pct": 2.45,
"_currency": "CNY"
@@ -185,9 +185,9 @@
"name": "中科电气",
"shares": 1400,
"cost": 22.29,
"price": 14.45,
"market_value": 20118.0,
"change_pct": 1.98,
"price": 14.3,
"market_value": 20020.0,
"change_pct": 0.92,
"currency": "CNY",
"position_pct": 2.42,
"_currency": "CNY"
@@ -197,9 +197,9 @@
"name": "模塑科技",
"shares": 1400,
"cost": 14.83,
"price": 17.55,
"market_value": 24304.0,
"change_pct": 4.03,
"price": 17.37,
"market_value": 24318.0,
"change_pct": 2.96,
"currency": "CNY",
"position_pct": 2.41,
"_currency": "CNY"
@@ -209,9 +209,9 @@
"name": "法拉电子",
"shares": 100,
"cost": 147.18,
"price": 157.0,
"market_value": 15900.0,
"change_pct": -4.44,
"price": 161.71,
"market_value": 16171.0,
"change_pct": -1.58,
"currency": "CNY",
"position_pct": 2.3,
"_currency": "CNY"
@@ -220,21 +220,21 @@
"code": "01088",
"name": "中国神华",
"shares": 500,
"cost": 45.89,
"price": 39.7,
"market_value": 17220.0,
"change_pct": 0.25,
"cost": 39.79,
"price": 34.84,
"market_value": 20090.0,
"change_pct": 1.465,
"currency": "HKD",
"position_pct": 2.14,
"_currency": "HKD"
}
],
"total_assets": 961185.34,
"total_mv": 829063.41,
"total_assets": 864781.03,
"total_mv": 784305.03,
"stock_value": null,
"cash": 132121.93,
"cash": 80476.0,
"frozen_cash": 0.0,
"position_pct": 86.25,
"position_pct": 90.69,
"currency": "CNY",
"updated_at": "2026-07-03 10:22"
"updated_at": "2026-07-03 12:10"
}
+800
View File
@@ -8309,6 +8309,806 @@
"event_label": "买入区间",
"timestamp": "2026-07-03T10:20:24.988282",
"date": "2026-07-03"
},
{
"code": "001309",
"name": "德明利",
"event_type": "entry_zone",
"price": 833.99,
"trigger_value": "818.3~838.69",
"event_label": "买入区间",
"timestamp": "2026-07-03T10:22:24.916521",
"date": "2026-07-03"
},
{
"code": "600519",
"name": "贵州茅台",
"event_type": "entry_zone",
"price": 1203.0,
"trigger_value": "1179.92~1206.14",
"event_label": "买入区间",
"timestamp": "2026-07-03T10:22:25.114242",
"date": "2026-07-03"
},
{
"code": "000657",
"name": "中钨高新",
"event_type": "entry_zone",
"price": 87.93,
"trigger_value": "86.73~90.27",
"event_label": "买入区间",
"timestamp": "2026-07-03T10:22:55.561315",
"date": "2026-07-03"
},
{
"code": "000711",
"name": "ST京蓝",
"event_type": "entry_zone",
"price": 5.24,
"trigger_value": "5.14~5.26",
"event_label": "买入区间",
"timestamp": "2026-07-03T10:22:55.893638",
"date": "2026-07-03"
},
{
"code": "688630",
"name": "芯碁微装",
"event_type": "entry_zone",
"price": 486.0,
"trigger_value": "475.2~486.17",
"event_label": "买入区间",
"timestamp": "2026-07-03T10:22:57.837686",
"date": "2026-07-03"
},
{
"code": "000657",
"name": "中钨高新",
"event_type": "entry_zone",
"price": 88.89,
"trigger_value": "86.27~89.79",
"event_label": "买入区间",
"timestamp": "2026-07-03T10:27:25.917807",
"date": "2026-07-03"
},
{
"code": "000711",
"name": "ST京蓝",
"event_type": "entry_zone",
"price": 5.23,
"trigger_value": "5.14~5.25",
"event_label": "买入区间",
"timestamp": "2026-07-03T10:27:31.525100",
"date": "2026-07-03"
},
{
"code": "02359",
"name": "药明康德",
"event_type": "entry_zone",
"price": 152.9,
"trigger_value": "149.45~154.86",
"event_label": "买入区间",
"timestamp": "2026-07-03T10:28:16.907063",
"date": "2026-07-03"
},
{
"code": "06160",
"name": "百济神州",
"event_type": "entry_zone",
"price": 181.6,
"trigger_value": "179.63~183.4",
"event_label": "买入区间",
"timestamp": "2026-07-03T10:28:19.403509",
"date": "2026-07-03"
},
{
"code": "06869",
"name": "长飞光纤光缆",
"event_type": "entry_zone",
"price": 200.4,
"trigger_value": "182.52~212.94",
"event_label": "买入区间",
"timestamp": "2026-07-03T10:28:19.456833",
"date": "2026-07-03"
},
{
"code": "09868",
"name": "小鹏汽车-W",
"event_type": "entry_zone",
"price": 50.25,
"trigger_value": "50.18~51.14",
"event_label": "买入区间",
"timestamp": "2026-07-03T10:28:19.522580",
"date": "2026-07-03"
},
{
"code": "09988",
"name": "阿里巴巴-W",
"event_type": "entry_zone",
"price": 94.1,
"trigger_value": "93.49~94.92",
"event_label": "买入区间",
"timestamp": "2026-07-03T10:28:19.602866",
"date": "2026-07-03"
},
{
"code": "000657",
"name": "中钨高新",
"event_type": "entry_zone",
"price": 88.67,
"trigger_value": "86.83~90.37",
"event_label": "买入区间",
"timestamp": "2026-07-03T10:30:19.791987",
"date": "2026-07-03"
},
{
"code": "000711",
"name": "ST京蓝",
"event_type": "entry_zone",
"price": 5.25,
"trigger_value": "5.14~5.25",
"event_label": "买入区间",
"timestamp": "2026-07-03T10:30:25.159354",
"date": "2026-07-03"
},
{
"code": "000657",
"name": "中钨高新",
"event_type": "entry_zone",
"price": 88.79,
"trigger_value": "86.83~90.37",
"event_label": "买入区间",
"timestamp": "2026-07-03T10:34:15.409862",
"date": "2026-07-03"
},
{
"code": "000711",
"name": "ST京蓝",
"event_type": "entry_zone",
"price": 5.25,
"trigger_value": "5.14~5.25",
"event_label": "买入区间",
"timestamp": "2026-07-03T10:34:15.537060",
"date": "2026-07-03"
},
{
"code": "300124",
"name": "汇川技术",
"event_type": "entry_zone",
"price": 71.24,
"trigger_value": "70.48~71.55",
"event_label": "买入区间",
"timestamp": "2026-07-03T10:38:29.251662",
"date": "2026-07-03"
},
{
"code": "000700",
"name": "模塑科技",
"event_type": "entry_zone",
"price": 17.31,
"trigger_value": "0~18.93",
"event_label": "止盈区间",
"timestamp": "2026-07-03T10:38:39.296028",
"date": "2026-07-03"
},
{
"code": "300124",
"name": "汇川技术",
"event_type": "entry_zone",
"price": 71.23,
"trigger_value": "70.48~71.55",
"event_label": "买入区间",
"timestamp": "2026-07-03T10:38:42.781286",
"date": "2026-07-03"
},
{
"code": "600563",
"name": "法拉电子",
"event_type": "entry_zone",
"price": 159.56,
"trigger_value": "0~171.96",
"event_label": "止盈区间",
"timestamp": "2026-07-03T10:38:42.981115",
"date": "2026-07-03"
},
{
"code": "601899",
"name": "紫金矿业",
"event_type": "entry_zone",
"price": 28.24,
"trigger_value": "0~30.38",
"event_label": "止盈区间",
"timestamp": "2026-07-03T10:38:43.067796",
"date": "2026-07-03"
},
{
"code": "688981",
"name": "中芯国际",
"event_type": "entry_zone",
"price": 142.5,
"trigger_value": "0~153.58",
"event_label": "止盈区间",
"timestamp": "2026-07-03T10:38:43.142300",
"date": "2026-07-03"
},
{
"code": "000711",
"name": "ST京蓝",
"event_type": "entry_zone",
"price": 5.26,
"trigger_value": "5.15~5.26",
"event_label": "买入区间",
"timestamp": "2026-07-03T10:44:30.388534",
"date": "2026-07-03"
},
{
"code": "000711",
"name": "ST京蓝",
"event_type": "entry_zone",
"price": 5.26,
"trigger_value": "5.15~5.26",
"event_label": "买入区间",
"timestamp": "2026-07-03T10:48:28.599391",
"date": "2026-07-03"
},
{
"code": "000711",
"name": "ST京蓝",
"event_type": "entry_zone",
"price": 5.26,
"trigger_value": "5.15~5.26",
"event_label": "买入区间",
"timestamp": "2026-07-03T10:50:35.989841",
"date": "2026-07-03"
},
{
"code": "300124",
"name": "汇川技术",
"event_type": "entry_zone",
"price": 71.12,
"trigger_value": "69.93~71.47",
"event_label": "买入区间",
"timestamp": "2026-07-03T11:10:13.492666",
"date": "2026-07-03"
},
{
"code": "688795",
"name": "摩尔线程-U",
"event_type": "entry_zone",
"price": 656.41,
"trigger_value": "640.4~661.55",
"event_label": "买入区间",
"timestamp": "2026-07-03T11:10:14.388968",
"date": "2026-07-03"
},
{
"code": "688802",
"name": "沐曦股份-U",
"event_type": "entry_zone",
"price": 747.1,
"trigger_value": "735.17~758.83",
"event_label": "买入区间",
"timestamp": "2026-07-03T11:10:14.569334",
"date": "2026-07-03"
},
{
"code": "301308",
"name": "江波龙",
"event_type": "entry_zone",
"price": 619.0,
"trigger_value": "613.93~620.18",
"event_label": "买入区间",
"timestamp": "2026-07-03T11:13:01.494442",
"date": "2026-07-03"
},
{
"code": "00700",
"name": "腾讯",
"event_type": "entry_zone",
"price": 437.0,
"trigger_value": "427.28~444.72",
"event_label": "买入区间",
"timestamp": "2026-07-03T11:14:23.996542",
"date": "2026-07-03"
},
{
"code": "00700",
"name": "腾讯",
"event_type": "entry_zone",
"price": 437.0,
"trigger_value": "0~470.88",
"event_label": "止盈区间",
"timestamp": "2026-07-03T11:14:24.624140",
"date": "2026-07-03"
},
{
"code": "00981",
"name": "中芯国际",
"event_type": "entry_zone",
"price": 80.7,
"trigger_value": "72.45~84.53",
"event_label": "买入区间",
"timestamp": "2026-07-03T11:14:24.732225",
"date": "2026-07-03"
},
{
"code": "00981",
"name": "中芯国际",
"event_type": "entry_zone",
"price": 80.7,
"trigger_value": "0~86.94",
"event_label": "止盈区间",
"timestamp": "2026-07-03T11:14:25.084481",
"date": "2026-07-03"
},
{
"code": "01088",
"name": "中国神华",
"event_type": "entry_zone",
"price": 40.16,
"trigger_value": "35.93~41.92",
"event_label": "买入区间",
"timestamp": "2026-07-03T11:14:28.993449",
"date": "2026-07-03"
},
{
"code": "01088",
"name": "中国神华",
"event_type": "entry_zone",
"price": 40.16,
"trigger_value": "0~43.11",
"event_label": "止盈区间",
"timestamp": "2026-07-03T11:14:29.629844",
"date": "2026-07-03"
},
{
"code": "01211",
"name": "比亚迪股份",
"event_type": "entry_zone",
"price": 82.6,
"trigger_value": "74.03~86.36",
"event_label": "买入区间",
"timestamp": "2026-07-03T11:14:29.965400",
"date": "2026-07-03"
},
{
"code": "01211",
"name": "比亚迪股份",
"event_type": "entry_zone",
"price": 82.6,
"trigger_value": "0~88.83",
"event_label": "止盈区间",
"timestamp": "2026-07-03T11:14:30.114407",
"date": "2026-07-03"
},
{
"code": "01478",
"name": "丘钛科技",
"event_type": "entry_zone",
"price": 7.05,
"trigger_value": "6.32~7.37",
"event_label": "买入区间",
"timestamp": "2026-07-03T11:14:30.304612",
"date": "2026-07-03"
},
{
"code": "01478",
"name": "丘钛科技",
"event_type": "entry_zone",
"price": 7.05,
"trigger_value": "0~7.58",
"event_label": "止盈区间",
"timestamp": "2026-07-03T11:14:35.358231",
"date": "2026-07-03"
},
{
"code": "01888",
"name": "建滔积层板",
"event_type": "entry_zone",
"price": 85.6,
"trigger_value": "78.08~91.09",
"event_label": "买入区间",
"timestamp": "2026-07-03T11:14:40.394717",
"date": "2026-07-03"
},
{
"code": "01888",
"name": "建滔积层板",
"event_type": "entry_zone",
"price": 85.6,
"trigger_value": "0~93.69",
"event_label": "止盈区间",
"timestamp": "2026-07-03T11:14:45.434897",
"date": "2026-07-03"
},
{
"code": "02202",
"name": "万科企业",
"event_type": "entry_zone",
"price": 2.32,
"trigger_value": "2.07~2.42",
"event_label": "买入区间",
"timestamp": "2026-07-03T11:14:50.523069",
"date": "2026-07-03"
},
{
"code": "02202",
"name": "万科企业",
"event_type": "entry_zone",
"price": 2.32,
"trigger_value": "0~2.48",
"event_label": "止盈区间",
"timestamp": "2026-07-03T11:14:59.705931",
"date": "2026-07-03"
},
{
"code": "06869",
"name": "长飞光纤光缆",
"event_type": "entry_zone",
"price": 206.8,
"trigger_value": "186.12~217.14",
"event_label": "买入区间",
"timestamp": "2026-07-03T11:15:00.287164",
"date": "2026-07-03"
},
{
"code": "06869",
"name": "长飞光纤光缆",
"event_type": "entry_zone",
"price": 206.8,
"trigger_value": "0~223.34",
"event_label": "止盈区间",
"timestamp": "2026-07-03T11:15:00.430498",
"date": "2026-07-03"
},
{
"code": "301308",
"name": "江波龙",
"event_type": "entry_zone",
"price": 618.02,
"trigger_value": "611.31~619.68",
"event_label": "买入区间",
"timestamp": "2026-07-03T11:22:07.308667",
"date": "2026-07-03"
},
{
"code": "300124",
"name": "汇川技术",
"event_type": "entry_zone",
"price": 71.11,
"trigger_value": "69.91~71.42",
"event_label": "买入区间",
"timestamp": "2026-07-03T11:22:20.859749",
"date": "2026-07-03"
},
{
"code": "301308",
"name": "江波龙",
"event_type": "entry_zone",
"price": 617.89,
"trigger_value": "611.31~619.68",
"event_label": "买入区间",
"timestamp": "2026-07-03T11:22:22.199650",
"date": "2026-07-03"
},
{
"code": "00700",
"name": "腾讯",
"event_type": "entry_zone",
"price": 438.6,
"trigger_value": "427.87~445.33",
"event_label": "买入区间",
"timestamp": "2026-07-03T11:52:11.456641",
"date": "2026-07-03"
},
{
"code": "00700",
"name": "腾讯",
"event_type": "entry_zone",
"price": 438.6,
"trigger_value": "0~471.53",
"event_label": "止盈区间",
"timestamp": "2026-07-03T11:52:18.294805",
"date": "2026-07-03"
},
{
"code": "00981",
"name": "中芯国际",
"event_type": "entry_zone",
"price": 79.9,
"trigger_value": "72.09~84.11",
"event_label": "买入区间",
"timestamp": "2026-07-03T11:52:26.253956",
"date": "2026-07-03"
},
{
"code": "00981",
"name": "中芯国际",
"event_type": "entry_zone",
"price": 79.9,
"trigger_value": "0~86.51",
"event_label": "止盈区间",
"timestamp": "2026-07-03T11:52:29.577070",
"date": "2026-07-03"
},
{
"code": "01088",
"name": "中国神华",
"event_type": "entry_zone",
"price": 40.3,
"trigger_value": "36.36~42.42",
"event_label": "买入区间",
"timestamp": "2026-07-03T11:52:30.340343",
"date": "2026-07-03"
},
{
"code": "01088",
"name": "中国神华",
"event_type": "entry_zone",
"price": 40.3,
"trigger_value": "0~43.63",
"event_label": "止盈区间",
"timestamp": "2026-07-03T11:52:32.714618",
"date": "2026-07-03"
},
{
"code": "01211",
"name": "比亚迪股份",
"event_type": "entry_zone",
"price": 82.6,
"trigger_value": "74.39~86.78",
"event_label": "买入区间",
"timestamp": "2026-07-03T11:52:36.541205",
"date": "2026-07-03"
},
{
"code": "01211",
"name": "比亚迪股份",
"event_type": "entry_zone",
"price": 82.6,
"trigger_value": "0~89.26",
"event_label": "止盈区间",
"timestamp": "2026-07-03T11:52:36.686582",
"date": "2026-07-03"
},
{
"code": "01478",
"name": "丘钛科技",
"event_type": "entry_zone",
"price": 7.1,
"trigger_value": "6.35~7.41",
"event_label": "买入区间",
"timestamp": "2026-07-03T11:52:37.242356",
"date": "2026-07-03"
},
{
"code": "01478",
"name": "丘钛科技",
"event_type": "entry_zone",
"price": 7.1,
"trigger_value": "0~7.62",
"event_label": "止盈区间",
"timestamp": "2026-07-03T11:52:37.443395",
"date": "2026-07-03"
},
{
"code": "01888",
"name": "建滔积层板",
"event_type": "entry_zone",
"price": 85.65,
"trigger_value": "76.77~89.56",
"event_label": "买入区间",
"timestamp": "2026-07-03T11:52:38.817321",
"date": "2026-07-03"
},
{
"code": "01888",
"name": "建滔积层板",
"event_type": "entry_zone",
"price": 85.65,
"trigger_value": "0~92.12",
"event_label": "止盈区间",
"timestamp": "2026-07-03T11:52:39.528996",
"date": "2026-07-03"
},
{
"code": "02202",
"name": "万科企业",
"event_type": "entry_zone",
"price": 2.34,
"trigger_value": "2.1~2.45",
"event_label": "买入区间",
"timestamp": "2026-07-03T11:52:39.720487",
"date": "2026-07-03"
},
{
"code": "02202",
"name": "万科企业",
"event_type": "entry_zone",
"price": 2.34,
"trigger_value": "0~2.52",
"event_label": "止盈区间",
"timestamp": "2026-07-03T11:52:41.301094",
"date": "2026-07-03"
},
{
"code": "06869",
"name": "长飞光纤光缆",
"event_type": "entry_zone",
"price": 206.6,
"trigger_value": "184.68~215.46",
"event_label": "买入区间",
"timestamp": "2026-07-03T11:52:45.543223",
"date": "2026-07-03"
},
{
"code": "06869",
"name": "长飞光纤光缆",
"event_type": "entry_zone",
"price": 206.6,
"trigger_value": "0~221.62",
"event_label": "止盈区间",
"timestamp": "2026-07-03T11:52:47.160071",
"date": "2026-07-03"
},
{
"code": "00700",
"name": "腾讯",
"event_type": "entry_zone",
"price": 437.8,
"trigger_value": "427.87~445.33",
"event_label": "买入区间",
"timestamp": "2026-07-03T11:54:14.974895",
"date": "2026-07-03"
},
{
"code": "00700",
"name": "腾讯",
"event_type": "entry_zone",
"price": 437.8,
"trigger_value": "0~471.53",
"event_label": "止盈区间",
"timestamp": "2026-07-03T11:54:17.221621",
"date": "2026-07-03"
},
{
"code": "00981",
"name": "中芯国际",
"event_type": "entry_zone",
"price": 79.7,
"trigger_value": "72.09~84.11",
"event_label": "买入区间",
"timestamp": "2026-07-03T11:54:22.810159",
"date": "2026-07-03"
},
{
"code": "00981",
"name": "中芯国际",
"event_type": "entry_zone",
"price": 79.7,
"trigger_value": "0~86.51",
"event_label": "止盈区间",
"timestamp": "2026-07-03T11:54:24.193676",
"date": "2026-07-03"
},
{
"code": "01088",
"name": "中国神华",
"event_type": "entry_zone",
"price": 40.22,
"trigger_value": "36.36~42.42",
"event_label": "买入区间",
"timestamp": "2026-07-03T11:54:24.477745",
"date": "2026-07-03"
},
{
"code": "01088",
"name": "中国神华",
"event_type": "entry_zone",
"price": 40.22,
"trigger_value": "0~43.63",
"event_label": "止盈区间",
"timestamp": "2026-07-03T11:54:24.655213",
"date": "2026-07-03"
},
{
"code": "01211",
"name": "比亚迪股份",
"event_type": "entry_zone",
"price": 82.8,
"trigger_value": "74.39~86.78",
"event_label": "买入区间",
"timestamp": "2026-07-03T11:54:25.091753",
"date": "2026-07-03"
},
{
"code": "01211",
"name": "比亚迪股份",
"event_type": "entry_zone",
"price": 82.8,
"trigger_value": "0~89.26",
"event_label": "止盈区间",
"timestamp": "2026-07-03T11:54:25.321463",
"date": "2026-07-03"
},
{
"code": "01478",
"name": "丘钛科技",
"event_type": "entry_zone",
"price": 7.08,
"trigger_value": "6.35~7.41",
"event_label": "买入区间",
"timestamp": "2026-07-03T11:54:26.301603",
"date": "2026-07-03"
},
{
"code": "01478",
"name": "丘钛科技",
"event_type": "entry_zone",
"price": 7.08,
"trigger_value": "0~7.62",
"event_label": "止盈区间",
"timestamp": "2026-07-03T11:54:29.534882",
"date": "2026-07-03"
},
{
"code": "01888",
"name": "建滔积层板",
"event_type": "entry_zone",
"price": 85.7,
"trigger_value": "76.77~89.56",
"event_label": "买入区间",
"timestamp": "2026-07-03T11:54:29.649302",
"date": "2026-07-03"
},
{
"code": "01888",
"name": "建滔积层板",
"event_type": "entry_zone",
"price": 85.7,
"trigger_value": "0~92.12",
"event_label": "止盈区间",
"timestamp": "2026-07-03T11:54:30.678788",
"date": "2026-07-03"
},
{
"code": "02202",
"name": "万科企业",
"event_type": "entry_zone",
"price": 2.34,
"trigger_value": "2.1~2.45",
"event_label": "买入区间",
"timestamp": "2026-07-03T11:54:30.768058",
"date": "2026-07-03"
},
{
"code": "02202",
"name": "万科企业",
"event_type": "entry_zone",
"price": 2.34,
"trigger_value": "0~2.52",
"event_label": "止盈区间",
"timestamp": "2026-07-03T11:54:30.829554",
"date": "2026-07-03"
},
{
"code": "06869",
"name": "长飞光纤光缆",
"event_type": "entry_zone",
"price": 206.2,
"trigger_value": "184.68~215.46",
"event_label": "买入区间",
"timestamp": "2026-07-03T11:54:30.916574",
"date": "2026-07-03"
},
{
"code": "06869",
"name": "长飞光纤光缆",
"event_type": "entry_zone",
"price": 206.2,
"trigger_value": "0~221.62",
"event_label": "止盈区间",
"timestamp": "2026-07-03T11:54:31.001389",
"date": "2026-07-03"
}
]
}
+29 -29
View File
@@ -25,8 +25,8 @@
{
"date": "2026-07-03",
"high": 1215.52,
"low": 1190.51,
"close": 1203.6
"low": 1190.5,
"close": 1191.31
}
],
"02202": [
@@ -48,7 +48,7 @@
"date": "2026-07-03",
"high": 53.0,
"low": 51.7,
"close": 52.5
"close": 52.45
}
],
"601318": [
@@ -61,8 +61,8 @@
{
"date": "2026-07-03",
"high": 50.2,
"low": 48.8,
"close": 49.12
"low": 48.6,
"close": 48.7
}
],
"02359": [
@@ -76,7 +76,7 @@
"date": "2026-07-03",
"high": 158.7,
"low": 151.1,
"close": 153.1
"close": 152.5
}
],
"02388": [
@@ -90,7 +90,7 @@
"date": "2026-07-03",
"high": 42.98,
"low": 41.94,
"close": 42.66
"close": 42.68
}
],
"02628": [
@@ -104,7 +104,7 @@
"date": "2026-07-03",
"high": 29.08,
"low": 27.5,
"close": 28.66
"close": 28.64
}
],
"688630": [
@@ -116,9 +116,9 @@
},
{
"date": "2026-07-03",
"high": 499.95,
"high": 502.0,
"low": 444.55,
"close": 486.69
"close": 495.95
}
],
"06160": [
@@ -132,7 +132,7 @@
"date": "2026-07-03",
"high": 183.9,
"low": 171.9,
"close": 183.3
"close": 182.9
}
],
"06869": [
@@ -154,7 +154,7 @@
"date": "2026-07-03",
"high": 687.04,
"low": 633.01,
"close": 642.05
"close": 654.0
}
],
"09868": [
@@ -168,7 +168,7 @@
"date": "2026-07-03",
"high": 53.7,
"low": 49.3,
"close": 51.1
"close": 50.9
}
],
"09988": [
@@ -182,7 +182,7 @@
"date": "2026-07-03",
"high": 97.4,
"low": 93.55,
"close": 95.5
"close": 95.15
}
],
"688802": [
@@ -194,9 +194,9 @@
},
{
"date": "2026-07-03",
"high": 733.99,
"high": 753.88,
"low": 713.0,
"close": 719.01
"close": 744.0
}
],
"300124": [
@@ -208,9 +208,9 @@
},
{
"date": "2026-07-03",
"high": 71.79,
"high": 72.38,
"low": 67.31,
"close": 71.07
"close": 72.29
}
],
"000657": [
@@ -223,8 +223,8 @@
{
"date": "2026-07-03",
"high": 101.5,
"low": 87.92,
"close": 88.6
"low": 87.88,
"close": 91.88
}
],
"000711": [
@@ -238,7 +238,7 @@
"date": "2026-07-03",
"high": 5.26,
"low": 4.87,
"close": 5.25
"close": 5.26
}
],
"001309": [
@@ -250,9 +250,9 @@
},
{
"date": "2026-07-03",
"high": 872.83,
"high": 882.5,
"low": 795.0,
"close": 835.99
"close": 874.23
}
],
"002594": [
@@ -266,7 +266,7 @@
"date": "2026-07-03",
"high": 87.28,
"low": 81.9,
"close": 86.88
"close": 86.56
}
],
"00700": [
@@ -288,7 +288,7 @@
"date": "2026-07-03",
"high": 2.15,
"low": 2.03,
"close": 2.11
"close": 2.1
}
],
"00981": [
@@ -308,9 +308,9 @@
},
{
"date": "2026-07-03",
"high": 13.31,
"high": 13.39,
"low": 12.63,
"close": 13.31
"close": 13.36
}
],
"01088": [
@@ -332,9 +332,9 @@
"301308": [
{
"date": "2026-07-03",
"high": 612.3,
"high": 631.56,
"low": 574.1,
"close": 600.8
"close": 620.0
}
]
}
+35 -83
View File
@@ -1,9 +1,9 @@
#!/usr/bin/env python3
"""
mo_data.py — MoFin 统一数据读取
mo_data.py — MoFin 统一数据层(纯 DB
替代 json.load(open(portfolio.json)) 等直接读 JSON 文件的方式
所有数据从 DB 读取,币种字段强制存在
所有数据从 SQLite 读取。不做 JSON fallback
JSON 文件已弃用,仅保留为历史备份
用法:
from mo_data import read_portfolio, read_decisions, read_watchlist
@@ -13,16 +13,11 @@ mo_data.py — MoFin 统一数据读取层
wl = read_watchlist() # 返回和 watchlist.json 一样的 dict 结构
"""
import sqlite3, json, os
import sqlite3, json
from datetime import datetime
DB_PATH = '/home/hmo/web-dashboard/data/mofin.db'
# JSON 文件路径(冷备,仅当 DB 不可用时 fallback
PORTFOLIO_JSON = '/home/hmo/web-dashboard/data/portfolio.json'
DECISIONS_JSON = '/home/hmo/web-dashboard/data/decisions.json'
WATCHLIST_JSON = '/home/hmo/web-dashboard/data/watchlist.json'
def _get_db():
db = sqlite3.connect(DB_PATH)
@@ -30,13 +25,11 @@ def _get_db():
return db
# ── portfolio.json → holdings + portfolio_summary ─────────────────
# ── portfolio ─────────────────────────────────────────────────────
def read_portfolio():
"""返回 portfolio.json 等价 dict。DB 优先,JSON 冷备"""
try:
"""返回 portfolio.json 等价 dict。DB。"""
db = _get_db()
# holdings
rows = db.execute(
"SELECT code, name, shares, cost, price, market_value, "
"change_pct, currency, position_pct "
@@ -45,10 +38,9 @@ def read_portfolio():
holdings = []
for r in rows:
h = dict(r)
h['_currency'] = h.get('currency', 'CNY') # 兼容旧代码的 _currency 字段名
h['_currency'] = h.get('currency', 'CNY')
holdings.append(h)
# summary
sum_row = db.execute("SELECT * FROM portfolio_summary WHERE id=1").fetchone()
summary = dict(sum_row) if sum_row else {}
@@ -65,21 +57,19 @@ def read_portfolio():
"currency": summary.get("currency", "CNY"),
"updated_at": summary.get("updated_at", ""),
}
except Exception:
pass
# JSON 冷备
try:
return json.load(open(PORTFOLIO_JSON, encoding='utf-8'))
except:
return {"holdings": [], "total_assets": 0, "cash": 0, "frozen_cash": 0}
# ── decisions.json → holding_strategies ────────────────────────────
# ── decisions ─────────────────────────────────────────────────────
def _parse_json(val, default):
if val:
try: return json.loads(val)
except: pass
return default
def read_decisions():
"""返回 decisions.json 等价 dict。DB 优先,JSON 冷备"""
try:
"""返回 decisions.json 等价 dict。DB。"""
db = _get_db()
rows = db.execute(
"SELECT code, name, version, price, cost, shares, "
@@ -87,7 +77,10 @@ def read_decisions():
"currency, strategy_type, action, timing_signal, "
"rr_ratio, tech_snapshot, stock_category, sector_context, "
"status, trigger_json, changelog_json, source, reason, "
"created_at, updated_at "
"created_at, updated_at, "
"avg_price, decision_timestamp, note, quality_check, "
"quality_checked_at, quality_issues_json, position_advice, "
"signal_factors_json, time_horizon, decision_type "
"FROM holding_strategies WHERE status IN ('active','updated') "
"ORDER BY code"
).fetchall()
@@ -95,22 +88,12 @@ def read_decisions():
decisions = []
for r in rows:
d = dict(r)
# 还原 trigger 字段
if d.get('trigger_json'):
try:
d['trigger'] = json.loads(d['trigger_json'])
except:
d['trigger'] = {}
else:
d['trigger'] = {}
# 还原 changelog 字段
if d.get('changelog_json'):
try:
d['changelog'] = json.loads(d['changelog_json'])
except:
d['changelog'] = []
else:
d['changelog'] = []
d['trigger'] = _parse_json(r['trigger_json'], {})
d['changelog'] = _parse_json(r['changelog_json'], [])
d['quality_issues'] = _parse_json(r['quality_issues_json'], {})
d['signal_factors'] = _parse_json(r['signal_factors_json'], [])
d['timestamp'] = r['decision_timestamp'] or r['created_at'] or ''
d['type'] = r['decision_type'] or r['strategy_type'] or '持仓策略'
decisions.append(d)
db.close()
@@ -120,20 +103,12 @@ def read_decisions():
"total": len(decisions),
"regenerated_at": datetime.now().strftime('%Y-%m-%d %H:%M'),
}
except Exception:
pass
try:
return json.load(open(DECISIONS_JSON, encoding='utf-8'))
except:
return {"decisions": [], "total": 0}
# ── watchlist.json → watchlist_stocks ──────────────────────────────
# ── watchlist ─────────────────────────────────────────────────────
def read_watchlist():
"""返回 watchlist.json 等价 dict。DB 优先,JSON 冷备"""
try:
"""返回 watchlist.json 等价 dict。DB。"""
db = _get_db()
rows = db.execute(
"SELECT code, name, price, entry_low, entry_high, "
@@ -145,12 +120,9 @@ def read_watchlist():
stocks = []
for r in rows:
s = dict(r)
if s.get('source_detail'):
try: s['source_detail'] = json.loads(s['source_detail'])
except: pass
if s.get('analysis_json'):
try: s['analysis'] = json.loads(s['analysis_json'])
except: s['analysis'] = {}
s['source_detail'] = _parse_json(r['source_detail'], None)
if r['analysis_json']:
s['analysis'] = _parse_json(r['analysis_json'], {})
else:
s['analysis'] = {}
stocks.append(s)
@@ -161,45 +133,25 @@ def read_watchlist():
"stocks": stocks,
"updated_at": datetime.now().strftime('%Y-%m-%d %H:%M'),
}
except Exception:
pass
try:
return json.load(open(WATCHLIST_JSON, encoding='utf-8'))
except:
return {"stocks": []}
# ── 便捷函数 ────────────────────────────────────────────────────────
# ── 便捷别名 ───────────────────────────────────────────────────────
def read_portfolio_json():
"""别名(兼容旧代码直接 import 后调用)"""
return read_portfolio()
def read_decisions_json():
"""别名"""
return read_decisions()
def read_watchlist_json():
"""别名"""
return read_watchlist()
# ── cash_log 写入 ─────────────────────────────────────────────────────
# ── cash_log 写入 ──────────────────────────────────────────────────
def write_cash_log(cash_before, cash_after, frozen_before, frozen_after,
source, note, verified=0):
"""记录现金变更到 cash_log 表。
参数:
cash_before/after — 变更前后可用现金
frozen_before/after — 变更前后冻结资金
source — 'screenshot'/'manual'/'trade'/'import_xls'
note — 备注(如"卖出法拉电子200股"
verified — 0=未验证 1=Dad已确认
返回:
log_id (int)
"""
"""记录现金变更到 cash_log 表。"""
change_amount = round(cash_after - cash_before, 2) if cash_after is not None and cash_before is not None else 0
db = sqlite3.connect(DB_PATH)
try:
@@ -217,7 +169,7 @@ def write_cash_log(cash_before, cash_after, frozen_before, frozen_after,
db.close()
# ── 自检 ───────────────────────────────────────────────────────────
# ── 自检 ───────────────────────────────────────────────────────────
if __name__ == "__main__":
pf = read_portfolio()
+50 -16
View File
@@ -145,8 +145,8 @@ def init_all_tables(conn: sqlite3.Connection):
code TEXT NOT NULL REFERENCES holdings(code),
name TEXT,
version INTEGER DEFAULT 1,
price REAL, -- 当前价格
cost REAL, -- 成本价
price REAL,
cost REAL,
shares INTEGER DEFAULT 0,
stop_loss REAL,
take_profit REAL,
@@ -154,20 +154,31 @@ def init_all_tables(conn: sqlite3.Connection):
entry_high REAL,
currency TEXT NOT NULL DEFAULT 'CNY' CHECK(currency IN ('CNY','HKD')),
strategy_type TEXT DEFAULT 'holding',
action TEXT, -- 买入/持有/卖出/观望
timing_signal TEXT, -- 时机信号
rr_ratio REAL, -- 盈亏比
tech_snapshot TEXT, -- 技术面快照
stock_category TEXT, -- 股票分类
sector_context TEXT, -- 板块背景
status TEXT DEFAULT 'active', -- active/updated/closed
trigger_json TEXT, -- trigger JSON (entry_zone/stop_loss/take_profit_zone)
changelog_json TEXT, -- changelog JSON 数组
action TEXT,
timing_signal TEXT,
rr_ratio REAL,
tech_snapshot TEXT,
stock_category TEXT,
sector_context TEXT,
status TEXT DEFAULT 'active',
trigger_json TEXT,
changelog_json TEXT,
source TEXT,
reason TEXT,
created_at TEXT DEFAULT (datetime('now','localtime')),
updated_at TEXT,
superseded_at TEXT
superseded_at TEXT,
-- 以下为 decisions.json→DB 迁移新增列
avg_price REAL,
decision_timestamp TEXT,
note TEXT,
quality_check TEXT,
quality_checked_at TEXT,
quality_issues_json TEXT,
position_advice TEXT,
signal_factors_json TEXT,
time_horizon TEXT,
decision_type TEXT
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_strategy_code ON holding_strategies(code);
CREATE INDEX IF NOT EXISTS idx_strategy_status ON holding_strategies(status);
@@ -954,7 +965,14 @@ def write_holding_strategy(conn, code: str, name: str, data: dict) -> tuple[bool
"""写入持仓策略(替代 decisions.json 单条写入)。data 必须包含 currency。"""
try:
currency = data.get('currency', 'CNY')
# DELETE + INSERT(避免依赖 code 的 UNIQUE 约束)
# Serialize JSON fields
import json as _json
trigger_j = _json.dumps(data.get('trigger', {}), ensure_ascii=False) if isinstance(data.get('trigger'), dict) else str(data.get('trigger', '{}'))
changelog_j = _json.dumps(data.get('changelog', []), ensure_ascii=False) if isinstance(data.get('changelog'), list) else str(data.get('changelog', '[]'))
quality_issues_j = _json.dumps(data.get('quality_issues', {}), ensure_ascii=False) if isinstance(data.get('quality_issues'), dict) else data.get('quality_issues_json', '')
signal_factors_j = _json.dumps(data.get('signal_factors', []), ensure_ascii=False) if isinstance(data.get('signal_factors'), list) else data.get('signal_factors_json', '')
# DELETE + INSERT
conn.execute("DELETE FROM holding_strategies WHERE code=?", (code,))
conn.execute("""
INSERT INTO holding_strategies
@@ -962,8 +980,13 @@ def write_holding_strategy(conn, code: str, name: str, data: dict) -> tuple[bool
entry_low, entry_high, currency, strategy_type, action,
timing_signal, rr_ratio, tech_snapshot, stock_category,
sector_context, status, trigger_json, changelog_json,
source, reason, updated_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,datetime('now','localtime'))
source, reason, updated_at,
avg_price, decision_timestamp, note, quality_check,
quality_checked_at, quality_issues_json, position_advice,
signal_factors_json, time_horizon, decision_type)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,
datetime('now','localtime'),
?,?,?,?,?,?,?,?,?)
""", (
code, name,
data.get('version', 1), data.get('price'), data.get('cost'),
@@ -973,8 +996,19 @@ def write_holding_strategy(conn, code: str, name: str, data: dict) -> tuple[bool
data.get('timing_signal'), data.get('rr_ratio'),
data.get('tech_snapshot'), data.get('stock_category'),
data.get('sector_context'), data.get('status', 'active'),
data.get('trigger_json'), data.get('changelog_json'),
trigger_j, changelog_j,
data.get('source'), data.get('reason'),
# new columns
data.get('avg_price', 0),
data.get('timestamp') or data.get('created_at', ''),
data.get('note', ''),
data.get('quality_check', ''),
data.get('quality_checked_at', ''),
quality_issues_j,
data.get('position_advice', ''),
signal_factors_j,
data.get('time_horizon', ''),
data.get('type', data.get('strategy_type', 'holding')),
))
conn.commit()
return True, f"策略 {code} 已写入"
+16 -15
View File
@@ -12,12 +12,9 @@ from datetime import datetime
# ── MoFin unified model ──────────────────────────────────────────────
from mo_models import is_hk_stock, get_hk_rate, calc_total_assets, calc_total_mv, calc_position_pct
from mofin_db import get_conn, write_holdings_batch, write_portfolio_summary, write_price_event, write_watchlist_stock
from mofin_db import get_conn, write_holdings_batch, write_portfolio_summary, write_price_event, write_watchlist_stock, write_holding_strategy
from mo_data import read_portfolio, read_decisions, read_watchlist
DECISIONS_PATH = "/home/hmo/web-dashboard/data/decisions.json"
PORTFOLIO_PATH = "/home/hmo/web-dashboard/data/portfolio.json"
WATCHLIST_PATH = "/home/hmo/web-dashboard/data/watchlist.json"
BREACH_PATH = "/home/hmo/.hermes/zone_breach.json"
STATE_PATH = os.path.expanduser("~/.hermes/price_trigger_state.json")
EVENTS_PATH = "/home/hmo/web-dashboard/data/price_events.json"
@@ -280,14 +277,17 @@ def refresh_data_prices():
import time; time.sleep((attempt+1)*1)
else:
print(f" [DB写入失败 3次重试后放弃] {e}", flush=True)
# 保留 JSON 副本作为冷备
json.dump(pf, open(PORTFOLIO_PATH, 'w'), ensure_ascii=False, indent=2)
elif pf.get('updated_at'):
try:
last_ts = datetime.strptime(pf['updated_at'], '%Y-%m-%d %H:%M')
if (datetime.now() - last_ts).total_seconds() > 600:
pf['updated_at'] = datetime.now().strftime('%Y-%m-%d %H:%M')
json.dump(pf, open(PORTFOLIO_PATH, 'w'), ensure_ascii=False, indent=2)
try:
conn = get_conn()
write_portfolio_summary(conn, pf)
conn.close()
except Exception:
pass
except:
pass
@@ -314,8 +314,6 @@ def refresh_data_prices():
conn.close()
except Exception as e:
print(f" [DB watchlist写入失败] {e}", flush=True)
# 保留 JSON 冷备
json.dump(wl, open(WATCHLIST_PATH, 'w'), ensure_ascii=False, indent=2)
# --- 汇总值重算(使用 mo_models 唯一公式)---
try:
@@ -336,8 +334,6 @@ def refresh_data_prices():
conn.close()
except Exception as e:
print(f" [DB汇总写入失败] {e}", flush=True)
# JSON 冷备
json.dump(pf, open(PORTFOLIO_PATH, 'w'), ensure_ascii=False, indent=2)
except Exception as e:
print(f" [汇总重算失败] {e}", flush=True)
# --- 结束汇总重算 ---
@@ -377,7 +373,13 @@ def _record_branch_trigger(code, branch_id, price):
b['last_trigger_price'] = round(price, 2)
b['last_triggered'] = datetime.now().isoformat()
break
json.dump(raw, open(DECISIONS_PATH, 'w'), ensure_ascii=False, indent=2)
try:
conn = get_conn()
for d in raw.get('decisions', []):
write_holding_strategy(conn, d['code'], d.get('name', ''), d)
conn.close()
except Exception:
pass
except Exception:
pass
@@ -504,10 +506,9 @@ def run_once(round_label=""):
# === 第二步:检查触发条件 ===
try:
with open(DECISIONS_PATH) as f:
dec = json.load(f)
dec = read_decisions()
except:
print(f"{label} 无法读取decisions.json", file=sys.stderr)
print(f"{label} 无法读取决策数据", file=sys.stderr)
return
active = [d for d in dec.get("decisions", []) if d.get("status") in ("active", "updated")]
+12 -5
View File
@@ -12,9 +12,7 @@ from .tracking import load_associations, get_associations_for_prompt_version
from .registry import get_prompt, get_version_history
PROJECT_DIR = Path("/home/hmo/projects/MoFin")
DECISIONS_PATH = PROJECT_DIR / "data" / "decisions.json"
ACCURACY_PATH = PROJECT_DIR / "data" / "accuracy_stats.json"
EVAL_PATH = PROJECT_DIR / "data" / "evaluation.json"
def _load_json(path, default=None):
@@ -25,10 +23,19 @@ def _load_json(path, default=None):
return {} if default is None else default
def _load_decisions():
"""从 DB 读取 decisions 数据"""
try:
from mo_data import read_decisions
return read_decisions()
except Exception:
return {"decisions": []}
def analyze_prompt_version_effectiveness() -> dict:
"""按提示词版本聚合策略评估结果
关联 decisions.json 中的 evaluation 字段和 associations.json 中的版本记录,
关联 DB holding_strategies 中的 evaluation 字段和 associations.json 中的版本记录,
计算出每个提示词版本的:
- 生成的策略总数
- 达到止盈数(成功)
@@ -37,7 +44,7 @@ def analyze_prompt_version_effectiveness() -> dict:
- 盈亏比平均值
"""
# 加载数据
decisions = _load_json(DECISIONS_PATH, {"decisions": []})
decisions = _load_decisions()
associations = load_associations().get("associations", [])
# 建立 code → 最新关联的映射
@@ -182,5 +189,5 @@ def generate_report() -> str:
lines.append("")
lines.append("---")
lines.append("注:数据来自 decisions.json evaluation + associations.json")
lines.append("注:数据来自 DB holding_strategies evaluation + associations.json")
return "\n".join(lines)
+3 -3
View File
@@ -282,9 +282,9 @@ add_version("knowledge-extraction", PromptVersion(
version="v1",
label="初始版本",
created_at="2026-06-11T16:30:00",
changelog="初始版本,从 decisions.json 和 evaluation.json 中提炼经验写入知识日志",
changelog="初始版本,从 DB holding_strategies 表中提炼经验写入知识日志",
content="""知识萃取 v1(当前):
1. 读 decisions.json 的 changelog 和 evaluation
1. 从 DB holding_strategies 表读取 changelog 和 evaluation 字段
2. 读当天分析输出的 anomaly 信号
3. 提炼 1-3 条可复用知识
4. 写入 /home/hmo/Obsidian/knowledge/finance/analyst-knowledge-log.md
@@ -370,7 +370,7 @@ add_version("system-health-check", PromptVersion(
content="""健康检查 v2(当前):18项检查
- 进程:mofin-dashboard, xmpp-zhiwei, ejabberd
- 端口:8899, 5222, 8643
- 数据:portfolio/watchlist/decisions/market/price_events/evaluation/accuracy_stats
- 数据:holds/strategies/watchlist/market/events (全部DB)
- 活动:价格事件数/策略评估数/建议记录数
- Cron: 两个profile
- 数据新鲜度:文件更新时间
+12 -8
View File
@@ -1,19 +1,17 @@
#!/usr/bin/env python3
"""批量再生所有持仓+自选策略,结合技术面支撑/压力位"""
import json
import sys
sys.path.insert(0, '/home/hmo/web-dashboard')
from technical_analysis import full_analysis
from strategy_lifecycle import reassess_strategy
PF = '/home/hmo/web-dashboard/data/portfolio.json'
WL = '/home/hmo/web-dashboard/data/watchlist.json'
from mo_data import read_portfolio, read_watchlist
from mofin_db import get_conn, write_holdings_batch, write_portfolio_summary, write_watchlist_stock
def main():
# 持仓
pf = json.load(open(PF))
pf = read_portfolio()
for s in pf['holdings']:
code = s['code']
name = s['name']
@@ -48,11 +46,14 @@ def main():
print(f"{result['stop_loss']}{result['take_profit']}{result['entry_low']}~{result['entry_high']}")
json.dump(pf, open(PF, 'w'), ensure_ascii=False, indent=2)
conn = get_conn()
write_holdings_batch(conn, pf['holdings'])
write_portfolio_summary(conn, pf)
conn.close()
print(f"\n持仓策略已更新: {len(pf['holdings'])}")
# 自选股 - 简单重新计算买入区
wl = json.load(open(WL))
wl = read_watchlist()
updated = 0
for s in wl['stocks']:
code = s['code']
@@ -88,7 +89,10 @@ def main():
updated += 1
print(f" {s['name']}({code}) 买入区={ws:.2f}~{wr:.2f}")
json.dump(wl, open(WL, 'w'), ensure_ascii=False, indent=2)
conn = get_conn()
for s in wl['stocks']:
write_watchlist_stock(conn, s)
conn.close()
print(f"\n自选策略已更新: {updated}")
print("\n✅ 全部策略再生完成")
+15 -7
View File
@@ -16,6 +16,8 @@ branch_evaluator.py — 分支自成长引擎
import json, sys, os, re
from datetime import datetime, date
from mo_data import read_portfolio, read_decisions
from mofin_db import get_conn, write_holding_strategy
# 路径
DECISIONS_PATH = "/home/hmo/web-dashboard/data/decisions.json"
@@ -37,8 +39,7 @@ def get_live_prices():
"""从 portfolio.json 读取实时价格"""
prices = {}
try:
with open(PORTFOLIO_PATH) as f:
pf = json.load(f)
pf = read_portfolio()
for h in pf.get("holdings", []):
code = str(h.get("code", ""))
prices[code] = h.get("price", 0)
@@ -50,8 +51,7 @@ def get_live_prices():
def evaluate_all():
"""评估所有已触发策略树的分支"""
try:
with open(DECISIONS_PATH) as f:
data = json.load(f)
data = read_decisions()
except Exception as e:
print(f"[错误] 读 decisions.json 失败: {e}", file=sys.stderr)
return
@@ -124,9 +124,17 @@ def evaluate_all():
# 标记评估时间
tree["last_evaluated"] = now_ts
# 写回文件
with open(DECISIONS_PATH, "w") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
# 写回 — DB 优先
try:
conn = get_conn()
for d in data.get("decisions", []):
write_holding_strategy(conn, d.get("code", ""), d.get("name", ""), d)
conn.close()
except Exception:
pass
# [migrated to DB] — cold backup removed
# with open(DECISIONS_PATH, "w") as f:
# json.dump(data, f, indent=2, ensure_ascii=False)
# 输出摘要(空 = 静默)
lines = []
+14 -4
View File
@@ -17,6 +17,8 @@ branch_scanner.py — 分支自成长数据采集器(全静默)
import json, sys, re
from datetime import datetime
from urllib.request import Request, urlopen
from mo_data import read_decisions
from mofin_db import get_conn, write_holding_strategy
DECISIONS_PATH = "/home/hmo/web-dashboard/data/decisions.json"
SCANNER_STATE = "/home/hmo/web-dashboard/data/scanner_state.json"
@@ -82,8 +84,7 @@ def main():
scenario = get_scenario()
sid = scenario.get("id", "unknown")
with open(DECISIONS_PATH) as f:
data = json.load(f)
data = read_decisions()
decisions = data.get("decisions", [])
for entry in decisions:
@@ -101,8 +102,17 @@ def main():
br["last_triggered"] = now.strftime("%Y-%m-%d")
break
with open(DECISIONS_PATH, "w") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
# DB 写入(替代 json.dump
try:
conn = get_conn()
for d in data.get("decisions", []):
write_holding_strategy(conn, d.get("code", ""), d.get("name", ""), d)
conn.close()
except Exception:
pass
# [migrated to DB] — cold backup removed
# with open(DECISIONS_PATH, "w") as f:
# json.dump(data, f, indent=2, ensure_ascii=False)
# 更新状态快照
state = {"scenario": sid, "updated_at": now.isoformat(), "branches": {}}
+5
View File
@@ -0,0 +1,5 @@
import sqlite3
db = sqlite3.connect('/home/hmo/web-dashboard/data/mofin.db')
sql = db.execute("SELECT sql FROM sqlite_master WHERE name='holding_strategies'").fetchone()
print(sql[0][:600] if sql else "NOT FOUND")
db.close()
+3 -1
View File
@@ -18,6 +18,8 @@
import json, os, sqlite3, time, urllib.request
from datetime import datetime, timedelta
from mo_data import read_decisions
from mo_data import read_decisions
from pathlib import Path
DB_PATH = Path("/home/hmo/MoFin/data/mofin.db")
@@ -245,7 +247,7 @@ if __name__ == "__main__":
cf = ChipFactors()
# 从decisions.json获取持仓+自选
dec = json.loads(open(str(MOFIN_ROOT / "data" / "decisions.json")).read())
dec = read_decisions()
stocks = [(s["code"], s.get("name","")) for s in dec.get("decisions", []) if s.get("status") != "closed"]
results = cf.batch_calc(stocks)
+27 -6
View File
@@ -3,12 +3,13 @@
import json, os
from mo_data import read_portfolio, read_decisions, read_watchlist
from mofin_db import get_conn, write_watchlist_stock, write_holding_strategy
WL = "/home/hmo/web-dashboard/data/watchlist.json"
DEC = "/home/hmo/web-dashboard/data/decisions.json"
holding_codes = set()
pf = mo_data.read_portfolio()
pf = read_portfolio()
for h in pf.get("holdings", []):
c = h.get("code", "")
if c:
@@ -17,7 +18,7 @@ for h in pf.get("holdings", []):
print(f"持仓 codes: {sorted(holding_codes)}")
# Load watchlist
wl = json.load(open(WL))
wl = read_watchlist()
stocks = wl.get("stocks", [])
before = len(stocks)
@@ -30,7 +31,14 @@ wl["stocks"] = new_stocks
# Backup
os.rename(WL, WL + ".bak2")
json.dump(wl, open(WL, "w"), indent=2, ensure_ascii=False)
# DB 写入
conn = get_conn()
for s in wl.get("stocks", []):
s.setdefault("currency", "CNY")
write_watchlist_stock(conn, s)
conn.close()
# [migrated to DB] — cold backup removed
# json.dump(wl, open(WL, "w"), indent=2, ensure_ascii=False)
print(f"\n自选: {before}{after}")
print(f"移除 {len(removed)} 只:")
@@ -38,7 +46,7 @@ for r in removed:
print(f" {r['code']} {r.get('name','')}")
# Also update decisions.json - set them to "managed_by_holdings" or remove watchlist-only fields
dec = json.load(open(DEC))
dec = read_decisions()
dec_changed = 0
for d in dec.get("decisions", []):
code = d.get("code", "")
@@ -50,7 +58,13 @@ for d in dec.get("decisions", []):
if dec_changed:
os.rename(DEC, DEC + ".bak3")
json.dump(dec, open(DEC, "w"), indent=2, ensure_ascii=False)
# DB 写入
conn = get_conn()
for d in dec.get("decisions", []):
write_holding_strategy(conn, d.get("code", ""), d.get("name", ""), d)
conn.close()
# [migrated to DB] — cold backup removed
# json.dump(dec, open(DEC, "w"), indent=2, ensure_ascii=False)
print(f"\ndecisions.json: {dec_changed} 只更新标签")
else:
print(f"\ndecisions.json: 无需更新")
@@ -91,7 +105,14 @@ if prev_held:
print(f" ← 已清仓→加回自选: {code} {info['name']}")
if added:
wl["stocks"] = new_stocks
json.dump(wl, open(WL, "w"), indent=2, ensure_ascii=False)
# DB 写入
conn = get_conn()
for s in wl.get("stocks", []):
s.setdefault("currency", "CNY")
write_watchlist_stock(conn, s)
conn.close()
# [migrated to DB] — cold backup removed
# json.dump(wl, open(WL, "w"), indent=2, ensure_ascii=False)
print(f"\n反过程: {added} 只清仓股已加回自选")
else:
print("\n反过程: 无清仓股需加回")
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env python3
"""Deep check 01888 decisions"""
import json, sqlite3
# Check decisions.json directly
with open('/home/hmo/web-dashboard/data/decisions.json') as f:
raw = json.load(f)
count = 0
for d in raw.get('decisions', []):
if d.get('code') == '01888':
count += 1
print(f"[JSON entry {count}] price={d.get('price')} cost={d.get('cost')} shares={d.get('shares')}")
print(f" curr={d.get('currency','none')} status={d.get('status')} type={d.get('type')}")
print(f" stop_loss={d.get('stop_loss')} take_profit={d.get('trigger',{}).get('take_profit')}")
print(f"\nTotal 01888 entries in decisions.json: {count}")
# Also check holding_strategies table
db = sqlite3.connect('/home/hmo/web-dashboard/data/mofin.db')
db.row_factory = sqlite3.Row
rows = db.execute("SELECT * FROM holding_strategies WHERE code='01888' ORDER BY updated_at DESC").fetchall()
print(f"\nholding_strategies entries: {len(rows)}")
for r in rows:
print(f" cost={r['cost']} price={r['price']} shares={r['shares']} curr={r['currency']} status={r['status']} updated={r['updated_at']}")
db.close()
+23
View File
@@ -0,0 +1,23 @@
import sqlite3, sys
sys.path.insert(0, '/home/hmo/MoFin')
from strategy_lifecycle import regenerate_all
db = sqlite3.connect('/home/hmo/web-dashboard/data/mofin.db')
# Step 1: Add UNIQUE index if not exists
try:
db.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_strategy_code ON holding_strategies(code)")
print("UNIQUE index added")
except sqlite3.OperationalError as e:
print(f"Index already exists or error: {e}")
# Step 2: Clear old stale data from holding_strategies (regenerate_all will rewrite)
deleted = db.execute("DELETE FROM holding_strategies").rowcount
print(f"Cleared {deleted} old strategy entries")
db.commit()
db.close()
# Step 3: Full regenerate
print("\n=== Running regenerate_all ===")
regenerate_all(stdout=True)
print("\nDone!")
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env python3
"""一次性修复:将 holdings 表中港股成本从 HKD 转为 CNY。
运行时机:import_holding_xls.py 修复后,旧数据需要转换。
"""
import sqlite3, sys, os
sys.path.insert(0, '/home/hmo/MoFin')
from mo_models import is_hk_stock
from hk_rate import hkd_to_cny
rate = hkd_to_cny()
DB = '/home/hmo/web-dashboard/data/mofin.db'
print(f"HK_RATE = {rate}")
print(f"DB = {DB}")
db = sqlite3.connect(DB)
rows = db.execute(
"SELECT code, name, cost, shares, currency FROM holdings WHERE is_active=1"
).fetchall()
fixed = 0
for r in rows:
code = r[0]
name = r[1]
cost = r[2] or 0
shares = r[3] or 0
curr = r[4] or 'CNY'
if not is_hk_stock(str(code)):
continue
if cost <= 0:
continue
cost_cny = round(cost * rate, 2)
print(f" FIX: {code} {name}: cost {cost} -> {cost_cny} CNY (shares={shares})")
db.execute(
"UPDATE holdings SET cost=?, currency=? WHERE code=?",
(cost_cny, 'CNY', code)
)
fixed += 1
db.commit()
db.close()
print(f"\nDone. Fixed {fixed} HK stock costs.")
+10 -4
View File
@@ -3,13 +3,13 @@
import json, sys, os
from mo_data import read_portfolio, read_decisions, read_watchlist
from mofin_db import get_conn, write_holding_strategy
DECISIONS_PATH = "/home/hmo/web-dashboard/data/decisions.json"
DECISIONS_BAK = DECISIONS_PATH + ".bak"
try:
with open(DECISIONS_PATH) as f:
dec = json.load(f)
dec = read_decisions()
except Exception as e:
print(f"读decisions.json失败: {e}")
sys.exit(1)
@@ -45,7 +45,13 @@ os.makedirs(os.path.dirname(DECISIONS_BAK), exist_ok=True)
with open(DECISIONS_BAK, 'w') as f:
json.dump(mo_data.read_decisions(), f, indent=2, ensure_ascii=False)
with open(DECISIONS_PATH, 'w') as f:
json.dump(dec, f, indent=2, ensure_ascii=False)
# DB 写入(替代 json.dump
conn = get_conn()
for d in dec.get("decisions", []):
write_holding_strategy(conn, d.get("code", ""), d.get("name", ""), d)
conn.close()
# [migrated to DB] — cold backup removed
# with open(DECISIONS_PATH, 'w') as f:
# json.dump(dec, f, indent=2, ensure_ascii=False)
print(f"\n{count}只,已更新trigger字段")
+8 -7
View File
@@ -10,13 +10,15 @@
"""
import json, sys
from datetime import datetime
from mo_data import read_portfolio, read_decisions
from mofin_db import get_conn, write_holdings_batch, write_portfolio_summary, write_holding_strategy
DECISIONS = "/home/hmo/web-dashboard/data/decisions.json"
PORTFOLIO = "/home/hmo/web-dashboard/data/portfolio.json"
def main():
dec = json.load(open(DECISIONS))
pf = json.load(open(PORTFOLIO))
dec = read_decisions()
pf = read_portfolio()
# Build maps
dmap = {d["code"]: d for d in dec.get("decisions", [])}
@@ -120,11 +122,9 @@ def main():
print(f"decisions stock_value: {dec_total:.2f}")
print(f"decisions count(shares>0): {len([d for d in dec['decisions'] if d.get('shares',0)>0])}")
# Write — DB 优先(强制币种约束)JSON 冷备
# Write — DB 优先,JSON 冷备已移除
dec["total"] = len(dec["decisions"])
try:
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from mofin_db import get_conn, write_holdings_batch, write_portfolio_summary, write_holding_strategy
conn = get_conn()
write_holdings_batch(conn, pf.get('holdings', []))
write_portfolio_summary(conn, pf)
@@ -133,8 +133,9 @@ def main():
conn.close()
except Exception as e:
print(f" [DB写入失败] {e}")
json.dump(dec, open(DECISIONS, "w"), ensure_ascii=False, indent=2)
json.dump(pf, open(PORTFOLIO, "w"), ensure_ascii=False, indent=2)
# [migrated to DB] — cold backup removed; DB writes above
# json.dump(dec, open(DECISIONS, "w"), ensure_ascii=False, indent=2)
# json.dump(pf, open(PORTFOLIO, "w"), ensure_ascii=False, indent=2)
print(f"done")
if __name__ == "__main__":
+17 -7
View File
@@ -15,6 +15,8 @@ import_holding_xls.py — 从 holding.xls 导入持仓到全系统
"""
import csv, json, sys, subprocess, sqlite3, os
from datetime import datetime
from mo_data import read_decisions
from mofin_db import get_conn, write_holdings_batch, write_portfolio_summary, write_holding_strategy
STOCKS_FILE = "/home/hmo/stocks/holding.xls"
PORTFOLIO_PATH = "/home/hmo/web-dashboard/data/portfolio.json"
@@ -149,11 +151,11 @@ def main():
'updated_at': datetime.now().strftime('%Y-%m-%d %H:%M'),
'source': STOCKS_FILE,
}
with open(PORTFOLIO_PATH, 'w') as f:
json.dump(portfolio, f, indent=2, ensure_ascii=False)
# [migrated to DB] — cold backup removed; DB writes below
# with open(PORTFOLIO_PATH, 'w') as f:
# json.dump(portfolio, f, indent=2, ensure_ascii=False)
# DB 写入
try:
from mofin_db import get_conn, write_holdings_batch, write_portfolio_summary
conn = get_conn()
write_holdings_batch(conn, portfolio.get('holdings', []))
write_portfolio_summary(conn, portfolio)
@@ -165,8 +167,7 @@ def main():
print("\n→ 重建决策树...")
sys.path.insert(0, '/home/hmo/web-dashboard')
from strategy_tree import init_default_branches
with open('/home/hmo/web-dashboard/data/decisions.json') as f:
data = json.load(f)
data = read_decisions()
ok = 0
for e in data.get('decisions', []):
branches = init_default_branches(
@@ -175,8 +176,17 @@ def main():
e.get('stop_loss', 0), e.get('take_profit', 0))
e['strategy_tree'] = {'branches': branches, 'created_at': datetime.now().strftime('%Y-%m-%d')}
ok += 1
with open('/home/hmo/web-dashboard/data/decisions.json', 'w') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
# DB 写入(替代 json.dump
try:
conn = get_conn()
for d in data.get('decisions', []):
_whs(conn, d.get('code', ''), d.get('name', ''), d)
conn.close()
except Exception:
pass
# [migrated to DB] — cold backup removed
# with open('/home/hmo/web-dashboard/data/decisions.json', 'w') as f:
# json.dump(data, f, indent=2, ensure_ascii=False)
print(f"\n{'='*50}")
print(f"导入完成:{len(holdings)}只持仓")
+11
View File
@@ -0,0 +1,11 @@
import json
with open('/home/hmo/web-dashboard/data/decisions.json') as f:
d = json.load(f)
for i, e in enumerate(d.get('decisions', [])[:3]):
print(f"\n=== Entry {i}: {e.get('code')} {e.get('name')} ===")
for k, v in sorted(e.items()):
t = type(v).__name__
sample = str(v)[:60] if v is not None else 'None'
print(f" {k}: {t} = {sample}")
if i >= 2:
break
+16 -12
View File
@@ -9,6 +9,10 @@ import sys, json, os, re
sys.path.insert(0, "/home/hmo/web-dashboard")
from strategy_lifecycle import reassess_with_context as reassess_strategy
from mo_data import read_decisions, read_portfolio
sys.path.insert(0, "/home/hmo/MoFin")
from mofin_db import get_conn, write_holding_strategy
DECISIONS_PATH = "/home/hmo/web-dashboard/data/decisions.json"
@@ -23,8 +27,7 @@ def main():
return
# 读现有 decisions
with open(DECISIONS_PATH) as f:
raw = json.load(f)
raw = read_decisions()
decisions_map = {d["code"]: d for d in raw.get("decisions", []) if d.get("code")}
ok = 0
@@ -57,14 +60,6 @@ def main():
db.close()
if price > 0:
print(f" 实时价: {price} (来自DB)")
else:
# fallback to portfolio.json
with open("/home/hmo/web-dashboard/data/portfolio.json") as _pf:
_pf_data = json.load(_pf)
for _h in _pf_data.get("holdings", []):
if _h["code"] == code_raw:
price = float(_h.get("price", 0))
break
if price <= 0:
price = entry.get("current_price") or entry.get("price") or 0
except Exception as e:
@@ -153,8 +148,17 @@ def main():
raw["total"] = len(raw["decisions"])
from datetime import datetime
raw["regenerated_at"] = datetime.now().strftime("%Y-%m-%d %H:%M")
with open(DECISIONS_PATH, "w") as f:
json.dump(raw, f, ensure_ascii=False, indent=2)
# DB 写入(替代 json.dump
try:
conn = get_conn()
for d in raw.get("decisions", []):
write_holding_strategy(conn, d.get("code", ""), d.get("name", ""), d)
conn.close()
except Exception:
pass
# [migrated to DB] — cold backup removed
# with open(DECISIONS_PATH, "w") as f:
# json.dump(raw, f, ensure_ascii=False, indent=2)
print(f"[DONE] {ok}成功 {skipped}跳过 {errors}失败")
+31
View File
@@ -0,0 +1,31 @@
"""Phase 1: Add missing columns to holding_strategies for decisions.json→DB migration"""
import sqlite3
db = sqlite3.connect('/home/hmo/web-dashboard/data/mofin.db')
new_cols = {
'avg_price': 'REAL',
'decision_timestamp': 'TEXT', # avoids conflict with SQLite timestamp
'note': 'TEXT',
'quality_check': 'TEXT',
'quality_checked_at': 'TEXT',
'quality_issues_json': 'TEXT',
'position_advice': 'TEXT',
'signal_factors_json': 'TEXT',
'time_horizon': 'TEXT',
'decision_type': 'TEXT',
}
# Get existing columns
existing = {r[1] for r in db.execute("PRAGMA table_info(holding_strategies)")}
for col, ctype in new_cols.items():
if col in existing:
print(f"SKIP: {col} already exists")
else:
db.execute(f"ALTER TABLE holding_strategies ADD COLUMN {col} {ctype}")
print(f"ADDED: {col} {ctype}")
db.commit()
db.close()
print("\nSchema migration done.")
+27 -11
View File
@@ -13,7 +13,7 @@ from datetime import datetime
# ── MoFin unified model ──────────────────────────────────────────────
sys.path.insert(0, "/home/hmo/MoFin")
from mo_models import is_hk_stock, get_hk_rate, calc_total_assets, calc_total_mv, calc_position_pct
from mofin_db import get_conn, write_holdings_batch, write_portfolio_summary, write_price_event, write_watchlist_stock
from mofin_db import get_conn, write_holdings_batch, write_portfolio_summary, write_price_event, write_watchlist_stock, write_holding_strategy, write_holding_strategy
from mo_data import read_portfolio, read_decisions, read_watchlist
DECISIONS_PATH = "/home/hmo/web-dashboard/data/decisions.json"
@@ -346,14 +346,22 @@ def refresh_data_prices():
conn.close()
except Exception as e:
print(f" [DB写入失败] {e}", flush=True)
# 保留 JSON 副本作为冷备
json.dump(pf, open(PORTFOLIO_PATH, 'w'), ensure_ascii=False, indent=2)
# [migrated to DB] — JSON cold backup removed
# json.dump(pf, open(PORTFOLIO_PATH, 'w'), ensure_ascii=False, indent=2)
elif pf.get('updated_at'):
try:
last_ts = datetime.strptime(pf['updated_at'], '%Y-%m-%d %H:%M')
if (datetime.now() - last_ts).total_seconds() > 600:
pf['updated_at'] = datetime.now().strftime('%Y-%m-%d %H:%M')
json.dump(pf, open(PORTFOLIO_PATH, 'w'), ensure_ascii=False, indent=2)
# DB 写入(时间戳更新)
try:
conn2 = get_conn()
write_portfolio_summary(conn2, pf)
conn2.close()
except Exception:
pass
# [migrated to DB] — cold backup removed
# json.dump(pf, open(PORTFOLIO_PATH, 'w'), ensure_ascii=False, indent=2)
except:
pass
@@ -383,8 +391,8 @@ def refresh_data_prices():
conn.close()
except Exception as e:
print(f" [DB watchlist写入失败] {e}", flush=True)
# 保留 JSON 冷备
json.dump(wl, open(WATCHLIST_PATH, 'w'), ensure_ascii=False, indent=2)
# [migrated to DB] — cold backup removed; DB writes above
# json.dump(wl, open(WATCHLIST_PATH, 'w'), ensure_ascii=False, indent=2)
# --- 汇总值重算(使用 mo_models 唯一公式)---
try:
@@ -405,8 +413,8 @@ def refresh_data_prices():
conn.close()
except Exception as e:
print(f" [DB汇总写入失败] {e}", flush=True)
# JSON 冷备
json.dump(pf, open(PORTFOLIO_PATH, 'w'), ensure_ascii=False, indent=2)
# [migrated to DB] — cold backup removed; DB writes above
# json.dump(pf, open(PORTFOLIO_PATH, 'w'), ensure_ascii=False, indent=2)
except Exception as e:
print(f" [汇总重算失败] {e}", flush=True)
# --- 结束汇总重算 ---
@@ -446,7 +454,16 @@ def _record_branch_trigger(code, branch_id, price):
b['last_trigger_price'] = round(price, 2)
b['last_triggered'] = datetime.now().isoformat()
break
json.dump(raw, open(DECISIONS_PATH, 'w'), ensure_ascii=False, indent=2)
# DB 写入(替代 json.dump
try:
conn3 = get_conn()
for d in raw.get('decisions', []):
write_holding_strategy(conn3, d.get('code', ''), d.get('name', ''), d)
conn3.close()
except Exception:
pass
# [migrated to DB] — cold backup removed
# json.dump(raw, open(DECISIONS_PATH, 'w'), ensure_ascii=False, indent=2)
except Exception:
pass
@@ -573,8 +590,7 @@ def run_once(round_label=""):
# === 第二步:检查触发条件 ===
try:
with open(DECISIONS_PATH) as f:
dec = json.load(f)
dec = read_decisions()
except:
print(f"{label} 无法读取decisions.json", file=sys.stderr)
return
+6 -7
View File
@@ -17,6 +17,8 @@ Dad 发交易截图后,填入以下信息运行此脚本:
import json, sys, os
from datetime import datetime
from mo_data import read_portfolio, read_decisions, read_watchlist
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from mofin_db import get_conn, write_holdings_batch, write_portfolio_summary, write_holding_strategy
PORTFOLIO_PATH = "/home/hmo/web-dashboard/data/portfolio.json"
DECISIONS_PATH = "/home/hmo/web-dashboard/data/decisions.json"
@@ -121,21 +123,18 @@ def main():
})
break
# 写入 — DB 优先
# 写入 — DB(替代 json.dump
pf["updated_at"] = now
try:
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from mofin_db import get_conn, write_holdings_batch, write_portfolio_summary, write_holding_strategy
conn = get_conn()
write_holdings_batch(conn, pf.get('holdings', []))
write_portfolio_summary(conn, pf)
for d in dec.get('decisions', []):
write_holding_strategy(conn, d.get('code', ''), d.get('name', ''), d)
conn.close()
except Exception:
pass
json.dump(pf, open(PORTFOLIO_PATH, "w"), indent=2, ensure_ascii=False)
json.dump(dec, open(DECISIONS_PATH, "w"), indent=2, ensure_ascii=False)
except Exception as e:
print(f" [DB写入失败] {e}", file=sys.stderr)
# [migrated to DB] — JSON cold backup removed
# 重算总资产
total_mv = sum((h.get("shares",0) or 0) * (h.get("price",0) or 0) for h in pf["holdings"])
+14 -4
View File
@@ -13,19 +13,29 @@ prune_branches.py — 每日剪枝
import json, sys, os
from datetime import datetime, timedelta
from mo_data import read_decisions
from mofin_db import get_conn, write_holding_strategy
DECISIONS_PATH = "/home/hmo/web-dashboard/data/decisions.json"
PRUNE_LOG = "/home/hmo/MoFin/data/prune_log.json"
def load_decisions():
with open(DECISIONS_PATH) as f:
return json.load(f)
return read_decisions()
def save_decisions(data):
with open(DECISIONS_PATH, "w") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
# DB 写入(替代 json.dump
try:
conn = get_conn()
for d in data.get("decisions", []):
write_holding_strategy(conn, d.get("code", ""), d.get("name", ""), d)
conn.close()
except Exception:
pass
# [migrated to DB] — cold backup removed
# with open(DECISIONS_PATH, "w") as f:
# json.dump(data, f, indent=2, ensure_ascii=False)
def main():
+3 -2
View File
@@ -10,6 +10,7 @@ review_needed_watchdog.py — review_needed 策略自动跟进
"""
import sys, json, os, datetime
from mo_data import read_decisions
sys.path.insert(0, "/home/hmo/web-dashboard")
os.chdir("/home/hmo/MoFin")
@@ -39,7 +40,7 @@ def push_xmpp(text):
print(f" [XMPP推送失败] {e}")
def main():
dec = json.load(open(DEC_PATH))
dec = read_decisions()
review_list = [d for d in dec.get("decisions", []) if d.get("status") == "review_needed"]
retry_data = load_retry()
today = datetime.date.today().isoformat()
@@ -70,7 +71,7 @@ def main():
print(f" {out[:200]}")
# 重读决策
dec2 = json.load(open(DEC_PATH))
dec2 = read_decisions()
for d2 in dec2.get("decisions", []):
if d2["code"] == code:
if d2.get("status") == "active":
+1 -2
View File
@@ -114,8 +114,7 @@ def main():
cash = 0
total_assets = 0
try:
with open(PORTFOLIO_PATH) as f:
pf = json.load(f)
pf = read_portfolio()
position_pct = pf.get("position_pct", 0)
cash = pf.get("cash", 0)
total_assets = pf.get("total_assets", 0)
+7 -11
View File
@@ -19,6 +19,7 @@ import os
import threading
import time
from datetime import datetime, time
from mo_data import read_portfolio, read_decisions
# ── MoFin unified model ──────────────────────────────────────────────
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
@@ -226,8 +227,7 @@ def trigger_regen_sync(stock_codes=None):
def load_cash():
"""从 portfolio.json 实时读可用现金(可用 ≈ 实时买力),不硬编码"""
try:
with open(PORTFOLIO_PATH) as f:
data = json.load(f)
data = read_portfolio()
if isinstance(data, dict):
# 先读 cash_available(拆分了可用/冻结),fallback 到 cash
return data.get("cash_available", data.get("cash", 0))
@@ -338,8 +338,7 @@ def main():
# 读 decisions.json 获取完整策略数据
code_data = {}
try:
with open("/home/hmo/web-dashboard/data/decisions.json") as f:
dec = json.load(f)
dec = read_decisions()
for e in dec.get("decisions", []):
code_data[e["code"]] = e
except Exception:
@@ -395,8 +394,7 @@ def main():
# 重评完成,re-read decisions.json 获取最新策略
code_data = {}
try:
with open("/home/hmo/web-dashboard/data/decisions.json") as f:
dec = json.load(f)
dec = read_decisions()
for e in dec.get("decisions", []):
code_data[e["code"]] = e
except Exception:
@@ -417,8 +415,7 @@ def main():
# 加载portfolio获取持仓信息(A/H去重用)
pf = {"holdings": []}
try:
with open(PORTFOLIO_PATH) as f:
pf = json.load(f)
pf = read_portfolio()
except Exception:
pass
@@ -492,8 +489,7 @@ def main():
total_assets = 0
available_cash = 0
try:
with open("/home/hmo/web-dashboard/data/portfolio.json") as f:
pf = json.load(f)
pf = read_portfolio()
available_cash = pf.get("cash_available", pf.get("cash", 0)) or 0
# 直接取 portfolio.json 的总资产(导入时已做港币→人民币换算)
total_assets = pf.get("total_assets", 0) or 0
@@ -845,7 +841,7 @@ def main():
# ── T+2前瞻:扫描近期可能入买区的A股,提前准备现金 ──
t2_lines = []
try:
dec_t2 = json.loads(open("/home/hmo/web-dashboard/data/decisions.json").read())
dec_t2 = read_decisions()
for entry in dec_t2.get("decisions", []):
if entry.get("status") == "closed" or entry.get("type") != "自选策略":
continue
+4 -4
View File
@@ -12,6 +12,8 @@
import json, sys, os, re, urllib.request
from datetime import datetime
from mo_data import read_decisions, read_portfolio
from mo_data import read_decisions, read_portfolio
DECISIONS_PATH = "/home/hmo/web-dashboard/data/decisions.json"
OUTPUT_PATH = "/home/hmo/web-dashboard/data/strategy_staleness_report.json"
@@ -59,8 +61,7 @@ def main():
path = DECISIONS_PATH if os.path.exists(DECISIONS_PATH) else FALLBACK_PATH
is_new_format = (path == DECISIONS_PATH)
with open(path) as f:
data = json.load(f)
data = read_decisions()
# Filter: exclude closed strategies
all_entries = data.get("decisions", [])
@@ -190,8 +191,7 @@ def main():
position_pct = 0
cash = 0
try:
with open(PORTFOLIO_PATH) as pf:
pdata = json.load(pf)
pdata = read_portfolio()
position_pct = pdata.get("position_pct", 0)
cash = pdata.get("cash", 0)
except: pass
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env python3
"""Verify 01888 data after fix"""
import sys
sys.path.insert(0, '/home/hmo/MoFin')
from mo_data import read_portfolio, read_decisions
import sqlite3
# Check DB
db = sqlite3.connect('/home/hmo/web-dashboard/data/mofin.db')
r = db.execute("SELECT code, name, cost, shares, currency FROM holdings WHERE code='01888'").fetchone()
if r:
print(f"[DB] {r[0]} {r[1]}: cost={r[2]} shares={r[3]} curr={r[4]}")
# Check decisions
dec = read_decisions()
for d in dec.get('decisions', []):
if d.get('code') == '01888':
print(f"[DEC] price={d.get('price')} cost={d.get('cost')} shares={d.get('shares')}")
print(f" curr={d.get('currency')} status={d.get('status')}")
c = d.get('cost', 0)
p = d.get('price', 0)
if c > 0 and p > 0:
print(f" PnL={(p-c)/c*100:.1f}%")
# Check portfolio
pf = read_portfolio()
for h in pf.get('holdings', []):
if h.get('code') == '01888':
print(f"[PF] {h.get('code')} cost={h.get('cost')} price={h.get('price')} shares={h.get('shares')}")
db.close()
+17 -5
View File
@@ -13,6 +13,8 @@ no_agent模式:有发现→输出,无→静默
import json, os, sqlite3, sys, time, urllib.request
from pathlib import Path
from datetime import datetime
from mo_data import read_watchlist
from mofin_db import write_watchlist_stock
BASE = Path("/home/hmo/MoFin")
DATA = BASE / "data"
@@ -64,7 +66,7 @@ def is_in_portfolio(conn, code):
return "watchlist"
# 也检查 watchlist.json
try:
wl = json.loads(WATCHLIST_PATH.read_text())
wl = read_watchlist()
for s in wl.get("stocks", []):
if s.get("code") == code or s.get("code", "").lstrip("0") == code_stripped:
return "watchlist"
@@ -240,20 +242,30 @@ def main():
results.append(f"{sector_name}({code}): {summary}")
# 写入 watchlist.json
try:
wl = json.loads(WATCHLIST_PATH.read_text())
wl = read_watchlist()
wl.setdefault("stocks", [])
# 检查是否已在
existing = [s for s in wl["stocks"] if s.get("code") == code]
if not existing:
wl["stocks"].append({
new_stock = {
"code": code,
"name": quote.get("name", sector_name),
"price": quote.get("price", 0),
"status": "watching",
"source": "xiaoguo_scanner",
"added_at": today,
})
WATCHLIST_PATH.write_text(json.dumps(wl, ensure_ascii=False, indent=2))
}
wl["stocks"].append(new_stock)
# DB 写入(watchlist_stocks
try:
conn2 = get_conn()
new_stock["currency"] = "CNY"
write_watchlist_stock(conn2, new_stock)
conn2.close()
except Exception:
pass
# [migrated to DB] — JSON cold backup removed
# WATCHLIST_PATH.write_text(json.dumps(wl, ensure_ascii=False, indent=2))
except:
pass
elif action == "monitor":
+64 -44
View File
@@ -15,6 +15,10 @@ from flask import Flask, jsonify, send_from_directory, request
# 提示词管理模块
from prompt_manager.dashboard_views import register_routes
# MoFin 数据层(纯 DB,不再读 JSON)
from mo_data import read_portfolio, read_decisions, read_watchlist
from mofin_db import get_conn, write_holdings_batch, write_portfolio_summary, write_watchlist_stock, write_holding_strategy
app = Flask(__name__, static_folder="static", static_url_path="")
DATA_DIR = Path(__file__).parent / "data"
@@ -26,6 +30,7 @@ API_KEY = "hermes123"
def _load_json(path, default=None):
"""仅用于非核心文件(reports, stocks, market 等)。portfolio/decisions/watchlist 已迁移到 DB。"""
try:
with open(path, encoding="utf-8") as f:
return json.load(f)
@@ -33,26 +38,41 @@ def _load_json(path, default=None):
return {} if default is None else default
def _load_from_db(query_func, json_path, default=None):
"""优先从 SQLite 读取,失败回退 JSON"""
try:
from mofin_db import get_conn
conn = get_conn()
result = query_func(conn)
conn.close()
if result:
return result
except Exception:
pass
return _load_json(json_path, default)
def _save_json(path, data):
"""仅用于非核心文件(reports, stocks, market 等)。portfolio/decisions/watchlist 已迁移到 DB。"""
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
def _save_portfolio(data):
"""写入持仓数据到 DB。data 必须包含 holdings[] 和顶层 summary 字段。"""
conn = get_conn()
try:
write_holdings_batch(conn, data.get('holdings', []))
write_portfolio_summary(conn, data)
finally:
conn.close()
def _save_decision(code, name, data):
"""写入单条决策到 DB。"""
conn = get_conn()
try:
write_holding_strategy(conn, code, name, data)
finally:
conn.close()
def _save_watchlist(data):
"""写入自选股列表到 DB。"""
conn = get_conn()
for s in data.get('stocks', []):
s.setdefault('currency', 'CNY')
write_watchlist_stock(conn, s)
conn.close()
# ── API 路由 ──────────────────────────────────────────
@app.route("/")
@@ -75,7 +95,7 @@ def api_portfolio():
return jsonify(data)
except Exception:
pass
return jsonify(_load_json(DATA_DIR / "portfolio.json"))
return jsonify({"error": "数据库查询失败"}), 500
@app.route("/api/watchlist")
@@ -90,7 +110,7 @@ def api_watchlist():
return jsonify({"stocks": stocks})
except Exception:
pass
return jsonify(_load_json(DATA_DIR / "watchlist.json"))
return jsonify({"error": "数据库查询失败"}), 500
@app.route("/api/overview")
@@ -121,7 +141,7 @@ def api_overview():
})
except Exception:
pass
portfolio = _load_json(DATA_DIR / "portfolio.json", [])
portfolio = read_portfolio()
market = _load_json(DATA_DIR / "market.json", {})
alerts = _load_json(DATA_DIR / "alerts.json", [])
total_assets = portfolio.get("total_assets", 0)
@@ -255,14 +275,14 @@ def api_xiaoguo_scan():
@app.route("/api/update/portfolio", methods=["POST"])
def update_portfolio():
data = request.get_json(force=True)
_save_json(DATA_DIR / "portfolio.json", data)
_save_portfolio(data)
return jsonify({"status": "ok"})
@app.route("/api/update/watchlist", methods=["POST"])
def update_watchlist():
data = request.get_json(force=True)
_save_json(DATA_DIR / "watchlist.json", data)
_save_watchlist(data)
return jsonify({"status": "ok"})
@@ -310,7 +330,7 @@ def analysis_batch():
# 更新持仓
if "holdings" in data:
pf = _load_json(DATA_DIR / "portfolio.json", {})
pf = read_portfolio()
idx = {h["code"]: i for i, h in enumerate(pf.get("holdings", []))}
for item in data["holdings"]:
code = item.get("code", "")
@@ -327,11 +347,11 @@ def analysis_batch():
"reason": item.get("reason"),
"updated_at": datetime.now().isoformat(),
}
_save_json(DATA_DIR / "portfolio.json", pf)
_save_portfolio(pf)
# 更新自选
if "watchlist" in data:
wl = _load_json(DATA_DIR / "watchlist.json", {})
wl = read_watchlist()
idx = {s["code"]: i for i, s in enumerate(wl.get("stocks", []))}
for item in data["watchlist"]:
code = item.get("code", "")
@@ -345,7 +365,7 @@ def analysis_batch():
"reason": item.get("reason"),
"updated_at": datetime.now().isoformat(),
}
_save_json(DATA_DIR / "watchlist.json", wl)
_save_watchlist(wl)
return jsonify({"status": "ok", "updated_at": datetime.now().isoformat()})
@@ -354,14 +374,14 @@ def analysis_batch():
@app.route("/api/decisions", methods=["GET"])
def get_decisions():
"""返回决策库数据,统一新旧格式"""
raw = _load_json(DATA_DIR / "decisions.json", {"decisions": []})
raw = read_decisions()
decisions = raw.get("decisions", [])
if not decisions and isinstance(raw, list):
decisions = raw
# portfolio 用来判断是持仓还是自选
portfolio = _load_json(DATA_DIR / "portfolio.json", {"holdings": []})
watchlist = _load_json(DATA_DIR / "watchlist.json", {"stocks": []})
portfolio = read_portfolio()
watchlist = read_watchlist()
holding_codes = {h.get("code","") for h in portfolio.get("holdings",[])}
watch_codes = {s.get("code","") for s in watchlist.get("stocks",[])}
@@ -508,7 +528,7 @@ def add_decision():
if not code:
return jsonify({"status": "error", "message": "code required"}), 400
d = _load_json(DATA_DIR / "decisions.json", {"decisions": []})
d = read_decisions()
# 同一股票旧决策标记为superseded
for e in d["decisions"]:
@@ -537,7 +557,7 @@ def add_decision():
"analysis": data.get("analysis", {}),
}
d["decisions"].append(entry)
_save_json(DATA_DIR / "decisions.json", d)
_save_decision(code, entry.get('name',''), entry)
return jsonify({"status": "ok", "entry": entry})
@@ -550,7 +570,7 @@ def set_decision_tag():
if not code:
return jsonify({"status": "error", "message": "code required"}), 400
d = _load_json(DATA_DIR / "decisions.json", {"decisions": []})
d = read_decisions()
found = False
for e in d.get("decisions", []):
if e.get("code") == code:
@@ -562,14 +582,14 @@ def set_decision_tag():
if not found:
return jsonify({"status": "error", "message": f"stock {code} not found"}), 404
_save_json(DATA_DIR / "decisions.json", d)
_save_decision(code, e.get('name',''), e)
return jsonify({"status": "ok", "code": code, "tag": tag})
@app.route("/api/decisions/pending")
def get_pending_decisions():
"""返回所有有未确认建议的条目"""
d = _load_json(DATA_DIR / "decisions.json", {"decisions": []})
d = read_decisions()
pending = []
for entry in d["decisions"]:
timeline = entry.get("advice_timeline", [])
@@ -595,7 +615,7 @@ def record_advice():
direction = data.get("direction", "持有")
today = datetime.now().strftime("%Y-%m-%d")
d = _load_json(DATA_DIR / "decisions.json", {"decisions": []})
d = read_decisions()
entry = None
for e in d["decisions"]:
@@ -625,7 +645,7 @@ def record_advice():
"status": "pending",
}
timeline.append(advice)
_save_json(DATA_DIR / "decisions.json", d)
_save_decision(code, entry.get('name',''), entry)
return jsonify({"status": "ok", "advice": advice})
@@ -638,7 +658,7 @@ def confirm_advice():
action = data.get("action", "confirmed") # confirmed | ignored | executed
result = data.get("result", "")
d = _load_json(DATA_DIR / "decisions.json", {"decisions": []})
d = read_decisions()
for e in d["decisions"]:
if e["code"] == code and e["status"] == "active":
timeline = e.get("advice_timeline", [])
@@ -649,7 +669,7 @@ def confirm_advice():
timeline[idx]["evaluated_at"] = datetime.now().strftime("%Y-%m-%d %H:%M")
if result:
timeline[idx]["result"] = result
_save_json(DATA_DIR / "decisions.json", d)
_save_decision(code, e.get('name',''), e)
return jsonify({"status": "ok"})
return jsonify({"status": "error", "message": "not found"}), 404
@@ -672,7 +692,7 @@ def get_evaluation():
return jsonify(strategies)
# 备选:从 decisions.json 的 evaluation 字段读取(尚未反写时的兼容)
decisions = _load_json(DECISIONS_PATH if 'DECISIONS_PATH' in dir() else DATA_DIR / "decisions.json", {"decisions": []})
decisions = read_decisions()
evals = []
for d in decisions.get("decisions", []):
e = d.get("evaluation", [])
@@ -937,7 +957,7 @@ def upload_confirm():
# 更新对应数据文件
if doc_type == "portfolio":
existing = _load_json(DATA_DIR / "portfolio.json", {})
existing = read_portfolio()
old_holdings = {h["code"]: h for h in existing.get("holdings", []) if h.get("code")}
new_holdings = []
for s in stocks:
@@ -984,11 +1004,11 @@ def upload_confirm():
# 计算仓位%
if existing["total_assets"] > 0:
existing["position_pct"] = round(existing["stock_value"] / existing["total_assets"] * 100, 2)
_save_json(DATA_DIR / "portfolio.json", existing)
_save_portfolio(existing)
msg = f"更新了 {len(stocks)} 只持仓股"
elif doc_type == "watchlist":
existing = _load_json(DATA_DIR / "watchlist.json", {})
existing = read_watchlist()
existing["stocks"] = [
{
"code": s.get("code", ""),
@@ -998,7 +1018,7 @@ def upload_confirm():
for s in stocks
]
existing["updated_at"] = datetime.now().isoformat()
_save_json(DATA_DIR / "watchlist.json", existing)
_save_watchlist(existing)
msg = f"更新了 {len(stocks)} 只自选股"
else:
@@ -1019,7 +1039,7 @@ def update_realtime():
return jsonify({"status": "error", "message": "没有股票数据"}), 400
# 更新 portfolio.json 中的实时价格(change_pct字段)
pf = _load_json(DATA_DIR / "portfolio.json", {"holdings": []})
pf = read_portfolio()
pf_holdings = {h["code"]: h for h in pf.get("holdings", [])}
updated = 0
@@ -1037,7 +1057,7 @@ def update_realtime():
updated += 1
# 也更新 watchlist.json
wl = _load_json(DATA_DIR / "watchlist.json", {"stocks": []})
wl = read_watchlist()
wl_stocks = {s["code"]: s for s in wl.get("stocks", [])}
for s in stocks:
@@ -1048,8 +1068,8 @@ def update_realtime():
pf["updated_at"] = datetime.now().isoformat()
wl["updated_at"] = datetime.now().isoformat()
_save_json(DATA_DIR / "portfolio.json", pf)
_save_json(DATA_DIR / "watchlist.json", wl)
_save_portfolio(pf)
_save_watchlist(wl)
return jsonify({
"status": "ok",
+9 -6
View File
@@ -20,10 +20,10 @@ import sys
import re
from datetime import datetime, timedelta
from pathlib import Path
from mo_data import read_decisions, read_portfolio
from mofin_db import get_conn, write_holding_strategy
DATA_DIR = Path(__file__).parent / "data"
DECISIONS_PATH = DATA_DIR / "decisions.json"
PORTFOLIO_PATH = DATA_DIR / "portfolio.json"
ACCURACY_PATH = DATA_DIR / "accuracy_stats.json"
UA = "Mozilla/5.0"
@@ -441,8 +441,8 @@ def evaluate_phase2(decision, price_info, holding, prev_eval):
def run():
decisions = load_json(DECISIONS_PATH, {"decisions": []})
portfolio = load_json(PORTFOLIO_PATH, {"holdings": []})
decisions = read_decisions()
portfolio = read_portfolio()
holdings_map = {h["code"]: h for h in portfolio.get("holdings", [])}
# 收集所有代码
@@ -476,8 +476,11 @@ def run():
# 更新 decisions.json 的 evaluation 字段
d["evaluation"] = [e for e in [eval1] + ([eval2] if prev_eval and prev_eval.get("phase") == 1 else [])]
# 保存更新
save_json(DECISIONS_PATH, decisions)
# 保存更新到 DB
conn = get_conn()
for d in decisions.get("decisions", []):
write_holding_strategy(conn, d["code"], d.get("name", ""), d)
conn.close()
# 汇总统计
for r in results:
+2 -3
View File
@@ -14,10 +14,9 @@ import json
import sys
from datetime import datetime, timedelta
from pathlib import Path
from mo_data import read_decisions
DATA_DIR = Path(__file__).parent / "data"
DECISIONS_PATH = DATA_DIR / "decisions.json"
PORTFOLIO_PATH = DATA_DIR / "portfolio.json"
ACCURACY_PATH = DATA_DIR / "accuracy_stats.json"
EVENTS_PATH = DATA_DIR / "price_events.json"
FEEDBACK_PATH = DATA_DIR / "strategy_feedback.json"
@@ -174,7 +173,7 @@ def generate_adjustment(decision, phase_check, accuracy_trend):
def run():
decisions = load_json(DECISIONS_PATH, {"decisions": []})
decisions = read_decisions()
# 优先从 SQLite 读取价格事件
try:
from mofin_db import get_conn, query_price_events
+13 -4
View File
@@ -18,9 +18,9 @@ strategy_tree.py — 情景化多分支策略决策引擎
import json, os, sys, re
from datetime import datetime, date, timedelta
from mo_data import read_portfolio, read_decisions, read_watchlist
from mofin_db import get_conn, write_holding_strategy
from mofin_db import get_conn, write_holding_strategy
DECISIONS_PATH = "/home/hmo/web-dashboard/data/decisions.json"
PORTFOLIO_PATH = "/home/hmo/web-dashboard/data/portfolio.json"
MACRO_PATH = "/home/hmo/web-dashboard/data/macro_context.json"
MARKET_PATH = "/home/hmo/web-dashboard/data/market.json"
TREND_PATH = "/home/hmo/web-dashboard/data/trend_signals.json"
@@ -248,7 +248,12 @@ def record_branch_trigger(code, branch_id):
br["last_triggered"] = datetime.now().isoformat()
break
break
json.dump(dec, open(DECISIONS_PATH, "w"), ensure_ascii=False, indent=2)
conn = get_conn()
for e in dec.get("decisions", []):
if e.get("code") == code:
write_holding_strategy(conn, code, e.get('name', ''), e)
break
conn.close()
except Exception:
pass
@@ -286,7 +291,11 @@ def prune_low_performance_branches(min_triggers=5, min_success_rate=0.3):
st["branches"] = kept
if pruned:
json.dump(dec, open(DECISIONS_PATH, "w"), ensure_ascii=False, indent=2)
conn = get_conn()
for e in dec.get("decisions", []):
if e.get("strategy_tree", {}).get("branches") is not None:
write_holding_strategy(conn, e.get("code"), e.get('name', ''), e)
conn.close()
return pruned
+11 -11
View File
@@ -6,6 +6,8 @@ import os
import re
from datetime import datetime
from pathlib import Path
from mo_data import read_decisions
from mofin_db import get_conn, write_holding_strategy
DATA_DIR = Path(__file__).parent / "data"
@@ -163,13 +165,10 @@ def extract_stock_mentions():
def sync_to_decisions():
"""将个股建议同步到决策库(advice_timeline),自动去重"""
decisions_path = DATA_DIR / "decisions.json"
if not decisions_path.exists():
return 0
decisions = json.loads(decisions_path.read_text(encoding="utf-8"))
decisions = read_decisions()
stocks_dir = DATA_DIR / "stocks"
synced = 0
conn = get_conn()
for f in sorted(stocks_dir.iterdir()):
if f.suffix != ".json":
@@ -186,13 +185,13 @@ def sync_to_decisions():
# 找决策库中是否有此股
existing = None
for d in decisions["decisions"]:
for d in decisions.get("decisions", []):
if d["code"] == code:
existing = d
break
if not existing:
# 无决策记录→生成inactive记录
# 无决策记录→生成inactive记录,写入 DB
existing = {
"code": code,
"name": stock.get("name", ""),
@@ -201,9 +200,11 @@ def sync_to_decisions():
"current": "自动从update_data同步",
"status": "inactive",
"updated_by": "system(update_data)",
"advice_timeline": []
"advice_timeline": [],
"source": "update_data",
}
decisions["decisions"].append(existing)
write_holding_strategy(conn, code, stock.get("name", ""), existing)
# 去重合并
timeline = existing.setdefault("advice_timeline", [])
@@ -244,11 +245,10 @@ def sync_to_decisions():
if new_count > 0:
# 按日期排序
timeline.sort(key=lambda e: e.get("date", ""))
write_holding_strategy(conn, code, existing.get("name", ""), existing)
synced += new_count
decisions_path.write_text(
json.dumps(decisions, ensure_ascii=False, indent=2), encoding="utf-8"
)
conn.close()
return synced