migrate: last 4 JSON files — live_prices, market, mtf_cache, capital_flow → DB

This commit is contained in:
知微
2026-07-03 13:44:10 +08:00
parent 7cc0ea0ef3
commit bb9b3922c9
27 changed files with 2064 additions and 1256 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
+35
View File
@@ -0,0 +1,35 @@
import sys; sys.path.insert(0, '/home/hmo/MoFin')
from mo_data import *
# Portfolio
pf = read_portfolio()
ta = pf.get('total_assets',0)
ca = pf.get('cash',0)
pp = pf.get('position_pct',0)
print(f"总资产: {ta:.0f} 现金: {ca:.0f} 仓位: {pp:.1f}%")
for h in pf.get('holdings', []):
c = h.get('currency','CNY')
price = h['price']
cost = h['cost']
profit_pct = (price/cost - 1)*100 if cost and cost else 0
ps = f"{price:.2f}{' HKD' if c=='HKD' else ''}"
pp_h = h.get('position_pct')
if pp_h is None: pp_h = 0
print(f" {h['code']} {h['name']}{ps}{pp_h:.1f}% 盈{profit_pct:.1f}%")
# Watchlist
print()
wl = read_watchlist()
for s in wl.get('stocks',[]):
try:
c = s.get('currency','CNY')
price = s.get('price')
if price is None: price = 0
ps = f"{price:.2f}{' HKD' if c=='HKD' else ''}"
el = s.get('entry_low')
eh = s.get('entry_high')
enl = f"{el:.2f}" if el is not None else '?'
enh = f"{eh:.2f}" if eh is not None else '?'
print(f" 自选 {s['code']} {s.get('name','')}{ps}{enl}~{enh}")
except Exception as e:
print(f" ERROR {s.get('code','?')}: {e}")
+194 -2
View File
@@ -1,6 +1,6 @@
{
"last_updated": "2026-07-03 11:32",
"total_candidates": 5,
"last_updated": "2026-07-03 13:42",
"total_candidates": 11,
"sectors_analyzed_today": [
"半导体",
"金属新材料",
@@ -503,6 +503,198 @@
"drop_reason": null,
"trend_warning": false,
"trend_note": ""
},
{
"code": "300124",
"name": "汇川技术",
"sector": "自动化设备",
"xiaoguo_score": 8.5,
"xiaoguo_reason": "工控龙头,PLC/伺服/机器人核心部件全覆盖,受益设备更新与新能源自动化需求,业绩确定性强,机构持仓集中。",
"xiaoguo_strategy": {
"entry_range": "58.00-62.00元",
"stop_loss": "54.00元",
"target": "72.00-78.00元"
},
"verified_price": 72.94,
"verified_change": 6.64,
"added_at": "2026-07-03 13:37",
"last_updated": "2026-07-03 13:37",
"num_observations": 1,
"score_history": [
{
"date": "2026-07-03 13:37",
"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": "002747",
"name": "埃斯顿",
"sector": "自动化设备",
"xiaoguo_score": 7.5,
"xiaoguo_reason": "国产工业机器人第一梯队,核心零部件自研率高,政策扶持明确,但短期毛利率承压,适合波段操作。",
"xiaoguo_strategy": {
"entry_range": "18.00-20.00元",
"stop_loss": "16.50元",
"target": "24.00-26.00元"
},
"verified_price": 44.77,
"verified_change": 10.0,
"added_at": "2026-07-03 13:37",
"last_updated": "2026-07-03 13:37",
"num_observations": 1,
"score_history": [
{
"date": "2026-07-03 13:37",
"score": 7.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": "300607",
"name": "拓斯达",
"sector": "自动化设备",
"xiaoguo_score": 7.0,
"xiaoguo_reason": "自动化产线集成+机器人双轮驱动,市值适中弹性大,AI视觉与协作机器人布局进展快,但业绩波动性较高。",
"xiaoguo_strategy": {
"entry_range": "12.00-13.50元",
"stop_loss": "11.00元",
"target": "16.00-18.00元"
},
"verified_price": 52.92,
"verified_change": 13.46,
"added_at": "2026-07-03 13:37",
"last_updated": "2026-07-03 13:37",
"num_observations": 1,
"score_history": [
{
"date": "2026-07-03 13:37",
"score": 7.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": ""
},
{
"code": "601689",
"name": "拓普集团",
"sector": "汽车零部件",
"xiaoguo_score": 8.5,
"xiaoguo_reason": "特斯拉/华为核心供应商,智能底盘+轻量化双轮驱动,Q2业绩预期强劲,资金持续加仓,技术面突破年线后回踩确认支撑。",
"xiaoguo_strategy": {
"entry_range": "58.5-60.2",
"stop_loss": "56.0",
"target": "68.0-72.0"
},
"verified_price": 62.43,
"verified_change": 10.01,
"added_at": "2026-07-03 13:40",
"last_updated": "2026-07-03 13:40",
"num_observations": 1,
"score_history": [
{
"date": "2026-07-03 13:40",
"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": "002126",
"name": "银轮股份",
"sector": "汽车零部件",
"xiaoguo_score": 8.0,
"xiaoguo_reason": "热管理龙头,绑定比亚迪/理想/海外车企,订单饱满,估值合理,趋势稳健,机构持仓集中,适合中线波段。",
"xiaoguo_strategy": {
"entry_range": "24.8-25.5",
"stop_loss": "23.5",
"target": "29.0-31.0"
},
"verified_price": 53.39,
"verified_change": 5.76,
"added_at": "2026-07-03 13:40",
"last_updated": "2026-07-03 13:40",
"num_observations": 1,
"score_history": [
{
"date": "2026-07-03 13:40",
"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": ""
},
{
"code": "603596",
"name": "伯特利",
"sector": "汽车零部件",
"xiaoguo_score": 8.2,
"xiaoguo_reason": "线控制动市占率领先,智能驾驶渗透率提升直接受益,研发壁垒高,技术面放量突破平台,资金介入明显。",
"xiaoguo_strategy": {
"entry_range": "42.0-43.5",
"stop_loss": "40.0",
"target": "48.0-52.0"
},
"verified_price": 27.65,
"verified_change": 9.98,
"added_at": "2026-07-03 13:40",
"last_updated": "2026-07-03 13:40",
"num_observations": 1,
"score_history": [
{
"date": "2026-07-03 13:40",
"score": 8.2
}
],
"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": ""
}
]
}
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+270 -30
View File
@@ -960,6 +960,14 @@
"high": 151.69,
"low": 141.26,
"volume": 94902364.0
},
{
"date": "2026-07-03",
"open": 144.89,
"close": 143.0,
"high": 146.45,
"low": 140.4,
"volume": 56102965.0
}
],
"weekly": [
@@ -1254,7 +1262,7 @@
"volume": 147766189.0
}
],
"updated_at": 1783051747.536662
"updated_at": 1783056811.510267
},
"688795": {
"daily": [
@@ -2217,6 +2225,14 @@
"high": 687.04,
"low": 639.4,
"volume": 3085878.0
},
{
"date": "2026-07-03",
"open": 643.88,
"close": 649.03,
"high": 664.48,
"low": 633.01,
"volume": 1568495.0
}
],
"weekly": [
@@ -2479,7 +2495,7 @@
"volume": 4788252.0
}
],
"updated_at": 1783051740.058768
"updated_at": 1783056712.9785304
},
"000657": {
"daily": [
@@ -3442,6 +3458,14 @@
"high": 101.5,
"low": 90.68,
"volume": 1054773.0
},
{
"date": "2026-07-03",
"open": 95.0,
"close": 92.06,
"high": 96.53,
"low": 87.88,
"volume": 721833.0
}
],
"weekly": [
@@ -3736,7 +3760,7 @@
"volume": 1051508.0
}
],
"updated_at": 1783051278.4453125
"updated_at": 1783056502.3469434
},
"000700": {
"daily": [
@@ -4699,6 +4723,14 @@
"high": 16.87,
"low": 15.59,
"volume": 1137587.0
},
{
"date": "2026-07-03",
"open": 16.08,
"close": 17.7,
"high": 17.88,
"low": 16.08,
"volume": 1515920.0
}
],
"weekly": [
@@ -4993,7 +5025,7 @@
"volume": 1265397.0
}
],
"updated_at": 1783051627.982171
"updated_at": 1783056719.8422928
},
"000711": {
"daily": [
@@ -5956,6 +5988,14 @@
"high": 5.01,
"low": 4.87,
"volume": 443973.0
},
{
"date": "2026-07-03",
"open": 5.18,
"close": 5.26,
"high": 5.26,
"low": 5.1,
"volume": 1131235.0
}
],
"weekly": [
@@ -6250,7 +6290,7 @@
"volume": 496248.0
}
],
"updated_at": 1783051282.8847363
"updated_at": 1783056503.0905344
},
"001309": {
"daily": [
@@ -7213,6 +7253,14 @@
"high": 872.83,
"low": 806.0,
"volume": 160378.0
},
{
"date": "2026-07-03",
"open": 804.75,
"close": 888.1,
"high": 892.1,
"low": 795.0,
"volume": 144171.0
}
],
"weekly": [
@@ -7507,7 +7555,7 @@
"volume": 216663.0
}
],
"updated_at": 1783051286.5401492
"updated_at": 1783056668.8085573
},
"002594": {
"daily": [
@@ -8470,6 +8518,14 @@
"high": 85.67,
"low": 81.9,
"volume": 825046.0
},
{
"date": "2026-07-03",
"open": 83.73,
"close": 86.66,
"high": 87.28,
"low": 83.6,
"volume": 477402.0
}
],
"weekly": [
@@ -8764,7 +8820,7 @@
"volume": 934285.0
}
],
"updated_at": 1783051286.9981902
"updated_at": 1783056675.2141986
},
"00700": {
"daily": [
@@ -9727,6 +9783,14 @@
"high": 447.0,
"low": 429.4,
"volume": 40905100.0
},
{
"date": "2026-07-03",
"open": 433.0,
"close": 432.6,
"high": 445.8,
"low": 432.4,
"volume": 15135668.0
}
],
"weekly": [
@@ -10029,7 +10093,7 @@
"volume": 13032847.0
}
],
"updated_at": 1783051633.4890287
"updated_at": 1783056724.989126
},
"00968": {
"daily": [
@@ -12257,6 +12321,14 @@
"high": 84.2,
"low": 78.55,
"volume": 178219425.0
},
{
"date": "2026-07-03",
"open": 80.0,
"close": 78.95,
"high": 81.45,
"low": 77.35,
"volume": 71251123.0
}
],
"weekly": [
@@ -12559,7 +12631,7 @@
"volume": 60114819.0
}
],
"updated_at": 1783051638.5061805
"updated_at": 1783056730.0097694
},
"01070": {
"daily": [
@@ -14787,6 +14859,14 @@
"high": 40.4,
"low": 39.16,
"volume": 16452660.0
},
{
"date": "2026-07-03",
"open": 39.7,
"close": 39.9,
"high": 40.5,
"low": 39.62,
"volume": 6092471.0
}
],
"weekly": [
@@ -15089,7 +15169,7 @@
"volume": 2870057.0
}
],
"updated_at": 1783051643.521601
"updated_at": 1783056735.0304337
},
"01211": {
"daily": [
@@ -16052,6 +16132,14 @@
"high": 79.6,
"low": 74.95,
"volume": 54549471.0
},
{
"date": "2026-07-03",
"open": 81.2,
"close": 82.85,
"high": 83.35,
"low": 80.0,
"volume": 25755799.0
}
],
"weekly": [
@@ -16354,7 +16442,7 @@
"volume": 13286402.0
}
],
"updated_at": 1783051648.5394585
"updated_at": 1783056740.0491588
},
"01478": {
"daily": [
@@ -17317,6 +17405,14 @@
"high": 7.08,
"low": 6.72,
"volume": 6257000.0
},
{
"date": "2026-07-03",
"open": 6.72,
"close": 7.04,
"high": 7.1,
"low": 6.72,
"volume": 2981000.0
}
],
"weekly": [
@@ -17619,7 +17715,7 @@
"volume": 1618000.0
}
],
"updated_at": 1783051653.5516405
"updated_at": 1783056745.0709894
},
"01888": {
"daily": [
@@ -18582,6 +18678,14 @@
"high": 90.1,
"low": 79.85,
"volume": 92637274.0
},
{
"date": "2026-07-03",
"open": 81.0,
"close": 85.6,
"high": 88.9,
"low": 80.6,
"volume": 57017840.0
}
],
"weekly": [
@@ -18884,7 +18988,7 @@
"volume": 29941901.0
}
],
"updated_at": 1783051658.5684023
"updated_at": 1783056750.0898428
},
"02202": {
"daily": [
@@ -19847,6 +19951,14 @@
"high": 2.27,
"low": 2.18,
"volume": 39512590.0
},
{
"date": "2026-07-03",
"open": 2.25,
"close": 2.31,
"high": 2.35,
"low": 2.23,
"volume": 18235400.0
}
],
"weekly": [
@@ -20149,7 +20261,7 @@
"volume": 19786580.0
}
],
"updated_at": 1783051663.581557
"updated_at": 1783056755.496929
},
"02318": {
"daily": [
@@ -27437,6 +27549,14 @@
"high": 229.2,
"low": 198.0,
"volume": 40078361.0
},
{
"date": "2026-07-03",
"open": 191.2,
"close": 207.2,
"high": 210.6,
"low": 190.0,
"volume": 14255822.0
}
],
"weekly": [
@@ -27739,7 +27859,7 @@
"volume": 15066251.0
}
],
"updated_at": 1783051668.878092
"updated_at": 1783056760.5189984
},
"09868": {
"daily": [
@@ -31232,6 +31352,14 @@
"high": 14.6,
"low": 14.1,
"volume": 155998.0
},
{
"date": "2026-07-03",
"open": 14.18,
"close": 14.4,
"high": 14.53,
"low": 14.16,
"volume": 102393.0
}
],
"weekly": [
@@ -31526,7 +31654,7 @@
"volume": 230937.0
}
],
"updated_at": 1783051674.5847087
"updated_at": 1783056765.5414958
},
"300124": {
"daily": [
@@ -32489,6 +32617,14 @@
"high": 71.79,
"low": 67.31,
"volume": 703358.0
},
{
"date": "2026-07-03",
"open": 67.5,
"close": 74.14,
"high": 74.23,
"low": 67.4,
"volume": 691416.0
}
],
"weekly": [
@@ -32783,7 +32919,7 @@
"volume": 722493.0
}
],
"updated_at": 1783051344.573262
"updated_at": 1783056692.3502543
},
"300308": {
"daily": [
@@ -33746,6 +33882,14 @@
"high": 1198.0,
"low": 1127.4,
"volume": 317620.0
},
{
"date": "2026-07-03",
"open": 1130.0,
"close": 1158.88,
"high": 1188.01,
"low": 1124.0,
"volume": 230014.0
}
],
"weekly": [
@@ -34040,7 +34184,7 @@
"volume": 389058.0
}
],
"updated_at": 1783051680.1109805
"updated_at": 1783056770.5614996
},
"300548": {
"daily": [
@@ -35003,6 +35147,14 @@
"high": 245.0,
"low": 220.0,
"volume": 174041.0
},
{
"date": "2026-07-03",
"open": 220.0,
"close": 228.99,
"high": 231.55,
"low": 218.99,
"volume": 102806.0
}
],
"weekly": [
@@ -35297,7 +35449,7 @@
"volume": 242727.0
}
],
"updated_at": 1783051685.6375031
"updated_at": 1783056775.5768287
},
"300750": {
"daily": [
@@ -36260,6 +36412,14 @@
"high": 390.99,
"low": 380.39,
"volume": 340754.0
},
{
"date": "2026-07-03",
"open": 381.96,
"close": 381.17,
"high": 387.95,
"low": 380.55,
"volume": 157731.0
}
],
"weekly": [
@@ -36554,7 +36714,7 @@
"volume": 551212.0
}
],
"updated_at": 1783051690.893728
"updated_at": 1783056780.5942247
},
"301308": {
"daily": [
@@ -37517,6 +37677,14 @@
"high": 636.99,
"low": 592.0,
"volume": 208005.0
},
{
"date": "2026-07-03",
"open": 589.0,
"close": 634.0,
"high": 646.85,
"low": 574.1,
"volume": 153435.0
}
],
"weekly": [
@@ -37811,7 +37979,7 @@
"volume": 296230.0
}
],
"updated_at": 1783051380.6922731
"updated_at": 1783056696.8459098
},
"518880": {
"daily": [
@@ -38774,6 +38942,14 @@
"high": 8.484,
"low": 8.397,
"volume": 5149790.0
},
{
"date": "2026-07-03",
"open": 8.71,
"close": 8.68,
"high": 8.73,
"low": 8.68,
"volume": 2689807.0
}
],
"weekly": [
@@ -39068,7 +39244,7 @@
"volume": 3915247.0
}
],
"updated_at": 1783051695.915503
"updated_at": 1783056785.6140218
},
"600519": {
"daily": [
@@ -40031,6 +40207,14 @@
"high": 1215.52,
"low": 1190.51,
"volume": 50870.0
},
{
"date": "2026-07-03",
"open": 1205.24,
"close": 1189.52,
"high": 1210.14,
"low": 1188.0,
"volume": 23088.0
}
],
"weekly": [
@@ -40325,7 +40509,7 @@
"volume": 64803.0
}
],
"updated_at": 1783051561.7579694
"updated_at": 1783056699.260756
},
"600563": {
"daily": [
@@ -41288,6 +41472,14 @@
"high": 173.3,
"low": 164.3,
"volume": 112859.0
},
{
"date": "2026-07-03",
"open": 160.27,
"close": 159.59,
"high": 163.68,
"low": 153.52,
"volume": 75944.0
}
],
"weekly": [
@@ -41582,7 +41774,7 @@
"volume": 180947.0
}
],
"updated_at": 1783051700.9725504
"updated_at": 1783056791.4344049
},
"601318": {
"daily": [
@@ -42545,6 +42737,14 @@
"high": 50.2,
"low": 48.8,
"volume": 920130.0
},
{
"date": "2026-07-03",
"open": 49.5,
"close": 48.43,
"high": 49.78,
"low": 48.41,
"volume": 556535.0
}
],
"weekly": [
@@ -42839,7 +43039,7 @@
"volume": 1746202.0
}
],
"updated_at": 1783051575.149798
"updated_at": 1783056700.660319
},
"601899": {
"daily": [
@@ -43802,6 +44002,14 @@
"high": 26.96,
"low": 25.52,
"volume": 5067417.0
},
{
"date": "2026-07-03",
"open": 27.5,
"close": 27.93,
"high": 28.3,
"low": 27.41,
"volume": 4183914.0
}
],
"weekly": [
@@ -44096,7 +44304,7 @@
"volume": 4780454.0
}
],
"updated_at": 1783051705.9927075
"updated_at": 1783056796.4605315
},
"688411": {
"daily": [
@@ -45059,6 +45267,14 @@
"high": 272.99,
"low": 254.8,
"volume": 6368531.0
},
{
"date": "2026-07-03",
"open": 255.8,
"close": 255.99,
"high": 261.48,
"low": 251.0,
"volume": 2572727.0
}
],
"weekly": [
@@ -45353,7 +45569,7 @@
"volume": 13672788.0
}
],
"updated_at": 1783051711.208435
"updated_at": 1783056801.4764674
},
"688630": {
"daily": [
@@ -46316,6 +46532,14 @@
"high": 499.95,
"low": 464.8,
"volume": 5841815.0
},
{
"date": "2026-07-03",
"open": 467.98,
"close": 497.0,
"high": 502.0,
"low": 444.55,
"volume": 4259432.0
}
],
"weekly": [
@@ -46610,7 +46834,7 @@
"volume": 9660790.0
}
],
"updated_at": 1783051587.7339969
"updated_at": 1783056704.6600807
},
"688639": {
"daily": [
@@ -47573,6 +47797,14 @@
"high": 17.4,
"low": 15.98,
"volume": 9065955.0
},
{
"date": "2026-07-03",
"open": 17.06,
"close": 16.71,
"high": 17.2,
"low": 16.52,
"volume": 4669797.0
}
],
"weekly": [
@@ -47867,7 +48099,7 @@
"volume": 13996588.0
}
],
"updated_at": 1783051733.2346125
"updated_at": 1783056806.4868052
},
"688802": {
"daily": [
@@ -48830,6 +49062,14 @@
"high": 784.0,
"low": 721.0,
"volume": 2036024.0
},
{
"date": "2026-07-03",
"open": 731.34,
"close": 741.27,
"high": 757.88,
"low": 713.0,
"volume": 1323959.0
}
],
"weekly": [
@@ -49092,6 +49332,6 @@
"volume": 3202146.0
}
],
"updated_at": 1783051743.6900556
"updated_at": 1783056714.5052435
}
}
+73 -73
View File
@@ -5,9 +5,9 @@
"name": "中际旭创",
"shares": 100,
"cost": 1316.53,
"price": 1157.97,
"market_value": 115797.0,
"change_pct": 1.31,
"price": 1145.0,
"market_value": 114101.0,
"change_pct": 0.17,
"currency": "CNY",
"position_pct": 15.27,
"_currency": "CNY"
@@ -18,32 +18,32 @@
"shares": 500,
"cost": 228.65,
"price": 178.26,
"market_value": 102700.0,
"market_value": 89910.0,
"change_pct": 3.859,
"currency": "HKD",
"currency": "CNY",
"position_pct": 13.47,
"_currency": "HKD"
"_currency": "CNY"
},
{
"code": "01478",
"name": "丘钛科技",
"shares": 11000,
"cost": 11.68,
"price": 6.11,
"market_value": 77550.0,
"change_pct": 4.911,
"currency": "HKD",
"price": 6.1,
"market_value": 67210.0,
"change_pct": 4.762,
"currency": "CNY",
"position_pct": 7.97,
"_currency": "HKD"
"_currency": "CNY"
},
{
"code": "601899",
"name": "紫金矿业",
"shares": 2400,
"cost": 39.89,
"price": 28.04,
"market_value": 67296.0,
"change_pct": 6.62,
"price": 27.8,
"market_value": 66864.0,
"change_pct": 5.7,
"currency": "CNY",
"position_pct": 7.34,
"_currency": "CNY"
@@ -53,9 +53,9 @@
"name": "海博思创",
"shares": 200,
"cost": 266.95,
"price": 257.52,
"market_value": 51504.0,
"change_pct": 0.71,
"price": 254.75,
"market_value": 50842.0,
"change_pct": -0.38,
"currency": "CNY",
"position_pct": 6.31,
"_currency": "CNY"
@@ -65,9 +65,9 @@
"name": "中芯国际",
"shares": 300,
"cost": 126.07,
"price": 143.65,
"market_value": 43095.0,
"change_pct": -0.31,
"price": 142.97,
"market_value": 42600.0,
"change_pct": -0.78,
"currency": "CNY",
"position_pct": 5.44,
"_currency": "CNY"
@@ -78,20 +78,20 @@
"shares": 500,
"cost": 76.5,
"price": 74.17,
"market_value": 42600.0,
"market_value": 37325.0,
"change_pct": 2.088,
"currency": "HKD",
"currency": "CNY",
"position_pct": 5.28,
"_currency": "HKD"
"_currency": "CNY"
},
{
"code": "688639",
"name": "华恒生物",
"shares": 2800,
"cost": 21.51,
"price": 16.69,
"market_value": 46732.0,
"change_pct": -1.53,
"price": 16.72,
"market_value": 46900.0,
"change_pct": -1.36,
"currency": "CNY",
"position_pct": 5.25,
"_currency": "CNY"
@@ -101,9 +101,9 @@
"name": "宁德时代",
"shares": 100,
"cost": 401.78,
"price": 384.16,
"market_value": 38416.0,
"change_pct": 0.47,
"price": 380.24,
"market_value": 38032.0,
"change_pct": -0.55,
"currency": "CNY",
"position_pct": 4.64,
"_currency": "CNY"
@@ -113,57 +113,57 @@
"name": "比亚迪股份",
"shares": 600,
"cost": 90.92,
"price": 71.66,
"market_value": 49560.0,
"change_pct": 5.556,
"currency": "HKD",
"price": 71.83,
"market_value": 43044.0,
"change_pct": 5.811,
"currency": "CNY",
"position_pct": 4.62,
"_currency": "HKD"
"_currency": "CNY"
},
{
"code": "02202",
"name": "万科企业",
"shares": 19700,
"cost": 4.05,
"price": 2.02,
"market_value": 45704.0,
"change_pct": 4.484,
"currency": "HKD",
"price": 1.99,
"market_value": 39203.0,
"change_pct": 2.69,
"currency": "CNY",
"position_pct": 4.6,
"_currency": "HKD"
"_currency": "CNY"
},
{
"code": "00700",
"name": "腾讯",
"shares": 100,
"cost": null,
"price": 379.05,
"market_value": 43700.0,
"change_pct": 1.627,
"currency": "HKD",
"price": 376.8,
"market_value": 37541.0,
"change_pct": 1.023,
"currency": "CNY",
"position_pct": null,
"_currency": "HKD"
"_currency": "CNY"
},
{
"code": "00981",
"name": "中芯国际",
"shares": 500,
"cost": 65.84,
"price": 69.1,
"market_value": 39825.0,
"change_pct": -0.871,
"currency": "HKD",
"price": 68.41,
"market_value": 34440.0,
"change_pct": -1.866,
"currency": "CNY",
"position_pct": 4.2,
"_currency": "HKD"
"_currency": "CNY"
},
{
"code": "300548",
"name": "长芯博创",
"shares": 100,
"cost": 231.46,
"price": 227.0,
"market_value": 22700.0,
"change_pct": 2.25,
"price": 226.5,
"market_value": 22599.0,
"change_pct": 2.03,
"currency": "CNY",
"position_pct": 3.2,
"_currency": "CNY"
@@ -173,9 +173,9 @@
"name": "黄金ETF华安",
"shares": 2400,
"cost": 12.19,
"price": 8.69,
"market_value": 20856.0,
"change_pct": 2.49,
"price": 8.67,
"market_value": 20832.0,
"change_pct": 2.32,
"currency": "CNY",
"position_pct": 2.45,
"_currency": "CNY"
@@ -185,9 +185,9 @@
"name": "中科电气",
"shares": 1400,
"cost": 22.29,
"price": 14.3,
"market_value": 20020.0,
"change_pct": 0.92,
"price": 14.38,
"market_value": 20062.0,
"change_pct": 1.48,
"currency": "CNY",
"position_pct": 2.42,
"_currency": "CNY"
@@ -197,9 +197,9 @@
"name": "模塑科技",
"shares": 1400,
"cost": 14.83,
"price": 17.37,
"market_value": 24318.0,
"change_pct": 2.96,
"price": 17.62,
"market_value": 25088.0,
"change_pct": 4.45,
"currency": "CNY",
"position_pct": 2.41,
"_currency": "CNY"
@@ -209,9 +209,9 @@
"name": "法拉电子",
"shares": 100,
"cost": 147.18,
"price": 161.71,
"market_value": 16171.0,
"change_pct": -1.58,
"price": 160.0,
"market_value": 15900.0,
"change_pct": -2.62,
"currency": "CNY",
"position_pct": 2.3,
"_currency": "CNY"
@@ -221,20 +221,20 @@
"name": "中国神华",
"shares": 500,
"cost": 39.79,
"price": 34.84,
"market_value": 20090.0,
"change_pct": 1.465,
"currency": "HKD",
"price": 34.59,
"market_value": 17305.0,
"change_pct": 0.758,
"currency": "CNY",
"position_pct": 2.14,
"_currency": "HKD"
"_currency": "CNY"
}
],
"total_assets": 864781.03,
"total_mv": 784305.03,
"total_assets": 860913.13,
"total_mv": 780437.13,
"stock_value": null,
"cash": 80476.0,
"frozen_cash": 0.0,
"position_pct": 90.69,
"position_pct": 90.65,
"currency": "CNY",
"updated_at": "2026-07-03 12:10"
"updated_at": "2026-07-03 13:42"
}
+20
View File
@@ -9109,6 +9109,26 @@
"event_label": "止盈区间",
"timestamp": "2026-07-03T11:54:31.001389",
"date": "2026-07-03"
},
{
"code": "000700",
"name": "模塑科技",
"event_type": "entry_zone",
"price": 17.92,
"trigger_value": "15.5~18.08",
"event_label": "买入区间",
"timestamp": "2026-07-03T13:28:17.259162",
"date": "2026-07-03"
},
{
"code": "000700",
"name": "模塑科技",
"event_type": "entry_zone",
"price": 17.88,
"trigger_value": "15.5~18.08",
"event_label": "买入区间",
"timestamp": "2026-07-03T13:30:30.458142",
"date": "2026-07-03"
}
]
}
+16 -16
View File
@@ -25,8 +25,8 @@
{
"date": "2026-07-03",
"high": 1215.52,
"low": 1190.5,
"close": 1191.31
"low": 1188.0,
"close": 1188.7
}
],
"02202": [
@@ -61,8 +61,8 @@
{
"date": "2026-07-03",
"high": 50.2,
"low": 48.6,
"close": 48.7
"low": 48.4,
"close": 48.44
}
],
"02359": [
@@ -118,7 +118,7 @@
"date": "2026-07-03",
"high": 502.0,
"low": 444.55,
"close": 495.95
"close": 494.57
}
],
"06160": [
@@ -154,7 +154,7 @@
"date": "2026-07-03",
"high": 687.04,
"low": 633.01,
"close": 654.0
"close": 646.83
}
],
"09868": [
@@ -194,9 +194,9 @@
},
{
"date": "2026-07-03",
"high": 753.88,
"high": 757.88,
"low": 713.0,
"close": 744.0
"close": 733.9
}
],
"300124": [
@@ -208,9 +208,9 @@
},
{
"date": "2026-07-03",
"high": 72.38,
"high": 74.63,
"low": 67.31,
"close": 72.29
"close": 73.13
}
],
"000657": [
@@ -224,7 +224,7 @@
"date": "2026-07-03",
"high": 101.5,
"low": 87.88,
"close": 91.88
"close": 91.78
}
],
"000711": [
@@ -250,9 +250,9 @@
},
{
"date": "2026-07-03",
"high": 882.5,
"high": 892.1,
"low": 795.0,
"close": 874.23
"close": 887.87
}
],
"002594": [
@@ -266,7 +266,7 @@
"date": "2026-07-03",
"high": 87.28,
"low": 81.9,
"close": 86.56
"close": 86.45
}
],
"00700": [
@@ -332,9 +332,9 @@
"301308": [
{
"date": "2026-07-03",
"high": 631.56,
"high": 646.85,
"low": 574.1,
"close": 620.0
"close": 635.0
}
]
}
+11 -29
View File
@@ -21,13 +21,14 @@ import json, os
from datetime import datetime, timedelta
from mo_data import read_portfolio, read_decisions, read_watchlist
# live_prices.json 已废弃,所有数据走 DB
LIVE_PRICES_PATH = "/home/hmo/web-dashboard/data/live_prices.json"
PORTFOLIO_PATH = "/home/hmo/web-dashboard/data/portfolio.json"
def is_market_hours():
now = datetime.now()
if now.weekday() >= 5: # 周六日
if now.weekday() >= 5:
return False, "weekend"
t = now.hour * 60 + now.minute
if 9*60+30 <= t <= 15*60:
@@ -38,42 +39,23 @@ def is_market_hours():
def check_fresh():
"""返回 (ok: bool, msg: str)"""
now = datetime.now()
# 先看是不是交易日
in_market, period = is_market_hours()
max_age_min = 5 if in_market else 120
# 主指标:live_prices.json
if os.path.exists(LIVE_PRICES_PATH):
# 主指标:DB portfolio_summary.updated_at
try:
lp = json.load(open(LIVE_PRICES_PATH))
lp_time = lp.get("updated_at", "")
if not lp_time:
return False, "live_prices.json updated_at 为空"
lp_dt = datetime.fromisoformat(lp_time)
age = (now - lp_dt).total_seconds() / 60
if age > max_age_min:
return False, f"live_prices.json 已 {age:.0f} 分钟未更新(阈值 {max_age_min} 分钟)"
return True, f"数据新鲜({age:.0f} 分钟前)"
except Exception as e:
return False, f"live_prices.json 读取失败: {e}"
else:
# fallback: portfolio.json
if os.path.exists(PORTFOLIO_PATH):
try:
pf = mo_data.read_portfolio()
pf = read_portfolio()
pf_time = pf.get("updated_at", "")
if not pf_time:
return False, "portfolio.json updated_at 为空"
if pf_time:
pf_dt = datetime.fromisoformat(pf_time)
age = (now - pf_dt).total_seconds() / 60
if age > max_age_min:
return False, f"portfolio.json {age:.0f} 分钟未更新(阈值 {max_age_min} 分钟)"
return True, f"数据新鲜(portfolio.json {age:.0f} 分钟前)"
return False, f"数据{age:.0f} 分钟未更新(阈值 {max_age_min} 分钟)"
return True, f"数据新鲜({age:.0f} 分钟前)"
except Exception as e:
return False, f"portfolio.json 读取失败: {e}"
return False, "live_prices.json 和 portfolio.json 均不存在"
return False, f"DB 读取失败: {e}"
return False, "无法获取数据新鲜度"
if __name__ == "__main__":
+1 -3
View File
@@ -203,10 +203,8 @@ def main():
}
DATA_DIR.mkdir(parents=True, exist_ok=True)
with open(DATA_DIR / "market.json", "w", encoding="utf-8") as f:
json.dump(market_data, f, ensure_ascii=False, indent=2)
# ── SQLite 双写 ──
# ── SQLite 写入(替代 market.json──
conn = get_conn()
init_all_tables(conn)
ok, msg, sid = write_market_snapshot(conn, market_data)
+67
View File
@@ -387,6 +387,28 @@ def init_all_tables(conn: sqlite3.Connection):
last_scanned_at TEXT,
found_count INTEGER DEFAULT 0
);
-- 实时价格快照(替代 live_prices.json
CREATE TABLE IF NOT EXISTS live_prices (
code TEXT PRIMARY KEY,
price REAL,
change_pct REAL,
updated_at TEXT DEFAULT (datetime('now','localtime'))
);
-- 多周期缓存(替代 multi_tf_cache.json
CREATE TABLE IF NOT EXISTS mtf_cache (
code TEXT PRIMARY KEY,
cache_json TEXT,
updated_at TEXT DEFAULT (datetime('now','localtime'))
);
-- 资金流缓存(替代 capital_flow_cache.json
CREATE TABLE IF NOT EXISTS capital_flow_cache (
id INTEGER PRIMARY KEY AUTOINCREMENT,
cache_json TEXT,
updated_at TEXT DEFAULT (datetime('now','localtime'))
);
""")
conn.commit()
@@ -1120,3 +1142,48 @@ def query_cash_log(conn, limit: int = 20) -> list[dict]:
"SELECT * FROM cash_log ORDER BY id DESC LIMIT ?", (limit,)
).fetchall()
return [dict(r) for r in rows]
# ═══ live_prices / mtf_cache / capital_flow_cache 写函数 ═══
def write_live_prices(conn, prices: dict):
"""写入实时价格快照(替代 live_prices.json"""
import json
for code, info in prices.items():
conn.execute(
"INSERT OR REPLACE INTO live_prices (code, price, change_pct, updated_at) VALUES (?,?,?,datetime('now','localtime'))",
(code, info.get('price'), info.get('change_pct'))
)
def read_live_prices(conn) -> dict:
rows = conn.execute("SELECT code, price, change_pct FROM live_prices").fetchall()
return {r['code']: {'price': r['price'], 'change_pct': r['change_pct']} for r in rows}
def write_mtf_cache(conn, code: str, data: dict):
"""写入多周期缓存(替代 multi_tf_cache.json 单条)"""
import json
conn.execute(
"INSERT OR REPLACE INTO mtf_cache (code, cache_json, updated_at) VALUES (?,?,datetime('now','localtime'))",
(code, json.dumps(data, ensure_ascii=False))
)
def read_mtf_cache(conn, code: str) -> dict:
import json
r = conn.execute("SELECT cache_json FROM mtf_cache WHERE code=?", (code,)).fetchone()
return json.loads(r['cache_json']) if r else {}
def write_capital_flow_cache(conn, data: dict):
"""写入资金流缓存(替代 capital_flow_cache.json"""
import json
conn.execute("DELETE FROM capital_flow_cache")
conn.execute(
"INSERT INTO capital_flow_cache (cache_json, updated_at) VALUES (?,datetime('now','localtime'))",
(json.dumps(data, ensure_ascii=False),)
)
def read_capital_flow_cache(conn) -> dict:
import json
r = conn.execute("SELECT cache_json FROM capital_flow_cache ORDER BY id DESC LIMIT 1").fetchone()
return json.loads(r['cache_json']) if r else {}
+33 -31
View File
@@ -19,7 +19,7 @@ from typing import Optional
DATA_DIR = "/home/hmo/web-dashboard/data"
HISTORY_PATH = os.path.join(DATA_DIR, "price_history.json")
MTF_CACHE_PATH = os.path.join(DATA_DIR, "multi_tf_cache.json") # 多周期缓存独立存储
# multi_tf_cache.json 已迁移到 DB (mtf_cache 表)
# 腾讯API K线端点
KLINE_URL = "http://web.ifzq.gtimg.cn/appstock/app/fqkline/get?param={market}{code},{period},,,{count},qfq"
@@ -80,40 +80,47 @@ def _user_agent() -> dict:
# 多周期缓存TTL(秒):日K线1小时,周/月K线1天
_KLINE_CACHE_TTL = {"day": 3600, "week": 86400, "month": 86400}
# 模块级缓存:避免每次fetch_kline都重新读/写大文件
_MTF_CACHE_DATA = None # {code: {daily:[], weekly:[], monthly:[], updated_at: float, fundamentals:{}}}
_MTF_CACHE_MTIME = 0 # 文件最后修改时间
# 模块级缓存:避免每次 fetch_kline 都重新读 DB
_MTF_CACHE_DATA = None
_MTF_CACHE_DIRTY = False
def _load_mtf_cache():
"""加载多周期缓存(带模块级缓存,避免频繁读盘)"""
global _MTF_CACHE_DATA, _MTF_CACHE_MTIME
import time
"""从 DB 加载多周期缓存"""
global _MTF_CACHE_DATA
if _MTF_CACHE_DATA is not None:
return _MTF_CACHE_DATA
try:
current_mtime = os.path.getmtime(MTF_CACHE_PATH)
if _MTF_CACHE_DATA is not None and current_mtime == _MTF_CACHE_MTIME:
return _MTF_CACHE_DATA
with open(MTF_CACHE_PATH) as f:
_MTF_CACHE_DATA = json.load(f)
_MTF_CACHE_MTIME = current_mtime
return _MTF_CACHE_DATA
except (FileNotFoundError, json.JSONDecodeError, OSError):
import sqlite3
db = sqlite3.connect('/home/hmo/web-dashboard/data/mofin.db')
rows = db.execute("SELECT code, cache_json FROM mtf_cache").fetchall()
_MTF_CACHE_DATA = {}
_MTF_CACHE_MTIME = 0
return {}
for code, json_str in rows:
try:
_MTF_CACHE_DATA[code] = json.loads(json_str)
except:
pass
db.close()
except Exception:
_MTF_CACHE_DATA = {}
return _MTF_CACHE_DATA
def _save_mtf_cache():
"""将模块级缓存写回磁盘"""
global _MTF_CACHE_DATA, _MTF_CACHE_MTIME
"""将模块级缓存写回 DB"""
global _MTF_CACHE_DATA
if _MTF_CACHE_DATA is None:
return
try:
os.makedirs(os.path.dirname(MTF_CACHE_PATH), exist_ok=True)
with open(MTF_CACHE_PATH, "w") as f:
json.dump(_MTF_CACHE_DATA, f, ensure_ascii=False, indent=2)
import time
_MTF_CACHE_MTIME = os.path.getmtime(MTF_CACHE_PATH) if os.path.exists(MTF_CACHE_PATH) else time.time()
import sqlite3
db = sqlite3.connect('/home/hmo/web-dashboard/data/mofin.db')
for code, data in _MTF_CACHE_DATA.items():
db.execute(
"INSERT OR REPLACE INTO mtf_cache (code, cache_json, updated_at) VALUES (?,?,datetime('now','localtime'))",
(code, json.dumps(data, ensure_ascii=False))
)
db.commit()
db.close()
except Exception:
pass
@@ -582,13 +589,8 @@ def _generate_strategy_adjustment(analysis: dict) -> dict:
def _load_local_history(code: str, period: str) -> list:
"""本地多周期缓存读取历史数据,不修改 price_history.json"""
try:
with open(MTF_CACHE_PATH) as f:
data = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
return []
""" DB 多周期缓存读取历史数据"""
data = _load_mtf_cache()
stock = data.get(code, {})
return stock.get(period, [])
+8 -6
View File
@@ -12,7 +12,7 @@ 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, write_holding_strategy
from mofin_db import get_conn, write_holdings_batch, write_portfolio_summary, write_price_event, write_watchlist_stock, write_live_prices, read_capital_flow_cache, write_holding_strategy
from mo_data import read_portfolio, read_decisions, read_watchlist
BREACH_PATH = "/home/hmo/.hermes/zone_breach.json"
@@ -231,14 +231,16 @@ def refresh_data_prices():
prices.update(hk_prices)
updated = 0
# 保存全量实时价快照(供报告管道消费,确保分析用最新数据
# 保存全量实时价快照到 DB(替代 live_prices.json
try:
live = {"updated_at": datetime.now().isoformat(), "prices": {}}
live = {}
for code in all_codes:
if code in prices:
p, c, chg = prices[code]
live["prices"][code] = {"price": p, "change_pct": chg}
json.dump(live, open("/home/hmo/web-dashboard/data/live_prices.json", "w"), indent=2)
live[code] = {"price": p, "change_pct": chg}
conn = get_conn()
write_live_prices(conn, live)
conn.close()
except Exception:
pass
@@ -663,7 +665,7 @@ def run_once(round_label=""):
# === 3.5 资金流异常检测(2026-06-27 新增)===
try:
cf = json.load(open("/home/hmo/web-dashboard/data/capital_flow_cache.json"))
cf = read_capital_flow_cache(get_conn())
# 检查所有 active decision 中的资金流异常
for d in active:
code = d["code"]
+5 -1
View File
@@ -12,6 +12,7 @@ from urllib.request import urlopen, Request
from concurrent.futures import ThreadPoolExecutor, as_completed
from threading import Semaphore
from mo_data import read_portfolio, read_decisions, read_watchlist
from mofin_db import get_conn, write_capital_flow_cache
DATA_DIR = "/home/hmo/web-dashboard/data"
DECISIONS_PATH = f"{DATA_DIR}/decisions.json"
@@ -175,7 +176,10 @@ def main():
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M"),
"stocks": all_flows,
}
json.dump(cache, open(CACHE_PATH, "w"), indent=2, ensure_ascii=False)
# 写 DB(替代 capital_flow_cache.json
conn = get_conn()
write_capital_flow_cache(conn, cache)
conn.close()
print(f"[capital_flow] {len(all_flows)}/{len(code_list)}只更新完成")
if __name__ == "__main__":
+33
View File
@@ -0,0 +1,33 @@
import sys, traceback
sys.path.insert(0, '/home/hmo/MoFin')
errors = []
for mod, name in [
('mo_data', 'read_portfolio'),
('mo_data', 'read_decisions'),
('mo_data', 'read_watchlist'),
('mofin_db', 'get_conn'),
('mofin_db', 'write_holdings_batch'),
('mofin_db', 'write_portfolio_summary'),
('mofin_db', 'write_watchlist_stock'),
('mofin_db', 'write_holding_strategy'),
('mo_models', 'is_hk_stock'),
('mo_models', 'get_hk_rate'),
]:
try:
m = __import__(mod, fromlist=[name])
getattr(m, name)
print(f"OK: {mod}.{name}")
except Exception as e:
print(f"FAIL: {mod}.{name} -> {e}")
errors.append(str(e))
print(f"\n=== price_monitor.py import test ===")
try:
import price_monitor
print("price_monitor imported OK")
except Exception as e:
print(f"FAIL: {traceback.format_exc()}")
if errors:
print(f"\n{len(errors)} import errors!")
+15
View File
@@ -0,0 +1,15 @@
import sqlite3
db = sqlite3.connect('/home/hmo/web-dashboard/data/mofin.db')
# Check when portfolio was last updated
r = db.execute("SELECT updated_at, total_assets, total_mv, cash FROM portfolio_summary WHERE id=1").fetchone()
print(f"Portfolio last updated: {r[0]}")
print(f"total_assets={r[1]} total_mv={r[2]} cash={r[3]}")
# Check hold prices
print("\nAll holdings:")
for r in db.execute("SELECT code, name, price, change_pct, cost, shares FROM holdings WHERE is_active=1 ORDER BY code"):
mv = (r[2] or 0) * (r[5] or 0)
print(f" {r[0]} {r[1]}: price={r[2]} chg={r[3]} cost={r[4]} shares={r[5]} mv={mv}")
db.close()
+5 -1
View File
@@ -1,7 +1,11 @@
#!/usr/bin/env python3
"""Remove held stocks from watchlist"""
import json, os
import json, os, sys
# 确保 MoFin 根目录在模块搜索路径中(兼容 cron 环境)
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from mo_data import read_portfolio, read_decisions, read_watchlist
from mofin_db import get_conn, write_watchlist_stock, write_holding_strategy
+35
View File
@@ -0,0 +1,35 @@
"""Diagnose: check HK stock costs, prices, total assets calculation"""
import sys
sys.path.insert(0, '/home/hmo/MoFin')
from mo_models import calc_total_assets, calc_total_mv, is_hk_stock, to_cny, get_hk_rate
from mo_data import read_portfolio
import json
pf = read_portfolio()
holdings = pf.get('holdings', [])
rate = get_hk_rate()
print(f"HK_RATE: {rate}")
print(f"total_assets (from DB): {pf.get('total_assets')}")
print(f"total_mv (from DB): {pf.get('total_mv')}")
print(f"cash: {pf.get('cash')}")
print(f"frozen_cash: {pf.get('frozen_cash')}")
print(f"position_pct: {pf.get('position_pct')}")
total_mv_calc = calc_total_mv(holdings)
total_assets_calc = calc_total_assets(pf)
print(f"\ncalc_total_mv: {total_mv_calc}")
print(f"calc_total_assets: {total_assets_calc}")
print(f"\n=== HK stocks ===")
for h in holdings:
code = h.get('code', '')
if is_hk_stock(str(code)):
cost = h.get('cost', 0) or 0
price = h.get('price', 0) or 0
shares = h.get('shares', 0) or 0
mv = price * shares
cost_calc = cost * shares
pnl = (price - cost) * shares if cost > 0 else 0
pnl_pct = (price - cost) / cost * 100 if cost > 0 else 0
print(f" {code} {h.get('name')}: cost={cost} price={price} shares={shares} mv={mv} pnl={pnl:.1f} ({pnl_pct:+.1f}%)")
+25
View File
@@ -0,0 +1,25 @@
"""Quick verification after JSON→DB migration"""
import sys
sys.path.insert(0, '/home/hmo/MoFin')
from mo_data import read_portfolio, read_decisions, read_watchlist
ok = 0
err = 0
for name, fn in [("portfolio", read_portfolio), ("decisions", read_decisions), ("watchlist", read_watchlist)]:
try:
r = fn()
if name == "portfolio":
n = len(r.get('holdings', []))
elif name == "decisions":
n = len(r.get('decisions', []))
else:
n = len(r.get('stocks', []))
print(f" {name}: {n} records OK")
ok += 1
except Exception as e:
print(f" {name}: ERROR -> {e}")
err += 1
print(f"\n{ok}/3 passed, {err} errors")
+36
View File
@@ -0,0 +1,36 @@
"""Test Eastmoney API response time for HK stocks"""
import urllib.request, json, time
codes = ['00700', '01888', '00981']
UA = 'Mozilla/5.0'
for code in codes:
url = f"https://push2.eastmoney.com/api/qt/stock/get?secid=116.{code}&fields=f43,f170&fltt=2"
start = time.time()
try:
req = urllib.request.Request(url, headers={"User-Agent": UA})
with urllib.request.urlopen(req, timeout=30) as r:
resp = json.loads(r.read().decode("utf-8"))
elapsed = time.time() - start
price = resp.get('data', {}).get('f43', '?')
print(f"{code}: {elapsed:.1f}s, price={price}, rc={resp.get('rc')}")
except Exception as e:
elapsed = time.time() - start
print(f"{code}: {elapsed:.1f}s, ERROR: {type(e).__name__}: {e}")
# Also test Tencent fallback
print("\nTencent fallback:")
url = "http://qt.gtimg.cn/q=hk00700,hk01888,hk00981"
start = time.time()
try:
req = urllib.request.Request(url, headers={"User-Agent": UA})
with urllib.request.urlopen(req, timeout=10) as r:
text = r.read().decode("gbk")
elapsed = time.time() - start
print(f"Tencent: {elapsed:.1f}s, {len(text)} bytes")
# Parse first line
line = text.strip().split('\n')[0]
print(f" sample: {line[:80]}...")
except Exception as e:
elapsed = time.time() - start
print(f"Tencent: {elapsed:.1f}s, ERROR: {e}")
+22
View File
@@ -0,0 +1,22 @@
"""Test if steady 5s interval avoids rate limit"""
import urllib.request, json, time
UA = 'Mozilla/5.0'
codes = ['00700', '01888', '00981']
for i, code in enumerate(codes):
url = f"https://push2.eastmoney.com/api/qt/stock/get?secid=116.{code}&fields=f43,f170&fltt=2"
start = time.time()
try:
req = urllib.request.Request(url, headers={"User-Agent": UA})
with urllib.request.urlopen(req, timeout=5) as r:
resp = json.loads(r.read().decode("utf-8"))
elapsed = time.time() - start
price = resp.get('data', {}).get('f43', '?')
print(f"#{i+1} {code}: OK in {elapsed:.1f}s, price={price}")
except Exception as e:
elapsed = time.time() - start
print(f"#{i+1} {code}: FAIL in {elapsed:.1f}s — {type(e).__name__}")
break
if i < len(codes) - 1:
time.sleep(5)
+49
View File
@@ -0,0 +1,49 @@
"""Test Eastmoney rate limit — find safe interval between requests"""
import urllib.request, json, time
UA = 'Mozilla/5.0'
CODE = '00700'
url = f"https://push2.eastmoney.com/api/qt/stock/get?secid=116.{CODE}&fields=f43,f170&fltt=2"
def try_fetch():
start = time.time()
try:
req = urllib.request.Request(url, headers={"User-Agent": UA})
with urllib.request.urlopen(req, timeout=5) as r:
resp = json.loads(r.read().decode("utf-8"))
elapsed = time.time() - start
return True, elapsed, resp.get('data', {}).get('f43', '?'), ''
except Exception as e:
elapsed = time.time() - start
return False, elapsed, 0, str(e)[:50]
# Test 1: single request
ok, t, price, err = try_fetch()
print(f"Single: {'OK' if ok else 'FAIL'} in {t:.2f}s price={price} {err}")
if ok:
# If it works, test minimal interval
for delay in [0.5, 1, 2, 3, 5]:
time.sleep(delay)
ok2, t2, p2, e2 = try_fetch()
print(f"After {delay:.1f}s: {'OK' if ok2 else 'FAIL'} in {t2:.2f}s price={p2} {e2}")
if not ok2:
print(f" -> Rate limited! Need > {delay}s between requests")
else:
# Currently blocked, wait and retry
print("Currently blocked. Waiting 10s then retry...")
time.sleep(10)
ok, t, price, err = try_fetch()
print(f"After 10s: {'OK' if ok else 'FAIL'} in {t:.2f}s price={price} {err}")
if ok:
time.sleep(2)
ok2, t2, p2, e2 = try_fetch()
print(f"After +2s: {'OK' if ok2 else 'FAIL'} in {t2:.2f}s price={p2} {e2}")
else:
for w in [20, 30, 60]:
print(f"Waiting {w}s...")
time.sleep(w)
ok, t, price, err = try_fetch()
print(f"After {w}s: {'OK' if ok else 'FAIL'} in {t:.2f}s price={price} {err}")
if ok:
break
+38
View File
@@ -0,0 +1,38 @@
"""Verify total assets and P&L calculation"""
import sqlite3, sys
sys.path.insert(0, '/home/hmo/MoFin')
from mo_models import calc_total_assets, calc_total_mv
db = sqlite3.connect('/home/hmo/web-dashboard/data/mofin.db')
r = db.execute("SELECT * FROM portfolio_summary WHERE id=1").fetchone()
keys = [d[0] for d in db.execute("SELECT * FROM portfolio_summary LIMIT 0").description]
summary = dict(zip(keys, r))
print(f"total_assets (stored): {summary.get('total_assets')}")
print(f"total_mv (stored): {summary.get('total_mv')}")
print(f"total_pnl (stored): {summary.get('total_pnl')}")
print(f"cash: {summary.get('cash')}")
print(f"frozen_cash: {summary.get('frozen_cash')}")
holdings = []
for r in db.execute("SELECT code, name, cost, price, shares FROM holdings WHERE is_active=1"):
holdings.append({'code': r[0], 'name': r[1], 'cost': r[2] or 0, 'price': r[3] or 0, 'shares': r[4] or 0})
mv = sum(h['price'] * h['shares'] for h in holdings)
total_cost = sum(h['cost'] * h['shares'] for h in holdings)
pnl = mv - total_cost
ta = mv + (summary.get('cash') or 0) + (summary.get('frozen_cash') or 0)
print(f"\nCalculated:")
print(f"total_mv = {mv:.2f}")
print(f"total_cost = {total_cost:.2f}")
print(f"total_pnl = {pnl:.2f}")
print(f"total_assets = {ta:.2f}")
# Check for HK stocks with cost=0 (never converted)
print("\nStocks with cost=0 or None:")
for h in holdings:
if h['cost'] <= 0 and h['shares'] > 0:
print(f" {h['code']} {h['name']}: cost={h['cost']} shares={h['shares']} price={h['price']}")
db.close()
+11 -2
View File
@@ -13,6 +13,10 @@ no_agent模式:有发现→输出,无→静默
import json, os, sqlite3, sys, time, urllib.request
from pathlib import Path
from datetime import datetime
# 确保 MoFin 根目录在模块搜索路径中(兼容 cron 环境)
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from mo_data import read_watchlist
from mofin_db import write_watchlist_stock
@@ -34,8 +38,13 @@ def clean_proxy():
def fetch_quote(code):
"""拉行情。DB 优先,腾讯 fallback"""
# DB 优先
try: from mofin_db import get_price_from_db; p, chg = get_price_from_db(code); return {"name":"", "code":code, "price":p, "change_pct":chg or 0} if p else None
except: pass
try:
from mofin_db import get_price_from_db
p, chg = get_price_from_db(code)
if p:
return {"name":"", "code":code, "price":p, "change_pct":chg or 0}
except:
pass
# Fallback: 腾讯
try:
prefix = "sh" if code.startswith(('60','68','51','56','50')) else "sz" if code.startswith(('00','30','15')) else "hk"