migrate: last 4 JSON files — live_prices, market, mtf_cache, capital_flow → DB
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
@@ -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": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
+270
-30
@@ -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
@@ -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"
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+8
-6
@@ -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
@@ -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()
|
||||
|
||||
@@ -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!")
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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}%)")
|
||||
@@ -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")
|
||||
@@ -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}")
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user