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
}
]
}
+63 -81
View File
@@ -1,81 +1,63 @@
#!/usr/bin/env python3
"""data_freshness.py — 数据新鲜度校验
所有报告管道在生成输出前必须调用 check_fresh()。
返回 (pass: bool, details: str),如果数据过期则阻止生成操作建议。
用法:
from data_freshness import check_fresh
ok, msg = check_fresh()
if not ok:
print(f"⚠️ 数据过期: {msg}")
sys.exit(0) # 不生成报告
校验规则:
- 盘中 (9:30~15:00)price/live_prices.json 必须在 5 分钟内刷新
- 盘后 (9:30以前/15:00以后):允许最长 120 分钟
- 周末/节假日:跳过校验
"""
import json, os
from datetime import datetime, timedelta
from mo_data import read_portfolio, read_decisions, read_watchlist
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: # 周六日
return False, "weekend"
t = now.hour * 60 + now.minute
if 9*60+30 <= t <= 15*60:
return True, "trading"
return False, "closed"
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):
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_time = pf.get("updated_at", "")
if not pf_time:
return False, "portfolio.json updated_at 为空"
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} 分钟前)"
except Exception as e:
return False, f"portfolio.json 读取失败: {e}"
return False, "live_prices.json 和 portfolio.json 均不存在"
if __name__ == "__main__":
ok, msg = check_fresh()
print(f"{'' if ok else ''} {msg}")
#!/usr/bin/env python3
"""data_freshness.py — 数据新鲜度校验
所有报告管道在生成输出前必须调用 check_fresh()。
返回 (pass: bool, details: str),如果数据过期则阻止生成操作建议。
用法:
from data_freshness import check_fresh
ok, msg = check_fresh()
if not ok:
print(f"⚠️ 数据过期: {msg}")
sys.exit(0) # 不生成报告
校验规则:
- 盘中 (9:30~15:00)price/live_prices.json 必须在 5 分钟内刷新
- 盘后 (9:30以前/15:00以后):允许最长 120 分钟
- 周末/节假日:跳过校验
"""
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"
def is_market_hours():
now = datetime.now()
if now.weekday() >= 5:
return False, "weekend"
t = now.hour * 60 + now.minute
if 9*60+30 <= t <= 15*60:
return True, "trading"
return False, "closed"
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
# 主指标:DB portfolio_summary.updated_at
try:
pf = read_portfolio()
pf_time = pf.get("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"数据已 {age:.0f} 分钟未更新(阈值 {max_age_min} 分钟)"
return True, f"数据新鲜({age:.0f} 分钟前)"
except Exception as e:
return False, f"DB 读取失败: {e}"
return False, "无法获取数据新鲜度"
if __name__ == "__main__":
ok, msg = check_fresh()
print(f"{'' if ok else ''} {msg}")
+221 -223
View File
@@ -1,223 +1,221 @@
#!/usr/bin/env python3
"""market_watch.py — 行業熱點數據採集,寫入 dashboard data/market.json
數據源優先級:
後端A:東方財富 push2 API(首選,有板塊代碼+實時指數)
後端B:同花順 THS / akshare(降級,有漲跌家數+資金流向)
注意:當前服務器無法連通東方財富API(已被封禁/域名不可達),
實際運行時自動降級到同花順 THS 後端。THS 提供90+行業板塊的
實時漲跌、上漲/下跌家數、淨流入資金等數據,足以滿足需求。
輸出:data/market.json → MoFin Dashboard 市場數據展示
"""
import json
from datetime import datetime
from pathlib import Path
from mofin_db import get_conn, init_all_tables, write_market_snapshot
DATA_DIR = Path(__file__).parent / "data"
# ── 後端A:東方財富 push2 API(首選,有板塊代碼+實時指數) ──
def _fetch_em(url):
"""通用 EM API 請求"""
import urllib.request
req = urllib.request.Request(
url,
headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"}
)
resp = urllib.request.urlopen(req, timeout=10)
return json.loads(resp.read().decode("utf-8"))
def fetch_sector_em():
"""東方財富行業板塊"""
try:
data = _fetch_em(
"https://push2.eastmoney.com/api/qt/clist/get?"
"pn=1&pz=60&po=1&np=1&fields=f2,f3,f4,f12,f14&fs=m:90+t:2"
)
return [{
"name": i["f14"],
"code": i["f12"],
"price": i.get("f2", 0),
"change": i.get("f3", 0),
} for i in data.get("data", {}).get("diff", [])]
except Exception:
return None
def fetch_concept_em():
"""東方財富概念板塊"""
try:
data = _fetch_em(
"https://push2.eastmoney.com/api/qt/clist/get?"
"pn=1&pz=30&po=1&np=1&fields=f2,f3,f4,f12,f14&fs=m:90+t:3"
)
return [{
"name": i["f14"],
"code": i["f12"],
"change": i.get("f3", 0),
} for i in data.get("data", {}).get("diff", [])]
except Exception:
return None
# ── 後端B:同花順 THS / akshare(降級) ──
def fetch_sector_ths():
"""THS 行業板塊(含漲跌家數、資金流向、領漲股)"""
try:
import akshare as ak
df = ak.stock_board_industry_summary_ths()
return [{
"name": r["板块"],
"code": "",
"price": 0,
"change": float(r.get("涨跌幅", 0)),
"volume": float(r.get("总成交量", 0)),
"turnover": float(r.get("总成交额", 0)),
"net_inflow": float(r.get("净流入", 0)),
"up_count": int(r.get("上涨家数", 0)),
"down_count": int(r.get("下跌家数", 0)),
"avg_price": float(r.get("均价", 0)),
"lead_stock": r.get("领涨股", ""),
"lead_stock_change": float(r.get("领涨股-涨跌幅", 0)),
} for _, r in df.iterrows()]
except Exception as e:
print(f"THS行業失敗: {e}", flush=True)
return []
def fetch_concept_ths():
"""THS 概念板塊(僅名稱,無實時漲跌)"""
try:
import akshare as ak
df = ak.stock_board_concept_name_ths()
return [{
"name": r["name"],
"code": str(r.get("code", "")),
"change": 0,
} for _, r in df.iterrows()]
except Exception as e:
print(f"THS概念失敗: {e}", flush=True)
return []
# ── 輔助函數 ──
def get_market_mood(sectors):
if not sectors:
return "unknown"
ratio = sum(1 for s in sectors if s.get("change", 0) > 0) / len(sectors)
return "bullish" if ratio > 0.7 else "neutral" if ratio > 0.4 else "bearish"
def get_market_verdict(up_ratio, mood, sectors):
"""Return (verdict, reason) based on sector data."""
if not sectors:
return "unknown", "数据不足"
if up_ratio < 25:
return "弱势", f"{up_ratio}%板块上涨,{mood}"
elif up_ratio < 40:
return "偏弱", f"{up_ratio}%板块上涨,结构分化"
elif up_ratio < 60:
return "均衡", f"{up_ratio}%板块上涨,涨跌均衡"
else:
return "强势", f"{up_ratio}%板块上涨,整体走强"
def get_hot_sectors(sectors, top_n=3):
"""Return sectors with highest positive change as hot sectors."""
hot = [s for s in sectors if s.get("change", 0) > 1.0]
hot.sort(key=lambda s: s.get("change", 0), reverse=True)
return [{
"name": s["name"],
"change": s.get("change", 0),
"reason": f"板块涨{s.get('change',0):.1f}%"
} for s in hot[:top_n]]
def get_danger_sectors(sectors, top_n=3):
"""Return sectors with lowest (negative) change as danger sectors."""
danger = [s for s in sectors if s.get("change", 0) < -1.0]
danger.sort(key=lambda s: s.get("change", 0))
return [{
"name": s["name"],
"change": s.get("change", 0),
"reason": f"板块跌{s.get('change',0):.1f}%"
} for s in danger[:top_n]]
# ── 主流程 ──
def main():
# 行業板塊:EM → THS → 兜底
sectors = fetch_sector_em()
source = "eastmoney"
if sectors is None:
sectors = fetch_sector_ths()
source = "ths"
# 概念板塊:EM → THS → 空
concepts = fetch_concept_em()
concept_source = "eastmoney"
if concepts is None:
concepts = fetch_concept_ths()
concept_source = "ths"
if not concepts:
concepts = []
concept_source = "unavailable"
# 排序
sorted_sectors = sorted(sectors, key=lambda s: s.get("change", 0), reverse=True)
top_gainers = [s for s in sorted_sectors if s.get("change", 0) > 0][:5]
top_losers = [s for s in reversed(sorted_sectors) if s.get("change", 0) < 0][:3]
# 计算大盘数据
up_ratio = round(
sum(1 for s in sectors if s.get("change", 0) > 0) / max(len(sectors), 1) * 100, 1
)
mood = get_market_mood(sectors)
verdict, verdict_reason = get_market_verdict(up_ratio, mood, sectors)
market_data = {
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M"),
"source": source,
"concept_source": concept_source,
"total_sectors": len(sectors),
"up_ratio": up_ratio,
"mood": mood,
"market_verdict": verdict,
"verdict_reason": verdict_reason,
"hot_sectors": get_hot_sectors(sectors),
"danger_sectors": get_danger_sectors(sectors),
"top_gainers": top_gainers,
"top_losers": top_losers,
"sectors": sectors,
"concepts": concepts,
}
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 双写 ──
conn = get_conn()
init_all_tables(conn)
ok, msg, sid = write_market_snapshot(conn, market_data)
if ok:
print(f"[DB] {msg}", flush=True)
else:
print(f"[DB] 写入失败(JSON 不受影响): {msg}", flush=True)
conn.close()
# 靜默:只寫文件,不輸出到stdout,避免cron推送
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""market_watch.py — 行業熱點數據採集,寫入 dashboard data/market.json
數據源優先級:
後端A:東方財富 push2 API(首選,有板塊代碼+實時指數)
後端B:同花順 THS / akshare(降級,有漲跌家數+資金流向)
注意:當前服務器無法連通東方財富API(已被封禁/域名不可達),
實際運行時自動降級到同花順 THS 後端。THS 提供90+行業板塊的
實時漲跌、上漲/下跌家數、淨流入資金等數據,足以滿足需求。
輸出:data/market.json → MoFin Dashboard 市場數據展示
"""
import json
from datetime import datetime
from pathlib import Path
from mofin_db import get_conn, init_all_tables, write_market_snapshot
DATA_DIR = Path(__file__).parent / "data"
# ── 後端A:東方財富 push2 API(首選,有板塊代碼+實時指數) ──
def _fetch_em(url):
"""通用 EM API 請求"""
import urllib.request
req = urllib.request.Request(
url,
headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"}
)
resp = urllib.request.urlopen(req, timeout=10)
return json.loads(resp.read().decode("utf-8"))
def fetch_sector_em():
"""東方財富行業板塊"""
try:
data = _fetch_em(
"https://push2.eastmoney.com/api/qt/clist/get?"
"pn=1&pz=60&po=1&np=1&fields=f2,f3,f4,f12,f14&fs=m:90+t:2"
)
return [{
"name": i["f14"],
"code": i["f12"],
"price": i.get("f2", 0),
"change": i.get("f3", 0),
} for i in data.get("data", {}).get("diff", [])]
except Exception:
return None
def fetch_concept_em():
"""東方財富概念板塊"""
try:
data = _fetch_em(
"https://push2.eastmoney.com/api/qt/clist/get?"
"pn=1&pz=30&po=1&np=1&fields=f2,f3,f4,f12,f14&fs=m:90+t:3"
)
return [{
"name": i["f14"],
"code": i["f12"],
"change": i.get("f3", 0),
} for i in data.get("data", {}).get("diff", [])]
except Exception:
return None
# ── 後端B:同花順 THS / akshare(降級) ──
def fetch_sector_ths():
"""THS 行業板塊(含漲跌家數、資金流向、領漲股)"""
try:
import akshare as ak
df = ak.stock_board_industry_summary_ths()
return [{
"name": r["板块"],
"code": "",
"price": 0,
"change": float(r.get("涨跌幅", 0)),
"volume": float(r.get("总成交量", 0)),
"turnover": float(r.get("总成交额", 0)),
"net_inflow": float(r.get("净流入", 0)),
"up_count": int(r.get("上涨家数", 0)),
"down_count": int(r.get("下跌家数", 0)),
"avg_price": float(r.get("均价", 0)),
"lead_stock": r.get("领涨股", ""),
"lead_stock_change": float(r.get("领涨股-涨跌幅", 0)),
} for _, r in df.iterrows()]
except Exception as e:
print(f"THS行業失敗: {e}", flush=True)
return []
def fetch_concept_ths():
"""THS 概念板塊(僅名稱,無實時漲跌)"""
try:
import akshare as ak
df = ak.stock_board_concept_name_ths()
return [{
"name": r["name"],
"code": str(r.get("code", "")),
"change": 0,
} for _, r in df.iterrows()]
except Exception as e:
print(f"THS概念失敗: {e}", flush=True)
return []
# ── 輔助函數 ──
def get_market_mood(sectors):
if not sectors:
return "unknown"
ratio = sum(1 for s in sectors if s.get("change", 0) > 0) / len(sectors)
return "bullish" if ratio > 0.7 else "neutral" if ratio > 0.4 else "bearish"
def get_market_verdict(up_ratio, mood, sectors):
"""Return (verdict, reason) based on sector data."""
if not sectors:
return "unknown", "数据不足"
if up_ratio < 25:
return "弱势", f"{up_ratio}%板块上涨,{mood}"
elif up_ratio < 40:
return "偏弱", f"{up_ratio}%板块上涨,结构分化"
elif up_ratio < 60:
return "均衡", f"{up_ratio}%板块上涨,涨跌均衡"
else:
return "强势", f"{up_ratio}%板块上涨,整体走强"
def get_hot_sectors(sectors, top_n=3):
"""Return sectors with highest positive change as hot sectors."""
hot = [s for s in sectors if s.get("change", 0) > 1.0]
hot.sort(key=lambda s: s.get("change", 0), reverse=True)
return [{
"name": s["name"],
"change": s.get("change", 0),
"reason": f"板块涨{s.get('change',0):.1f}%"
} for s in hot[:top_n]]
def get_danger_sectors(sectors, top_n=3):
"""Return sectors with lowest (negative) change as danger sectors."""
danger = [s for s in sectors if s.get("change", 0) < -1.0]
danger.sort(key=lambda s: s.get("change", 0))
return [{
"name": s["name"],
"change": s.get("change", 0),
"reason": f"板块跌{s.get('change',0):.1f}%"
} for s in danger[:top_n]]
# ── 主流程 ──
def main():
# 行業板塊:EM → THS → 兜底
sectors = fetch_sector_em()
source = "eastmoney"
if sectors is None:
sectors = fetch_sector_ths()
source = "ths"
# 概念板塊:EM → THS → 空
concepts = fetch_concept_em()
concept_source = "eastmoney"
if concepts is None:
concepts = fetch_concept_ths()
concept_source = "ths"
if not concepts:
concepts = []
concept_source = "unavailable"
# 排序
sorted_sectors = sorted(sectors, key=lambda s: s.get("change", 0), reverse=True)
top_gainers = [s for s in sorted_sectors if s.get("change", 0) > 0][:5]
top_losers = [s for s in reversed(sorted_sectors) if s.get("change", 0) < 0][:3]
# 计算大盘数据
up_ratio = round(
sum(1 for s in sectors if s.get("change", 0) > 0) / max(len(sectors), 1) * 100, 1
)
mood = get_market_mood(sectors)
verdict, verdict_reason = get_market_verdict(up_ratio, mood, sectors)
market_data = {
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M"),
"source": source,
"concept_source": concept_source,
"total_sectors": len(sectors),
"up_ratio": up_ratio,
"mood": mood,
"market_verdict": verdict,
"verdict_reason": verdict_reason,
"hot_sectors": get_hot_sectors(sectors),
"danger_sectors": get_danger_sectors(sectors),
"top_gainers": top_gainers,
"top_losers": top_losers,
"sectors": sectors,
"concepts": concepts,
}
DATA_DIR.mkdir(parents=True, exist_ok=True)
# ── SQLite 写入(替代 market.json)──
conn = get_conn()
init_all_tables(conn)
ok, msg, sid = write_market_snapshot(conn, market_data)
if ok:
print(f"[DB] {msg}", flush=True)
else:
print(f"[DB] 写入失败(JSON 不受影响): {msg}", flush=True)
conn.close()
# 靜默:只寫文件,不輸出到stdout,避免cron推送
if __name__ == "__main__":
main()
+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 {}
+642 -640
View File
File diff suppressed because it is too large Load Diff
+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"]
+186 -182
View File
@@ -1,182 +1,186 @@
#!/usr/bin/env python3
"""capital_flow_collector.py — 个股资金流数据采集器
每30分钟拉一次持仓+自选的超大单/大单/中单/小单资金流向。
输出到 capital_flow_cache.json 供 price_monitor 和报告使用。
API: push2his.eastmoney.com 个股资金流日线
"""
import json, os, sys, time, urllib.request
from datetime import datetime
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
DATA_DIR = "/home/hmo/web-dashboard/data"
DECISIONS_PATH = f"{DATA_DIR}/decisions.json"
CACHE_PATH = f"{DATA_DIR}/capital_flow_cache.json"
UA = "Mozilla/5.0"
# 限速器:最多5个并发,每请求后强制间隔0.3s
RATE_LIMIT = Semaphore(5)
MIN_INTERVAL = 0.3
_last_req = 0
def _rate_limited_request(url):
"""带速率限制的HTTP GET,用Semaphore控制并发数"""
global _last_req
with RATE_LIMIT:
elapsed = time.time() - _last_req
if elapsed < MIN_INTERVAL:
time.sleep(MIN_INTERVAL - elapsed)
proxy_handler = urllib.request.ProxyHandler({})
opener = urllib.request.build_opener(proxy_handler)
req = Request(url, headers={"User-Agent": UA, "Referer": "https://data.eastmoney.com/"})
try:
resp = opener.open(req, timeout=8)
_last_req = time.time()
return json.loads(resp.read().decode("utf-8"))
except Exception:
return None
# eastmoney secid: 1=上海 0=深圳
def secid(code):
code = str(code).strip()
if code.startswith(("6", "9")):
return f"1.{code}"
return f"0.{code}"
def fetch_flow(code, days=5):
"""拉取个股近N日资金流(带限速+代理绕过)"""
sid = secid(code)
url = f"http://push2his.eastmoney.com/api/qt/stock/fflow/daykline/get?secid={sid}&fields1=f1,f2,f3,f7&fields2=f51,f52,f53,f54,f55,f56,f57&lmt={days}"
data = _rate_limited_request(url)
if not data:
return None
klines = data.get("data", {}).get("klines", [])
if not klines:
return None
result = []
for k in klines:
p = k.split(",")
if len(p) >= 7:
result.append({
"date": p[0],
"main_net": float(p[1]), # 主力净流入(元)
"super_large": float(p[2]), # 超大单净流入(元)
"large": float(p[3]), # 大单净流入(元)
"medium": float(p[4]), # 单净流入(元)
"small": float(p[5]), # 单净流入(元)
})
return result
def fetch_flow_intraday(code):
"""拉取当日分时资金流(用于盘中判断)"""
sid = secid(code)
url = f"http://push2.eastmoney.com/api/qt/stock/fflow/kline/get?secid={sid}&fields1=f1,f2,f3,f7&fields2=f51,f52,f53,f54,f55,f56,f57&klt=1&lmt=120"
try:
resp = urlopen(url, timeout=5)
data = json.loads(resp.read().decode("utf-8"))
klines = data.get("data", {}).get("klines", [])
if not klines:
return None
latest = klines[-1].split(",")
return {
"main_net": float(latest[1]),
"super_large": float(latest[2]),
"large": float(latest[3]),
}
except:
return None
def analyze_flow(flow_data):
"""分析资金流模式"""
if not flow_data or len(flow_data) < 2:
return {}
result = {"alerts": [], "pattern": ""}
# 最近两日对比
d1 = flow_data[-1] # 最新日
d2 = flow_data[-2] # 前一
# 超大单信号
sl1 = d1["super_large"]
sl2 = d2["super_large"]
# 连续形态判断
main_trend = sum(d["main_net"] for d in flow_data[-3:])
sl_trend = sum(d["super_large"] for d in flow_data[-3:])
# 1. 主力连续流入
if main_trend > 50000000 and sl1 > 0 and sl2 > 0:
result["pattern"] = "主力持续流入"
result["alerts"].append("主力连续3日净流入")
# 2. 超大单突然转向(连续流入→流出 或 流出→流入)
if sl1 * sl2 < 0: # 方向反转
if sl1 > 0 and sl2 < 0:
result["pattern"] = "超大单由出转入"
result["alerts"].append("超大单转为净买入(暗示消息即将落地)")
elif sl1 < 0 and sl2 > 0:
result["pattern"] = "超大单由入转出"
result["alerts"].append("超大单转为净卖出(利好出货嫌疑)")
# 3. 价格与资金流背离(缺当前价格作比较,在主脚本中完成)
# 4. 单日暴量
max_sl = max(abs(d["super_large"]) for d in flow_data)
if max_sl == abs(sl1) and abs(sl1) > 100000000:
result["pattern"] = "单日资金暴量"
result["alerts"].append(f"今日超大单异常: {sl1/100000000:.2f}亿")
return result
def main():
codes = set()
# 读取持仓+自选
try:
dec = mo_data.read_decisions()
for d in dec.get("decisions", []):
c = d.get("code", "")
if c:
codes.add(c)
except:
pass
all_flows = {}
# 并行抓取:ThreadPoolExecutor + 内置限速器(Semaphore 5 + 0.3s间隔)
code_list = sorted(codes)
if not code_list:
print("[capital_flow] 无代码需要采集")
return
def fetch_one(code):
flow = fetch_flow(code, days=5)
if flow:
analysis = analyze_flow(flow)
return (code, {
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M"),
"flow": flow,
"analysis": analysis,
})
return (code, None)
with ThreadPoolExecutor(max_workers=5) as pool:
futures = {pool.submit(fetch_one, c): c for c in code_list}
for f in as_completed(futures):
code, result = f.result()
if result:
all_flows[code] = result
# 写缓存
cache = {
"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)
print(f"[capital_flow] {len(all_flows)}/{len(code_list)}只更新完成")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""capital_flow_collector.py — 个股资金流数据采集器
每30分钟拉一次持仓+自选的超大单/大单/中单/小单资金流向。
输出到 capital_flow_cache.json 供 price_monitor 和报告使用。
API: push2his.eastmoney.com 个股资金流日线
"""
import json, os, sys, time, urllib.request
from datetime import datetime
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"
CACHE_PATH = f"{DATA_DIR}/capital_flow_cache.json"
UA = "Mozilla/5.0"
# 限速器:最多5个并发,每请求后强制间隔0.3s
RATE_LIMIT = Semaphore(5)
MIN_INTERVAL = 0.3
_last_req = 0
def _rate_limited_request(url):
"""带速率限制的HTTP GET,用Semaphore控制并发数"""
global _last_req
with RATE_LIMIT:
elapsed = time.time() - _last_req
if elapsed < MIN_INTERVAL:
time.sleep(MIN_INTERVAL - elapsed)
proxy_handler = urllib.request.ProxyHandler({})
opener = urllib.request.build_opener(proxy_handler)
req = Request(url, headers={"User-Agent": UA, "Referer": "https://data.eastmoney.com/"})
try:
resp = opener.open(req, timeout=8)
_last_req = time.time()
return json.loads(resp.read().decode("utf-8"))
except Exception:
return None
# eastmoney secid: 1=上海 0=深圳
def secid(code):
code = str(code).strip()
if code.startswith(("6", "9")):
return f"1.{code}"
return f"0.{code}"
def fetch_flow(code, days=5):
"""拉取个股近N日资金流(带限速+代理绕过)"""
sid = secid(code)
url = f"http://push2his.eastmoney.com/api/qt/stock/fflow/daykline/get?secid={sid}&fields1=f1,f2,f3,f7&fields2=f51,f52,f53,f54,f55,f56,f57&lmt={days}"
data = _rate_limited_request(url)
if not data:
return None
klines = data.get("data", {}).get("klines", [])
if not klines:
return None
result = []
for k in klines:
p = k.split(",")
if len(p) >= 7:
result.append({
"date": p[0],
"main_net": float(p[1]), # 主力净流入(元)
"super_large": float(p[2]), # 大单净流入(元)
"large": float(p[3]), # 单净流入(元)
"medium": float(p[4]), # 单净流入(元)
"small": float(p[5]), # 小单净流入(元)
})
return result
def fetch_flow_intraday(code):
"""拉取当日分时资金流(用于盘中判断)"""
sid = secid(code)
url = f"http://push2.eastmoney.com/api/qt/stock/fflow/kline/get?secid={sid}&fields1=f1,f2,f3,f7&fields2=f51,f52,f53,f54,f55,f56,f57&klt=1&lmt=120"
try:
resp = urlopen(url, timeout=5)
data = json.loads(resp.read().decode("utf-8"))
klines = data.get("data", {}).get("klines", [])
if not klines:
return None
latest = klines[-1].split(",")
return {
"main_net": float(latest[1]),
"super_large": float(latest[2]),
"large": float(latest[3]),
}
except:
return None
def analyze_flow(flow_data):
"""分析资金流模式"""
if not flow_data or len(flow_data) < 2:
return {}
result = {"alerts": [], "pattern": ""}
# 最近两日对比
d1 = flow_data[-1] # 最新
d2 = flow_data[-2] # 前一日
# 超大单信号
sl1 = d1["super_large"]
sl2 = d2["super_large"]
# 连续形态判断
main_trend = sum(d["main_net"] for d in flow_data[-3:])
sl_trend = sum(d["super_large"] for d in flow_data[-3:])
# 1. 主力连续流入
if main_trend > 50000000 and sl1 > 0 and sl2 > 0:
result["pattern"] = "主力持续流入"
result["alerts"].append("主力连续3日净流入")
# 2. 超大单突然转向(连续流入→流出 或 流出→流入)
if sl1 * sl2 < 0: # 方向反转
if sl1 > 0 and sl2 < 0:
result["pattern"] = "超大单由出转入"
result["alerts"].append("超大单转为净买入(暗示消息即将落地)")
elif sl1 < 0 and sl2 > 0:
result["pattern"] = "超大单由入转出"
result["alerts"].append("超大单转为净卖出(利好出货嫌疑)")
# 3. 价格与资金流背离(缺当前价格作比较,在主脚本中完成)
# 4. 单日暴量
max_sl = max(abs(d["super_large"]) for d in flow_data)
if max_sl == abs(sl1) and abs(sl1) > 100000000:
result["pattern"] = "单日资金暴量"
result["alerts"].append(f"今日超大单异常: {sl1/100000000:.2f}亿")
return result
def main():
codes = set()
# 读取持仓+自选
try:
dec = mo_data.read_decisions()
for d in dec.get("decisions", []):
c = d.get("code", "")
if c:
codes.add(c)
except:
pass
all_flows = {}
# 并行抓取:ThreadPoolExecutor + 内置限速器(Semaphore 5 + 0.3s间隔)
code_list = sorted(codes)
if not code_list:
print("[capital_flow] 无代码需要采集")
return
def fetch_one(code):
flow = fetch_flow(code, days=5)
if flow:
analysis = analyze_flow(flow)
return (code, {
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M"),
"flow": flow,
"analysis": analysis,
})
return (code, None)
with ThreadPoolExecutor(max_workers=5) as pool:
futures = {pool.submit(fetch_one, c): c for c in code_list}
for f in as_completed(futures):
code, result = f.result()
if result:
all_flows[code] = result
# 写缓存
cache = {
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M"),
"stocks": all_flows,
}
# 写 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__":
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"