chore: capture deployed state

This commit is contained in:
知微
2026-07-20 17:30:43 +08:00
parent 4dcfee8140
commit 5d3b8e6fdd
14 changed files with 33422 additions and 2882 deletions
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+29640
View File
File diff suppressed because it is too large Load Diff
+304 -154
View File
@@ -35,10 +35,10 @@
{ {
"date": "2026-07-20", "date": "2026-07-20",
"high": 15.75, "high": 15.75,
"low": 14.1, "low": 13.05,
"close": 14.21, "close": 13.41,
"volume": 37396292.0, "volume": 41864386.0,
"amount": 55424.0 "amount": 56681.0
} }
], ],
"688775": [ "688775": [
@@ -77,10 +77,10 @@
{ {
"date": "2026-07-20", "date": "2026-07-20",
"high": 149.0, "high": 149.0,
"low": 135.76, "low": 129.5,
"close": 138.34, "close": 136.32,
"volume": 6157193.0, "volume": 4713131.0,
"amount": 86656.0 "amount": 63136.0
} }
], ],
"688475": [ "688475": [
@@ -118,11 +118,11 @@
}, },
{ {
"date": "2026-07-20", "date": "2026-07-20",
"high": 27.74, "high": 27.78,
"low": 26.4, "low": 26.4,
"close": 26.64, "close": 27.73,
"volume": 5999240.0, "volume": 6881480.0,
"amount": 16223.0 "amount": 18584.0
} }
], ],
"688778": [ "688778": [
@@ -161,10 +161,10 @@
{ {
"date": "2026-07-20", "date": "2026-07-20",
"high": 46.45, "high": 46.45,
"low": 44.09, "low": 42.05,
"close": 44.14, "close": 43.0,
"volume": 7684619.0, "volume": 9254116.0,
"amount": 34599.0 "amount": 40066.0
} }
], ],
"688501": [ "688501": [
@@ -203,10 +203,10 @@
{ {
"date": "2026-07-20", "date": "2026-07-20",
"high": 17.8, "high": 17.8,
"low": 16.64, "low": 15.42,
"close": 16.79, "close": 15.91,
"volume": 1904231.0, "volume": 2834080.0,
"amount": 3272.0 "amount": 4591.0
} }
], ],
"688510": [ "688510": [
@@ -245,10 +245,10 @@
{ {
"date": "2026-07-20", "date": "2026-07-20",
"high": 22.46, "high": 22.46,
"low": 20.63, "low": 20.25,
"close": 21.0, "close": 20.76,
"volume": 5596808.0, "volume": 6770593.0,
"amount": 12011.0 "amount": 14184.0
} }
], ],
"688513": [ "688513": [
@@ -287,10 +287,10 @@
{ {
"date": "2026-07-20", "date": "2026-07-20",
"high": 59.9, "high": 59.9,
"low": 54.5, "low": 53.67,
"close": 54.63, "close": 57.6,
"volume": 4293163.0, "volume": 3704960.0,
"amount": 24190.0 "amount": 20838.0
} }
], ],
"688518": [ "688518": [
@@ -329,10 +329,10 @@
{ {
"date": "2026-07-20", "date": "2026-07-20",
"high": 19.78, "high": 19.78,
"low": 18.31, "low": 16.81,
"close": 18.46, "close": 17.24,
"volume": 18674329.0, "volume": 21448924.0,
"amount": 35390.0 "amount": 38124.0
} }
], ],
"00700": [ "00700": [
@@ -372,9 +372,9 @@
"date": "2026-07-20", "date": "2026-07-20",
"high": 488.8, "high": 488.8,
"low": 458.0, "low": 458.0,
"close": 461.6, "close": 477.8,
"volume": 36237657.0, "volume": 30822839.0,
"amount": 16928705332.905 "amount": 14678311361.3
} }
], ],
"688533": [ "688533": [
@@ -413,10 +413,10 @@
{ {
"date": "2026-07-20", "date": "2026-07-20",
"high": 21.74, "high": 21.74,
"low": 20.4, "low": 19.96,
"close": 20.59, "close": 20.22,
"volume": 1359591.0, "volume": 992552.0,
"amount": 2855.0 "amount": 2026.0
} }
], ],
"01088": [ "01088": [
@@ -454,11 +454,11 @@
}, },
{ {
"date": "2026-07-20", "date": "2026-07-20",
"high": 42.5, "high": 43.66,
"low": 41.54, "low": 41.54,
"close": 42.4, "close": 43.6,
"volume": 13003969.0, "volume": 11166601.0,
"amount": 547347699.16 "amount": 483013474.62
} }
], ],
"688566": [ "688566": [
@@ -497,10 +497,10 @@
{ {
"date": "2026-07-20", "date": "2026-07-20",
"high": 27.11, "high": 27.11,
"low": 25.0, "low": 24.81,
"close": 25.0, "close": 25.25,
"volume": 3344706.0, "volume": 2923287.0,
"amount": 8599.0 "amount": 7436.0
} }
], ],
"01211": [ "01211": [
@@ -538,11 +538,11 @@
}, },
{ {
"date": "2026-07-20", "date": "2026-07-20",
"high": 91.45, "high": 91.5,
"low": 87.25, "low": 87.25,
"close": 88.7, "close": 90.15,
"volume": 26427225.0, "volume": 18345094.0,
"amount": 2340941950.5 "amount": 1653609373.75
} }
], ],
"688639": [ "688639": [
@@ -581,10 +581,10 @@
{ {
"date": "2026-07-20", "date": "2026-07-20",
"high": 17.56, "high": 17.56,
"low": 16.1, "low": 15.56,
"close": 16.14, "close": 15.99,
"volume": 10030710.0, "volume": 9039229.0,
"amount": 16760.0 "amount": 14474.0
} }
], ],
"688582": [ "688582": [
@@ -623,10 +623,10 @@
{ {
"date": "2026-07-20", "date": "2026-07-20",
"high": 43.8, "high": 43.8,
"low": 38.5, "low": 36.01,
"close": 38.61, "close": 36.95,
"volume": 10970620.0, "volume": 9774815.0,
"amount": 44852.0 "amount": 37047.0
} }
], ],
"01478": [ "01478": [
@@ -666,9 +666,9 @@
"date": "2026-07-20", "date": "2026-07-20",
"high": 7.02, "high": 7.02,
"low": 6.52, "low": 6.52,
"close": 6.67, "close": 6.73,
"volume": 7833000.0, "volume": 4247000.0,
"amount": 52372235.0 "amount": 28512618.0
} }
], ],
"688755": [ "688755": [
@@ -707,10 +707,10 @@
{ {
"date": "2026-07-20", "date": "2026-07-20",
"high": 25.89, "high": 25.89,
"low": 23.8, "low": 22.97,
"close": 24.01, "close": 23.23,
"volume": 1862009.0, "volume": 1436355.0,
"amount": 4574.0 "amount": 3390.0
} }
], ],
"02202": [ "02202": [
@@ -750,9 +750,9 @@
"date": "2026-07-20", "date": "2026-07-20",
"high": 2.55, "high": 2.55,
"low": 2.34, "low": 2.34,
"close": 2.48, "close": 2.44,
"volume": 40325958.0, "volume": 13295500.0,
"amount": 98037795.66 "amount": 32619014.9
} }
], ],
"688692": [ "688692": [
@@ -791,10 +791,10 @@
{ {
"date": "2026-07-20", "date": "2026-07-20",
"high": 231.68, "high": 231.68,
"low": 215.0, "low": 209.0,
"close": 215.4, "close": 216.1,
"volume": 1706538.0, "volume": 1795104.0,
"amount": 38102.0 "amount": 38484.0
} }
], ],
"300750": [ "300750": [
@@ -832,11 +832,11 @@
}, },
{ {
"date": "2026-07-20", "date": "2026-07-20",
"high": 375.24, "high": 377.6,
"low": 360.0, "low": 353.8,
"close": 360.0, "close": 376.43,
"volume": 512453.0, "volume": 447908.0,
"amount": 1884671.0 "amount": 1663278.0
} }
], ],
"688696": [ "688696": [
@@ -875,10 +875,10 @@
{ {
"date": "2026-07-20", "date": "2026-07-20",
"high": 67.39, "high": 67.39,
"low": 62.37, "low": 61.5,
"close": 62.9, "close": 62.72,
"volume": 1185875.0, "volume": 1078476.0,
"amount": 7647.0 "amount": 6756.0
} }
], ],
"518880": [ "518880": [
@@ -916,11 +916,11 @@
}, },
{ {
"date": "2026-07-20", "date": "2026-07-20",
"high": 8.326, "high": 8.362,
"low": 8.256, "low": 8.256,
"close": 8.311, "close": 8.318,
"volume": 2844769.0, "volume": 2007208.0,
"amount": 235805.0 "amount": 167139.0
} }
], ],
"688708": [ "688708": [
@@ -959,10 +959,10 @@
{ {
"date": "2026-07-20", "date": "2026-07-20",
"high": 46.18, "high": 46.18,
"low": 43.51, "low": 42.31,
"close": 44.09, "close": 44.91,
"volume": 1442625.0, "volume": 1781103.0,
"amount": 6448.0 "amount": 7823.0
} }
], ],
"688779": [ "688779": [
@@ -1000,11 +1000,11 @@
}, },
{ {
"date": "2026-07-20", "date": "2026-07-20",
"high": 7.03, "high": 7.04,
"low": 6.67, "low": 6.67,
"close": 6.69, "close": 6.9,
"volume": 26278826.0, "volume": 37370857.0,
"amount": 17957.0 "amount": 25733.0
} }
], ],
"300035": [ "300035": [
@@ -1043,10 +1043,10 @@
{ {
"date": "2026-07-20", "date": "2026-07-20",
"high": 12.68, "high": 12.68,
"low": 12.05, "low": 11.85,
"close": 12.08, "close": 12.09,
"volume": 158047.0, "volume": 186464.0,
"amount": 19469.0 "amount": 22594.0
} }
], ],
"688789": [ "688789": [
@@ -1084,11 +1084,11 @@
}, },
{ {
"date": "2026-07-20", "date": "2026-07-20",
"high": 47.73, "high": 47.88,
"low": 45.19, "low": 45.19,
"close": 46.1, "close": 47.71,
"volume": 2179608.0, "volume": 1814330.0,
"amount": 10118.0 "amount": 8528.0
} }
], ],
"600563": [ "600563": [
@@ -1146,9 +1146,9 @@
"date": "2026-07-20", "date": "2026-07-20",
"high": 1072.0, "high": 1072.0,
"low": 950.55, "low": 950.55,
"close": 979.46, "close": 1004.0,
"volume": 561861.0, "volume": 453665.0,
"amount": 5648563.0 "amount": 4609438.0
} }
], ],
"601899": [ "601899": [
@@ -1186,11 +1186,11 @@
}, },
{ {
"date": "2026-07-20", "date": "2026-07-20",
"high": 28.82, "high": 29.45,
"low": 27.92, "low": 27.92,
"close": 28.36, "close": 29.35,
"volume": 2904169.0, "volume": 2757683.0,
"amount": 824106.0 "amount": 799203.0
} }
], ],
"688981": [ "688981": [
@@ -1229,10 +1229,10 @@
{ {
"date": "2026-07-20", "date": "2026-07-20",
"high": 155.51, "high": 155.51,
"low": 139.02, "low": 138.11,
"close": 139.88, "close": 144.0,
"volume": 102952043.0, "volume": 93942500.0,
"amount": 1517166.0 "amount": 1346494.0
} }
], ],
"688372": [ "688372": [
@@ -1271,10 +1271,10 @@
{ {
"date": "2026-07-20", "date": "2026-07-20",
"high": 139.2, "high": 139.2,
"low": 120.66, "low": 115.0,
"close": 120.73, "close": 120.5,
"volume": 11689864.0, "volume": 13508171.0,
"amount": 151702.0 "amount": 166596.0
} }
], ],
"688411": [ "688411": [
@@ -1313,10 +1313,10 @@
{ {
"date": "2026-07-20", "date": "2026-07-20",
"high": 193.48, "high": 193.48,
"low": 179.03, "low": 175.6,
"close": 181.68, "close": 181.35,
"volume": 4238973.0, "volume": 4399767.0,
"amount": 78494.0 "amount": 80465.0
} }
], ],
"688271": [ "688271": [
@@ -1354,11 +1354,11 @@
}, },
{ {
"date": "2026-07-20", "date": "2026-07-20",
"high": 113.43, "high": 119.85,
"low": 109.86, "low": 107.89,
"close": 110.37, "close": 118.0,
"volume": 8809320.0, "volume": 13911975.0,
"amount": 97913.0 "amount": 158683.0
} }
], ],
"688439": [ "688439": [
@@ -1397,10 +1397,10 @@
{ {
"date": "2026-07-20", "date": "2026-07-20",
"high": 42.38, "high": 42.38,
"low": 39.58, "low": 37.05,
"close": 39.58, "close": 37.89,
"volume": 3189314.0, "volume": 3734574.0,
"amount": 13157.0 "amount": 14394.0
} }
], ],
"688588": [ "688588": [
@@ -1439,10 +1439,10 @@
{ {
"date": "2026-07-20", "date": "2026-07-20",
"high": 9.19, "high": 9.19,
"low": 8.55, "low": 8.46,
"close": 8.6, "close": 8.63,
"volume": 4173682.0, "volume": 4281284.0,
"amount": 3684.0 "amount": 3721.0
} }
], ],
"688621": [ "688621": [
@@ -1481,10 +1481,10 @@
{ {
"date": "2026-07-20", "date": "2026-07-20",
"high": 61.75, "high": 61.75,
"low": 56.73, "low": 54.72,
"close": 57.58, "close": 56.88,
"volume": 6154202.0, "volume": 4638705.0,
"amount": 36271.0 "amount": 26217.0
} }
], ],
"000850": [ "000850": [
@@ -1515,10 +1515,10 @@
{ {
"date": "2026-07-20", "date": "2026-07-20",
"high": 4.24, "high": 4.24,
"low": 3.98, "low": 3.94,
"close": 4.08, "close": 3.98,
"volume": 159057.0, "volume": 110497.0,
"amount": 6535.0 "amount": 4439.0
} }
], ],
"301308": [ "301308": [
@@ -1617,10 +1617,10 @@
{ {
"date": "2026-07-20", "date": "2026-07-20",
"high": 10.58, "high": 10.58,
"low": 10.3, "low": 10.15,
"close": 10.3, "close": 10.35,
"volume": 245805.0, "volume": 193759.0,
"amount": 25674.0 "amount": 19951.0
} }
], ],
"688087": [ "688087": [
@@ -1642,11 +1642,11 @@
}, },
{ {
"date": "2026-07-20", "date": "2026-07-20",
"high": 38.23, "high": 38.97,
"low": 35.99, "low": 35.99,
"close": 36.18, "close": 38.57,
"volume": 2780918.0, "volume": 2483595.0,
"amount": 10256.0 "amount": 9341.0
} }
], ],
"688466": [ "688466": [
@@ -1669,10 +1669,10 @@
{ {
"date": "2026-07-20", "date": "2026-07-20",
"high": 13.19, "high": 13.19,
"low": 12.44, "low": 11.94,
"close": 12.6, "close": 12.28,
"volume": 1788822.0, "volume": 1944699.0,
"amount": 2286.0 "amount": 2412.0
} }
], ],
"600111": [ "600111": [
@@ -1694,5 +1694,155 @@
"volume": 35790245.0, "volume": 35790245.0,
"amount": 1105458.0 "amount": 1105458.0
} }
],
"688425": [
{
"date": "2026-07-20",
"high": 4.15,
"low": 4.08,
"close": 4.14,
"volume": 1496345.0,
"amount": 618.0
}
],
"688608": [
{
"date": "2026-07-20",
"high": 115.2,
"low": 110.0,
"close": 114.41,
"volume": 1143940.0,
"amount": 12834.0
}
],
"688800": [
{
"date": "2026-07-20",
"high": 65.11,
"low": 62.65,
"close": 63.51,
"volume": 1447347.0,
"amount": 9241.0
}
],
"688758": [
{
"date": "2026-07-20",
"high": 25.26,
"low": 22.95,
"close": 24.79,
"volume": 3826638.0,
"amount": 9338.0
}
],
"688289": [
{
"date": "2026-07-20",
"high": 15.64,
"low": 15.35,
"close": 15.52,
"volume": 755498.0,
"amount": 1172.0
}
],
"688726": [
{
"date": "2026-07-20",
"high": 39.6,
"low": 38.03,
"close": 38.48,
"volume": 1020332.0,
"amount": 3950.0
}
],
"688576": [
{
"date": "2026-07-20",
"high": 39.29,
"low": 38.18,
"close": 38.67,
"volume": 61912.0,
"amount": 239.0
}
],
"688122": [
{
"date": "2026-07-20",
"high": 49.28,
"low": 48.3,
"close": 48.95,
"volume": 1345698.0,
"amount": 6582.0
}
],
"605090": [
{
"date": "2026-07-20",
"high": 39.6,
"low": 37.7,
"close": 38.19,
"volume": 87773.0,
"amount": 33970.0
}
],
"603766": [
{
"date": "2026-07-20",
"high": 13.15,
"low": 12.91,
"close": 13.01,
"volume": 26823.0,
"amount": 3485.0
}
],
"688550": [
{
"date": "2026-07-20",
"high": 44.8,
"low": 43.0,
"close": 44.58,
"volume": 977623.0,
"amount": 4270.0
}
],
"688281": [
{
"date": "2026-07-20",
"high": 44.4,
"low": 43.0,
"close": 43.01,
"volume": 473467.0,
"amount": 2067.0
}
],
"688617": [
{
"date": "2026-07-20",
"high": 224.9,
"low": 201.31,
"close": 220.22,
"volume": 2634862.0,
"amount": 55058.0
}
],
"688606": [
{
"date": "2026-07-20",
"high": 45.44,
"low": 43.62,
"close": 44.26,
"volume": 544130.0,
"amount": 2421.0
}
],
"688314": [
{
"date": "2026-07-20",
"high": 35.91,
"low": 30.24,
"close": 31.14,
"volume": 2832553.0,
"amount": 9286.0
}
] ]
} }
File diff suppressed because it is too large Load Diff
+203 -203
View File
@@ -1,203 +1,203 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""market_insight.py — 基于 market.json 数据生成基础洞察 + 潜力挖掘 """market_insight.py — 基于 market.json 数据生成基础洞察 + 潜力挖掘
输出:更新 data/market.json 中的 insights / potential_stocks 字段 输出:更新 data/market.json 中的 insights / potential_stocks 字段
策略: 策略:
1. 行业热点 vs 持仓匹配 → 相关影响 1. 行业热点 vs 持仓匹配 → 相关影响
2. 资金流向异常 → 关注信号 2. 资金流向异常 → 关注信号
3. 市场情绪 → 每日研判 3. 市场情绪 → 每日研判
4. 潜力挖掘 → 强势行业中寻找持仓相关标的 4. 潜力挖掘 → 强势行业中寻找持仓相关标的
""" """
import json import json
import sys import sys
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
DATA_DIR = Path(__file__).parent.parent / "data" DATA_DIR = Path(__file__).parent.parent / "data"
# ── 持仓股 → 行业映射(从 stock_profiles 自动提取) ── # ── 持仓股 → 行业映射(从 stock_profiles 自动提取) ──
def load_holding_industry_map(): def load_holding_industry_map():
"""从 stock_profiles 和 portfolio 提取持仓→行业映射""" """从 stock_profiles 和 portfolio 提取持仓→行业映射"""
try: try:
with open(DATA_DIR / "stock_profiles.json", "r", encoding="utf-8") as f: with open(DATA_DIR / "stock_profiles.json", "r", encoding="utf-8") as f:
profiles = json.load(f).get("profiles", []) profiles = json.load(f).get("profiles", [])
# 优先从DB读取持仓(Dad铁律:禁用JSON直读) # 优先从DB读取持仓(Dad铁律:禁用JSON直读)
from mo_data import read_portfolio from mo_data import read_portfolio
portfolio = read_portfolio() portfolio = read_portfolio()
except FileNotFoundError: except FileNotFoundError:
return {} return {}
# 构建 code→name 映射(从 portfolio # 构建 code→name 映射(从 portfolio
code_to_name = {} code_to_name = {}
for item in portfolio.get("holdings", []): for item in portfolio.get("holdings", []):
code_to_name[item.get("code", "")] = item.get("name", "") code_to_name[item.get("code", "")] = item.get("name", "")
# 构建行业→持仓列表 # 构建行业→持仓列表
industry_holdings = {} industry_holdings = {}
for p in profiles: for p in profiles:
code = p.get("code", "") code = p.get("code", "")
name = p.get("name", "") name = p.get("name", "")
sector = p.get("sector", "") sector = p.get("sector", "")
if not sector or sector == "待补全": if not sector or sector == "待补全":
continue continue
# 提取一级行业(取斜杠前第一个) # 提取一级行业(取斜杠前第一个)
primary = sector.split("/")[0].split("")[0].strip() primary = sector.split("/")[0].split("")[0].strip()
if primary: if primary:
industry_holdings.setdefault(primary, []).append({ industry_holdings.setdefault(primary, []).append({
"code": code, "code": code,
"name": name, "name": name,
"sector": sector, "sector": sector,
}) })
return industry_holdings return industry_holdings
def generate(): def generate():
# market_path 在DB和fallback两个分支后都会用到,所以提前定义 # market_path 在DB和fallback两个分支后都会用到,所以提前定义
market_path = DATA_DIR / "market.json" market_path = DATA_DIR / "market.json"
# 优先从 SQLite 读取市场数据 # 优先从 SQLite 读取市场数据
try: try:
from mofin_db import get_conn, query_latest_market from mofin_db import get_conn, query_latest_market
conn = get_conn() conn = get_conn()
market = query_latest_market(conn) market = query_latest_market(conn)
conn.close() conn.close()
if market and market.get("sectors"): if market and market.get("sectors"):
sectors = market["sectors"] sectors = market["sectors"]
top_gainers = market.get("top_gainers", []) top_gainers = market.get("top_gainers", [])
top_losers = market.get("top_losers", []) top_losers = market.get("top_losers", [])
mood = market.get("mood", "unknown") mood = market.get("mood", "unknown")
up_ratio = market.get("up_ratio", 0) up_ratio = market.get("up_ratio", 0)
timestamp = market.get("timestamp", "") timestamp = market.get("timestamp", "")
# 字段名适配 # 字段名适配
for s in sectors: for s in sectors:
s["change"] = s.get("change_pct", 0) s["change"] = s.get("change_pct", 0)
for g in top_gainers: for g in top_gainers:
g["change"] = g.get("change_pct", 0) g["change"] = g.get("change_pct", 0)
for l in top_losers: for l in top_losers:
l["change"] = l.get("change_pct", 0) l["change"] = l.get("change_pct", 0)
else: else:
raise Exception("no data") raise Exception("no data")
except Exception: except Exception:
market_path = DATA_DIR / "market.json" market_path = DATA_DIR / "market.json"
with open(market_path, "r", encoding="utf-8") as f: with open(market_path, "r", encoding="utf-8") as f:
market = json.load(f) market = json.load(f)
sectors = market.get("sectors", []) sectors = market.get("sectors", [])
top_gainers = market.get("top_gainers", []) top_gainers = market.get("top_gainers", [])
top_losers = market.get("top_losers", []) top_losers = market.get("top_losers", [])
mood = market.get("mood", "unknown") mood = market.get("mood", "unknown")
up_ratio = market.get("up_ratio", 0) up_ratio = market.get("up_ratio", 0)
timestamp = market.get("timestamp", "") timestamp = market.get("timestamp", "")
industry_holdings = load_holding_industry_map() industry_holdings = load_holding_industry_map()
insights = [] insights = []
potentials = [] potentials = []
# ── 洞察1:市场情绪总览 ── # ── 洞察1:市场情绪总览 ──
mood_cn = {"bullish": "偏强", "neutral": "中性", "bearish": "偏弱", "unknown": "未知"} mood_cn = {"bullish": "偏强", "neutral": "中性", "bearish": "偏弱", "unknown": "未知"}
insights.append( insights.append(
f"市场情绪{mood_cn.get(mood, '未知')},上涨占比{up_ratio}%" f"市场情绪{mood_cn.get(mood, '未知')},上涨占比{up_ratio}%"
) )
# ── 洞察2:领涨行业 vs 持仓影响 ── # ── 洞察2:领涨行业 vs 持仓影响 ──
gainer_insights = [] gainer_insights = []
for g in top_gainers[:3]: for g in top_gainers[:3]:
name = g.get("name", "") name = g.get("name", "")
change = g.get("change", 0) change = g.get("change", 0)
# 看持仓中是否有该行业 # 看持仓中是否有该行业
matched = [] matched = []
for industry, holdings in industry_holdings.items(): for industry, holdings in industry_holdings.items():
if industry in name or name in industry: if industry in name or name in industry:
matched.extend([h["name"] for h in holdings]) matched.extend([h["name"] for h in holdings])
if matched: if matched:
gainer_insights.append( gainer_insights.append(
f"{name}+{change}%, 关联持仓{'/'.join(matched[:3])}受益" f"{name}+{change}%, 关联持仓{'/'.join(matched[:3])}受益"
) )
else: else:
gainer_insights.append(f"{name}+{change}%, 暂无持仓") gainer_insights.append(f"{name}+{change}%, 暂无持仓")
if gainer_insights: if gainer_insights:
insights.append("领涨板块: " + " | ".join(gainer_insights[:2])) insights.append("领涨板块: " + " | ".join(gainer_insights[:2]))
# ── 洞察3:领跌行业 vs 持仓风险 ── # ── 洞察3:领跌行业 vs 持仓风险 ──
loser_insights = [] loser_insights = []
for g in top_losers[:3]: for g in top_losers[:3]:
name = g.get("name", "") name = g.get("name", "")
change = g.get("change", 0) change = g.get("change", 0)
matched = [] matched = []
for industry, holdings in industry_holdings.items(): for industry, holdings in industry_holdings.items():
if industry in name or name in industry: if industry in name or name in industry:
matched.extend([h["name"] for h in holdings]) matched.extend([h["name"] for h in holdings])
if matched: if matched:
loser_insights.append( loser_insights.append(
f"{name}{change}%, {'/'.join(matched[:2])}需关注" f"{name}{change}%, {'/'.join(matched[:2])}需关注"
) )
else: else:
loser_insights.append(f"{name}{change}%") loser_insights.append(f"{name}{change}%")
if loser_insights: if loser_insights:
insights.append("风险板块: " + " | ".join(loser_insights[:3])) insights.append("风险板块: " + " | ".join(loser_insights[:3]))
# ── 洞察4:资金流向异动 ── # ── 洞察4:资金流向异动 ──
big_inflow = [s for s in sectors if s.get("net_inflow", 0) > 50] big_inflow = [s for s in sectors if (s.get("net_inflow") or 0) > 50]
big_outflow = [s for s in sectors if s.get("net_inflow", 0) < -50] big_outflow = [s for s in sectors if (s.get("net_inflow") or 0) < -50]
if big_inflow: if big_inflow:
top = max(big_inflow, key=lambda s: s["net_inflow"]) top = max(big_inflow, key=lambda s: s["net_inflow"])
insights.append( insights.append(
f"资金流入最大: {top['name']} {top['net_inflow']}亿" f"资金流入最大: {top['name']} {top['net_inflow']}亿"
) )
if big_outflow: if big_outflow:
top = min(big_outflow, key=lambda s: s["net_inflow"]) top = min(big_outflow, key=lambda s: s["net_inflow"])
insights.append( insights.append(
f"资金流出最大: {top['name']} {top['net_inflow']}亿" f"资金流出最大: {top['name']} {top['net_inflow']}亿"
) )
# ── 潜力股挖掘:从强势行业中找持仓或自选相关 ── # ── 潜力股挖掘:从强势行业中找持仓或自选相关 ──
for g in top_gainers[:5]: for g in top_gainers[:5]:
name = g.get("name", "") name = g.get("name", "")
change = g.get("change", 0) change = g.get("change", 0)
if change < 2: if change < 2:
continue # 只关注涨>2%的 continue # 只关注涨>2%的
# 找该行业指数有没有关联持仓 # 找该行业指数有没有关联持仓
lead_stock = g.get("lead_stock", "") lead_stock = g.get("lead_stock", "")
if lead_stock: if lead_stock:
potentials.append({ potentials.append({
"name": lead_stock, "name": lead_stock,
"reason": f"{name}领涨股, 板块+{change}%", "reason": f"{name}领涨股, 板块+{change}%",
}) })
# 看持仓中是否有该行业 # 看持仓中是否有该行业
for industry, holdings in industry_holdings.items(): for industry, holdings in industry_holdings.items():
if industry in name or name in industry: if industry in name or name in industry:
for h in holdings: for h in holdings:
potentials.append({ potentials.append({
"name": h["name"], "name": h["name"],
"reason": f"所在行业{name}{change}%", "reason": f"所在行业{name}{change}%",
}) })
# 去重(最多5条) # 去重(最多5条)
seen = set() seen = set()
unique_potentials = [] unique_potentials = []
for p in potentials: for p in potentials:
key = p["name"] key = p["name"]
if key not in seen: if key not in seen:
seen.add(key) seen.add(key)
unique_potentials.append(p) unique_potentials.append(p)
if len(unique_potentials) >= 5: if len(unique_potentials) >= 5:
break break
potentials = unique_potentials potentials = unique_potentials
# ── 写入 market.json ── # ── 写入 market.json ──
market["insights"] = insights market["insights"] = insights
market["potential_stocks"] = potentials market["potential_stocks"] = potentials
market["insight_timestamp"] = datetime.now().strftime("%Y-%m-%d %H:%M") market["insight_timestamp"] = datetime.now().strftime("%Y-%m-%d %H:%M")
with open(market_path, "w", encoding="utf-8") as f: with open(market_path, "w", encoding="utf-8") as f:
json.dump(market, f, ensure_ascii=False, indent=2) json.dump(market, f, ensure_ascii=False, indent=2)
print(f"生成{len(insights)}条洞察 + {len(potentials)}条潜力挖掘") print(f"生成{len(insights)}条洞察 + {len(potentials)}条潜力挖掘")
if __name__ == "__main__": if __name__ == "__main__":
generate() generate()
+205 -205
View File
@@ -1,205 +1,205 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""market_scanner.py — 全市场异动扫描(替代小果扫描线) """market_scanner.py — 全市场异动扫描(替代小果扫描线)
每15分钟扫描: 每15分钟扫描:
1. 板块轮动检测(从已采集的 sector_snapshots 读) 1. 板块轮动检测(从已采集的 sector_snapshots 读)
2. 热门板块领涨股扫描(从腾讯API批量拉) 2. 热门板块领涨股扫描(从腾讯API批量拉)
3. 资金流向异常检测(从已有 capital_flow_cache 读) 3. 资金流向异常检测(从已有 capital_flow_cache 读)
4. 输出候选股到 candidates 表 4. 输出候选股到 candidates 表
不依赖小果LLM,纯数据驱动。 不依赖小果LLM,纯数据驱动。
""" """
import sys, json, sqlite3, urllib.request, re, time import sys, json, sqlite3, urllib.request, re, time
from pathlib import Path from pathlib import Path
from datetime import datetime from datetime import datetime
# XMPP 日志 hook # XMPP 日志 hook
try: try:
sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from xmpp_logger import log_xmpp from xmpp_logger import log_xmpp
except ImportError: except ImportError:
def log_xmpp(*a, **kw): pass def log_xmpp(*a, **kw): pass
DB_PATH = Path("/home/hmo/MoFin/data/mofin.db") DB_PATH = Path("/home/hmo/MoFin/data/mofin.db")
CANDIDATES_FILE = Path("/home/hmo/web-dashboard/data/candidate_pool.json") CANDIDATES_FILE = Path("/home/hmo/web-dashboard/data/candidate_pool.json")
UA = "Mozilla/5.0" UA = "Mozilla/5.0"
def get_conn(): def get_conn():
return sqlite3.connect(str(DB_PATH)) return sqlite3.connect(str(DB_PATH))
def fetch_qq_batch(symbols): def fetch_qq_batch(symbols):
"""腾讯批量行情""" """腾讯批量行情"""
if not symbols: if not symbols:
return {} return {}
url = f"http://qt.gtimg.cn/q={','.join(symbols)}" url = f"http://qt.gtimg.cn/q={','.join(symbols)}"
try: try:
req = urllib.request.Request(url, headers={"User-Agent": UA}) req = urllib.request.Request(url, headers={"User-Agent": UA})
proxy = urllib.request.ProxyHandler({}) proxy = urllib.request.ProxyHandler({})
opener = urllib.request.build_opener(proxy) opener = urllib.request.build_opener(proxy)
with opener.open(req, timeout=15) as r: with opener.open(req, timeout=15) as r:
text = r.read().decode("gbk") text = r.read().decode("gbk")
results = {} results = {}
for line in text.strip().split("\n"): for line in text.strip().split("\n"):
if "~" not in line: if "~" not in line:
continue continue
parts = line.split("~") parts = line.split("~")
if len(parts) < 40: if len(parts) < 40:
continue continue
m = re.search(r'_(\w+)=', parts[0]) m = re.search(r'_(\w+)=', parts[0])
market = m.group(1) if m else "" market = m.group(1) if m else ""
code = parts[2] code = parts[2]
name = parts[1] name = parts[1]
price = float(parts[3]) if parts[3] else 0 price = float(parts[3]) if parts[3] else 0
chg_pct = float(parts[32]) if parts[32] else 0 chg_pct = float(parts[32]) if parts[32] else 0
high = float(parts[33]) if parts[33] else 0 high = float(parts[33]) if parts[33] else 0
low = float(parts[34]) if parts[34] else 0 low = float(parts[34]) if parts[34] else 0
volume = int(parts[6]) if parts[6] else 0 volume = int(parts[6]) if parts[6] else 0
amount = float(parts[37]) if len(parts) > 37 and parts[37] else 0 amount = float(parts[37]) if len(parts) > 37 and parts[37] else 0
if price > 0: if price > 0:
results[code] = {"code": code, "name": name, "price": price, results[code] = {"code": code, "name": name, "price": price,
"change_pct": chg_pct, "high": high, "low": low, "change_pct": chg_pct, "high": high, "low": low,
"volume": volume, "amount": amount, "volume": volume, "amount": amount,
"market": "SH" if market == "sh" else "SZ" if market == "sz" else "HK"} "market": "SH" if market == "sh" else "SZ" if market == "sz" else "HK"}
return results return results
except Exception as e: except Exception as e:
print(f"[SCANNER] 腾讯API错误: {e}", file=sys.stderr) print(f"[SCANNER] 腾讯API错误: {e}", file=sys.stderr)
return {} return {}
def scan_hot_sectors(): def scan_hot_sectors():
"""从DB读热门板块,返回板块名+领涨股""" """从DB读热门板块,返回板块名+领涨股"""
conn = get_conn() conn = get_conn()
latest = conn.execute("SELECT MAX(id) FROM market_snapshots").fetchone()[0] latest = conn.execute("SELECT MAX(id) FROM market_snapshots").fetchone()[0]
if not latest: if not latest:
conn.close() conn.close()
return [] return []
sectors = conn.execute(""" sectors = conn.execute("""
SELECT name, change_pct, lead_stock, lead_stock_code, up_count, down_count SELECT name, change_pct, lead_stock, lead_stock_code, up_count, down_count
FROM sector_snapshots WHERE snapshot_id=? FROM sector_snapshots WHERE snapshot_id=?
ORDER BY change_pct DESC LIMIT 15 ORDER BY change_pct DESC LIMIT 15
""", (latest,)).fetchall() """, (latest,)).fetchall()
conn.close() conn.close()
return [{"name": s[0], "change": s[1], "lead_stock": s[2], return [{"name": s[0], "change": s[1], "lead_stock": s[2],
"lead_code": s[3], "up": s[4], "down": s[5]} for s in sectors if s[1] > 2.0] "lead_code": s[3], "up": s[4], "down": s[5]} for s in sectors if s[1] > 2.0]
def scan_candidates(): def scan_candidates():
"""主扫描流程""" """主扫描流程"""
print(f"[SCANNER] {datetime.now().strftime('%H:%M')} 开始扫描", flush=True) print(f"[SCANNER] {datetime.now().strftime('%H:%M')} 开始扫描", flush=True)
# 1. 热门板块领涨股 # 1. 热门板块领涨股
hot = scan_hot_sectors() hot = scan_hot_sectors()
print(f" 热门板块(涨幅>2%): {len(hot)}", flush=True) print(f" 热门板块(涨幅>2%): {len(hot)}", flush=True)
candidates = {} candidates = {}
# 从热门板块拉领涨股 # 从热门板块拉领涨股
lead_codes = [] lead_codes = []
for s in hot: for s in hot:
if s.get("lead_code"): if s.get("lead_code"):
lc = str(s["lead_code"]).strip() lc = str(s["lead_code"]).strip()
if lc and lc not in candidates: if lc and lc not in candidates:
lead_codes.append(lc) lead_codes.append(lc)
candidates[lc] = {"source": f"板块:{s['name']}(+{s['change']:.1f}%)", "sector": s["name"]} candidates[lc] = {"source": f"板块:{s['name']}(+{s['change']:.1f}%)", "sector": s["name"]}
print(f" 领涨股待查: {len(lead_codes)}", flush=True) print(f" 领涨股待查: {len(lead_codes)}", flush=True)
# 2. 腾讯API批量查行情 # 2. 腾讯API批量查行情
symbols = [] symbols = []
for c in lead_codes: for c in lead_codes:
if len(c) == 6: if len(c) == 6:
if c.startswith(("5", "6", "9")): if c.startswith(("5", "6", "9")):
symbols.append(f"sh{c}") symbols.append(f"sh{c}")
else: else:
symbols.append(f"sz{c}") symbols.append(f"sz{c}")
else: else:
symbols.append(f"hk{c}") symbols.append(f"hk{c}")
prices = fetch_qq_batch(symbols) prices = fetch_qq_batch(symbols)
print(f" 行情返回: {len(prices)}", flush=True) print(f" 行情返回: {len(prices)}", flush=True)
# 3. 评估候选 # 3. 评估候选
new_candidates = [] new_candidates = []
for code, info in prices.items(): for code, info in prices.items():
if code not in candidates: if code not in candidates:
continue continue
src = candidates[code] src = candidates[code]
price = info["price"] price = info["price"]
chg = info["change_pct"] chg = info["change_pct"]
name = info["name"] name = info["name"]
vol = info["volume"] vol = info["volume"]
amt = info["amount"] amt = info["amount"]
# 条件:涨幅>3%,有量 # 条件:涨幅>3%,有量
if chg < 3.0: if chg < 3.0:
continue continue
if vol <= 0: if vol <= 0:
continue continue
score = min(10, round(3 + chg * 0.5 + (amt / 1e8 if amt > 0 else 0) * 0.1, 1)) score = min(10, round(3 + chg * 0.5 + (amt / 1e8 if amt > 0 else 0) * 0.1, 1))
entry_low = round(price * 0.95, 2) entry_low = round(price * 0.95, 2)
entry_high = round(price, 2) entry_high = round(price, 2)
stop_loss = round(price * 0.92, 2) stop_loss = round(price * 0.92, 2)
take_profit = round(price * 1.15, 2) take_profit = round(price * 1.15, 2)
candidate = { candidate = {
"code": code, "code": code,
"name": name, "name": name,
"price": price, "price": price,
"change_pct": chg, "change_pct": chg,
"score": score, "score": score,
"entry_low": entry_low, "entry_low": entry_low,
"entry_high": entry_high, "entry_high": entry_high,
"stop_loss": stop_loss, "stop_loss": stop_loss,
"take_profit": take_profit, "take_profit": take_profit,
"source": src["source"], "source": src["source"],
"sector": src.get("sector", ""), "sector": src.get("sector", ""),
"reason": f"热门板块{src['sector']}领涨+{chg:.1f}%" "reason": f"热门板块{src['sector']}领涨+{chg:.1f}%"
} }
new_candidates.append(candidate) new_candidates.append(candidate)
print(f"{code} {name}{price} (+{chg:.1f}%) 评分{score}", flush=True) print(f"{code} {name}{price} (+{chg:.1f}%) 评分{score}", flush=True)
# 4. 写入candidates表 # 4. 写入candidates表
if new_candidates: if new_candidates:
conn = get_conn() conn = get_conn()
for c in new_candidates: for c in new_candidates:
conn.execute( conn.execute(
"INSERT OR REPLACE INTO candidates (code, name, price, change_pct, score, " "INSERT OR REPLACE INTO candidates (code, name, price, change_pct, score, "
"entry_low, entry_high, stop_loss, take_profit, source, sector, reason, created_at) " "entry_low, entry_high, stop_loss, take_profit, source, sector, reason, created_at) "
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,datetime('now','localtime'))", "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,datetime('now','localtime'))",
(c["code"], c["name"], c["price"], c["change_pct"], c["score"], (c["code"], c["name"], c["price"], c["change_pct"], c["score"],
c["entry_low"], c["entry_high"], c["stop_loss"], c["take_profit"], c["entry_low"], c["entry_high"], c["stop_loss"], c["take_profit"],
c["source"], c["sector"], c["reason"]) c["source"], c["sector"], c["reason"])
) )
conn.commit() conn.commit()
conn.close() conn.close()
print(f" ✅ 写入{len(new_candidates)}只候选", flush=True) print(f" ✅ 写入{len(new_candidates)}只候选", flush=True)
else: else:
print(f" ⚪ 无新候选", flush=True) print(f" ⚪ 无新候选", flush=True)
# 5. 推送到Dad(只推高分) # 5. 推送到Dad(只推高分)
high_score = [c for c in new_candidates if c["score"] >= 6] high_score = [c for c in new_candidates if c["score"] >= 6]
if high_score: if high_score:
lines = ["🔍 市场扫描发现潜在机会:"] lines = ["🔍 市场扫描发现潜在机会:"]
for c in high_score[:3]: for c in high_score[:3]:
lines.append( lines.append(
f" {c['name']}({c['code']}) 价{c['price']:.2f}(+{c['change_pct']:.1f}%) " f" {c['name']}({c['code']}) 价{c['price']:.2f}(+{c['change_pct']:.1f}%) "
f"评分{c['score']}/10 | {c['reason']}" f"评分{c['score']}/10 | {c['reason']}"
) )
msg = "\n".join(lines) msg = "\n".join(lines)
# 推XMPP(通过知微 Bot HTTP 桥 :5805 # 推XMPP(通过知微 Bot HTTP 桥 :5805
import time as _time import time as _time
t0 = _time.time() t0 = _time.time()
try: try:
payload = json.dumps({"to": "hmo@yoin.fun", "body": msg, "type": "chat"}).encode() payload = json.dumps({"to": "hmo@yoin.fun", "body": msg, "type": "chat"}).encode()
req = urllib.request.Request("http://127.0.0.1:5805/", data=payload, req = urllib.request.Request("http://127.0.0.1:5805/", data=payload,
headers={"Content-Type": "application/json"}) headers={"Content-Type": "application/json"})
urllib.request.urlopen(req, timeout=5) urllib.request.urlopen(req, timeout=5)
print(f" 📨 已推送{len(high_score)}只高评分候选", flush=True) print(f" 📨 已推送{len(high_score)}只高评分候选", flush=True)
log_xmpp("out", "zhiwei@yoin.fun", "hmo@yoin.fun", msg, "ok", None, int((_time.time()-t0)*1000)) log_xmpp("out", "zhiwei@yoin.fun", "hmo@yoin.fun", msg, "ok", None, int((_time.time()-t0)*1000))
except Exception as e: except Exception as e:
print(f" ⚠️ 推送失败: {e}", file=sys.stderr) print(f" ⚠️ 推送失败: {e}", file=sys.stderr)
log_xmpp("out", "zhiwei@yoin.fun", "hmo@yoin.fun", msg, "error", str(e)[:200], int((_time.time()-t0)*1000)) log_xmpp("out", "zhiwei@yoin.fun", "hmo@yoin.fun", msg, "error", str(e)[:200], int((_time.time()-t0)*1000))
if __name__ == "__main__": if __name__ == "__main__":
scan_candidates() scan_candidates()
File diff suppressed because it is too large Load Diff
+59 -64
View File
@@ -1,64 +1,59 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""premarket_full_review.py — 盘前全量重评 """premarket_full_review.py — 盘前全量重评
执行顺序: 执行顺序:
1. regenerate_all() 全量技术参数重评(持仓+自选) 1. regenerate_all() 全量技术参数重评(持仓+自选)
2. batch_reassess.py --type holding --today 持仓12维LLM分析(每日强制刷新) 2. batch_reassess.py --type holding --today 持仓12维LLM分析(每日强制刷新)
3. watchlist_auto_exit() 自选退出检查 3. watchlist_auto_exit() 自选退出检查
4. 输出摘要 4. 输出摘要
调度:交易日 08:10(A股09:30开盘) 调度:交易日 08:10(A股09:30开盘)
""" """
import sys, os, json import sys, os, json
sys.path.insert(0, '/home/hmo/MoFin') sys.path.insert(0, '/home/hmo/MoFin')
# Step 1: 全量技术参数重评 # Step 1: 全量技术参数重评
print("=" * 50) print("=" * 50)
print("📊 盘前全量重评开始") print("📊 盘前全量重评开始")
print("=" * 50) print("=" * 50)
from strategy_lifecycle import regenerate_all from strategy_lifecycle import regenerate_all
result = regenerate_all(stdout=True) result = regenerate_all(stdout=True)
print(f"\n重评完成: {result.get('ok',0)}/{result.get('total',0)}成功") print(f"\n重评完成: {result.get('ok',0)}/{result.get('total',0)}成功")
# Step 1.5: 持仓 12 维 LLM 深度分析(每日强制,14只约8-10分钟 # Step 1.5: 持仓 12 维 LLM 深度分析——后台分离执行(12-40分钟,不能阻塞 cron 的 120s 超时
print("\n" + "=" * 50) print("\n" + "=" * 50)
print("🧠 持仓12维LLM分析(每日强制刷新") print("🧠 持仓12维LLM分析(后台分离启动")
print("=" * 50) print("=" * 50)
import subprocess as _sp import subprocess as _sp
analysis_result = {"ok": 0, "fail": 0, "skip": 0} analysis_result = {"mode": "detached"}
try: try:
r = _sp.run( _log = open("/tmp/holdings_12d_daily.log", "a")
["python3", "/home/hmo/.hermes/profiles/position-analyst/scripts/batch_reassess.py", _sp.Popen(
"--type", "holding", "--today"], ["python3", "/home/hmo/.hermes/profiles/position-analyst/scripts/batch_reassess.py",
capture_output=True, text=True, timeout=3600) "--type", "holding", "--today"],
print(r.stdout[-2000:] if len(r.stdout) > 2000 else r.stdout) stdout=_log, stderr=_log, start_new_session=True)
if r.returncode != 0 and r.stderr: print(" ✅ 12维分析已后台启动,日志: /tmp/holdings_12d_daily.log(结果落DB,不阻塞盘前流程)")
print(f"⚠️ stderr: {r.stderr[:300]}") except Exception as e:
# 从输出尾部解析统计 print(f" ⚠️ 12维分析启动失败: {e}")
import re as _re analysis_result = {"mode": "detached", "error": str(e)[:100]}
m = _re.search(r"完成: (\d+)成功, (\d+)失败, (\d+)跳过", r.stdout)
if m: # Step 2: 自选退出
analysis_result = {"ok": int(m.group(1)), "fail": int(m.group(2)), "skip": int(m.group(3))} print("\n" + "=" * 50)
except Exception as e: print("🔍 自选退出检查")
print(f"⚠️ 12维分析步骤异常: {e}") print("=" * 50)
from scripts.watchlist_auto_exit import main as auto_exit
# Step 2: 自选退出 exited = auto_exit(dry_run=False)
print("\n" + "=" * 50)
print("🔍 自选退出检查") # Step 3: 写入摘要供开盘简报引用
print("=" * 50) summary = {
from scripts.watchlist_auto_exit import main as auto_exit "premarket_at": __import__('datetime').datetime.now().isoformat(),
exited = auto_exit(dry_run=False) "reassess": result,
"llm_analysis_12d": analysis_result,
# Step 3: 写入摘要供开盘简报引用 "auto_exit": [{"code": c, "name": n, "reason": r} for c, n, s, r in exited],
summary = { "total_kept": result.get('total', 0) - len(exited),
"premarket_at": __import__('datetime').datetime.now().isoformat(), }
"reassess": result, os.makedirs("/tmp/mofin_premarket", exist_ok=True)
"llm_analysis_12d": analysis_result, with open("/tmp/mofin_premarket/summary.json", "w") as f:
"auto_exit": [{"code": c, "name": n, "reason": r} for c, n, s, r in exited], json.dump(summary, f, ensure_ascii=False, indent=2)
"total_kept": result.get('total', 0) - len(exited),
} print(f"\n✅ 盘前重评完毕")
os.makedirs("/tmp/mofin_premarket", exist_ok=True)
with open("/tmp/mofin_premarket/summary.json", "w") as f:
json.dump(summary, f, ensure_ascii=False, indent=2)
print(f"\n✅ 盘前重评完毕")
File diff suppressed because it is too large Load Diff
+136 -135
View File
@@ -1,135 +1,136 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""promote_candidates.py — 自动提拔候选股入自选 """promote_candidates.py — 自动提拔候选股入自选
从 candidates 表读未提拔的候选,评估后自动加入 holding_strategies。 从 candidates 表读未提拔的候选,评估后自动加入 holding_strategies。
""" """
import sys, json, sqlite3 import sys, json, sqlite3
from pathlib import Path from pathlib import Path
from datetime import datetime from datetime import datetime
DB_PATH = Path("/home/hmo/MoFin/data/mofin.db") DB_PATH = Path("/home/hmo/MoFin/data/mofin.db")
def main(): def main():
conn = sqlite3.connect(str(DB_PATH)) conn = sqlite3.connect(str(DB_PATH), timeout=30)
conn.row_factory = sqlite3.Row conn.execute("PRAGMA busy_timeout=30000")
conn.row_factory = sqlite3.Row
# 读未提拔候选(按评分降序)
rows = conn.execute(""" # 读未提拔候选(按评分降序)
SELECT c.code, c.name, c.score_final, c.entry_range, c.stop_loss, c.target rows = conn.execute("""
FROM candidates c SELECT c.code, c.name, c.score_final, c.entry_range, c.stop_loss, c.target
WHERE (c.promoted IS NULL OR c.promoted = 0) FROM candidates c
AND (c.dropped IS NULL OR c.dropped = 0) WHERE (c.promoted IS NULL OR c.promoted = 0)
AND c.score_final >= 4 AND (c.dropped IS NULL OR c.dropped = 0)
ORDER BY c.score_final DESC AND c.score_final >= 4
""").fetchall() ORDER BY c.score_final DESC
""").fetchall()
if not rows:
print("[PROMOTE] 无待提拔候选") if not rows:
conn.close() print("[PROMOTE] 无待提拔候选")
return conn.close()
return
promoted = 0
for r in rows: promoted = 0
code = str(r[0]) for r in rows:
name = r[1] or code code = str(r[0])
score = r[2] or 0 name = r[1] or code
entry_range = r[3] or "" score = r[2] or 0
sl = r[4] or 0 entry_range = r[3] or ""
tp = r[5] or 0 sl = r[4] or 0
tp = r[5] or 0
# 解析 entry_range
el, eh = 0, 0 # 解析 entry_range
if "~" in entry_range: el, eh = 0, 0
parts = entry_range.split("~") if "~" in entry_range:
try: parts = entry_range.split("~")
el = float(parts[0]) try:
eh = float(parts[1]) el = float(parts[0])
except: pass eh = float(parts[1])
except: pass
# 查是否已在 holding_strategies
exists = conn.execute( # 查是否已在 holding_strategies
"SELECT id FROM holding_strategies WHERE code=? AND status='active'", exists = conn.execute(
(code,) "SELECT id FROM holding_strategies WHERE code=? AND status='active'",
).fetchone() (code,)
if exists: ).fetchone()
conn.execute("UPDATE candidates SET promoted=1 WHERE code=?", (code,)) if exists:
print(f"{code} {name} 已在自选中,标记promoted") conn.execute("UPDATE candidates SET promoted=1 WHERE code=?", (code,))
continue print(f"{code} {name} 已在自选中,标记promoted")
continue
# 验证实时价格:无有效价格的候选股不入自选(防假数据污染)
try: # 验证实时价格:无有效价格的候选股不入自选(防假数据污染)
import subprocess, json as _jj try:
_r = subprocess.run(["python3", "/home/hmo/.hermes/profiles/position-analyst/scripts/stock_quote.py", code], import subprocess, json as _jj
capture_output=True, text=True, timeout=10) _r = subprocess.run(["python3", "/home/hmo/.hermes/profiles/position-analyst/scripts/stock_quote.py", code],
_q = _jj.loads(_r.stdout) capture_output=True, text=True, timeout=10)
if float(_q.get("price", 0)) <= 0: _q = _jj.loads(_r.stdout)
print(f"{code} {name} 无实时价格,跳过") if float(_q.get("price", 0)) <= 0:
continue print(f"{code} {name} 无实时价格,跳过")
except Exception as _e: continue
print(f"{code} {name} 价格获取失败({_e}),跳过") except Exception as _e:
continue print(f"{code} {name} 价格获取失败({_e}),跳过")
continue
# 构建策略
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") # 构建策略
timing_signal = "买入" if score >= 7 else "关注" now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
price_est = (el + eh) / 2 if el > 0 and eh > 0 else 0 timing_signal = "买入" if score >= 7 else "关注"
reason_text = [] price_est = (el + eh) / 2 if el > 0 and eh > 0 else 0
if el > 0: reason_text.append(f"{el}~{eh}") reason_text = []
if sl > 0: reason_text.append(f"{sl}") if el > 0: reason_text.append(f"{el}~{eh}")
if tp > 0: reason_text.append(f"{tp}") if sl > 0: reason_text.append(f"{sl}")
if sl > 0 and tp > 0 and price_est > 0: if tp > 0: reason_text.append(f"{tp}")
rr = (tp - price_est) / (price_est - sl) if (price_est - sl) > 0 else 0 if sl > 0 and tp > 0 and price_est > 0:
reason_text.append(f"RR{rr:.1f}") rr = (tp - price_est) / (price_est - sl) if (price_est - sl) > 0 else 0
reason_text.append(f"评分{score}") reason_text.append(f"RR{rr:.1f}")
action = " | ".join(reason_text) if reason_text else f"市场扫描发现(评分{score})" reason_text.append(f"评分{score}")
action = " | ".join(reason_text) if reason_text else f"市场扫描发现(评分{score})"
cur = conn.execute("""
INSERT OR IGNORE INTO holding_strategies cur = conn.execute("""
(code, name, price, entry_low, entry_high, stop_loss, take_profit, INSERT OR IGNORE INTO holding_strategies
timing_signal, action, decision_type, strategy_type, status, (code, name, price, entry_low, entry_high, stop_loss, take_profit,
rr_ratio, stock_category, created_at, updated_at, timing_signal, action, decision_type, strategy_type, status,
sector_context, quality_check) rr_ratio, stock_category, created_at, updated_at,
VALUES (?,?,?,?,?,?,?,?,?,'自选策略','scan', sector_context, quality_check)
'active',0,'关注',?,?,'', 'pending') VALUES (?,?,?,?,?,?,?,?,?,'自选策略','scan',
""", (code, name, 0, el, eh, sl, tp, timing_signal, action, now, now)) 'active',0,'关注',?,?,'', 'pending')
newly_added = cur.rowcount > 0 """, (code, name, 0, el, eh, sl, tp, timing_signal, action, now, now))
newly_added = cur.rowcount > 0
conn.execute("UPDATE candidates SET promoted=1 WHERE code=?", (code,))
if newly_added: conn.execute("UPDATE candidates SET promoted=1 WHERE code=?", (code,))
promoted += 1 if newly_added:
print(f"{code} {name} 评分{score} → 已加入自选({timing_signal})", flush=True) promoted += 1
else: print(f"{code} {name} 评分{score} → 已加入自选({timing_signal})", flush=True)
print(f"{code} {name} 已在自选策略中,标记promoted", flush=True) else:
print(f"{code} {name} 已在自选策略中,标记promoted", flush=True)
# 触发全量重评(生成完整9维策略)——仅新插入的股票需要
if newly_added: # 触发全量重评(生成完整9维策略)——仅新插入的股票需要
try: if newly_added:
import subprocess as _sp try:
r = _sp.run(["python3", "/home/hmo/MoFin/scripts/per_stock_reassess.py", code], import subprocess as _sp
capture_output=True, text=True, timeout=60) r = _sp.run(["python3", "/home/hmo/MoFin/scripts/per_stock_reassess.py", code],
if r.returncode == 0: capture_output=True, text=True, timeout=60)
print(f" 重评完成", flush=True) if r.returncode == 0:
else: print(f" 重评完成", flush=True)
print(f" 重评失败: {r.stderr.strip()[:100]}", flush=True) else:
except Exception as e: print(f" 重评失败: {r.stderr.strip()[:100]}", flush=True)
print(f" 重评异常: {e}", flush=True) except Exception as e:
print(f" 重评异常: {e}", flush=True)
conn.commit()
print(f"\n[PROMOTE] 本次提拔{promoted}", flush=True) conn.commit()
print(f"\n[PROMOTE] 本次提拔{promoted}", flush=True)
# 推XMPP
if promoted > 0: # 推XMPP
try: if promoted > 0:
import urllib.request try:
msg = f"📈 自动提拔{promoted}只候选入自选" import urllib.request
payload = json.dumps({"to": "hmo@yoin.fun", "body": msg, "type": "chat"}).encode() msg = f"📈 自动提拔{promoted}只候选入自选"
req = urllib.request.Request("http://127.0.0.1:5805/", data=payload, payload = json.dumps({"to": "hmo@yoin.fun", "body": msg, "type": "chat"}).encode()
headers={"Content-Type": "application/json"}) req = urllib.request.Request("http://127.0.0.1:5805/", data=payload,
urllib.request.urlopen(req, timeout=5) headers={"Content-Type": "application/json"})
except Exception: urllib.request.urlopen(req, timeout=5)
pass except Exception:
pass
conn.close()
conn.close()
if __name__ == "__main__":
main() if __name__ == "__main__":
main()
@@ -13,7 +13,7 @@
import json, sys, os, re, urllib.request, sqlite3 import json, sys, os, re, urllib.request, sqlite3
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from datetime import datetime from datetime import datetime
from mo_data import read_decisions, read_portfolio, get_price from mo_data import read_decisions, read_portfolio
DB_PATH = '/home/hmo/web-dashboard/data/mofin.db' DB_PATH = '/home/hmo/web-dashboard/data/mofin.db'
OUTPUT_PATH = "/home/hmo/web-dashboard/data/strategy_staleness_report.json" OUTPUT_PATH = "/home/hmo/web-dashboard/data/strategy_staleness_report.json"
@@ -23,7 +23,25 @@ CRITICAL_DAYS = 21 # 超过21天→严重警告
DIVERGENCE_WARN = 30 # 偏离买入区>30%→警告 DIVERGENCE_WARN = 30 # 偏离买入区>30%→警告
DIVERGENCE_CRIT = 50 # 偏离>50%→严重 DIVERGENCE_CRIT = 50 # 偏离>50%→严重
# ── 使用 mo_data.get_price 统一获取价格 ── def get_price(code):
"""从腾讯API获取当前价"""
try:
market = "sh" if code.startswith("6") else "sz" if code.startswith("0") or code.startswith("3") else ""
if code.startswith(("00", "30")) or code.startswith("68"):
market = "sh" if code.startswith("6") else "sz"
elif code.startswith(("01", "02", "03")):
market = "sz"
url = f"http://qt.gtimg.cn/q={market}{code}"
req = urllib.request.Request(url, headers={"User-Agent": "curl/7.81"})
with urllib.request.urlopen(req, timeout=5) as resp:
raw = resp.read().decode("gbk")
parts = raw.split("~")
if len(parts) > 3:
price = float(parts[3]) if parts[3] else 0
chg = float(parts[32]) if parts[32] else 0
return price, chg if price > 0 else (None, None)
except: pass
return None, None
def parse_buy_zone(current): def parse_buy_zone(current):
"""从策略current字段提取买入区间最低和最高""" """从策略current字段提取买入区间最低和最高"""
+13
View File
@@ -0,0 +1,13 @@
#!/bin/bash
# sync_profile_scripts.sh — 把 deploy/profile-scripts 全部硬链接到 profile scripts 目录
# 每次部署(scp/git)后必须跑:scp替换文件会破坏硬链接(inode变更),导致cron跑旧版本。
set -e
SRC="/home/hmo/MoFin/deploy/profile-scripts"
DST="/home/hmo/.hermes/profiles/position-analyst/scripts"
count=0
for f in "$SRC"/*.py; do
name=$(basename "$f")
ln -f "$f" "$DST/$name"
count=$((count+1))
done
echo "synced $count scripts (hardlink)"
+393 -327
View File
File diff suppressed because it is too large Load Diff