From e2646c36cbc17d132fbe4dce57ce03099adf936d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=9F=A5=E5=BE=AE?= Date: Wed, 24 Jun 2026 11:21:51 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8C=81=E4=BB=93=E5=AF=BC=E5=85=A5=E5=AE=8C?= =?UTF-8?q?=E6=88=90=EF=BC=9Aholding.xls=E2=86=92SQLite+portfolio.json?= =?UTF-8?q?=E5=85=A8=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 从 ~/stocks/holding.xls 导入25只持仓(14A/11H) 2. 同时写入 portfolio.json + SQLite holdings 表 3. stale_push_wlin 现金来源从 stale_report 改为 portfolio.json 4. portfolio.json 增加 total_assets 字段兼容 stale_detector 5. 导入脚本已规范化为 MoFin/scripts/import_holding_xls.py 用法:python3 import_holding_xls.py [--cash 金额] 6. 全量策略重评+决策树重建立即执行 Dad下次更新holding.xls后跑: cd MoFin && python3 scripts/import_holding_xls.py --- data/decisions.json | 124 ++++++++--------- data/portfolio.json | 86 ++++++------ data/price_history.json | 10 +- scripts/import_holding_xls.py | 108 +++++++-------- scripts/stale_detector.py | 244 ++++++++++++++++++++++++++++++++++ 5 files changed, 414 insertions(+), 158 deletions(-) create mode 100644 scripts/stale_detector.py diff --git a/data/decisions.json b/data/decisions.json index 525125b..25ce3ae 100644 --- a/data/decisions.json +++ b/data/decisions.json @@ -315,18 +315,18 @@ { "code": "000711", "name": "ST京蓝", - "price": 5.16, + "price": 5.0, "cost": 0, "shares": 0, "avg_price": 0, - "action": "盈利持有 | ⚠️盈亏比不足1:1.5,不建议买入 | 目标5.38 | 止损4.4 | 买入区5.06~5.11 | 信号:观望", - "stop_loss": 4.4, - "entry_low": 5.06, - "entry_high": 5.11, - "tech_snapshot": "形态:带下影阳线/neutral 量价:主动卖盘占优 强撑:4.86 弱撑:4.93 弱压:5.32 强压:5.38", + "action": "盈利持有 | 目标5.38 | 止损4.82 | 买入区4.9~5.04 | 信号:观望", + "stop_loss": 4.82, + "entry_low": 4.9, + "entry_high": 5.04, + "tech_snapshot": "形态:倒T线/射击之星/bearish 量价:主动卖盘占优 强撑:4.86 弱撑:4.82 弱压:5.21 强压:5.38", "timing_signal": "观望", - "rr_ratio": 0.96, - "status": "review", + "rr_ratio": 2.11, + "status": "updated", "note": "⚠️盈亏比不足1:1.5,不建议买入", "timestamp": "2026-06-24 11:16", "updated_at": "2026-06-24 11:16", @@ -430,7 +430,7 @@ { "id": "000711_stop_loss", "condition": { - "price": "<4.4" + "price": "<4.82" }, "action": { "type": "sell", @@ -447,13 +447,13 @@ "id": "000711_buy_dip", "condition": { "scenario": "weak_consolidation", - "price": "<=5.11", - "price_lower": ">=5.06" + "price": "<=5.04", + "price_lower": ">=4.9" }, "action": { "type": "buy", "amount": "normal", - "limit": 5.06, + "limit": 4.9, "reason": "回调支撑买入" }, "priority": 1, @@ -743,17 +743,17 @@ { "code": "002594", "name": "比亚迪", - "price": 82.82, + "price": 82.79, "cost": 0, "shares": 0, "avg_price": 0, - "action": "盈利持有 | 目标92.55 | 止损80.34 | 买入区81.16~84.48 | 信号:观望", - "stop_loss": 80.34, - "entry_low": 81.16, - "entry_high": 84.48, - "tech_snapshot": "形态:光头光脚阴线/bearish 量价:主动卖盘占优 强撑:76.5 弱撑:82.17 弱压:85.0 强压:92.55", + "action": "盈利持有 | 目标92.54 | 止损80.31 | 买入区81.13~84.45 | 信号:观望", + "stop_loss": 80.31, + "entry_low": 81.13, + "entry_high": 84.45, + "tech_snapshot": "形态:光头光脚阴线/bearish 量价:主动卖盘占优 强撑:76.5 弱撑:82.15 弱压:85.0 强压:92.54", "timing_signal": "观望", - "rr_ratio": 3.92, + "rr_ratio": 3.93, "status": "updated", "note": "", "timestamp": "2026-06-24 11:16", @@ -765,7 +765,7 @@ "position_advice": "减仓或观望", "time_horizon": "观望", "created_at": "2026-06-18 17:15", - "take_profit": 92.55, + "take_profit": 92.54, "updated_reason": "技术面重评: 止损77.92→77.93 | 形态:光头光脚阴线/bearish 量价:主动卖盘占优 强撑:76.5 弱撑:82.17 弱压:85.0 强压:92.5", "changelog": [ { @@ -865,7 +865,7 @@ { "id": "002594_stop_loss", "condition": { - "price": "<80.34" + "price": "<80.31" }, "action": { "type": "sell", @@ -882,13 +882,13 @@ "id": "002594_buy_dip", "condition": { "scenario": "weak_consolidation", - "price": "<=84.48", - "price_lower": ">=81.16" + "price": "<=84.45", + "price_lower": ">=81.13" }, "action": { "type": "buy", "amount": "normal", - "limit": 81.16, + "limit": 81.13, "reason": "回调支撑买入" }, "priority": 1, @@ -901,7 +901,7 @@ "id": "002594_breakout_chase", "condition": { "scenario": "bullish_recovery", - "price": ">=92.55" + "price": ">=92.54" }, "action": { "type": "buy", @@ -935,7 +935,7 @@ { "id": "002594_take_profit", "condition": { - "price": ">=92.55" + "price": ">=92.54" }, "action": { "type": "sell", @@ -5192,17 +5192,17 @@ { "code": "300124", "name": "汇川技术", - "price": 65.87, + "price": 66.01, "cost": 0, "shares": 0, "avg_price": 0, - "action": "盈利持有 | 目标72.99 | 止损63.89 | 买入区64.55~67.19 | 信号:观望", - "stop_loss": 63.89, - "entry_low": 64.55, - "entry_high": 67.19, - "tech_snapshot": "形态:带上影阴线/bearish 量价:主动卖盘占优 强撑:59.75 弱撑:65.24 弱压:66.99 强压:72.99", + "action": "盈利持有 | 目标73.03 | 止损64.03 | 买入区64.69~67.33 | 信号:观望", + "stop_loss": 64.03, + "entry_low": 64.69, + "entry_high": 67.33, + "tech_snapshot": "形态:带上影阴线/bearish 量价:主动卖盘占优 强撑:59.79 弱撑:65.34 弱压:67.09 强压:73.03", "timing_signal": "观望", - "rr_ratio": 3.6, + "rr_ratio": 3.55, "status": "updated", "note": "", "timestamp": "2026-06-24 11:17", @@ -5214,7 +5214,7 @@ "position_advice": "减仓或观望", "time_horizon": "观望", "created_at": "2026-06-18 17:15", - "take_profit": 72.99, + "take_profit": 73.03, "updated_reason": "技术面重评: 止损61.97→62.13 | 形态:带上影阴线/bearish 量价:主动卖盘占优 强撑:59.78 弱撑:65.32 弱压:67.07 强压:73.", "changelog": [ { @@ -5307,7 +5307,7 @@ { "id": "300124_stop_loss", "condition": { - "price": "<63.89" + "price": "<64.03" }, "action": { "type": "sell", @@ -5324,13 +5324,13 @@ "id": "300124_buy_dip", "condition": { "scenario": "weak_consolidation", - "price": "<=67.19", - "price_lower": ">=64.55" + "price": "<=67.33", + "price_lower": ">=64.69" }, "action": { "type": "buy", "amount": "normal", - "limit": 64.55, + "limit": 64.69, "reason": "回调支撑买入" }, "priority": 1, @@ -5343,7 +5343,7 @@ "id": "300124_breakout_chase", "condition": { "scenario": "bullish_recovery", - "price": ">=72.99" + "price": ">=73.03" }, "action": { "type": "buy", @@ -5377,7 +5377,7 @@ { "id": "300124_take_profit", "condition": { - "price": ">=72.99" + "price": ">=73.03" }, "action": { "type": "sell", @@ -7264,11 +7264,11 @@ "cost": 0, "shares": 0, "avg_price": 0, - "action": "盈利持有 | 目标53.71 | 止损47.9 | 买入区48.39~50.22 | 信号:观望", + "action": "盈利持有 | 目标53.72 | 止损47.9 | 买入区48.39~50.23 | 信号:观望", "stop_loss": 47.9, "entry_low": 48.39, - "entry_high": 50.22, - "tech_snapshot": "形态:光头光脚阴线/bearish 量价:主动卖盘占优 强撑:45.81 弱撑:48.65 弱压:50.5 强压:53.71", + "entry_high": 50.23, + "tech_snapshot": "形态:光头光脚阴线/bearish 量价:主动卖盘占优 强撑:45.82 弱撑:48.65 弱压:50.5 强压:53.72", "timing_signal": "观望", "rr_ratio": 2.93, "status": "updated", @@ -7282,7 +7282,7 @@ "position_advice": "减仓或观望", "time_horizon": "观望", "created_at": "2026-06-22 11:50", - "take_profit": 53.71, + "take_profit": 53.72, "changelog": [ { "date": "2026-06-22 12:01", @@ -7335,7 +7335,7 @@ "id": "601318_buy_dip", "condition": { "scenario": "weak_consolidation", - "price": "<=50.22", + "price": "<=50.23", "price_lower": ">=48.39" }, "action": { @@ -7354,7 +7354,7 @@ "id": "601318_breakout_chase", "condition": { "scenario": "bullish_recovery", - "price": ">=53.71" + "price": ">=53.72" }, "action": { "type": "buy", @@ -7388,7 +7388,7 @@ { "id": "601318_take_profit", "condition": { - "price": ">=53.71" + "price": ">=53.72" }, "action": { "type": "sell", @@ -8426,17 +8426,17 @@ { "code": "688795", "name": "摩尔线程-U", - "price": 678.5, + "price": 679.95, "cost": 0, "shares": 0, "avg_price": 0, - "action": "盈利持有 | 目标816.82 | 止损658.14 | 买入区664.93~692.07 | 信号:观望", - "stop_loss": 658.14, - "entry_low": 664.93, - "entry_high": 692.07, - "tech_snapshot": "形态:带下影阴线/neutral 量价:主动卖盘占优 强撑:563.22 弱撑:665.33 弱压:704.03 强压:816.82", + "action": "盈利持有 | 目标817.3 | 止损659.55 | 买入区666.35~693.55 | 信号:观望", + "stop_loss": 659.55, + "entry_low": 666.35, + "entry_high": 693.55, + "tech_snapshot": "形态:带下影阴线/neutral 量价:主动卖盘占优 强撑:563.22 弱撑:666.3 弱压:704.03 强压:817.3", "timing_signal": "观望", - "rr_ratio": 6.79, + "rr_ratio": 6.73, "status": "updated", "note": "", "timestamp": "2026-06-24 11:17", @@ -8448,7 +8448,7 @@ "position_advice": "正常配置", "time_horizon": "数月~1年", "created_at": "2026-06-18 17:15", - "take_profit": 816.82, + "take_profit": 817.3, "updated_reason": "技术面重评: 止损635.13→637.94 | 形态:光头光脚阴线/bearish 量价:主动卖盘占优 强撑:563.22 弱撑:665.0 弱压:704.03 强压:", "changelog": [ { @@ -8541,7 +8541,7 @@ { "id": "688795_stop_loss", "condition": { - "price": "<658.14" + "price": "<659.55" }, "action": { "type": "sell", @@ -8558,13 +8558,13 @@ "id": "688795_buy_dip", "condition": { "scenario": "weak_consolidation", - "price": "<=692.07", - "price_lower": ">=664.93" + "price": "<=693.55", + "price_lower": ">=666.35" }, "action": { "type": "buy", "amount": "normal", - "limit": 664.93, + "limit": 666.35, "reason": "回调支撑买入" }, "priority": 1, @@ -8577,7 +8577,7 @@ "id": "688795_breakout_chase", "condition": { "scenario": "bullish_recovery", - "price": ">=816.82" + "price": ">=817.3" }, "action": { "type": "buy", @@ -8611,7 +8611,7 @@ { "id": "688795_take_profit", "condition": { - "price": ">=816.82" + "price": ">=817.3" }, "action": { "type": "sell", @@ -9065,5 +9065,5 @@ } ], "total": 42, - "regenerated_at": "2026-06-24 11:19" + "regenerated_at": "2026-06-24 11:21" } \ No newline at end of file diff --git a/data/portfolio.json b/data/portfolio.json index 2209d23..317443d 100644 --- a/data/portfolio.json +++ b/data/portfolio.json @@ -5,21 +5,22 @@ "name": "德明利", "shares": 100, "avail_shares": 100, - "price": 790.0, + "price": 790.9, "cost_price": 737.0374, "pl": 5096.26, "pl_pct": 6.91, "currency": "CNY", "market_val": 78800.0, "cost_amount": 73703.74, - "exchange_rate": 0.866 + "exchange_rate": 0.866, + "change_pct": 4.75 }, { "code": "01478", "name": "丘钛科技", "shares": 11000, "avail_shares": 11000, - "price": 7.57, + "price": 7.54, "cost_price": 13.8089, "pl": -59438.6, "pl_pct": -45.18, @@ -27,14 +28,14 @@ "market_val": 72120.15, "cost_amount": 131558.75, "exchange_rate": 0.8661, - "change_pct": -3.93 + "change_pct": -4.19 }, { "code": "600739", "name": "辽宁成大", "shares": 6600, "avail_shares": 6600, - "price": 10.44, + "price": 10.45, "cost_price": 12.2862, "pl": -11789.02, "pl_pct": -14.54, @@ -42,14 +43,14 @@ "market_val": 69300.0, "cost_amount": 81089.02, "exchange_rate": 0.866, - "change_pct": -1.97 + "change_pct": -1.88 }, { "code": "601899", "name": "紫金矿业", "shares": 2400, "avail_shares": 2400, - "price": 27.38, + "price": 27.36, "cost_price": 40.2685, "pl": -30572.29, "pl_pct": -31.63, @@ -63,21 +64,22 @@ "name": "华恒生物", "shares": 2800, "avail_shares": 0, - "price": 21.31, + "price": 21.35, "cost_price": 21.5085, "pl": -135.75, "pl_pct": -0.23, "currency": "CNY", "market_val": 60088.0, "cost_amount": 60223.75, - "exchange_rate": 0.866 + "exchange_rate": 0.866, + "change_pct": -2.2 }, { "code": "09988", "name": "阿里巴巴-W", "shares": 700, "avail_shares": 700, - "price": 98.55, + "price": 98.6, "cost_price": 126.1516, "pl": -16582.48, "pl_pct": -21.68, @@ -85,7 +87,7 @@ "market_val": 59899.48, "cost_amount": 76481.96, "exchange_rate": 0.8661, - "change_pct": -0.4 + "change_pct": -0.35 }, { "code": "603259", @@ -106,7 +108,7 @@ "name": "中芯国际", "shares": 300, "avail_shares": 300, - "price": 149.89, + "price": 150.64, "cost_price": 126.0681, "pl": 7173.58, "pl_pct": 18.97, @@ -114,7 +116,7 @@ "market_val": 44994.0, "cost_amount": 37820.42, "exchange_rate": 0.866, - "change_pct": 5.78 + "change_pct": 6.31 }, { "code": "01888", @@ -150,7 +152,7 @@ "name": "中银香港", "shares": 1000, "avail_shares": 1000, - "price": 46.22, + "price": 46.26, "cost_price": 43.7892, "pl": 2053.39, "pl_pct": 5.41, @@ -158,14 +160,14 @@ "market_val": 39979.18, "cost_amount": 37925.79, "exchange_rate": 0.8661, - "change_pct": -1.66 + "change_pct": -1.62 }, { "code": "300750", "name": "宁德时代", "shares": 100, "avail_shares": 100, - "price": 391.09, + "price": 391.19, "cost_price": 401.7803, "pl": -1063.03, "pl_pct": -2.65, @@ -173,21 +175,22 @@ "market_val": 39115.0, "cost_amount": 40178.03, "exchange_rate": 0.866, - "change_pct": -0.36 + "change_pct": -0.34 }, { "code": "01211", "name": "比亚迪股份", "shares": 600, "avail_shares": 600, - "price": 75.0, + "price": 74.85, "cost_price": 105.0542, "pl": -15539.97, "pl_pct": -28.47, "currency": "HKD", "market_val": 39052.45, "cost_amount": 54592.42, - "exchange_rate": 0.8661 + "exchange_rate": 0.8661, + "change_pct": -1.25 }, { "code": "00700", @@ -209,7 +212,7 @@ "name": "中芯国际", "shares": 500, "avail_shares": 500, - "price": 83.15, + "price": 83.25, "cost_price": 76.0724, "pl": 3151.55, "pl_pct": 9.57, @@ -217,28 +220,29 @@ "market_val": 36094.72, "cost_amount": 32943.17, "exchange_rate": 0.8661, - "change_pct": 6.81 + "change_pct": 6.62 }, { "code": "09868", "name": "小鹏集团-W", "shares": 700, "avail_shares": 700, - "price": 50.2, + "price": 50.1, "cost_price": 51.3644, "pl": -675.62, "pl_pct": -2.17, "currency": "HKD", "market_val": 30465.07, "cost_amount": 31140.69, - "exchange_rate": 0.8661 + "exchange_rate": 0.8661, + "change_pct": 1.5 }, { "code": "600036", "name": "招商银行", "shares": 800, "avail_shares": 800, - "price": 37.06, + "price": 37.05, "cost_price": 38.1582, "pl": -846.53, "pl_pct": -2.77, @@ -246,14 +250,14 @@ "market_val": 29680.0, "cost_amount": 30526.53, "exchange_rate": 0.866, - "change_pct": -0.91 + "change_pct": -0.94 }, { "code": "300548", "name": "长芯博创", "shares": 100, "avail_shares": 100, - "price": 276.12, + "price": 276.79, "cost_price": 231.46, "pl": 4766.0, "pl_pct": 20.59, @@ -261,14 +265,14 @@ "market_val": 27912.0, "cost_amount": 23146.0, "exchange_rate": 0.866, - "change_pct": -3.46 + "change_pct": -3.23 }, { "code": "02318", "name": "中国平安", "shares": 500, "avail_shares": 500, - "price": 52.7, + "price": 52.8, "cost_price": 54.8199, "pl": -896.35, "pl_pct": -3.78, @@ -276,7 +280,7 @@ "market_val": 22843.39, "cost_amount": 23739.74, "exchange_rate": 0.8661, - "change_pct": -1.68 + "change_pct": -1.49 }, { "code": "300035", @@ -290,14 +294,15 @@ "currency": "CNY", "market_val": 22442.0, "cost_amount": 31207.91, - "exchange_rate": 0.866 + "exchange_rate": 0.866, + "change_pct": -2.08 }, { "code": "000700", "name": "模塑科技", "shares": 1400, "avail_shares": 1400, - "price": 14.53, + "price": 14.52, "cost_price": 14.8336, "pl": -327.0, "pl_pct": -1.57, @@ -305,14 +310,14 @@ "market_val": 20440.0, "cost_amount": 20767.0, "exchange_rate": 0.866, - "change_pct": -3.0 + "change_pct": -3.07 }, { "code": "518880", "name": "黄金ETF华安", "shares": 2400, "avail_shares": 2400, - "price": 8.43, + "price": 8.44, "cost_price": 12.1915, "pl": -8982.04, "pl_pct": -30.7, @@ -320,21 +325,22 @@ "market_val": 20277.6, "cost_amount": 29259.64, "exchange_rate": 0.866, - "change_pct": -1.21 + "change_pct": -1.16 }, { "code": "01088", "name": "中国神华", "shares": 500, "avail_shares": 500, - "price": 41.92, + "price": 41.84, "cost_price": 45.974, "pl": -1764.27, "pl_pct": -8.86, "currency": "HKD", "market_val": 18144.8, "cost_amount": 19909.06, - "exchange_rate": 0.8661 + "exchange_rate": 0.8661, + "change_pct": -0.52 }, { "code": "600563", @@ -355,14 +361,15 @@ "name": "双一科技", "shares": 400, "avail_shares": 400, - "price": 22.7, + "price": 22.68, "cost_price": 27.178, "pl": -1759.2, "pl_pct": -16.18, "currency": "CNY", "market_val": 9112.0, "cost_amount": 10871.2, - "exchange_rate": 0.866 + "exchange_rate": 0.866, + "change_pct": -3.98 } ], "cash": 80476, @@ -370,5 +377,6 @@ "total_pl": -164810.3, "position_pct": 92.0, "updated_at": "2026-06-24 11:47", - "source": "/home/hmo/stocks/holding.xls" + "source": "/home/hmo/stocks/holding.xls", + "total_assets": 1010675.93 } \ No newline at end of file diff --git a/data/price_history.json b/data/price_history.json index 4ba63ed..3c854b1 100644 --- a/data/price_history.json +++ b/data/price_history.json @@ -108,7 +108,7 @@ "date": "2026-06-24", "high": 88.32, "low": 82.78, - "close": 82.82 + "close": 82.79 } ], "00700": [ @@ -1284,7 +1284,7 @@ "date": "2026-06-24", "high": 68.8, "low": 65.74, - "close": 65.87 + "close": 66.01 } ], "300548": [ @@ -2316,7 +2316,7 @@ "date": "2026-06-24", "high": 745.2, "low": 660.01, - "close": 678.5 + "close": 679.95 } ], "688802": [ @@ -2466,7 +2466,7 @@ "date": "2026-06-24", "high": 5.38, "low": 4.86, - "close": 5.16 + "close": 5.0 } ], "688630": [ @@ -2610,7 +2610,7 @@ "date": "2026-06-24", "high": 52.43, "low": 49.03, - "close": 49.38 + "close": 49.39 } ], "002171": [ diff --git a/scripts/import_holding_xls.py b/scripts/import_holding_xls.py index a7257c9..66556b4 100755 --- a/scripts/import_holding_xls.py +++ b/scripts/import_holding_xls.py @@ -1,19 +1,16 @@ #!/usr/bin/env python3 """ -import_holding_xls.py — 从券商导出文件 holding.xls 导入持仓数据 +import_holding_xls.py — 从holding.xls导入持仓到SQLite + portfolio.json -读取 /home/hmo/stocks/holding.xls(TSV格式,GBK编码) -写入 /home/hmo/web-dashboard/data/portfolio.json -触发 per_stock_reassess 全量重评(更新 decisions.json + 决策树) - -用法:python3 import_holding_xls.py +用法:python3 import_holding_xls.py [--cash 现金金额] """ -import csv, json, sys, subprocess +import csv, json, sys, subprocess, sqlite3 from datetime import datetime STOCKS_FILE = "/home/hmo/stocks/holding.xls" PORTFOLIO_PATH = "/home/hmo/web-dashboard/data/portfolio.json" -CASH = 80476 # 现金余额(手动更新或从其他源读取) +DB_PATH = "/home/hmo/web-dashboard/data/mofin.db" +CASH = 80476 # default cash def clean_cell(v): @@ -26,6 +23,12 @@ def clean_cell(v): def main(): + # Parse args + global CASH + for i, a in enumerate(sys.argv[1:]): + if a == '--cash' and i + 2 < len(sys.argv): + CASH = float(sys.argv[i + 2]) + with open(STOCKS_FILE, 'r', encoding='gbk') as f: reader = csv.reader(f, delimiter='\t') rows = list(reader) @@ -34,73 +37,81 @@ def main(): holdings = [] total_mv_cny = 0 - total_pl = 0 for r in rows[1:]: code = clean_cell(r[0]) name = r[1].strip() shares = int(clean_cell(r[2])) - avail = int(clean_cell(r[3])) - price_raw = r[4].strip() currency = 'HKD' if '港币' in price_raw or '港' in r[10] else 'CNY' price_str = price_raw.replace('港币', '').replace('港元', '').replace('港', '').strip() price = float(price_str) - cost_price = float(clean_cell(r[5])) pl = float(clean_cell(r[6])) pl_pct = float(clean_cell(r[7])) if r[7].strip() else 0.0 mkt_val = float(clean_cell(r[11])) cost_amount = float(clean_cell(r[15])) if r[15].strip() and r[15].strip() != '--' else 0 - rate_str = clean_cell(r[16]) rate = float(rate_str) if rate_str and rate_str != '--' else 0.866 - holdings.append({ - 'code': code, - 'name': name, - 'shares': shares, - 'avail_shares': avail, - 'price': price, - 'cost_price': cost_price, - 'pl': pl, - 'pl_pct': pl_pct, - 'currency': currency, - 'market_val': mkt_val, - 'cost_amount': cost_amount, - 'exchange_rate': rate, - }) - - total_pl += pl mv_cny = mkt_val if currency == 'CNY' else mkt_val * rate total_mv_cny += mv_cny + holdings.append({ + 'code': code, 'name': name, 'shares': shares, + 'price': price, 'cost_price': round(cost_price, 2), + 'currency': currency, 'market_val': mkt_val, + 'cost_amount': cost_amount, 'exchange_rate': rate, + }) + pfx = 'HK$' if currency == 'HKD' else '¥' - print(f" {code} {name} {shares}股 {pfx}{price:.2f} 盈亏{pl:+,.0f}({pl_pct:+.1f}%)") + print(f" {code} {name} {shares}股 {pfx}{price:.2f} 成本{cost_price:.2f} 盈亏{pl:+,.0f}({pl_pct:+.1f}%)") total_assets = total_mv_cny + CASH - position_pct = round(total_mv_cny / total_assets * 100, 1) if total_assets > 0 else 0 + position_pct = round(total_mv_cny / total_assets * 100, 2) if total_assets > 0 else 0 + # Write portfolio.json portfolio = { 'holdings': holdings, 'cash': CASH, 'total_market_value': round(total_mv_cny, 2), - 'total_pl': round(total_pl, 2), + 'total_assets': round(total_assets, 2), + 'total_pl': 0, 'position_pct': position_pct, '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) + print(f" → {PORTFOLIO_PATH}") - print(f"\n已写入 {len(holdings)} 只持仓") - print(f"A股: {sum(1 for h in holdings if h['currency']=='CNY')} | 港股: {sum(1 for h in holdings if h['currency']=='HKD')}") - print(f"总市值: {round(total_mv_cny):,.0f}元 | 现金: {CASH:,}元") - print(f"总资产: {round(total_assets):,.0f}元 | 仓位: {position_pct}%") - print(f"累计盈亏: {round(total_pl):+,}元") + # Write SQLite + conn = sqlite3.connect(DB_PATH) + c = conn.cursor() + c.execute('DELETE FROM holdings') + c.execute('DELETE FROM portfolio_summary') + for h in holdings: + pos_pct = round(h['market_val'] / total_assets * 100, 2) if total_assets > 0 else 0 + c.execute(''' + INSERT INTO holdings (code, name, shares, cost, position_pct, added_at, is_active) + VALUES (?, ?, ?, ?, ?, ?, 1) + ''', (h['code'], h['name'], h['shares'], h['cost_price'], pos_pct, + datetime.now().strftime('%Y-%m-%d'))) + c.execute(''' + INSERT INTO portfolio_summary (total_assets, stock_value, cash, position_pct, total_pnl, updated_at) + VALUES (?, ?, ?, ?, ?, ?) + ''', (round(total_assets, 2), round(total_mv_cny, 2), CASH, position_pct, 0, + datetime.now().strftime('%Y-%m-%d %H:%M'))) + conn.commit() + conn.close() + print(f" → SQLite 已更新") - # 触发策略重评 + print(f"\n统计: {len(holdings)}只持仓 | " + f"总资产{round(total_assets):,.0f}元 | " + f"现金{CASH:,.0f}元 | " + f"仓位{position_pct}%") + + # Trigger full reassessment print("\n→ 触发 per_stock_reassess 全量重评...") r = subprocess.run( ["python3", "/home/hmo/.hermes/profiles/position-analyst/scripts/per_stock_reassess.py"], @@ -108,7 +119,7 @@ def main(): ) print(r.stdout[-500:] if len(r.stdout) > 500 else r.stdout) - # 重建决策树 + # Rebuild decision trees print("\n→ 重建决策树...") sys.path.insert(0, '/home/hmo/web-dashboard') from strategy_tree import init_default_branches @@ -117,21 +128,14 @@ def main(): ok = 0 for e in data.get('decisions', []): branches = init_default_branches( - e.get('code', ''), - e.get('name', ''), - e.get('entry_low', 0), - e.get('entry_high', 0), - e.get('stop_loss', 0), - e.get('take_profit', 0), - ) - e['strategy_tree'] = { - 'branches': branches, - 'created_at': datetime.now().strftime('%Y-%m-%d'), - } + e.get('code', ''), e.get('name', ''), + e.get('entry_low', 0), e.get('entry_high', 0), + 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) - print(f"决策树重建完成: {ok}/{len(data.get('decisions',[]))}") + print(f"决策树重建: {ok}/{len(data.get('decisions',[]))}") if __name__ == '__main__': diff --git a/scripts/stale_detector.py b/scripts/stale_detector.py new file mode 100644 index 0000000..e624a83 --- /dev/null +++ b/scripts/stale_detector.py @@ -0,0 +1,244 @@ +#!/usr/bin/env python3 +"""stale_detector.py — 检查所有策略,标记价格偏离/过期的策略 + +读取 decisions.json 的扁平列表。自选策略和持仓策略分开判断。 +可被 cron no_agent 模式调用:stdout 注入到后续 LLM 分析。 + +输出格式: + [FLAG] [自选/持仓] 股票名(代码) 价XX | 买入A~B | 问题 + +用法: + python3 stale_detector.py +""" +import json +import sys +import os +from datetime import datetime, timezone + +DECISIONS_PATH = "/home/hmo/web-dashboard/data/decisions.json" +PORTFOLIO_PATH = "/home/hmo/web-dashboard/data/portfolio.json" + + +def fetch_prices(codes): + import urllib.request + if not codes: + return {} + symbols, code_map = [], {} + for c in codes: + c = str(c).strip() + p = "sh" if (len(c) == 6 and c[0] in "569") else "sz" if len(c) == 6 else "hk" + sym = f"{p}{c}" + symbols.append(sym) + code_map[sym] = c + try: + req = urllib.request.Request( + f"http://qt.gtimg.cn/q={','.join(symbols)}", + headers={"User-Agent": "curl/7.81"}, + ) + with urllib.request.urlopen(req, timeout=10) as r: + text = r.read().decode("gbk") + except Exception as e: + print(f"FETCH_FAIL: {e}", file=sys.stderr) + return {} + + results = {} + for line in text.strip().split("\n"): + if "=" not in line: + continue + try: + raw = line.split("=", 1)[1].strip().strip('"').strip(";") + fld = raw.split("~") + if len(fld) < 6: + continue + sym = line.split("=", 1)[0].strip().lstrip("v_") + oc = code_map.get(sym) + if not oc: + continue + p = float(fld[3]) if fld[3] else 0 + c = fld[32] if len(fld) > 32 else "0" + results[oc] = (p, c) + except (ValueError, IndexError): + continue + return results + + +def main(): + decisions_list = json.load(open(DECISIONS_PATH)) + if not isinstance(decisions_list, list): + decisions_list = decisions_list.get("decisions", []) if isinstance(decisions_list, dict) else [] + + # 只保留有买入区的条目,排除已关闭的(inactive/closed) + EXCLUDED_STATUSES = ("closed", "inactive") + to_check = [d for d in decisions_list if (d.get("entry_low") is not None or d.get("entry_high") is not None) and d.get("status") not in EXCLUDED_STATUSES] + if not to_check: + print("[SILENT] 无需要检查的策略") + return 0 + + # ----- 组合级监测:读取总仓位 + 弱势比例 ----- + position_pct = 0 + cash = 0 + total_assets = 0 + try: + with open(PORTFOLIO_PATH) as f: + pf = json.load(f) + position_pct = pf.get("position_pct", 0) + cash = pf.get("cash", 0) + total_assets = pf.get("total_assets", 0) + except Exception: + pass + # 统计持仓策略中弱势/深套的比例 + weak_count = 0 + holding_count = 0 + for d in decisions_list: + if d.get("type") == "持仓策略" and d.get("status") not in ("closed", "inactive"): + holding_count += 1 + cat = d.get("stock_category", "") + if cat in ("弱势", "深套"): + weak_count += 1 + weak_ratio = (weak_count / holding_count * 100) if holding_count > 0 else 0 + + prices = fetch_prices([d["code"] for d in to_check]) + now = datetime.now(timezone.utc).astimezone() + found = 0 + + for d in to_check: + code = d["code"] + name = d.get("name", code) + el = d.get("entry_low") + eh = d.get("entry_high") + sl = d.get("stop_loss") + tp = d.get("take_profit") + ts = d.get("created_at") or d.get("timestamp") or d.get("updated_at", "") + is_wl = "自选" in (d.get("type", "")) + + pi = prices.get(code) + if not pi: + continue + price, chg = pi + if price <= 0: + continue + + issues, flags = [], [] + tag = "[自选]" if is_wl else "[持仓]" + + # -- 偏离 -- + if is_wl and el and eh: + # 读取 timing_signal 判断策略有效性(timing_signal 字段优先,fallback to action) + current_str = d.get("current", "") or "" + timing_signal = d.get("timing_signal", "") or current_str + has_nonbuy_signal = any(kw in timing_signal for kw in [ + "等企稳再入", "等企稳", "弱势持有", "观望", + "不建议买入", "谨慎买入", + ]) + + # 直接计算 R/R(不依赖文本匹配) + rr_invalid = False + if sl and sl > 0 and tp and tp > 0 and price > sl: + rr = (tp - price) / (price - sl) + if rr < 1.5: + rr_invalid = True + # 也检查 tp 是否接近或低于成本(微盈/浮亏止盈) + cost = d.get("cost", 0) + if cost and cost > 0 and tp <= cost * 1.05: + rr_invalid = True + + strategy_deficient = has_nonbuy_signal or rr_invalid + # 对自选无止盈位的也标记(策略不完整) + if not tp or tp == 0: + strategy_deficient = True + + if el <= price <= eh: + flags.append("[WL_IN]") + if strategy_deficient: + flags.append("[STRATEGY_STALE]") + prefix = "⚠️仓位挤占 " if position_pct > 80 else "" + issues.append(f"[STRATEGY_STALE] {prefix}价{price:.2f}在买入区{el}~{eh}但策略不完整({'RR='+f'{rr:.2f}<1.5' if rr_invalid else '无止盈位' if not tp else '非买入信号'}),买入区需重评") + else: + prefix = "⚠️仓位挤占 " if position_pct > 80 else "" + issues.append(f"[PUSH] {prefix}价{price:.2f}入买入区{el}~{eh}") + elif price > eh * 1.35: + flags.append("[WL_HIGH]") + issues.append(f"价{price:.2f}高出买入区+{((price/eh)-1)*100:.0f}%,买入区需重评") + elif price > eh * 1.20: + flags.append("[WL_DRIFT]") + issues.append(f"价{price:.2f}高于买入区+{((price/eh)-1)*100:.0f}%") + elif not is_wl and eh: + dp = (price / eh - 1) * 100 + if dp > 35: + flags.append("[SEVERE]") + issues.append(f"偏离买入区上沿+{dp:.0f}%") + elif dp > 20: + flags.append("[DRIFT]") + issues.append(f"偏离买入区上沿+{dp:.0f}%") + elif dp > 10: + flags.append("[WARN]") + issues.append(f"偏离买入区上沿+{dp:.0f}%") + # 持仓在买入区内但 R/R 不达标 + if el and sl and sl > 0 and tp and tp > 0 and price > sl: + if el <= price <= eh: + rr = (tp - price) / (price - sl) + if rr < 1.5: + flags.append("[RR_WARN]") + issues.append(f"买入区内RR仅{rr:.2f}<1.5,策略需重评") + + # -- 距止损/止盈(仅持仓) -- + if not is_wl: + if sl and sl > 0: + dsl = (price / sl - 1) * 100 + if dsl < 5: + flags.append("[NEAR_SL]") + issues.append(f"距止损仅{dsl:.1f}%") + if tp and tp > 0: + dtp = (tp / price - 1) * 100 + if dtp < 5: + # 成本基准校验:止盈标记只有在盈利≥5%时才有效 + cost_check = True + cost = d.get("cost") + if cost and cost > 0 and price < cost * 1.05: + cost_check = False + if cost_check: + flags.append("[NEAR_TP]") + issues.append(f"距止盈仅{dtp:.1f}%") + + # -- 过期 -- + stale_limit = 30 if is_wl else 14 + if ts: + try: + ud = datetime.fromisoformat(ts) + if ud.tzinfo is None: + ud = ud.replace(tzinfo=timezone.utc) + days = (now - ud).days + if days > stale_limit: + flags.append("[STALE]") + issues.append(f"{days}天未更新(>{stale_limit})") + except (ValueError, TypeError): + pass + + if issues: + print(f"{' '.join(flags)} {tag} {name}({code}) 价{price:.2f}{chg} | 买入{el}~{eh} | {'; '.join(issues)}") + found += 1 + + if found == 0: + print("[SILENT] 所有策略正常") + + # ----- 组合级警报 ----- + portfolio_alerts = 0 + if holding_count > 0: + if weak_ratio > 40: + print(f"\n[PORTFOLIO_WEAK] 组合弱势比例{weak_ratio:.0f}% ({weak_count}/{holding_count})!仓位{position_pct:.1f}% → 建议系统性减仓") + portfolio_alerts += 1 + elif weak_ratio > 30: + print(f"\n[PORTFOLIO_WEAK_MILD] 组合弱势比例{weak_ratio:.0f}% ({weak_count}/{holding_count}),仓位{position_pct:.1f}%,关注") + portfolio_alerts += 1 + if position_pct > 80 and holding_count > 0: + # 仓位过满提醒 + print(f"[PORTFOLIO_FULL] 总仓位{position_pct:.1f}% > 80%,现金{cash:.0f}({cash/total_assets*100:.1f}%)") + portfolio_alerts += 1 + if portfolio_alerts > 0: + found += portfolio_alerts + + return found + + +if __name__ == "__main__": + main()