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",
|
"last_updated": "2026-07-03 13:42",
|
||||||
"total_candidates": 5,
|
"total_candidates": 11,
|
||||||
"sectors_analyzed_today": [
|
"sectors_analyzed_today": [
|
||||||
"半导体",
|
"半导体",
|
||||||
"金属新材料",
|
"金属新材料",
|
||||||
@@ -503,6 +503,198 @@
|
|||||||
"drop_reason": null,
|
"drop_reason": null,
|
||||||
"trend_warning": false,
|
"trend_warning": false,
|
||||||
"trend_note": ""
|
"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,
|
"high": 151.69,
|
||||||
"low": 141.26,
|
"low": 141.26,
|
||||||
"volume": 94902364.0
|
"volume": 94902364.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-07-03",
|
||||||
|
"open": 144.89,
|
||||||
|
"close": 143.0,
|
||||||
|
"high": 146.45,
|
||||||
|
"low": 140.4,
|
||||||
|
"volume": 56102965.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"weekly": [
|
"weekly": [
|
||||||
@@ -1254,7 +1262,7 @@
|
|||||||
"volume": 147766189.0
|
"volume": 147766189.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"updated_at": 1783051747.536662
|
"updated_at": 1783056811.510267
|
||||||
},
|
},
|
||||||
"688795": {
|
"688795": {
|
||||||
"daily": [
|
"daily": [
|
||||||
@@ -2217,6 +2225,14 @@
|
|||||||
"high": 687.04,
|
"high": 687.04,
|
||||||
"low": 639.4,
|
"low": 639.4,
|
||||||
"volume": 3085878.0
|
"volume": 3085878.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-07-03",
|
||||||
|
"open": 643.88,
|
||||||
|
"close": 649.03,
|
||||||
|
"high": 664.48,
|
||||||
|
"low": 633.01,
|
||||||
|
"volume": 1568495.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"weekly": [
|
"weekly": [
|
||||||
@@ -2479,7 +2495,7 @@
|
|||||||
"volume": 4788252.0
|
"volume": 4788252.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"updated_at": 1783051740.058768
|
"updated_at": 1783056712.9785304
|
||||||
},
|
},
|
||||||
"000657": {
|
"000657": {
|
||||||
"daily": [
|
"daily": [
|
||||||
@@ -3442,6 +3458,14 @@
|
|||||||
"high": 101.5,
|
"high": 101.5,
|
||||||
"low": 90.68,
|
"low": 90.68,
|
||||||
"volume": 1054773.0
|
"volume": 1054773.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-07-03",
|
||||||
|
"open": 95.0,
|
||||||
|
"close": 92.06,
|
||||||
|
"high": 96.53,
|
||||||
|
"low": 87.88,
|
||||||
|
"volume": 721833.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"weekly": [
|
"weekly": [
|
||||||
@@ -3736,7 +3760,7 @@
|
|||||||
"volume": 1051508.0
|
"volume": 1051508.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"updated_at": 1783051278.4453125
|
"updated_at": 1783056502.3469434
|
||||||
},
|
},
|
||||||
"000700": {
|
"000700": {
|
||||||
"daily": [
|
"daily": [
|
||||||
@@ -4699,6 +4723,14 @@
|
|||||||
"high": 16.87,
|
"high": 16.87,
|
||||||
"low": 15.59,
|
"low": 15.59,
|
||||||
"volume": 1137587.0
|
"volume": 1137587.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-07-03",
|
||||||
|
"open": 16.08,
|
||||||
|
"close": 17.7,
|
||||||
|
"high": 17.88,
|
||||||
|
"low": 16.08,
|
||||||
|
"volume": 1515920.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"weekly": [
|
"weekly": [
|
||||||
@@ -4993,7 +5025,7 @@
|
|||||||
"volume": 1265397.0
|
"volume": 1265397.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"updated_at": 1783051627.982171
|
"updated_at": 1783056719.8422928
|
||||||
},
|
},
|
||||||
"000711": {
|
"000711": {
|
||||||
"daily": [
|
"daily": [
|
||||||
@@ -5956,6 +5988,14 @@
|
|||||||
"high": 5.01,
|
"high": 5.01,
|
||||||
"low": 4.87,
|
"low": 4.87,
|
||||||
"volume": 443973.0
|
"volume": 443973.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-07-03",
|
||||||
|
"open": 5.18,
|
||||||
|
"close": 5.26,
|
||||||
|
"high": 5.26,
|
||||||
|
"low": 5.1,
|
||||||
|
"volume": 1131235.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"weekly": [
|
"weekly": [
|
||||||
@@ -6250,7 +6290,7 @@
|
|||||||
"volume": 496248.0
|
"volume": 496248.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"updated_at": 1783051282.8847363
|
"updated_at": 1783056503.0905344
|
||||||
},
|
},
|
||||||
"001309": {
|
"001309": {
|
||||||
"daily": [
|
"daily": [
|
||||||
@@ -7213,6 +7253,14 @@
|
|||||||
"high": 872.83,
|
"high": 872.83,
|
||||||
"low": 806.0,
|
"low": 806.0,
|
||||||
"volume": 160378.0
|
"volume": 160378.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-07-03",
|
||||||
|
"open": 804.75,
|
||||||
|
"close": 888.1,
|
||||||
|
"high": 892.1,
|
||||||
|
"low": 795.0,
|
||||||
|
"volume": 144171.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"weekly": [
|
"weekly": [
|
||||||
@@ -7507,7 +7555,7 @@
|
|||||||
"volume": 216663.0
|
"volume": 216663.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"updated_at": 1783051286.5401492
|
"updated_at": 1783056668.8085573
|
||||||
},
|
},
|
||||||
"002594": {
|
"002594": {
|
||||||
"daily": [
|
"daily": [
|
||||||
@@ -8470,6 +8518,14 @@
|
|||||||
"high": 85.67,
|
"high": 85.67,
|
||||||
"low": 81.9,
|
"low": 81.9,
|
||||||
"volume": 825046.0
|
"volume": 825046.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-07-03",
|
||||||
|
"open": 83.73,
|
||||||
|
"close": 86.66,
|
||||||
|
"high": 87.28,
|
||||||
|
"low": 83.6,
|
||||||
|
"volume": 477402.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"weekly": [
|
"weekly": [
|
||||||
@@ -8764,7 +8820,7 @@
|
|||||||
"volume": 934285.0
|
"volume": 934285.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"updated_at": 1783051286.9981902
|
"updated_at": 1783056675.2141986
|
||||||
},
|
},
|
||||||
"00700": {
|
"00700": {
|
||||||
"daily": [
|
"daily": [
|
||||||
@@ -9727,6 +9783,14 @@
|
|||||||
"high": 447.0,
|
"high": 447.0,
|
||||||
"low": 429.4,
|
"low": 429.4,
|
||||||
"volume": 40905100.0
|
"volume": 40905100.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-07-03",
|
||||||
|
"open": 433.0,
|
||||||
|
"close": 432.6,
|
||||||
|
"high": 445.8,
|
||||||
|
"low": 432.4,
|
||||||
|
"volume": 15135668.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"weekly": [
|
"weekly": [
|
||||||
@@ -10029,7 +10093,7 @@
|
|||||||
"volume": 13032847.0
|
"volume": 13032847.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"updated_at": 1783051633.4890287
|
"updated_at": 1783056724.989126
|
||||||
},
|
},
|
||||||
"00968": {
|
"00968": {
|
||||||
"daily": [
|
"daily": [
|
||||||
@@ -12257,6 +12321,14 @@
|
|||||||
"high": 84.2,
|
"high": 84.2,
|
||||||
"low": 78.55,
|
"low": 78.55,
|
||||||
"volume": 178219425.0
|
"volume": 178219425.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-07-03",
|
||||||
|
"open": 80.0,
|
||||||
|
"close": 78.95,
|
||||||
|
"high": 81.45,
|
||||||
|
"low": 77.35,
|
||||||
|
"volume": 71251123.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"weekly": [
|
"weekly": [
|
||||||
@@ -12559,7 +12631,7 @@
|
|||||||
"volume": 60114819.0
|
"volume": 60114819.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"updated_at": 1783051638.5061805
|
"updated_at": 1783056730.0097694
|
||||||
},
|
},
|
||||||
"01070": {
|
"01070": {
|
||||||
"daily": [
|
"daily": [
|
||||||
@@ -14787,6 +14859,14 @@
|
|||||||
"high": 40.4,
|
"high": 40.4,
|
||||||
"low": 39.16,
|
"low": 39.16,
|
||||||
"volume": 16452660.0
|
"volume": 16452660.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-07-03",
|
||||||
|
"open": 39.7,
|
||||||
|
"close": 39.9,
|
||||||
|
"high": 40.5,
|
||||||
|
"low": 39.62,
|
||||||
|
"volume": 6092471.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"weekly": [
|
"weekly": [
|
||||||
@@ -15089,7 +15169,7 @@
|
|||||||
"volume": 2870057.0
|
"volume": 2870057.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"updated_at": 1783051643.521601
|
"updated_at": 1783056735.0304337
|
||||||
},
|
},
|
||||||
"01211": {
|
"01211": {
|
||||||
"daily": [
|
"daily": [
|
||||||
@@ -16052,6 +16132,14 @@
|
|||||||
"high": 79.6,
|
"high": 79.6,
|
||||||
"low": 74.95,
|
"low": 74.95,
|
||||||
"volume": 54549471.0
|
"volume": 54549471.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-07-03",
|
||||||
|
"open": 81.2,
|
||||||
|
"close": 82.85,
|
||||||
|
"high": 83.35,
|
||||||
|
"low": 80.0,
|
||||||
|
"volume": 25755799.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"weekly": [
|
"weekly": [
|
||||||
@@ -16354,7 +16442,7 @@
|
|||||||
"volume": 13286402.0
|
"volume": 13286402.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"updated_at": 1783051648.5394585
|
"updated_at": 1783056740.0491588
|
||||||
},
|
},
|
||||||
"01478": {
|
"01478": {
|
||||||
"daily": [
|
"daily": [
|
||||||
@@ -17317,6 +17405,14 @@
|
|||||||
"high": 7.08,
|
"high": 7.08,
|
||||||
"low": 6.72,
|
"low": 6.72,
|
||||||
"volume": 6257000.0
|
"volume": 6257000.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-07-03",
|
||||||
|
"open": 6.72,
|
||||||
|
"close": 7.04,
|
||||||
|
"high": 7.1,
|
||||||
|
"low": 6.72,
|
||||||
|
"volume": 2981000.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"weekly": [
|
"weekly": [
|
||||||
@@ -17619,7 +17715,7 @@
|
|||||||
"volume": 1618000.0
|
"volume": 1618000.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"updated_at": 1783051653.5516405
|
"updated_at": 1783056745.0709894
|
||||||
},
|
},
|
||||||
"01888": {
|
"01888": {
|
||||||
"daily": [
|
"daily": [
|
||||||
@@ -18582,6 +18678,14 @@
|
|||||||
"high": 90.1,
|
"high": 90.1,
|
||||||
"low": 79.85,
|
"low": 79.85,
|
||||||
"volume": 92637274.0
|
"volume": 92637274.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-07-03",
|
||||||
|
"open": 81.0,
|
||||||
|
"close": 85.6,
|
||||||
|
"high": 88.9,
|
||||||
|
"low": 80.6,
|
||||||
|
"volume": 57017840.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"weekly": [
|
"weekly": [
|
||||||
@@ -18884,7 +18988,7 @@
|
|||||||
"volume": 29941901.0
|
"volume": 29941901.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"updated_at": 1783051658.5684023
|
"updated_at": 1783056750.0898428
|
||||||
},
|
},
|
||||||
"02202": {
|
"02202": {
|
||||||
"daily": [
|
"daily": [
|
||||||
@@ -19847,6 +19951,14 @@
|
|||||||
"high": 2.27,
|
"high": 2.27,
|
||||||
"low": 2.18,
|
"low": 2.18,
|
||||||
"volume": 39512590.0
|
"volume": 39512590.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-07-03",
|
||||||
|
"open": 2.25,
|
||||||
|
"close": 2.31,
|
||||||
|
"high": 2.35,
|
||||||
|
"low": 2.23,
|
||||||
|
"volume": 18235400.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"weekly": [
|
"weekly": [
|
||||||
@@ -20149,7 +20261,7 @@
|
|||||||
"volume": 19786580.0
|
"volume": 19786580.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"updated_at": 1783051663.581557
|
"updated_at": 1783056755.496929
|
||||||
},
|
},
|
||||||
"02318": {
|
"02318": {
|
||||||
"daily": [
|
"daily": [
|
||||||
@@ -27437,6 +27549,14 @@
|
|||||||
"high": 229.2,
|
"high": 229.2,
|
||||||
"low": 198.0,
|
"low": 198.0,
|
||||||
"volume": 40078361.0
|
"volume": 40078361.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-07-03",
|
||||||
|
"open": 191.2,
|
||||||
|
"close": 207.2,
|
||||||
|
"high": 210.6,
|
||||||
|
"low": 190.0,
|
||||||
|
"volume": 14255822.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"weekly": [
|
"weekly": [
|
||||||
@@ -27739,7 +27859,7 @@
|
|||||||
"volume": 15066251.0
|
"volume": 15066251.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"updated_at": 1783051668.878092
|
"updated_at": 1783056760.5189984
|
||||||
},
|
},
|
||||||
"09868": {
|
"09868": {
|
||||||
"daily": [
|
"daily": [
|
||||||
@@ -31232,6 +31352,14 @@
|
|||||||
"high": 14.6,
|
"high": 14.6,
|
||||||
"low": 14.1,
|
"low": 14.1,
|
||||||
"volume": 155998.0
|
"volume": 155998.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-07-03",
|
||||||
|
"open": 14.18,
|
||||||
|
"close": 14.4,
|
||||||
|
"high": 14.53,
|
||||||
|
"low": 14.16,
|
||||||
|
"volume": 102393.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"weekly": [
|
"weekly": [
|
||||||
@@ -31526,7 +31654,7 @@
|
|||||||
"volume": 230937.0
|
"volume": 230937.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"updated_at": 1783051674.5847087
|
"updated_at": 1783056765.5414958
|
||||||
},
|
},
|
||||||
"300124": {
|
"300124": {
|
||||||
"daily": [
|
"daily": [
|
||||||
@@ -32489,6 +32617,14 @@
|
|||||||
"high": 71.79,
|
"high": 71.79,
|
||||||
"low": 67.31,
|
"low": 67.31,
|
||||||
"volume": 703358.0
|
"volume": 703358.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-07-03",
|
||||||
|
"open": 67.5,
|
||||||
|
"close": 74.14,
|
||||||
|
"high": 74.23,
|
||||||
|
"low": 67.4,
|
||||||
|
"volume": 691416.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"weekly": [
|
"weekly": [
|
||||||
@@ -32783,7 +32919,7 @@
|
|||||||
"volume": 722493.0
|
"volume": 722493.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"updated_at": 1783051344.573262
|
"updated_at": 1783056692.3502543
|
||||||
},
|
},
|
||||||
"300308": {
|
"300308": {
|
||||||
"daily": [
|
"daily": [
|
||||||
@@ -33746,6 +33882,14 @@
|
|||||||
"high": 1198.0,
|
"high": 1198.0,
|
||||||
"low": 1127.4,
|
"low": 1127.4,
|
||||||
"volume": 317620.0
|
"volume": 317620.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-07-03",
|
||||||
|
"open": 1130.0,
|
||||||
|
"close": 1158.88,
|
||||||
|
"high": 1188.01,
|
||||||
|
"low": 1124.0,
|
||||||
|
"volume": 230014.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"weekly": [
|
"weekly": [
|
||||||
@@ -34040,7 +34184,7 @@
|
|||||||
"volume": 389058.0
|
"volume": 389058.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"updated_at": 1783051680.1109805
|
"updated_at": 1783056770.5614996
|
||||||
},
|
},
|
||||||
"300548": {
|
"300548": {
|
||||||
"daily": [
|
"daily": [
|
||||||
@@ -35003,6 +35147,14 @@
|
|||||||
"high": 245.0,
|
"high": 245.0,
|
||||||
"low": 220.0,
|
"low": 220.0,
|
||||||
"volume": 174041.0
|
"volume": 174041.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-07-03",
|
||||||
|
"open": 220.0,
|
||||||
|
"close": 228.99,
|
||||||
|
"high": 231.55,
|
||||||
|
"low": 218.99,
|
||||||
|
"volume": 102806.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"weekly": [
|
"weekly": [
|
||||||
@@ -35297,7 +35449,7 @@
|
|||||||
"volume": 242727.0
|
"volume": 242727.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"updated_at": 1783051685.6375031
|
"updated_at": 1783056775.5768287
|
||||||
},
|
},
|
||||||
"300750": {
|
"300750": {
|
||||||
"daily": [
|
"daily": [
|
||||||
@@ -36260,6 +36412,14 @@
|
|||||||
"high": 390.99,
|
"high": 390.99,
|
||||||
"low": 380.39,
|
"low": 380.39,
|
||||||
"volume": 340754.0
|
"volume": 340754.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-07-03",
|
||||||
|
"open": 381.96,
|
||||||
|
"close": 381.17,
|
||||||
|
"high": 387.95,
|
||||||
|
"low": 380.55,
|
||||||
|
"volume": 157731.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"weekly": [
|
"weekly": [
|
||||||
@@ -36554,7 +36714,7 @@
|
|||||||
"volume": 551212.0
|
"volume": 551212.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"updated_at": 1783051690.893728
|
"updated_at": 1783056780.5942247
|
||||||
},
|
},
|
||||||
"301308": {
|
"301308": {
|
||||||
"daily": [
|
"daily": [
|
||||||
@@ -37517,6 +37677,14 @@
|
|||||||
"high": 636.99,
|
"high": 636.99,
|
||||||
"low": 592.0,
|
"low": 592.0,
|
||||||
"volume": 208005.0
|
"volume": 208005.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-07-03",
|
||||||
|
"open": 589.0,
|
||||||
|
"close": 634.0,
|
||||||
|
"high": 646.85,
|
||||||
|
"low": 574.1,
|
||||||
|
"volume": 153435.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"weekly": [
|
"weekly": [
|
||||||
@@ -37811,7 +37979,7 @@
|
|||||||
"volume": 296230.0
|
"volume": 296230.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"updated_at": 1783051380.6922731
|
"updated_at": 1783056696.8459098
|
||||||
},
|
},
|
||||||
"518880": {
|
"518880": {
|
||||||
"daily": [
|
"daily": [
|
||||||
@@ -38774,6 +38942,14 @@
|
|||||||
"high": 8.484,
|
"high": 8.484,
|
||||||
"low": 8.397,
|
"low": 8.397,
|
||||||
"volume": 5149790.0
|
"volume": 5149790.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-07-03",
|
||||||
|
"open": 8.71,
|
||||||
|
"close": 8.68,
|
||||||
|
"high": 8.73,
|
||||||
|
"low": 8.68,
|
||||||
|
"volume": 2689807.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"weekly": [
|
"weekly": [
|
||||||
@@ -39068,7 +39244,7 @@
|
|||||||
"volume": 3915247.0
|
"volume": 3915247.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"updated_at": 1783051695.915503
|
"updated_at": 1783056785.6140218
|
||||||
},
|
},
|
||||||
"600519": {
|
"600519": {
|
||||||
"daily": [
|
"daily": [
|
||||||
@@ -40031,6 +40207,14 @@
|
|||||||
"high": 1215.52,
|
"high": 1215.52,
|
||||||
"low": 1190.51,
|
"low": 1190.51,
|
||||||
"volume": 50870.0
|
"volume": 50870.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-07-03",
|
||||||
|
"open": 1205.24,
|
||||||
|
"close": 1189.52,
|
||||||
|
"high": 1210.14,
|
||||||
|
"low": 1188.0,
|
||||||
|
"volume": 23088.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"weekly": [
|
"weekly": [
|
||||||
@@ -40325,7 +40509,7 @@
|
|||||||
"volume": 64803.0
|
"volume": 64803.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"updated_at": 1783051561.7579694
|
"updated_at": 1783056699.260756
|
||||||
},
|
},
|
||||||
"600563": {
|
"600563": {
|
||||||
"daily": [
|
"daily": [
|
||||||
@@ -41288,6 +41472,14 @@
|
|||||||
"high": 173.3,
|
"high": 173.3,
|
||||||
"low": 164.3,
|
"low": 164.3,
|
||||||
"volume": 112859.0
|
"volume": 112859.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-07-03",
|
||||||
|
"open": 160.27,
|
||||||
|
"close": 159.59,
|
||||||
|
"high": 163.68,
|
||||||
|
"low": 153.52,
|
||||||
|
"volume": 75944.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"weekly": [
|
"weekly": [
|
||||||
@@ -41582,7 +41774,7 @@
|
|||||||
"volume": 180947.0
|
"volume": 180947.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"updated_at": 1783051700.9725504
|
"updated_at": 1783056791.4344049
|
||||||
},
|
},
|
||||||
"601318": {
|
"601318": {
|
||||||
"daily": [
|
"daily": [
|
||||||
@@ -42545,6 +42737,14 @@
|
|||||||
"high": 50.2,
|
"high": 50.2,
|
||||||
"low": 48.8,
|
"low": 48.8,
|
||||||
"volume": 920130.0
|
"volume": 920130.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-07-03",
|
||||||
|
"open": 49.5,
|
||||||
|
"close": 48.43,
|
||||||
|
"high": 49.78,
|
||||||
|
"low": 48.41,
|
||||||
|
"volume": 556535.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"weekly": [
|
"weekly": [
|
||||||
@@ -42839,7 +43039,7 @@
|
|||||||
"volume": 1746202.0
|
"volume": 1746202.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"updated_at": 1783051575.149798
|
"updated_at": 1783056700.660319
|
||||||
},
|
},
|
||||||
"601899": {
|
"601899": {
|
||||||
"daily": [
|
"daily": [
|
||||||
@@ -43802,6 +44002,14 @@
|
|||||||
"high": 26.96,
|
"high": 26.96,
|
||||||
"low": 25.52,
|
"low": 25.52,
|
||||||
"volume": 5067417.0
|
"volume": 5067417.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-07-03",
|
||||||
|
"open": 27.5,
|
||||||
|
"close": 27.93,
|
||||||
|
"high": 28.3,
|
||||||
|
"low": 27.41,
|
||||||
|
"volume": 4183914.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"weekly": [
|
"weekly": [
|
||||||
@@ -44096,7 +44304,7 @@
|
|||||||
"volume": 4780454.0
|
"volume": 4780454.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"updated_at": 1783051705.9927075
|
"updated_at": 1783056796.4605315
|
||||||
},
|
},
|
||||||
"688411": {
|
"688411": {
|
||||||
"daily": [
|
"daily": [
|
||||||
@@ -45059,6 +45267,14 @@
|
|||||||
"high": 272.99,
|
"high": 272.99,
|
||||||
"low": 254.8,
|
"low": 254.8,
|
||||||
"volume": 6368531.0
|
"volume": 6368531.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-07-03",
|
||||||
|
"open": 255.8,
|
||||||
|
"close": 255.99,
|
||||||
|
"high": 261.48,
|
||||||
|
"low": 251.0,
|
||||||
|
"volume": 2572727.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"weekly": [
|
"weekly": [
|
||||||
@@ -45353,7 +45569,7 @@
|
|||||||
"volume": 13672788.0
|
"volume": 13672788.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"updated_at": 1783051711.208435
|
"updated_at": 1783056801.4764674
|
||||||
},
|
},
|
||||||
"688630": {
|
"688630": {
|
||||||
"daily": [
|
"daily": [
|
||||||
@@ -46316,6 +46532,14 @@
|
|||||||
"high": 499.95,
|
"high": 499.95,
|
||||||
"low": 464.8,
|
"low": 464.8,
|
||||||
"volume": 5841815.0
|
"volume": 5841815.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-07-03",
|
||||||
|
"open": 467.98,
|
||||||
|
"close": 497.0,
|
||||||
|
"high": 502.0,
|
||||||
|
"low": 444.55,
|
||||||
|
"volume": 4259432.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"weekly": [
|
"weekly": [
|
||||||
@@ -46610,7 +46834,7 @@
|
|||||||
"volume": 9660790.0
|
"volume": 9660790.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"updated_at": 1783051587.7339969
|
"updated_at": 1783056704.6600807
|
||||||
},
|
},
|
||||||
"688639": {
|
"688639": {
|
||||||
"daily": [
|
"daily": [
|
||||||
@@ -47573,6 +47797,14 @@
|
|||||||
"high": 17.4,
|
"high": 17.4,
|
||||||
"low": 15.98,
|
"low": 15.98,
|
||||||
"volume": 9065955.0
|
"volume": 9065955.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-07-03",
|
||||||
|
"open": 17.06,
|
||||||
|
"close": 16.71,
|
||||||
|
"high": 17.2,
|
||||||
|
"low": 16.52,
|
||||||
|
"volume": 4669797.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"weekly": [
|
"weekly": [
|
||||||
@@ -47867,7 +48099,7 @@
|
|||||||
"volume": 13996588.0
|
"volume": 13996588.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"updated_at": 1783051733.2346125
|
"updated_at": 1783056806.4868052
|
||||||
},
|
},
|
||||||
"688802": {
|
"688802": {
|
||||||
"daily": [
|
"daily": [
|
||||||
@@ -48830,6 +49062,14 @@
|
|||||||
"high": 784.0,
|
"high": 784.0,
|
||||||
"low": 721.0,
|
"low": 721.0,
|
||||||
"volume": 2036024.0
|
"volume": 2036024.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2026-07-03",
|
||||||
|
"open": 731.34,
|
||||||
|
"close": 741.27,
|
||||||
|
"high": 757.88,
|
||||||
|
"low": 713.0,
|
||||||
|
"volume": 1323959.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"weekly": [
|
"weekly": [
|
||||||
@@ -49092,6 +49332,6 @@
|
|||||||
"volume": 3202146.0
|
"volume": 3202146.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"updated_at": 1783051743.6900556
|
"updated_at": 1783056714.5052435
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+73
-73
@@ -5,9 +5,9 @@
|
|||||||
"name": "中际旭创",
|
"name": "中际旭创",
|
||||||
"shares": 100,
|
"shares": 100,
|
||||||
"cost": 1316.53,
|
"cost": 1316.53,
|
||||||
"price": 1157.97,
|
"price": 1145.0,
|
||||||
"market_value": 115797.0,
|
"market_value": 114101.0,
|
||||||
"change_pct": 1.31,
|
"change_pct": 0.17,
|
||||||
"currency": "CNY",
|
"currency": "CNY",
|
||||||
"position_pct": 15.27,
|
"position_pct": 15.27,
|
||||||
"_currency": "CNY"
|
"_currency": "CNY"
|
||||||
@@ -18,32 +18,32 @@
|
|||||||
"shares": 500,
|
"shares": 500,
|
||||||
"cost": 228.65,
|
"cost": 228.65,
|
||||||
"price": 178.26,
|
"price": 178.26,
|
||||||
"market_value": 102700.0,
|
"market_value": 89910.0,
|
||||||
"change_pct": 3.859,
|
"change_pct": 3.859,
|
||||||
"currency": "HKD",
|
"currency": "CNY",
|
||||||
"position_pct": 13.47,
|
"position_pct": 13.47,
|
||||||
"_currency": "HKD"
|
"_currency": "CNY"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"code": "01478",
|
"code": "01478",
|
||||||
"name": "丘钛科技",
|
"name": "丘钛科技",
|
||||||
"shares": 11000,
|
"shares": 11000,
|
||||||
"cost": 11.68,
|
"cost": 11.68,
|
||||||
"price": 6.11,
|
"price": 6.1,
|
||||||
"market_value": 77550.0,
|
"market_value": 67210.0,
|
||||||
"change_pct": 4.911,
|
"change_pct": 4.762,
|
||||||
"currency": "HKD",
|
"currency": "CNY",
|
||||||
"position_pct": 7.97,
|
"position_pct": 7.97,
|
||||||
"_currency": "HKD"
|
"_currency": "CNY"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"code": "601899",
|
"code": "601899",
|
||||||
"name": "紫金矿业",
|
"name": "紫金矿业",
|
||||||
"shares": 2400,
|
"shares": 2400,
|
||||||
"cost": 39.89,
|
"cost": 39.89,
|
||||||
"price": 28.04,
|
"price": 27.8,
|
||||||
"market_value": 67296.0,
|
"market_value": 66864.0,
|
||||||
"change_pct": 6.62,
|
"change_pct": 5.7,
|
||||||
"currency": "CNY",
|
"currency": "CNY",
|
||||||
"position_pct": 7.34,
|
"position_pct": 7.34,
|
||||||
"_currency": "CNY"
|
"_currency": "CNY"
|
||||||
@@ -53,9 +53,9 @@
|
|||||||
"name": "海博思创",
|
"name": "海博思创",
|
||||||
"shares": 200,
|
"shares": 200,
|
||||||
"cost": 266.95,
|
"cost": 266.95,
|
||||||
"price": 257.52,
|
"price": 254.75,
|
||||||
"market_value": 51504.0,
|
"market_value": 50842.0,
|
||||||
"change_pct": 0.71,
|
"change_pct": -0.38,
|
||||||
"currency": "CNY",
|
"currency": "CNY",
|
||||||
"position_pct": 6.31,
|
"position_pct": 6.31,
|
||||||
"_currency": "CNY"
|
"_currency": "CNY"
|
||||||
@@ -65,9 +65,9 @@
|
|||||||
"name": "中芯国际",
|
"name": "中芯国际",
|
||||||
"shares": 300,
|
"shares": 300,
|
||||||
"cost": 126.07,
|
"cost": 126.07,
|
||||||
"price": 143.65,
|
"price": 142.97,
|
||||||
"market_value": 43095.0,
|
"market_value": 42600.0,
|
||||||
"change_pct": -0.31,
|
"change_pct": -0.78,
|
||||||
"currency": "CNY",
|
"currency": "CNY",
|
||||||
"position_pct": 5.44,
|
"position_pct": 5.44,
|
||||||
"_currency": "CNY"
|
"_currency": "CNY"
|
||||||
@@ -78,20 +78,20 @@
|
|||||||
"shares": 500,
|
"shares": 500,
|
||||||
"cost": 76.5,
|
"cost": 76.5,
|
||||||
"price": 74.17,
|
"price": 74.17,
|
||||||
"market_value": 42600.0,
|
"market_value": 37325.0,
|
||||||
"change_pct": 2.088,
|
"change_pct": 2.088,
|
||||||
"currency": "HKD",
|
"currency": "CNY",
|
||||||
"position_pct": 5.28,
|
"position_pct": 5.28,
|
||||||
"_currency": "HKD"
|
"_currency": "CNY"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"code": "688639",
|
"code": "688639",
|
||||||
"name": "华恒生物",
|
"name": "华恒生物",
|
||||||
"shares": 2800,
|
"shares": 2800,
|
||||||
"cost": 21.51,
|
"cost": 21.51,
|
||||||
"price": 16.69,
|
"price": 16.72,
|
||||||
"market_value": 46732.0,
|
"market_value": 46900.0,
|
||||||
"change_pct": -1.53,
|
"change_pct": -1.36,
|
||||||
"currency": "CNY",
|
"currency": "CNY",
|
||||||
"position_pct": 5.25,
|
"position_pct": 5.25,
|
||||||
"_currency": "CNY"
|
"_currency": "CNY"
|
||||||
@@ -101,9 +101,9 @@
|
|||||||
"name": "宁德时代",
|
"name": "宁德时代",
|
||||||
"shares": 100,
|
"shares": 100,
|
||||||
"cost": 401.78,
|
"cost": 401.78,
|
||||||
"price": 384.16,
|
"price": 380.24,
|
||||||
"market_value": 38416.0,
|
"market_value": 38032.0,
|
||||||
"change_pct": 0.47,
|
"change_pct": -0.55,
|
||||||
"currency": "CNY",
|
"currency": "CNY",
|
||||||
"position_pct": 4.64,
|
"position_pct": 4.64,
|
||||||
"_currency": "CNY"
|
"_currency": "CNY"
|
||||||
@@ -113,57 +113,57 @@
|
|||||||
"name": "比亚迪股份",
|
"name": "比亚迪股份",
|
||||||
"shares": 600,
|
"shares": 600,
|
||||||
"cost": 90.92,
|
"cost": 90.92,
|
||||||
"price": 71.66,
|
"price": 71.83,
|
||||||
"market_value": 49560.0,
|
"market_value": 43044.0,
|
||||||
"change_pct": 5.556,
|
"change_pct": 5.811,
|
||||||
"currency": "HKD",
|
"currency": "CNY",
|
||||||
"position_pct": 4.62,
|
"position_pct": 4.62,
|
||||||
"_currency": "HKD"
|
"_currency": "CNY"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"code": "02202",
|
"code": "02202",
|
||||||
"name": "万科企业",
|
"name": "万科企业",
|
||||||
"shares": 19700,
|
"shares": 19700,
|
||||||
"cost": 4.05,
|
"cost": 4.05,
|
||||||
"price": 2.02,
|
"price": 1.99,
|
||||||
"market_value": 45704.0,
|
"market_value": 39203.0,
|
||||||
"change_pct": 4.484,
|
"change_pct": 2.69,
|
||||||
"currency": "HKD",
|
"currency": "CNY",
|
||||||
"position_pct": 4.6,
|
"position_pct": 4.6,
|
||||||
"_currency": "HKD"
|
"_currency": "CNY"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"code": "00700",
|
"code": "00700",
|
||||||
"name": "腾讯",
|
"name": "腾讯",
|
||||||
"shares": 100,
|
"shares": 100,
|
||||||
"cost": null,
|
"cost": null,
|
||||||
"price": 379.05,
|
"price": 376.8,
|
||||||
"market_value": 43700.0,
|
"market_value": 37541.0,
|
||||||
"change_pct": 1.627,
|
"change_pct": 1.023,
|
||||||
"currency": "HKD",
|
"currency": "CNY",
|
||||||
"position_pct": null,
|
"position_pct": null,
|
||||||
"_currency": "HKD"
|
"_currency": "CNY"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"code": "00981",
|
"code": "00981",
|
||||||
"name": "中芯国际",
|
"name": "中芯国际",
|
||||||
"shares": 500,
|
"shares": 500,
|
||||||
"cost": 65.84,
|
"cost": 65.84,
|
||||||
"price": 69.1,
|
"price": 68.41,
|
||||||
"market_value": 39825.0,
|
"market_value": 34440.0,
|
||||||
"change_pct": -0.871,
|
"change_pct": -1.866,
|
||||||
"currency": "HKD",
|
"currency": "CNY",
|
||||||
"position_pct": 4.2,
|
"position_pct": 4.2,
|
||||||
"_currency": "HKD"
|
"_currency": "CNY"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"code": "300548",
|
"code": "300548",
|
||||||
"name": "长芯博创",
|
"name": "长芯博创",
|
||||||
"shares": 100,
|
"shares": 100,
|
||||||
"cost": 231.46,
|
"cost": 231.46,
|
||||||
"price": 227.0,
|
"price": 226.5,
|
||||||
"market_value": 22700.0,
|
"market_value": 22599.0,
|
||||||
"change_pct": 2.25,
|
"change_pct": 2.03,
|
||||||
"currency": "CNY",
|
"currency": "CNY",
|
||||||
"position_pct": 3.2,
|
"position_pct": 3.2,
|
||||||
"_currency": "CNY"
|
"_currency": "CNY"
|
||||||
@@ -173,9 +173,9 @@
|
|||||||
"name": "黄金ETF华安",
|
"name": "黄金ETF华安",
|
||||||
"shares": 2400,
|
"shares": 2400,
|
||||||
"cost": 12.19,
|
"cost": 12.19,
|
||||||
"price": 8.69,
|
"price": 8.67,
|
||||||
"market_value": 20856.0,
|
"market_value": 20832.0,
|
||||||
"change_pct": 2.49,
|
"change_pct": 2.32,
|
||||||
"currency": "CNY",
|
"currency": "CNY",
|
||||||
"position_pct": 2.45,
|
"position_pct": 2.45,
|
||||||
"_currency": "CNY"
|
"_currency": "CNY"
|
||||||
@@ -185,9 +185,9 @@
|
|||||||
"name": "中科电气",
|
"name": "中科电气",
|
||||||
"shares": 1400,
|
"shares": 1400,
|
||||||
"cost": 22.29,
|
"cost": 22.29,
|
||||||
"price": 14.3,
|
"price": 14.38,
|
||||||
"market_value": 20020.0,
|
"market_value": 20062.0,
|
||||||
"change_pct": 0.92,
|
"change_pct": 1.48,
|
||||||
"currency": "CNY",
|
"currency": "CNY",
|
||||||
"position_pct": 2.42,
|
"position_pct": 2.42,
|
||||||
"_currency": "CNY"
|
"_currency": "CNY"
|
||||||
@@ -197,9 +197,9 @@
|
|||||||
"name": "模塑科技",
|
"name": "模塑科技",
|
||||||
"shares": 1400,
|
"shares": 1400,
|
||||||
"cost": 14.83,
|
"cost": 14.83,
|
||||||
"price": 17.37,
|
"price": 17.62,
|
||||||
"market_value": 24318.0,
|
"market_value": 25088.0,
|
||||||
"change_pct": 2.96,
|
"change_pct": 4.45,
|
||||||
"currency": "CNY",
|
"currency": "CNY",
|
||||||
"position_pct": 2.41,
|
"position_pct": 2.41,
|
||||||
"_currency": "CNY"
|
"_currency": "CNY"
|
||||||
@@ -209,9 +209,9 @@
|
|||||||
"name": "法拉电子",
|
"name": "法拉电子",
|
||||||
"shares": 100,
|
"shares": 100,
|
||||||
"cost": 147.18,
|
"cost": 147.18,
|
||||||
"price": 161.71,
|
"price": 160.0,
|
||||||
"market_value": 16171.0,
|
"market_value": 15900.0,
|
||||||
"change_pct": -1.58,
|
"change_pct": -2.62,
|
||||||
"currency": "CNY",
|
"currency": "CNY",
|
||||||
"position_pct": 2.3,
|
"position_pct": 2.3,
|
||||||
"_currency": "CNY"
|
"_currency": "CNY"
|
||||||
@@ -221,20 +221,20 @@
|
|||||||
"name": "中国神华",
|
"name": "中国神华",
|
||||||
"shares": 500,
|
"shares": 500,
|
||||||
"cost": 39.79,
|
"cost": 39.79,
|
||||||
"price": 34.84,
|
"price": 34.59,
|
||||||
"market_value": 20090.0,
|
"market_value": 17305.0,
|
||||||
"change_pct": 1.465,
|
"change_pct": 0.758,
|
||||||
"currency": "HKD",
|
"currency": "CNY",
|
||||||
"position_pct": 2.14,
|
"position_pct": 2.14,
|
||||||
"_currency": "HKD"
|
"_currency": "CNY"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"total_assets": 864781.03,
|
"total_assets": 860913.13,
|
||||||
"total_mv": 784305.03,
|
"total_mv": 780437.13,
|
||||||
"stock_value": null,
|
"stock_value": null,
|
||||||
"cash": 80476.0,
|
"cash": 80476.0,
|
||||||
"frozen_cash": 0.0,
|
"frozen_cash": 0.0,
|
||||||
"position_pct": 90.69,
|
"position_pct": 90.65,
|
||||||
"currency": "CNY",
|
"currency": "CNY",
|
||||||
"updated_at": "2026-07-03 12:10"
|
"updated_at": "2026-07-03 13:42"
|
||||||
}
|
}
|
||||||
@@ -9109,6 +9109,26 @@
|
|||||||
"event_label": "止盈区间",
|
"event_label": "止盈区间",
|
||||||
"timestamp": "2026-07-03T11:54:31.001389",
|
"timestamp": "2026-07-03T11:54:31.001389",
|
||||||
"date": "2026-07-03"
|
"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",
|
"date": "2026-07-03",
|
||||||
"high": 1215.52,
|
"high": 1215.52,
|
||||||
"low": 1190.5,
|
"low": 1188.0,
|
||||||
"close": 1191.31
|
"close": 1188.7
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"02202": [
|
"02202": [
|
||||||
@@ -61,8 +61,8 @@
|
|||||||
{
|
{
|
||||||
"date": "2026-07-03",
|
"date": "2026-07-03",
|
||||||
"high": 50.2,
|
"high": 50.2,
|
||||||
"low": 48.6,
|
"low": 48.4,
|
||||||
"close": 48.7
|
"close": 48.44
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"02359": [
|
"02359": [
|
||||||
@@ -118,7 +118,7 @@
|
|||||||
"date": "2026-07-03",
|
"date": "2026-07-03",
|
||||||
"high": 502.0,
|
"high": 502.0,
|
||||||
"low": 444.55,
|
"low": 444.55,
|
||||||
"close": 495.95
|
"close": 494.57
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"06160": [
|
"06160": [
|
||||||
@@ -154,7 +154,7 @@
|
|||||||
"date": "2026-07-03",
|
"date": "2026-07-03",
|
||||||
"high": 687.04,
|
"high": 687.04,
|
||||||
"low": 633.01,
|
"low": 633.01,
|
||||||
"close": 654.0
|
"close": 646.83
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"09868": [
|
"09868": [
|
||||||
@@ -194,9 +194,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-07-03",
|
"date": "2026-07-03",
|
||||||
"high": 753.88,
|
"high": 757.88,
|
||||||
"low": 713.0,
|
"low": 713.0,
|
||||||
"close": 744.0
|
"close": 733.9
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"300124": [
|
"300124": [
|
||||||
@@ -208,9 +208,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-07-03",
|
"date": "2026-07-03",
|
||||||
"high": 72.38,
|
"high": 74.63,
|
||||||
"low": 67.31,
|
"low": 67.31,
|
||||||
"close": 72.29
|
"close": 73.13
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"000657": [
|
"000657": [
|
||||||
@@ -224,7 +224,7 @@
|
|||||||
"date": "2026-07-03",
|
"date": "2026-07-03",
|
||||||
"high": 101.5,
|
"high": 101.5,
|
||||||
"low": 87.88,
|
"low": 87.88,
|
||||||
"close": 91.88
|
"close": 91.78
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"000711": [
|
"000711": [
|
||||||
@@ -250,9 +250,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-07-03",
|
"date": "2026-07-03",
|
||||||
"high": 882.5,
|
"high": 892.1,
|
||||||
"low": 795.0,
|
"low": 795.0,
|
||||||
"close": 874.23
|
"close": 887.87
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"002594": [
|
"002594": [
|
||||||
@@ -266,7 +266,7 @@
|
|||||||
"date": "2026-07-03",
|
"date": "2026-07-03",
|
||||||
"high": 87.28,
|
"high": 87.28,
|
||||||
"low": 81.9,
|
"low": 81.9,
|
||||||
"close": 86.56
|
"close": 86.45
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"00700": [
|
"00700": [
|
||||||
@@ -332,9 +332,9 @@
|
|||||||
"301308": [
|
"301308": [
|
||||||
{
|
{
|
||||||
"date": "2026-07-03",
|
"date": "2026-07-03",
|
||||||
"high": 631.56,
|
"high": 646.85,
|
||||||
"low": 574.1,
|
"low": 574.1,
|
||||||
"close": 620.0
|
"close": 635.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
+63
-81
@@ -1,81 +1,63 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""data_freshness.py — 数据新鲜度校验
|
"""data_freshness.py — 数据新鲜度校验
|
||||||
|
|
||||||
所有报告管道在生成输出前必须调用 check_fresh()。
|
所有报告管道在生成输出前必须调用 check_fresh()。
|
||||||
返回 (pass: bool, details: str),如果数据过期则阻止生成操作建议。
|
返回 (pass: bool, details: str),如果数据过期则阻止生成操作建议。
|
||||||
|
|
||||||
用法:
|
用法:
|
||||||
from data_freshness import check_fresh
|
from data_freshness import check_fresh
|
||||||
ok, msg = check_fresh()
|
ok, msg = check_fresh()
|
||||||
if not ok:
|
if not ok:
|
||||||
print(f"⚠️ 数据过期: {msg}")
|
print(f"⚠️ 数据过期: {msg}")
|
||||||
sys.exit(0) # 不生成报告
|
sys.exit(0) # 不生成报告
|
||||||
|
|
||||||
校验规则:
|
校验规则:
|
||||||
- 盘中 (9:30~15:00):price/live_prices.json 必须在 5 分钟内刷新
|
- 盘中 (9:30~15:00):price/live_prices.json 必须在 5 分钟内刷新
|
||||||
- 盘后 (9:30以前/15:00以后):允许最长 120 分钟
|
- 盘后 (9:30以前/15:00以后):允许最长 120 分钟
|
||||||
- 周末/节假日:跳过校验
|
- 周末/节假日:跳过校验
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json, os
|
import json, os
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from mo_data import read_portfolio, read_decisions, read_watchlist
|
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"
|
# live_prices.json 已废弃,所有数据走 DB
|
||||||
|
LIVE_PRICES_PATH = "/home/hmo/web-dashboard/data/live_prices.json"
|
||||||
|
|
||||||
def is_market_hours():
|
|
||||||
now = datetime.now()
|
def is_market_hours():
|
||||||
if now.weekday() >= 5: # 周六日
|
now = datetime.now()
|
||||||
return False, "weekend"
|
if now.weekday() >= 5:
|
||||||
t = now.hour * 60 + now.minute
|
return False, "weekend"
|
||||||
if 9*60+30 <= t <= 15*60:
|
t = now.hour * 60 + now.minute
|
||||||
return True, "trading"
|
if 9*60+30 <= t <= 15*60:
|
||||||
return False, "closed"
|
return True, "trading"
|
||||||
|
return False, "closed"
|
||||||
|
|
||||||
def check_fresh():
|
|
||||||
"""返回 (ok: bool, msg: str)"""
|
def check_fresh():
|
||||||
now = datetime.now()
|
"""返回 (ok: bool, msg: str)"""
|
||||||
|
now = datetime.now()
|
||||||
# 先看是不是交易日
|
in_market, period = is_market_hours()
|
||||||
in_market, period = is_market_hours()
|
max_age_min = 5 if in_market else 120
|
||||||
|
|
||||||
max_age_min = 5 if in_market else 120
|
# 主指标:DB portfolio_summary.updated_at
|
||||||
|
try:
|
||||||
# 主指标:live_prices.json
|
pf = read_portfolio()
|
||||||
if os.path.exists(LIVE_PRICES_PATH):
|
pf_time = pf.get("updated_at", "")
|
||||||
try:
|
if pf_time:
|
||||||
lp = json.load(open(LIVE_PRICES_PATH))
|
pf_dt = datetime.fromisoformat(pf_time)
|
||||||
lp_time = lp.get("updated_at", "")
|
age = (now - pf_dt).total_seconds() / 60
|
||||||
if not lp_time:
|
if age > max_age_min:
|
||||||
return False, "live_prices.json updated_at 为空"
|
return False, f"数据已 {age:.0f} 分钟未更新(阈值 {max_age_min} 分钟)"
|
||||||
lp_dt = datetime.fromisoformat(lp_time)
|
return True, f"数据新鲜({age:.0f} 分钟前)"
|
||||||
age = (now - lp_dt).total_seconds() / 60
|
except Exception as e:
|
||||||
if age > max_age_min:
|
return False, f"DB 读取失败: {e}"
|
||||||
return False, f"live_prices.json 已 {age:.0f} 分钟未更新(阈值 {max_age_min} 分钟)"
|
|
||||||
return True, f"数据新鲜({age:.0f} 分钟前)"
|
return False, "无法获取数据新鲜度"
|
||||||
except Exception as e:
|
|
||||||
return False, f"live_prices.json 读取失败: {e}"
|
|
||||||
else:
|
if __name__ == "__main__":
|
||||||
# fallback: portfolio.json
|
ok, msg = check_fresh()
|
||||||
if os.path.exists(PORTFOLIO_PATH):
|
print(f"{'✅' if ok else '❌'} {msg}")
|
||||||
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}")
|
|
||||||
|
|||||||
+221
-223
@@ -1,223 +1,221 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""market_watch.py — 行業熱點數據採集,寫入 dashboard data/market.json
|
"""market_watch.py — 行業熱點數據採集,寫入 dashboard data/market.json
|
||||||
|
|
||||||
數據源優先級:
|
數據源優先級:
|
||||||
後端A:東方財富 push2 API(首選,有板塊代碼+實時指數)
|
後端A:東方財富 push2 API(首選,有板塊代碼+實時指數)
|
||||||
後端B:同花順 THS / akshare(降級,有漲跌家數+資金流向)
|
後端B:同花順 THS / akshare(降級,有漲跌家數+資金流向)
|
||||||
|
|
||||||
注意:當前服務器無法連通東方財富API(已被封禁/域名不可達),
|
注意:當前服務器無法連通東方財富API(已被封禁/域名不可達),
|
||||||
實際運行時自動降級到同花順 THS 後端。THS 提供90+行業板塊的
|
實際運行時自動降級到同花順 THS 後端。THS 提供90+行業板塊的
|
||||||
實時漲跌、上漲/下跌家數、淨流入資金等數據,足以滿足需求。
|
實時漲跌、上漲/下跌家數、淨流入資金等數據,足以滿足需求。
|
||||||
|
|
||||||
輸出:data/market.json → MoFin Dashboard 市場數據展示
|
輸出:data/market.json → MoFin Dashboard 市場數據展示
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from mofin_db import get_conn, init_all_tables, write_market_snapshot
|
from mofin_db import get_conn, init_all_tables, write_market_snapshot
|
||||||
|
|
||||||
DATA_DIR = Path(__file__).parent / "data"
|
DATA_DIR = Path(__file__).parent / "data"
|
||||||
|
|
||||||
|
|
||||||
# ── 後端A:東方財富 push2 API(首選,有板塊代碼+實時指數) ──
|
# ── 後端A:東方財富 push2 API(首選,有板塊代碼+實時指數) ──
|
||||||
|
|
||||||
def _fetch_em(url):
|
def _fetch_em(url):
|
||||||
"""通用 EM API 請求"""
|
"""通用 EM API 請求"""
|
||||||
import urllib.request
|
import urllib.request
|
||||||
req = urllib.request.Request(
|
req = urllib.request.Request(
|
||||||
url,
|
url,
|
||||||
headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"}
|
headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"}
|
||||||
)
|
)
|
||||||
resp = urllib.request.urlopen(req, timeout=10)
|
resp = urllib.request.urlopen(req, timeout=10)
|
||||||
return json.loads(resp.read().decode("utf-8"))
|
return json.loads(resp.read().decode("utf-8"))
|
||||||
|
|
||||||
|
|
||||||
def fetch_sector_em():
|
def fetch_sector_em():
|
||||||
"""東方財富行業板塊"""
|
"""東方財富行業板塊"""
|
||||||
try:
|
try:
|
||||||
data = _fetch_em(
|
data = _fetch_em(
|
||||||
"https://push2.eastmoney.com/api/qt/clist/get?"
|
"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"
|
"pn=1&pz=60&po=1&np=1&fields=f2,f3,f4,f12,f14&fs=m:90+t:2"
|
||||||
)
|
)
|
||||||
return [{
|
return [{
|
||||||
"name": i["f14"],
|
"name": i["f14"],
|
||||||
"code": i["f12"],
|
"code": i["f12"],
|
||||||
"price": i.get("f2", 0),
|
"price": i.get("f2", 0),
|
||||||
"change": i.get("f3", 0),
|
"change": i.get("f3", 0),
|
||||||
} for i in data.get("data", {}).get("diff", [])]
|
} for i in data.get("data", {}).get("diff", [])]
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def fetch_concept_em():
|
def fetch_concept_em():
|
||||||
"""東方財富概念板塊"""
|
"""東方財富概念板塊"""
|
||||||
try:
|
try:
|
||||||
data = _fetch_em(
|
data = _fetch_em(
|
||||||
"https://push2.eastmoney.com/api/qt/clist/get?"
|
"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"
|
"pn=1&pz=30&po=1&np=1&fields=f2,f3,f4,f12,f14&fs=m:90+t:3"
|
||||||
)
|
)
|
||||||
return [{
|
return [{
|
||||||
"name": i["f14"],
|
"name": i["f14"],
|
||||||
"code": i["f12"],
|
"code": i["f12"],
|
||||||
"change": i.get("f3", 0),
|
"change": i.get("f3", 0),
|
||||||
} for i in data.get("data", {}).get("diff", [])]
|
} for i in data.get("data", {}).get("diff", [])]
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
# ── 後端B:同花順 THS / akshare(降級) ──
|
# ── 後端B:同花順 THS / akshare(降級) ──
|
||||||
|
|
||||||
def fetch_sector_ths():
|
def fetch_sector_ths():
|
||||||
"""THS 行業板塊(含漲跌家數、資金流向、領漲股)"""
|
"""THS 行業板塊(含漲跌家數、資金流向、領漲股)"""
|
||||||
try:
|
try:
|
||||||
import akshare as ak
|
import akshare as ak
|
||||||
df = ak.stock_board_industry_summary_ths()
|
df = ak.stock_board_industry_summary_ths()
|
||||||
return [{
|
return [{
|
||||||
"name": r["板块"],
|
"name": r["板块"],
|
||||||
"code": "",
|
"code": "",
|
||||||
"price": 0,
|
"price": 0,
|
||||||
"change": float(r.get("涨跌幅", 0)),
|
"change": float(r.get("涨跌幅", 0)),
|
||||||
"volume": float(r.get("总成交量", 0)),
|
"volume": float(r.get("总成交量", 0)),
|
||||||
"turnover": float(r.get("总成交额", 0)),
|
"turnover": float(r.get("总成交额", 0)),
|
||||||
"net_inflow": float(r.get("净流入", 0)),
|
"net_inflow": float(r.get("净流入", 0)),
|
||||||
"up_count": int(r.get("上涨家数", 0)),
|
"up_count": int(r.get("上涨家数", 0)),
|
||||||
"down_count": int(r.get("下跌家数", 0)),
|
"down_count": int(r.get("下跌家数", 0)),
|
||||||
"avg_price": float(r.get("均价", 0)),
|
"avg_price": float(r.get("均价", 0)),
|
||||||
"lead_stock": r.get("领涨股", ""),
|
"lead_stock": r.get("领涨股", ""),
|
||||||
"lead_stock_change": float(r.get("领涨股-涨跌幅", 0)),
|
"lead_stock_change": float(r.get("领涨股-涨跌幅", 0)),
|
||||||
} for _, r in df.iterrows()]
|
} for _, r in df.iterrows()]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"THS行業失敗: {e}", flush=True)
|
print(f"THS行業失敗: {e}", flush=True)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
def fetch_concept_ths():
|
def fetch_concept_ths():
|
||||||
"""THS 概念板塊(僅名稱,無實時漲跌)"""
|
"""THS 概念板塊(僅名稱,無實時漲跌)"""
|
||||||
try:
|
try:
|
||||||
import akshare as ak
|
import akshare as ak
|
||||||
df = ak.stock_board_concept_name_ths()
|
df = ak.stock_board_concept_name_ths()
|
||||||
return [{
|
return [{
|
||||||
"name": r["name"],
|
"name": r["name"],
|
||||||
"code": str(r.get("code", "")),
|
"code": str(r.get("code", "")),
|
||||||
"change": 0,
|
"change": 0,
|
||||||
} for _, r in df.iterrows()]
|
} for _, r in df.iterrows()]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"THS概念失敗: {e}", flush=True)
|
print(f"THS概念失敗: {e}", flush=True)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
# ── 輔助函數 ──
|
# ── 輔助函數 ──
|
||||||
|
|
||||||
def get_market_mood(sectors):
|
def get_market_mood(sectors):
|
||||||
if not sectors:
|
if not sectors:
|
||||||
return "unknown"
|
return "unknown"
|
||||||
ratio = sum(1 for s in sectors if s.get("change", 0) > 0) / len(sectors)
|
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"
|
return "bullish" if ratio > 0.7 else "neutral" if ratio > 0.4 else "bearish"
|
||||||
|
|
||||||
|
|
||||||
def get_market_verdict(up_ratio, mood, sectors):
|
def get_market_verdict(up_ratio, mood, sectors):
|
||||||
"""Return (verdict, reason) based on sector data."""
|
"""Return (verdict, reason) based on sector data."""
|
||||||
if not sectors:
|
if not sectors:
|
||||||
return "unknown", "数据不足"
|
return "unknown", "数据不足"
|
||||||
if up_ratio < 25:
|
if up_ratio < 25:
|
||||||
return "弱势", f"仅{up_ratio}%板块上涨,{mood}"
|
return "弱势", f"仅{up_ratio}%板块上涨,{mood}"
|
||||||
elif up_ratio < 40:
|
elif up_ratio < 40:
|
||||||
return "偏弱", f"{up_ratio}%板块上涨,结构分化"
|
return "偏弱", f"{up_ratio}%板块上涨,结构分化"
|
||||||
elif up_ratio < 60:
|
elif up_ratio < 60:
|
||||||
return "均衡", f"{up_ratio}%板块上涨,涨跌均衡"
|
return "均衡", f"{up_ratio}%板块上涨,涨跌均衡"
|
||||||
else:
|
else:
|
||||||
return "强势", f"{up_ratio}%板块上涨,整体走强"
|
return "强势", f"{up_ratio}%板块上涨,整体走强"
|
||||||
|
|
||||||
|
|
||||||
def get_hot_sectors(sectors, top_n=3):
|
def get_hot_sectors(sectors, top_n=3):
|
||||||
"""Return sectors with highest positive change as hot sectors."""
|
"""Return sectors with highest positive change as hot sectors."""
|
||||||
hot = [s for s in sectors if s.get("change", 0) > 1.0]
|
hot = [s for s in sectors if s.get("change", 0) > 1.0]
|
||||||
hot.sort(key=lambda s: s.get("change", 0), reverse=True)
|
hot.sort(key=lambda s: s.get("change", 0), reverse=True)
|
||||||
return [{
|
return [{
|
||||||
"name": s["name"],
|
"name": s["name"],
|
||||||
"change": s.get("change", 0),
|
"change": s.get("change", 0),
|
||||||
"reason": f"板块涨{s.get('change',0):.1f}%"
|
"reason": f"板块涨{s.get('change',0):.1f}%"
|
||||||
} for s in hot[:top_n]]
|
} for s in hot[:top_n]]
|
||||||
|
|
||||||
|
|
||||||
def get_danger_sectors(sectors, top_n=3):
|
def get_danger_sectors(sectors, top_n=3):
|
||||||
"""Return sectors with lowest (negative) change as danger sectors."""
|
"""Return sectors with lowest (negative) change as danger sectors."""
|
||||||
danger = [s for s in sectors if s.get("change", 0) < -1.0]
|
danger = [s for s in sectors if s.get("change", 0) < -1.0]
|
||||||
danger.sort(key=lambda s: s.get("change", 0))
|
danger.sort(key=lambda s: s.get("change", 0))
|
||||||
return [{
|
return [{
|
||||||
"name": s["name"],
|
"name": s["name"],
|
||||||
"change": s.get("change", 0),
|
"change": s.get("change", 0),
|
||||||
"reason": f"板块跌{s.get('change',0):.1f}%"
|
"reason": f"板块跌{s.get('change',0):.1f}%"
|
||||||
} for s in danger[:top_n]]
|
} for s in danger[:top_n]]
|
||||||
|
|
||||||
|
|
||||||
# ── 主流程 ──
|
# ── 主流程 ──
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
# 行業板塊:EM → THS → 兜底
|
# 行業板塊:EM → THS → 兜底
|
||||||
sectors = fetch_sector_em()
|
sectors = fetch_sector_em()
|
||||||
source = "eastmoney"
|
source = "eastmoney"
|
||||||
if sectors is None:
|
if sectors is None:
|
||||||
sectors = fetch_sector_ths()
|
sectors = fetch_sector_ths()
|
||||||
source = "ths"
|
source = "ths"
|
||||||
|
|
||||||
# 概念板塊:EM → THS → 空
|
# 概念板塊:EM → THS → 空
|
||||||
concepts = fetch_concept_em()
|
concepts = fetch_concept_em()
|
||||||
concept_source = "eastmoney"
|
concept_source = "eastmoney"
|
||||||
if concepts is None:
|
if concepts is None:
|
||||||
concepts = fetch_concept_ths()
|
concepts = fetch_concept_ths()
|
||||||
concept_source = "ths"
|
concept_source = "ths"
|
||||||
if not concepts:
|
if not concepts:
|
||||||
concepts = []
|
concepts = []
|
||||||
concept_source = "unavailable"
|
concept_source = "unavailable"
|
||||||
|
|
||||||
# 排序
|
# 排序
|
||||||
sorted_sectors = sorted(sectors, key=lambda s: s.get("change", 0), reverse=True)
|
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_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]
|
top_losers = [s for s in reversed(sorted_sectors) if s.get("change", 0) < 0][:3]
|
||||||
|
|
||||||
# 计算大盘数据
|
# 计算大盘数据
|
||||||
up_ratio = round(
|
up_ratio = round(
|
||||||
sum(1 for s in sectors if s.get("change", 0) > 0) / max(len(sectors), 1) * 100, 1
|
sum(1 for s in sectors if s.get("change", 0) > 0) / max(len(sectors), 1) * 100, 1
|
||||||
)
|
)
|
||||||
mood = get_market_mood(sectors)
|
mood = get_market_mood(sectors)
|
||||||
verdict, verdict_reason = get_market_verdict(up_ratio, mood, sectors)
|
verdict, verdict_reason = get_market_verdict(up_ratio, mood, sectors)
|
||||||
|
|
||||||
market_data = {
|
market_data = {
|
||||||
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M"),
|
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M"),
|
||||||
"source": source,
|
"source": source,
|
||||||
"concept_source": concept_source,
|
"concept_source": concept_source,
|
||||||
"total_sectors": len(sectors),
|
"total_sectors": len(sectors),
|
||||||
"up_ratio": up_ratio,
|
"up_ratio": up_ratio,
|
||||||
"mood": mood,
|
"mood": mood,
|
||||||
"market_verdict": verdict,
|
"market_verdict": verdict,
|
||||||
"verdict_reason": verdict_reason,
|
"verdict_reason": verdict_reason,
|
||||||
"hot_sectors": get_hot_sectors(sectors),
|
"hot_sectors": get_hot_sectors(sectors),
|
||||||
"danger_sectors": get_danger_sectors(sectors),
|
"danger_sectors": get_danger_sectors(sectors),
|
||||||
"top_gainers": top_gainers,
|
"top_gainers": top_gainers,
|
||||||
"top_losers": top_losers,
|
"top_losers": top_losers,
|
||||||
"sectors": sectors,
|
"sectors": sectors,
|
||||||
"concepts": concepts,
|
"concepts": concepts,
|
||||||
}
|
}
|
||||||
|
|
||||||
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
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 写入(替代 market.json)──
|
||||||
|
conn = get_conn()
|
||||||
# ── SQLite 双写 ──
|
init_all_tables(conn)
|
||||||
conn = get_conn()
|
ok, msg, sid = write_market_snapshot(conn, market_data)
|
||||||
init_all_tables(conn)
|
if ok:
|
||||||
ok, msg, sid = write_market_snapshot(conn, market_data)
|
print(f"[DB] {msg}", flush=True)
|
||||||
if ok:
|
else:
|
||||||
print(f"[DB] {msg}", flush=True)
|
print(f"[DB] 写入失败(JSON 不受影响): {msg}", flush=True)
|
||||||
else:
|
conn.close()
|
||||||
print(f"[DB] 写入失败(JSON 不受影响): {msg}", flush=True)
|
|
||||||
conn.close()
|
# 靜默:只寫文件,不輸出到stdout,避免cron推送
|
||||||
|
|
||||||
# 靜默:只寫文件,不輸出到stdout,避免cron推送
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|||||||
+67
@@ -387,6 +387,28 @@ def init_all_tables(conn: sqlite3.Connection):
|
|||||||
last_scanned_at TEXT,
|
last_scanned_at TEXT,
|
||||||
found_count INTEGER DEFAULT 0
|
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()
|
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,)
|
"SELECT * FROM cash_log ORDER BY id DESC LIMIT ?", (limit,)
|
||||||
).fetchall()
|
).fetchall()
|
||||||
return [dict(r) for r in rows]
|
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 ──────────────────────────────────────────────
|
# ── MoFin unified model ──────────────────────────────────────────────
|
||||||
from mo_models import is_hk_stock, get_hk_rate, calc_total_assets, calc_total_mv, calc_position_pct
|
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
|
from mo_data import read_portfolio, read_decisions, read_watchlist
|
||||||
|
|
||||||
BREACH_PATH = "/home/hmo/.hermes/zone_breach.json"
|
BREACH_PATH = "/home/hmo/.hermes/zone_breach.json"
|
||||||
@@ -231,14 +231,16 @@ def refresh_data_prices():
|
|||||||
prices.update(hk_prices)
|
prices.update(hk_prices)
|
||||||
updated = 0
|
updated = 0
|
||||||
|
|
||||||
# 保存全量实时价快照(供报告管道消费,确保分析用最新数据)
|
# 保存全量实时价快照到 DB(替代 live_prices.json)
|
||||||
try:
|
try:
|
||||||
live = {"updated_at": datetime.now().isoformat(), "prices": {}}
|
live = {}
|
||||||
for code in all_codes:
|
for code in all_codes:
|
||||||
if code in prices:
|
if code in prices:
|
||||||
p, c, chg = prices[code]
|
p, c, chg = prices[code]
|
||||||
live["prices"][code] = {"price": p, "change_pct": chg}
|
live[code] = {"price": p, "change_pct": chg}
|
||||||
json.dump(live, open("/home/hmo/web-dashboard/data/live_prices.json", "w"), indent=2)
|
conn = get_conn()
|
||||||
|
write_live_prices(conn, live)
|
||||||
|
conn.close()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -663,7 +665,7 @@ def run_once(round_label=""):
|
|||||||
|
|
||||||
# === 3.5 资金流异常检测(2026-06-27 新增)===
|
# === 3.5 资金流异常检测(2026-06-27 新增)===
|
||||||
try:
|
try:
|
||||||
cf = json.load(open("/home/hmo/web-dashboard/data/capital_flow_cache.json"))
|
cf = read_capital_flow_cache(get_conn())
|
||||||
# 检查所有 active decision 中的资金流异常
|
# 检查所有 active decision 中的资金流异常
|
||||||
for d in active:
|
for d in active:
|
||||||
code = d["code"]
|
code = d["code"]
|
||||||
|
|||||||
+186
-182
@@ -1,182 +1,186 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""capital_flow_collector.py — 个股资金流数据采集器
|
"""capital_flow_collector.py — 个股资金流数据采集器
|
||||||
|
|
||||||
每30分钟拉一次持仓+自选的超大单/大单/中单/小单资金流向。
|
每30分钟拉一次持仓+自选的超大单/大单/中单/小单资金流向。
|
||||||
输出到 capital_flow_cache.json 供 price_monitor 和报告使用。
|
输出到 capital_flow_cache.json 供 price_monitor 和报告使用。
|
||||||
|
|
||||||
API: push2his.eastmoney.com 个股资金流日线
|
API: push2his.eastmoney.com 个股资金流日线
|
||||||
"""
|
"""
|
||||||
import json, os, sys, time, urllib.request
|
import json, os, sys, time, urllib.request
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from urllib.request import urlopen, Request
|
from urllib.request import urlopen, Request
|
||||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
from threading import Semaphore
|
from threading import Semaphore
|
||||||
from mo_data import read_portfolio, read_decisions, read_watchlist
|
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"
|
DATA_DIR = "/home/hmo/web-dashboard/data"
|
||||||
CACHE_PATH = f"{DATA_DIR}/capital_flow_cache.json"
|
DECISIONS_PATH = f"{DATA_DIR}/decisions.json"
|
||||||
|
CACHE_PATH = f"{DATA_DIR}/capital_flow_cache.json"
|
||||||
UA = "Mozilla/5.0"
|
|
||||||
# 限速器:最多5个并发,每请求后强制间隔0.3s
|
UA = "Mozilla/5.0"
|
||||||
RATE_LIMIT = Semaphore(5)
|
# 限速器:最多5个并发,每请求后强制间隔0.3s
|
||||||
MIN_INTERVAL = 0.3
|
RATE_LIMIT = Semaphore(5)
|
||||||
_last_req = 0
|
MIN_INTERVAL = 0.3
|
||||||
|
_last_req = 0
|
||||||
def _rate_limited_request(url):
|
|
||||||
"""带速率限制的HTTP GET,用Semaphore控制并发数"""
|
def _rate_limited_request(url):
|
||||||
global _last_req
|
"""带速率限制的HTTP GET,用Semaphore控制并发数"""
|
||||||
with RATE_LIMIT:
|
global _last_req
|
||||||
elapsed = time.time() - _last_req
|
with RATE_LIMIT:
|
||||||
if elapsed < MIN_INTERVAL:
|
elapsed = time.time() - _last_req
|
||||||
time.sleep(MIN_INTERVAL - elapsed)
|
if elapsed < MIN_INTERVAL:
|
||||||
proxy_handler = urllib.request.ProxyHandler({})
|
time.sleep(MIN_INTERVAL - elapsed)
|
||||||
opener = urllib.request.build_opener(proxy_handler)
|
proxy_handler = urllib.request.ProxyHandler({})
|
||||||
req = Request(url, headers={"User-Agent": UA, "Referer": "https://data.eastmoney.com/"})
|
opener = urllib.request.build_opener(proxy_handler)
|
||||||
try:
|
req = Request(url, headers={"User-Agent": UA, "Referer": "https://data.eastmoney.com/"})
|
||||||
resp = opener.open(req, timeout=8)
|
try:
|
||||||
_last_req = time.time()
|
resp = opener.open(req, timeout=8)
|
||||||
return json.loads(resp.read().decode("utf-8"))
|
_last_req = time.time()
|
||||||
except Exception:
|
return json.loads(resp.read().decode("utf-8"))
|
||||||
return None
|
except Exception:
|
||||||
|
return None
|
||||||
# eastmoney secid: 1=上海 0=深圳
|
|
||||||
def secid(code):
|
# eastmoney secid: 1=上海 0=深圳
|
||||||
code = str(code).strip()
|
def secid(code):
|
||||||
if code.startswith(("6", "9")):
|
code = str(code).strip()
|
||||||
return f"1.{code}"
|
if code.startswith(("6", "9")):
|
||||||
return f"0.{code}"
|
return f"1.{code}"
|
||||||
|
return f"0.{code}"
|
||||||
def fetch_flow(code, days=5):
|
|
||||||
"""拉取个股近N日资金流(带限速+代理绕过)"""
|
def fetch_flow(code, days=5):
|
||||||
sid = secid(code)
|
"""拉取个股近N日资金流(带限速+代理绕过)"""
|
||||||
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}"
|
sid = secid(code)
|
||||||
data = _rate_limited_request(url)
|
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}"
|
||||||
if not data:
|
data = _rate_limited_request(url)
|
||||||
return None
|
if not data:
|
||||||
klines = data.get("data", {}).get("klines", [])
|
return None
|
||||||
if not klines:
|
klines = data.get("data", {}).get("klines", [])
|
||||||
return None
|
if not klines:
|
||||||
result = []
|
return None
|
||||||
for k in klines:
|
result = []
|
||||||
p = k.split(",")
|
for k in klines:
|
||||||
if len(p) >= 7:
|
p = k.split(",")
|
||||||
result.append({
|
if len(p) >= 7:
|
||||||
"date": p[0],
|
result.append({
|
||||||
"main_net": float(p[1]), # 主力净流入(元)
|
"date": p[0],
|
||||||
"super_large": float(p[2]), # 超大单净流入(元)
|
"main_net": float(p[1]), # 主力净流入(元)
|
||||||
"large": float(p[3]), # 大单净流入(元)
|
"super_large": float(p[2]), # 超大单净流入(元)
|
||||||
"medium": float(p[4]), # 中单净流入(元)
|
"large": float(p[3]), # 大单净流入(元)
|
||||||
"small": float(p[5]), # 小单净流入(元)
|
"medium": float(p[4]), # 中单净流入(元)
|
||||||
})
|
"small": float(p[5]), # 小单净流入(元)
|
||||||
return result
|
})
|
||||||
|
return result
|
||||||
def fetch_flow_intraday(code):
|
|
||||||
"""拉取当日分时资金流(用于盘中判断)"""
|
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"
|
sid = secid(code)
|
||||||
try:
|
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"
|
||||||
resp = urlopen(url, timeout=5)
|
try:
|
||||||
data = json.loads(resp.read().decode("utf-8"))
|
resp = urlopen(url, timeout=5)
|
||||||
klines = data.get("data", {}).get("klines", [])
|
data = json.loads(resp.read().decode("utf-8"))
|
||||||
if not klines:
|
klines = data.get("data", {}).get("klines", [])
|
||||||
return None
|
if not klines:
|
||||||
latest = klines[-1].split(",")
|
return None
|
||||||
return {
|
latest = klines[-1].split(",")
|
||||||
"main_net": float(latest[1]),
|
return {
|
||||||
"super_large": float(latest[2]),
|
"main_net": float(latest[1]),
|
||||||
"large": float(latest[3]),
|
"super_large": float(latest[2]),
|
||||||
}
|
"large": float(latest[3]),
|
||||||
except:
|
}
|
||||||
return None
|
except:
|
||||||
|
return None
|
||||||
def analyze_flow(flow_data):
|
|
||||||
"""分析资金流模式"""
|
def analyze_flow(flow_data):
|
||||||
if not flow_data or len(flow_data) < 2:
|
"""分析资金流模式"""
|
||||||
return {}
|
if not flow_data or len(flow_data) < 2:
|
||||||
|
return {}
|
||||||
result = {"alerts": [], "pattern": ""}
|
|
||||||
|
result = {"alerts": [], "pattern": ""}
|
||||||
# 最近两日对比
|
|
||||||
d1 = flow_data[-1] # 最新日
|
# 最近两日对比
|
||||||
d2 = flow_data[-2] # 前一日
|
d1 = flow_data[-1] # 最新日
|
||||||
|
d2 = flow_data[-2] # 前一日
|
||||||
# 超大单信号
|
|
||||||
sl1 = d1["super_large"]
|
# 超大单信号
|
||||||
sl2 = d2["super_large"]
|
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:])
|
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:
|
# 1. 主力连续流入
|
||||||
result["pattern"] = "主力持续流入"
|
if main_trend > 50000000 and sl1 > 0 and sl2 > 0:
|
||||||
result["alerts"].append("主力连续3日净流入")
|
result["pattern"] = "主力持续流入"
|
||||||
|
result["alerts"].append("主力连续3日净流入")
|
||||||
# 2. 超大单突然转向(连续流入→流出 或 流出→流入)
|
|
||||||
if sl1 * sl2 < 0: # 方向反转
|
# 2. 超大单突然转向(连续流入→流出 或 流出→流入)
|
||||||
if sl1 > 0 and sl2 < 0:
|
if sl1 * sl2 < 0: # 方向反转
|
||||||
result["pattern"] = "超大单由出转入"
|
if sl1 > 0 and sl2 < 0:
|
||||||
result["alerts"].append("超大单转为净买入(暗示消息即将落地)")
|
result["pattern"] = "超大单由出转入"
|
||||||
elif sl1 < 0 and sl2 > 0:
|
result["alerts"].append("超大单转为净买入(暗示消息即将落地)")
|
||||||
result["pattern"] = "超大单由入转出"
|
elif sl1 < 0 and sl2 > 0:
|
||||||
result["alerts"].append("超大单转为净卖出(利好出货嫌疑)")
|
result["pattern"] = "超大单由入转出"
|
||||||
|
result["alerts"].append("超大单转为净卖出(利好出货嫌疑)")
|
||||||
# 3. 价格与资金流背离(缺当前价格作比较,在主脚本中完成)
|
|
||||||
# 4. 单日暴量
|
# 3. 价格与资金流背离(缺当前价格作比较,在主脚本中完成)
|
||||||
max_sl = max(abs(d["super_large"]) for d in flow_data)
|
# 4. 单日暴量
|
||||||
if max_sl == abs(sl1) and abs(sl1) > 100000000:
|
max_sl = max(abs(d["super_large"]) for d in flow_data)
|
||||||
result["pattern"] = "单日资金暴量"
|
if max_sl == abs(sl1) and abs(sl1) > 100000000:
|
||||||
result["alerts"].append(f"今日超大单异常: {sl1/100000000:.2f}亿")
|
result["pattern"] = "单日资金暴量"
|
||||||
|
result["alerts"].append(f"今日超大单异常: {sl1/100000000:.2f}亿")
|
||||||
return result
|
|
||||||
|
return result
|
||||||
def main():
|
|
||||||
codes = set()
|
def main():
|
||||||
# 读取持仓+自选
|
codes = set()
|
||||||
try:
|
# 读取持仓+自选
|
||||||
dec = mo_data.read_decisions()
|
try:
|
||||||
for d in dec.get("decisions", []):
|
dec = mo_data.read_decisions()
|
||||||
c = d.get("code", "")
|
for d in dec.get("decisions", []):
|
||||||
if c:
|
c = d.get("code", "")
|
||||||
codes.add(c)
|
if c:
|
||||||
except:
|
codes.add(c)
|
||||||
pass
|
except:
|
||||||
|
pass
|
||||||
all_flows = {}
|
|
||||||
|
all_flows = {}
|
||||||
# 并行抓取:ThreadPoolExecutor + 内置限速器(Semaphore 5 + 0.3s间隔)
|
|
||||||
code_list = sorted(codes)
|
# 并行抓取:ThreadPoolExecutor + 内置限速器(Semaphore 5 + 0.3s间隔)
|
||||||
if not code_list:
|
code_list = sorted(codes)
|
||||||
print("[capital_flow] 无代码需要采集")
|
if not code_list:
|
||||||
return
|
print("[capital_flow] 无代码需要采集")
|
||||||
|
return
|
||||||
def fetch_one(code):
|
|
||||||
flow = fetch_flow(code, days=5)
|
def fetch_one(code):
|
||||||
if flow:
|
flow = fetch_flow(code, days=5)
|
||||||
analysis = analyze_flow(flow)
|
if flow:
|
||||||
return (code, {
|
analysis = analyze_flow(flow)
|
||||||
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M"),
|
return (code, {
|
||||||
"flow": flow,
|
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M"),
|
||||||
"analysis": analysis,
|
"flow": flow,
|
||||||
})
|
"analysis": analysis,
|
||||||
return (code, None)
|
})
|
||||||
|
return (code, None)
|
||||||
with ThreadPoolExecutor(max_workers=5) as pool:
|
|
||||||
futures = {pool.submit(fetch_one, c): c for c in code_list}
|
with ThreadPoolExecutor(max_workers=5) as pool:
|
||||||
for f in as_completed(futures):
|
futures = {pool.submit(fetch_one, c): c for c in code_list}
|
||||||
code, result = f.result()
|
for f in as_completed(futures):
|
||||||
if result:
|
code, result = f.result()
|
||||||
all_flows[code] = result
|
if result:
|
||||||
|
all_flows[code] = result
|
||||||
# 写缓存
|
|
||||||
cache = {
|
# 写缓存
|
||||||
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M"),
|
cache = {
|
||||||
"stocks": all_flows,
|
"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)}只更新完成")
|
# 写 DB(替代 capital_flow_cache.json)
|
||||||
|
conn = get_conn()
|
||||||
if __name__ == "__main__":
|
write_capital_flow_cache(conn, cache)
|
||||||
main()
|
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
|
#!/usr/bin/env python3
|
||||||
"""Remove held stocks from watchlist"""
|
"""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 mo_data import read_portfolio, read_decisions, read_watchlist
|
||||||
from mofin_db import get_conn, write_watchlist_stock, write_holding_strategy
|
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
|
import json, os, sqlite3, sys, time, urllib.request
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime
|
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 mo_data import read_watchlist
|
||||||
from mofin_db import write_watchlist_stock
|
from mofin_db import write_watchlist_stock
|
||||||
|
|
||||||
@@ -34,8 +38,13 @@ def clean_proxy():
|
|||||||
def fetch_quote(code):
|
def fetch_quote(code):
|
||||||
"""拉行情。DB 优先,腾讯 fallback"""
|
"""拉行情。DB 优先,腾讯 fallback"""
|
||||||
# DB 优先
|
# 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
|
try:
|
||||||
except: pass
|
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: 腾讯
|
# Fallback: 腾讯
|
||||||
try:
|
try:
|
||||||
prefix = "sh" if code.startswith(('60','68','51','56','50')) else "sz" if code.startswith(('00','30','15')) else "hk"
|
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