自成长系统:四层循环架构文档 + 三个代码改动 + 所有日间修复
内容: - docs/SELF_GROWTH_SYSTEM.md (NEW) — 完整的 Sense→Respond→Adapt→Improve 架构文档 - docs/SYSTEM_ARCHITECTURE.md (UPDATED) — 总索引指向新文档,cron数从14更新为31 - hk_rate.py (NEW) — HKD汇率模块,缓存+上次有效汇率自动恢复 - price_monitor.py (MODIFIED) — 价格监控注入分支评估+情景切换检测 - strategy_lifecycle.py (MODIFIED) — 策略生命周期评估上下文 - strategy_tree.py (NEW) — 情景化多分支决策引擎 日间修复(2026-06-23): - stale_push_wlin: cash硬编码146837→读portfolio.json - stale_push_wlin: lot_cost汇率0.93→hkd_to_cny动态 - stale_push_wlin: HK每手默认500股→Tencent API实时f[60] - stale_push_wlin: 重评异步→串行(先重评再出报告) - hk_rate: FALLBACK=0.87硬编码→缓存上次有效汇率 - 新增 cron: 分支扫描每30分, 分支剪枝周六, 硬编码审计17:25 - hardcode_scanner.py 每日扫描所有.py中大额数字
This commit is contained in:
+226
-5
@@ -541,10 +541,29 @@ def reassess_strategy(code, name, price, cost, shares, current_action,
|
||||
new_stop = trailing_stop
|
||||
print(f" 已启用移动止损: {new_stop} (保护+{profit_pct:.1f}%利润)", file=sys.stderr)
|
||||
|
||||
# 最小止损距离:止损不能高于现价的97%(即至少3%下行空间)
|
||||
min_stop = round(price * 0.97, 2)
|
||||
# 最小止损距离 —— 随趋势强度调整(2026-06-23 震度保护规则)
|
||||
# 强趋势(多周期看多 + MA多头排列):最小1.5%下行空间
|
||||
# 普通/弱势:最小3%下行空间
|
||||
is_strong_trend = False
|
||||
trend_align = mtf_adj.get("trend_alignment", "")
|
||||
strong_trend_indicators = ["多周期看多", "多周期多头", "上升"]
|
||||
try:
|
||||
if any(ind in trend_align for ind in strong_trend_indicators) and ma20 > ma60 and cur >= ma20:
|
||||
is_strong_trend = True
|
||||
except (NameError, TypeError):
|
||||
pass # ma20/ma60/cur may be unbound if MTF data insufficient
|
||||
|
||||
if is_strong_trend:
|
||||
min_stop_gap = 0.015 # 1.5%
|
||||
else:
|
||||
min_stop_gap = 0.03 # 3%
|
||||
|
||||
min_stop = round(price * (1 - min_stop_gap), 2)
|
||||
if new_stop > min_stop and not is_deep_loss:
|
||||
old_stop = new_stop
|
||||
new_stop = min_stop
|
||||
if old_stop != new_stop:
|
||||
print(f" 最小止损 {round(min_stop_gap*100)}%间距约束: {old_stop}→{new_stop} (趋势{'强' if is_strong_trend else '普通'})")
|
||||
|
||||
# ----- 止盈设置 -----
|
||||
if is_short_term_strong_trend and not is_new_entry:
|
||||
@@ -866,11 +885,155 @@ def load_fundamentals(code):
|
||||
return {}
|
||||
|
||||
|
||||
def _get_portfolio_risk_state():
|
||||
"""读取 portfolio 组合风险状态(2026-06-23 引擎协调)"""
|
||||
try:
|
||||
# 数据一致性检查:警告多副本(2026-06-23 bugfix)
|
||||
_check_portfolio_consistency()
|
||||
p = json.load(open('/home/hmo/web-dashboard/data/portfolio.json'))
|
||||
pos_pct = p.get('position_pct', 0)
|
||||
cash = p.get('cash', 0)
|
||||
holdings = p.get('holdings', [])
|
||||
weak_cnt = sum(1 for h in holdings if h.get('change_pct', 0) < -15)
|
||||
total = len(holdings) or 1
|
||||
weak_ratio = weak_cnt / total
|
||||
return {
|
||||
'position_pct': pos_pct,
|
||||
'cash': cash,
|
||||
'is_high_position': pos_pct > 80,
|
||||
'is_very_high_position': pos_pct > 90,
|
||||
'is_high_weak': weak_ratio > 0.35,
|
||||
'weak_ratio': round(weak_ratio * 100),
|
||||
'total_holdings': total,
|
||||
}
|
||||
except:
|
||||
return {}
|
||||
|
||||
|
||||
def _is_buy_signal(signal):
|
||||
"""判断信号是否为买入/持有类(用于防洗盘)"""
|
||||
if not signal:
|
||||
return False
|
||||
buy_keywords = ['买入', '持有', '加仓', '阳线企稳', '温和放量', '量价齐升',
|
||||
'缩量回踩', '接近支撑', '关注', '回调机会', '价稳']
|
||||
for kw in buy_keywords:
|
||||
if kw in signal:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _check_portfolio_consistency():
|
||||
"""数据一致性检查:如果存在多份 portfolio.json 则报警(2026-06-23 bugfix)"""
|
||||
main = '/home/hmo/web-dashboard/data/portfolio.json'
|
||||
main_cash = None
|
||||
try:
|
||||
import json
|
||||
main_cash = json.load(open(main)).get('cash')
|
||||
except Exception:
|
||||
return
|
||||
for path in [
|
||||
'/home/hmo/data/portfolio.json',
|
||||
'/home/hmo/projects/MoFin/data/portfolio.json',
|
||||
'/home/hmo/web-dashboard.bak/data/portfolio.json',
|
||||
]:
|
||||
if os.path.exists(path):
|
||||
try:
|
||||
other = json.load(open(path))
|
||||
if other.get('cash') != main_cash:
|
||||
print(f"⚠️ 数据一致性: {os.path.realpath(path)} cash={other.get('cash')} ≠ 主文件 cash={main_cash} (需清理)", file=sys.stderr)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _check_contradiction(code, today_only=True):
|
||||
"""反馈循环核——检查本股是否有刚卖出的记录
|
||||
|
||||
返回 dict or None:
|
||||
- sold_reason: 'portfolio_trim'|'stop_loss'
|
||||
- sold_at: 卖出日期
|
||||
- days_ago: 卖出距今交易日数
|
||||
- is_today: 是否今日卖出
|
||||
- tag: 追加到信号的标注
|
||||
"""
|
||||
try:
|
||||
from datetime import datetime, date
|
||||
dec = json.load(open('/home/hmo/web-dashboard/data/decisions.json'))
|
||||
for e in dec.get('decisions', []):
|
||||
if e.get('code') != code:
|
||||
continue
|
||||
sold_at = e.get('sold_at', '')
|
||||
if not sold_at:
|
||||
return None
|
||||
try:
|
||||
sd = datetime.strptime(sold_at, '%Y-%m-%d').date()
|
||||
td = date.today()
|
||||
days = (td - sd).days
|
||||
except:
|
||||
return None
|
||||
|
||||
reason = e.get('sold_reason', 'portfolio_trim')
|
||||
if reason == 'stop_loss':
|
||||
tag = '止损离场(逻辑破坏,短期不关注)'
|
||||
else:
|
||||
tag = '组合减仓后关注(已清仓,等回踩确认)'
|
||||
|
||||
return {
|
||||
'sold_reason': reason,
|
||||
'sold_at': sold_at,
|
||||
'days_ago': days,
|
||||
'is_today': days == 0,
|
||||
'tag': tag,
|
||||
}
|
||||
except:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def _get_sell_priority_list():
|
||||
"""减仓优先级排序:深套>亏损>微盈>盈利(2026-06-23 反馈循环)
|
||||
|
||||
返回 [(code, name, change_pct, position_pct, priority_label), ...]
|
||||
按卖出的优先顺序排列(最先应该卖的在最前)
|
||||
"""
|
||||
try:
|
||||
p = json.load(open('/home/hmo/web-dashboard/data/portfolio.json'))
|
||||
holdings = p.get('holdings', [])
|
||||
ranked = []
|
||||
for h in holdings:
|
||||
chg = h.get('change_pct', 0)
|
||||
pos = h.get('position_pct', 0)
|
||||
if chg < -30:
|
||||
label = '深套(>30%),优先减'
|
||||
rank = 0
|
||||
elif chg < -20:
|
||||
label = '深套(>20%),优先减'
|
||||
rank = 1
|
||||
elif chg < -10:
|
||||
label = '亏损,建议减'
|
||||
rank = 2
|
||||
elif chg < 0:
|
||||
label = '微亏,可减'
|
||||
rank = 3
|
||||
elif chg < 10:
|
||||
label = '微盈,持有'
|
||||
rank = 4
|
||||
else:
|
||||
label = '盈利,最后减'
|
||||
rank = 5
|
||||
ranked.append((rank, h['code'], h.get('name',''), chg, pos, label))
|
||||
ranked.sort(key=lambda x: (x[0], -x[4])) # 优先 rank, 其次仓位大优先
|
||||
return [{'code':c,'name':n,'change_pct':chg,'position_pct':pos,'label':l}
|
||||
for r,c,n,chg,pos,l in ranked]
|
||||
except:
|
||||
return []
|
||||
|
||||
|
||||
def enrich_timing_signal(base_signal, macro_desc="", sector_note="",
|
||||
profit_pct=0, stock_category="", is_new_entry=False,
|
||||
fundamentals=None, news_sentiment=None,
|
||||
timing_signal_override=None):
|
||||
"""多因子合成timing_signal——大盘+行业+基本面+技术
|
||||
timing_signal_override=None,
|
||||
portfolio_context=None): # 新增参数
|
||||
"""多因子合成timing_signal——大盘+行业+基本面+技术+组合风险
|
||||
|
||||
返回 (enriched_signal, factors_list)
|
||||
- enriched_signal: 可读的多因子信号描述
|
||||
@@ -960,6 +1123,21 @@ def enrich_timing_signal(base_signal, macro_desc="", sector_note="",
|
||||
if base_signal and base_signal != "neutral":
|
||||
factors.append(base_signal)
|
||||
|
||||
# 5.5 组合风险因子(2026-06-23 双引擎协调)
|
||||
if portfolio_context and not is_new_entry:
|
||||
if portfolio_context.get('is_very_high_position'):
|
||||
factors.append("组合仓位极重(>90%)")
|
||||
elif portfolio_context.get('is_high_position'):
|
||||
factors.append("组合仓位偏重(>80%)")
|
||||
if portfolio_context.get('is_high_weak'):
|
||||
factors.append(f"弱势占{portfolio_context.get('weak_ratio')}%")
|
||||
elif portfolio_context and is_new_entry:
|
||||
# 新买入推荐:注明组合上下文
|
||||
if portfolio_context.get('is_high_position'):
|
||||
factors.append(f"仓{portfolio_context.get('position_pct')}%现金有限")
|
||||
elif portfolio_context.get('is_high_weak'):
|
||||
factors.append("组合风险信号")
|
||||
|
||||
# 如果没有足够因素,返回信号不充分
|
||||
if not factors:
|
||||
return "信号不充分", []
|
||||
@@ -995,7 +1173,7 @@ def reassess_with_context(code, name, price, cost, shares, current_action,
|
||||
news_sentiment = {}
|
||||
fund = {}
|
||||
|
||||
enriched, _ = enrich_timing_signal(
|
||||
enriched, factors = enrich_timing_signal(
|
||||
base_signal=result.get("timing_signal", ""),
|
||||
macro_desc=macro_desc,
|
||||
sector_note=sector_note,
|
||||
@@ -1004,9 +1182,52 @@ def reassess_with_context(code, name, price, cost, shares, current_action,
|
||||
is_new_entry=is_watchlist,
|
||||
fundamentals=fund,
|
||||
news_sentiment=news_sentiment,
|
||||
portfolio_context=_get_portfolio_risk_state(),
|
||||
)
|
||||
result["timing_signal"] = enriched
|
||||
|
||||
# 6. 防洗盘:信号不要一天一翻(2026-06-23)
|
||||
# 如果旧信号是买入/持有类,新信号是谨慎/等待类,但中期趋势未破→维持旧信号
|
||||
try:
|
||||
dec = json.load(open('/home/hmo/web-dashboard/data/decisions.json'))
|
||||
for e in dec.get('decisions', []):
|
||||
if e.get('code') == code:
|
||||
old_signal = e.get('timing_signal', '')
|
||||
if old_signal and _is_buy_signal(old_signal) and not _is_buy_signal(enriched):
|
||||
# 中等趋势检查:MA5 > MA20 + 多周期看多
|
||||
mtf = result.get('multi_tf_context', '')
|
||||
if '看多' in mtf or '多头' in mtf:
|
||||
try:
|
||||
closes = [float(k.split()[2]) for k in mtf.split('|') if 'MA5' in k]
|
||||
except:
|
||||
closes = []
|
||||
has_uptrend = 'MA5' in mtf and 'MA20' in mtf
|
||||
if has_uptrend:
|
||||
print(f" 防洗盘: {old_signal}→保持旧信号(中期趋势完整)")
|
||||
result["timing_signal"] = f"{old_signal}(正常回调价稳)"
|
||||
break
|
||||
except Exception as e:
|
||||
print(f" 防洗盘跳过: {e}")
|
||||
|
||||
# 7. 反馈循环核:检查本股是否有刚卖出的记录(2026-06-23)
|
||||
contradiction = _check_contradiction(code)
|
||||
if contradiction and contradiction.get('is_today'):
|
||||
# 今日刚卖出 → 不屏蔽信号,但必须自标注矛盾
|
||||
print(f" 反馈循环: {contradiction.get('tag')} (sold_at={contradiction.get('sold_at')})")
|
||||
if _is_buy_signal(result.get('timing_signal', '')):
|
||||
result['action_note'] = contradiction['tag']
|
||||
# 在 timing_signal 中追加反馈标注,供报告层可见
|
||||
curr_signal = result.get('timing_signal', '')
|
||||
if '⚠️' not in curr_signal:
|
||||
result['timing_signal'] = f"⚠️{contradiction['tag']}|{curr_signal}"
|
||||
elif contradiction:
|
||||
# 非今日卖出但近期卖出 → 标注已清仓
|
||||
print(f" 近期清仓: sold_at={contradiction.get('sold_at')} ({contradiction.get('days_ago')}日前)")
|
||||
if _is_buy_signal(result.get('timing_signal', '')):
|
||||
curr_signal = result.get('timing_signal', '')
|
||||
if '已清仓' not in curr_signal:
|
||||
result['timing_signal'] = f"已清仓,{curr_signal}"
|
||||
|
||||
# 重建 action 文本(同步多因子信号)
|
||||
try:
|
||||
if new_action_needs_refresh(result, {"source": "auto"}, price):
|
||||
|
||||
Reference in New Issue
Block a user