feat: 初始提交(仅核心代码,已排除大文件)
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"name_comment": "显示的名",
|
||||
"name": "流动性状态机",
|
||||
"card_order": 6,
|
||||
"card_type": "overseas",
|
||||
"card_span_comment": "卡片横向占位列数",
|
||||
"card_span": 2,
|
||||
"enabled_comment": "是否启用该监",
|
||||
"enabled": true,
|
||||
"display_comment": "是否显示该卡",
|
||||
"display": true,
|
||||
"schedule_comment": "刷新频率 (",
|
||||
"schedule": "6h",
|
||||
"symbol_comment": "监控的代",
|
||||
"symbol": "us_liquidity_state",
|
||||
"url_comment": "点击卡片右上角房子图标跳转的地址",
|
||||
"url": "https://fred.stlouisfed.org/series/FEDFUNDS",
|
||||
"source_comment": "数据来源 (",
|
||||
"source": "fred",
|
||||
"alerts_comment": "预警阈值配",
|
||||
"alerts": {
|
||||
"change_percent_down_comment": "跌幅预警阈",
|
||||
"change_percent_down": -20,
|
||||
"urgent_abs_change_percent_comment": "严重告警阈",
|
||||
"urgent_abs_change_percent": 40,
|
||||
"urgent_cooldown_minutes_comment": "严重告警冷却分钟",
|
||||
"urgent_cooldown_minutes": 240,
|
||||
"change_percent_up_comment": "涨幅预警阈",
|
||||
"change_percent_up": 20
|
||||
},
|
||||
"email_comment": "邮件通知配置",
|
||||
"email": {
|
||||
"enabled_comment": "是否启用邮件通知",
|
||||
"enabled": true,
|
||||
"from_comment": "发件人邮",
|
||||
"from": "yangdafe@gmail.com",
|
||||
"to_comment": "收件人邮箱列",
|
||||
"to": [
|
||||
"yangxiangyuan@umer.com.cn"
|
||||
],
|
||||
"smtp_comment": "SMTP 服务器配",
|
||||
"smtp": {
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 465,
|
||||
"secure": true,
|
||||
"auth": {
|
||||
"user": "yangdafe@gmail.com",
|
||||
"pass": "{{GMAIL_SMTP_PASS}}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"email_reminder_enabled_comment": "是否发送邮件提",
|
||||
"email_reminder_enabled": true,
|
||||
"calendar_reminder_enabled_comment": "是否写入日历提醒",
|
||||
"calendar_reminder_enabled": true,
|
||||
"push_targets_comment": "推送目标列",
|
||||
"push_targets": [
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
"url": "http://localhost:{{PORT}}/api/weekly/ext/event",
|
||||
"enabled_comment": "是否启用",
|
||||
"enabled": true,
|
||||
"key_comment": "Weekly 工具",
|
||||
"key": ""
|
||||
},
|
||||
{
|
||||
"url_comment": "推送接口地址",
|
||||
"url": "http://localhost:{{PORT}}/api/calendar/events",
|
||||
"enabled_comment": "是否启用",
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"market_status": {
|
||||
"timezone": "America/New_York",
|
||||
"sessions": [
|
||||
{
|
||||
"start": "00:00",
|
||||
"end": "23:59"
|
||||
}
|
||||
],
|
||||
"display_label": "美国宏观"
|
||||
},
|
||||
"market_status_comment": "Defines the source market timezone and trading sessions for status display."
|
||||
}
|
||||
@@ -0,0 +1,442 @@
|
||||
const { fetchFredCurl } = require('../fred_curl')
|
||||
|
||||
const FRED_SERIES = {
|
||||
ffr: 'FEDFUNDS',
|
||||
us2y: 'DGS2',
|
||||
us10y: 'DGS10',
|
||||
cpi_index: 'CPIAUCSL',
|
||||
unemployment: 'UNRATE'
|
||||
}
|
||||
|
||||
const toNum = v => {
|
||||
const n = Number(v)
|
||||
return Number.isFinite(n) ? n : NaN
|
||||
}
|
||||
|
||||
const round = (v, d = 2) => {
|
||||
const n = Number(v)
|
||||
if (!Number.isFinite(n)) return NaN
|
||||
const p = 10 ** d
|
||||
return Math.round(n * p) / p
|
||||
}
|
||||
|
||||
const clamp = (v, min, max) => Math.min(max, Math.max(min, v))
|
||||
|
||||
const parseFredCsv = raw => {
|
||||
const text = typeof raw === 'string' ? raw : ''
|
||||
const lines = text.split(/\r?\n/).filter(Boolean)
|
||||
const out = []
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const row = lines[i]
|
||||
const idx = row.indexOf(',')
|
||||
if (idx <= 0) continue
|
||||
const date = row.slice(0, idx).trim()
|
||||
const valueRaw = row.slice(idx + 1).trim()
|
||||
if (!date || valueRaw === '.' || valueRaw === '') continue
|
||||
const value = toNum(valueRaw)
|
||||
if (!Number.isFinite(value)) continue
|
||||
out.push({ date, value })
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
const fetchFredSeries = async (id) => {
|
||||
const csv = await fetchFredCurl(id)
|
||||
return parseFredCsv(csv)
|
||||
}
|
||||
|
||||
const tail = (arr, n) => (Array.isArray(arr) ? arr.slice(Math.max(0, arr.length - n)) : [])
|
||||
|
||||
const avg = arr => {
|
||||
if (!Array.isArray(arr) || arr.length === 0) return NaN
|
||||
const nums = arr.map(x => toNum(x)).filter(Number.isFinite)
|
||||
if (nums.length === 0) return NaN
|
||||
return nums.reduce((a, b) => a + b, 0) / nums.length
|
||||
}
|
||||
|
||||
const sma = (series, n) => {
|
||||
if (!Array.isArray(series) || series.length === 0) return NaN
|
||||
const values = tail(series.map(x => x.value), Math.max(1, n))
|
||||
return avg(values)
|
||||
}
|
||||
|
||||
const valueNBack = (series, n) => {
|
||||
if (!Array.isArray(series) || series.length === 0) return NaN
|
||||
const idx = Math.max(0, series.length - 1 - Math.max(0, n))
|
||||
return toNum(series[idx] && series[idx].value)
|
||||
}
|
||||
|
||||
const cpiYoYSeries = cpiSeries => {
|
||||
const out = []
|
||||
if (!Array.isArray(cpiSeries) || cpiSeries.length < 13) return out
|
||||
for (let i = 12; i < cpiSeries.length; i++) {
|
||||
const curr = toNum(cpiSeries[i].value)
|
||||
const prev = toNum(cpiSeries[i - 12].value)
|
||||
if (!Number.isFinite(curr) || !Number.isFinite(prev) || prev <= 0) continue
|
||||
out.push({
|
||||
date: cpiSeries[i].date,
|
||||
value: ((curr / prev) - 1) * 100
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
const scoreFromDelta = (delta, easingThreshold, tighteningThreshold) => {
|
||||
const d = toNum(delta)
|
||||
if (!Number.isFinite(d)) return 50
|
||||
if (d <= easingThreshold) return 100
|
||||
if (d >= tighteningThreshold) return 0
|
||||
const mid = (easingThreshold + tighteningThreshold) / 2
|
||||
const half = (tighteningThreshold - easingThreshold) / 2 || 1
|
||||
const scaled = 50 - ((d - mid) / half) * 50
|
||||
return clamp(scaled, 0, 100)
|
||||
}
|
||||
|
||||
const scoreRateDirection = ffrSeries => {
|
||||
const latest = valueNBack(ffrSeries, 0)
|
||||
const prev3 = valueNBack(ffrSeries, 3)
|
||||
const delta = latest - prev3
|
||||
return { delta, score: scoreFromDelta(delta, -0.1, 0.1) }
|
||||
}
|
||||
|
||||
const scoreYieldExpectation = us2ySeries => {
|
||||
const latest = valueNBack(us2ySeries, 0)
|
||||
const prev30 = valueNBack(us2ySeries, 30)
|
||||
const delta = latest - prev30
|
||||
return { delta, score: scoreFromDelta(delta, -0.3, 0.3) }
|
||||
}
|
||||
|
||||
const scoreCpiTrend = cpiYoY => {
|
||||
const recent = tail(cpiYoY, 4)
|
||||
if (recent.length < 4) return { delta: NaN, score: 50 }
|
||||
const m3 = toNum(recent[0].value)
|
||||
const m2 = toNum(recent[1].value)
|
||||
const m1 = toNum(recent[2].value)
|
||||
const m0 = toNum(recent[3].value)
|
||||
const delta = m0 - m3
|
||||
const isThreeMonthDown = m0 < m1 && m1 < m2
|
||||
const isThreeMonthUp = m0 > m1 && m1 > m2
|
||||
if (isThreeMonthDown) return { delta, score: 100 }
|
||||
if (isThreeMonthUp) return { delta, score: 0 }
|
||||
return { delta, score: scoreFromDelta(delta, -0.3, 0.3) }
|
||||
}
|
||||
|
||||
const scoreUnemployment = unrateSeries => {
|
||||
const latest = valueNBack(unrateSeries, 0)
|
||||
const prev3 = valueNBack(unrateSeries, 3)
|
||||
const delta = latest - prev3
|
||||
return { delta, score: scoreFromDelta(delta, 0.3, -0.2) }
|
||||
}
|
||||
|
||||
const buildState = score => {
|
||||
if (score >= 85) return { state: '宽松', emoji: '🟢', impact: '流动性充分改善,利好纳指与标普,纳指弹性更高' }
|
||||
if (score >= 70) return { state: '宽松初期', emoji: '🟢+', impact: '宽松预期先行,通常是纳指表现最优的阶段' }
|
||||
if (score >= 45) return { state: '中性', emoji: '🟡', impact: '标普与纳指以震荡为主,等待增量信号' }
|
||||
if (score >= 30) return { state: '紧缩中', emoji: '🟠', impact: '估值受压但未到极端,纳指波动通常大于标普' }
|
||||
return { state: '紧缩', emoji: '🔴', impact: '高利率压制风险资产,纳指承压最明显' }
|
||||
}
|
||||
|
||||
const fmt = (v, d = 2, suffix = '') => Number.isFinite(v) ? `${round(v, d).toFixed(d)}${suffix}` : '—'
|
||||
const trendArrow = (delta, weak, strong) => {
|
||||
const d = toNum(delta)
|
||||
if (!Number.isFinite(d)) return '→'
|
||||
const ad = Math.abs(d)
|
||||
if (ad < weak) return '→'
|
||||
if (d > 0) return ad >= strong ? '↑↑↑' : '↑'
|
||||
return ad >= strong ? '↓↓↓' : '↓'
|
||||
}
|
||||
const dominantFactorFromDeltas = ({ rateDelta, y2Delta, cpiDelta, unrateDelta }) => {
|
||||
const candidates = [
|
||||
{ key: 'FFR利率方向', short: 'FFR利率', delta: rateDelta, unitThreshold: 0.1, easingWhenNegative: true },
|
||||
{ key: '2Y利率预期', short: '2Y预期', delta: y2Delta, unitThreshold: 0.3, easingWhenNegative: true },
|
||||
{ key: 'CPI通胀趋势', short: 'CPI趋势', delta: cpiDelta, unitThreshold: 0.3, easingWhenNegative: true },
|
||||
{ key: '失业率变化', short: '失业率', delta: unrateDelta, unitThreshold: 0.3, easingWhenNegative: false }
|
||||
].map(x => ({
|
||||
...x,
|
||||
strength: Number.isFinite(x.delta) ? Math.abs(x.delta) / x.unitThreshold : -1
|
||||
}))
|
||||
candidates.sort((a, b) => b.strength - a.strength)
|
||||
const top = candidates[0]
|
||||
const delta = toNum(top && top.delta)
|
||||
const easing = Number.isFinite(delta)
|
||||
? (top.easingWhenNegative ? delta < 0 : delta > 0)
|
||||
: false
|
||||
return {
|
||||
dominant_factor: top ? top.short : '综合因素',
|
||||
dominant_factor_full: top ? top.key : '综合因素',
|
||||
dominant_bias: easing ? '宽松预期' : '紧缩预期'
|
||||
}
|
||||
}
|
||||
const buildMarketPhase = ({ score, ffrLevel, rateDelta, y2Delta }) => {
|
||||
if (rateDelta > 0.1) return '紧缩期(加息)'
|
||||
if (score >= 85 && rateDelta < 0 && y2Delta < -0.2) return '宽松释放期(大牛)'
|
||||
if ((y2Delta <= -0.15 && rateDelta <= 0.05) || (score >= 65 && score < 85)) return '降息预期交易期(Pre-Easing)'
|
||||
if (ffrLevel >= 4 || score < 65) return '高位压制期'
|
||||
return '高位压制期'
|
||||
}
|
||||
const buildIndexView = ({ score, y2Delta, cpiDelta, unrateDelta }) => {
|
||||
const nasdaqBias = (score >= 70 || y2Delta <= -0.15) ? '当前偏正面' : (score >= 45 ? '中性偏震荡' : '当前偏负面')
|
||||
const sp500Bias = (score >= 85 && cpiDelta < 0 && unrateDelta >= 0)
|
||||
? '偏正面'
|
||||
: (score < 45 ? '偏谨慎' : '仍有分歧')
|
||||
return {
|
||||
nasdaq: `纳指驱动:流动性 + 利率预期(${nasdaqBias})`,
|
||||
sp500: `标普驱动:盈利 + 宏观(${sp500Bias})`,
|
||||
nasdaq_bias: nasdaqBias,
|
||||
sp500_bias: sp500Bias
|
||||
}
|
||||
}
|
||||
const buildSignalStrength = score => {
|
||||
if (score >= 85) return '★★★★★'
|
||||
if (score >= 70) return '★★★☆☆'
|
||||
if (score >= 45) return '★★☆☆☆'
|
||||
if (score >= 30) return '★☆☆☆☆'
|
||||
return '☆☆☆☆☆'
|
||||
}
|
||||
const buildCoreConclusion = marketPhase => {
|
||||
if (marketPhase === '降息预期交易期(Pre-Easing)') {
|
||||
return '市场进入“降息预期交易期”,流动性方向转松,当前更利好纳指(科技股),而非全面牛市。'
|
||||
}
|
||||
if (marketPhase === '宽松释放期(大牛)') {
|
||||
return '市场进入“宽松释放期”,流动性进入释放阶段,纳指与标普更容易共振上行。'
|
||||
}
|
||||
if (marketPhase === '紧缩期(加息)') {
|
||||
return '市场处于“紧缩期”,流动性方向偏紧,纳指与标普整体承压。'
|
||||
}
|
||||
return '市场处于“高位压制期”,风险偏好恢复有限,结构性行情强于全面行情。'
|
||||
}
|
||||
const buildTrendNarrative = ({ rateDelta, y2Delta, cpiDelta, unrateDelta }) => ({
|
||||
ffr: rateDelta < 0 ? '政策开始转向' : (rateDelta > 0 ? '政策偏紧延续' : '政策维持稳定'),
|
||||
us2y: y2Delta < 0 ? '市场正在押注降息' : (y2Delta > 0 ? '市场预期反向' : '市场预期观望'),
|
||||
cpi: cpiDelta < 0 ? '支持降息' : (cpiDelta > 0 ? '约束降息' : '通胀信号中性'),
|
||||
unemployment: unrateDelta > 0 ? '经济走弱' : (unrateDelta < 0 ? '经济尚稳' : '就业信号中性')
|
||||
})
|
||||
const buildRiskHints = ({ rateDelta, y2Delta, cpiDelta, marketPhase }) => {
|
||||
const divergence = (rateDelta < 0 && y2Delta > 0.1) || (rateDelta > 0 && y2Delta < -0.1)
|
||||
return [
|
||||
divergence ? '2Y与FFR出现分歧(预期 vs 现实)' : '2Y与FFR短期一致,需警惕后续再分歧',
|
||||
cpiDelta > 0 ? 'CPI出现反弹迹象,降息预期存在回摆风险' : '若CPI反弹 → 降息预期失效',
|
||||
marketPhase === '宽松释放期(大牛)' ? '宽松释放已启动,仍需跟踪盈利兑现节奏' : '当前仍未进入真实宽松(资金未释放)'
|
||||
]
|
||||
}
|
||||
const buildDominantDisplay = ({ dominantFactorFull, arrows, trendNarrative }) => {
|
||||
if (dominantFactorFull === 'FFR利率方向') return { arrow: arrows.ffr, period: '3个月', text: trendNarrative.ffr }
|
||||
if (dominantFactorFull === 'CPI通胀趋势') return { arrow: arrows.cpi, period: '3个月', text: trendNarrative.cpi }
|
||||
if (dominantFactorFull === '失业率变化') return { arrow: arrows.unemployment, period: '3个月', text: trendNarrative.unemployment }
|
||||
return { arrow: arrows.us2y, period: '30日', text: trendNarrative.us2y }
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
run: async (context, config) => {
|
||||
const { logJSON } = context
|
||||
try {
|
||||
const [ffr, us2y, us10y, cpiIndex, unemployment] = await Promise.all([
|
||||
fetchFredSeries(FRED_SERIES.ffr),
|
||||
fetchFredSeries(FRED_SERIES.us2y),
|
||||
fetchFredSeries(FRED_SERIES.us10y),
|
||||
fetchFredSeries(FRED_SERIES.cpi_index),
|
||||
fetchFredSeries(FRED_SERIES.unemployment)
|
||||
])
|
||||
|
||||
if (!ffr.length || !us2y.length || !us10y.length || !cpiIndex.length || !unemployment.length) {
|
||||
logJSON('skill.liquidity.fetch.empty', {
|
||||
ffr: ffr.length,
|
||||
us2y: us2y.length,
|
||||
us10y: us10y.length,
|
||||
cpiIndex: cpiIndex.length,
|
||||
unemployment: unemployment.length
|
||||
}, 'market_skills')
|
||||
return null
|
||||
}
|
||||
|
||||
const cpiYoY = cpiYoYSeries(cpiIndex)
|
||||
const ratePart = scoreRateDirection(ffr)
|
||||
const yieldPart = scoreYieldExpectation(us2y)
|
||||
const cpiPart = scoreCpiTrend(cpiYoY)
|
||||
const unratePart = scoreUnemployment(unemployment)
|
||||
const totalScore = clamp(
|
||||
ratePart.score * 0.4 +
|
||||
yieldPart.score * 0.3 +
|
||||
cpiPart.score * 0.2 +
|
||||
unratePart.score * 0.1,
|
||||
0,
|
||||
100
|
||||
)
|
||||
const totalScoreRounded = round(totalScore, 2)
|
||||
const state = buildState(totalScoreRounded)
|
||||
|
||||
const latestFfr = tail(ffr, 1)[0]
|
||||
const latestUs2y = tail(us2y, 1)[0]
|
||||
const latestUs10y = tail(us10y, 1)[0]
|
||||
const latestCpiYoY = tail(cpiYoY, 1)[0]
|
||||
const latestUnrate = tail(unemployment, 1)[0]
|
||||
const spread = toNum(latestUs10y.value) - toNum(latestUs2y.value)
|
||||
const capturedAt = new Date().toISOString().replace('T', ' ').slice(0, 19)
|
||||
const ffrTrend = latestFfr.value - valueNBack(ffr, 3)
|
||||
const us2yTrend = latestUs2y.value - valueNBack(us2y, 30)
|
||||
const cpiTrend = (latestCpiYoY ? latestCpiYoY.value : NaN) - valueNBack(cpiYoY, 3)
|
||||
const unrateTrend = latestUnrate.value - valueNBack(unemployment, 3)
|
||||
const ffrArrow = trendArrow(ffrTrend, 0.03, 0.1)
|
||||
const us2yArrow = trendArrow(us2yTrend, 0.05, 0.3)
|
||||
const cpiArrow = trendArrow(cpiTrend, 0.05, 0.3)
|
||||
const unrateArrow = trendArrow(unrateTrend, 0.05, 0.2)
|
||||
const dominant = dominantFactorFromDeltas({
|
||||
rateDelta: ratePart.delta,
|
||||
y2Delta: yieldPart.delta,
|
||||
cpiDelta: cpiPart.delta,
|
||||
unrateDelta: unratePart.delta
|
||||
})
|
||||
const marketPhase = buildMarketPhase({
|
||||
score: totalScoreRounded,
|
||||
ffrLevel: latestFfr.value,
|
||||
rateDelta: ratePart.delta,
|
||||
y2Delta: yieldPart.delta
|
||||
})
|
||||
const indexView = buildIndexView({
|
||||
score: totalScoreRounded,
|
||||
y2Delta: yieldPart.delta,
|
||||
cpiDelta: cpiPart.delta,
|
||||
unrateDelta: unratePart.delta
|
||||
})
|
||||
const signalStrength = buildSignalStrength(totalScoreRounded)
|
||||
const coreConclusion = buildCoreConclusion(marketPhase)
|
||||
const trendNarrative = buildTrendNarrative({
|
||||
rateDelta: ratePart.delta,
|
||||
y2Delta: yieldPart.delta,
|
||||
cpiDelta: cpiPart.delta,
|
||||
unrateDelta: unratePart.delta
|
||||
})
|
||||
const riskHints = buildRiskHints({
|
||||
rateDelta: ratePart.delta,
|
||||
y2Delta: yieldPart.delta,
|
||||
cpiDelta: cpiPart.delta,
|
||||
marketPhase
|
||||
})
|
||||
const structureSummary = '科技优先上涨,而非全面上涨'
|
||||
const dominantDisplay = buildDominantDisplay({
|
||||
dominantFactorFull: dominant.dominant_factor_full,
|
||||
arrows: {
|
||||
ffr: ffrArrow,
|
||||
us2y: us2yArrow,
|
||||
cpi: cpiArrow,
|
||||
unemployment: unrateArrow
|
||||
},
|
||||
trendNarrative
|
||||
})
|
||||
|
||||
const html = `
|
||||
<div style="font-size:16px;font-weight:700;line-height:1.7">
|
||||
${state.emoji} ${state.state}(${marketPhase.includes('Pre-Easing') ? 'Pre-Easing' : state.state})<br/>
|
||||
流动性评分:${fmt(totalScoreRounded, 0)} / 100 | 信号强度:${signalStrength}
|
||||
</div>
|
||||
<div style="font-size:13px;color:#1f1f1f;line-height:1.7;margin-top:8px">
|
||||
👉 核心结论:${coreConclusion}
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:12px;margin-top:10px">
|
||||
<div style="border:1px solid rgba(0,0,0,0.1);border-radius:10px;padding:10px 12px;background:rgba(255,255,255,0.78)">
|
||||
<div style="font-size:13px;font-weight:700;color:#262626">🎯 主导因子</div>
|
||||
<div style="font-size:13px;color:#262626;line-height:1.7;margin-top:4px">
|
||||
${dominant.dominant_factor_full} ${dominantDisplay.arrow}(${dominantDisplay.period})<br/>
|
||||
→ ${dominantDisplay.text}
|
||||
</div>
|
||||
<div style="margin:8px 0;border-top:1px dashed rgba(0,0,0,0.15)"></div>
|
||||
<div style="font-size:13px;font-weight:700;color:#262626">🧭 市场结构判断</div>
|
||||
<div style="font-size:13px;color:#262626;line-height:1.7;margin-top:4px">
|
||||
纳指:✔ 长久期资产受益 | ✔ 估值扩张预期增强<br/>
|
||||
标普:△ 盈利支撑仍在 | △ 宏观存在分歧<br/>
|
||||
👉 当前结构:${structureSummary}
|
||||
</div>
|
||||
</div>
|
||||
<div style="border:1px solid rgba(0,0,0,0.1);border-radius:10px;padding:10px 12px;background:rgba(255,255,255,0.78)">
|
||||
<div style="font-size:13px;font-weight:700;color:#262626">⚠️ 风险提示</div>
|
||||
<div style="font-size:13px;color:#262626;line-height:1.7;margin-top:4px">
|
||||
- ${riskHints[0]}<br/>
|
||||
- ${riskHints[1]}<br/>
|
||||
- ${riskHints[2]}
|
||||
</div>
|
||||
<div style="margin:8px 0;border-top:1px dashed rgba(0,0,0,0.15)"></div>
|
||||
<div style="font-size:13px;font-weight:700;color:#262626">📊 关键趋势</div>
|
||||
<div style="font-size:13px;color:#262626;line-height:1.7;margin-top:4px">
|
||||
FFR:${fmt(latestFfr.value, 2, '%')} ${ffrArrow}(${trendNarrative.ffr})<br/>
|
||||
2Y:${fmt(latestUs2y.value, 2, '%')} ${us2yArrow}(${trendNarrative.us2y})<br/>
|
||||
CPI:${fmt(latestCpiYoY && latestCpiYoY.value, 2, '%')} ${cpiArrow}(${trendNarrative.cpi})<br/>
|
||||
失业率:${fmt(latestUnrate.value, 2, '%')} ${unrateArrow}(${trendNarrative.unemployment})
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="font-size:13px;color:#595959;line-height:1.7;margin-top:8px">
|
||||
分项评分:利率方向 ${fmt(ratePart.score, 0)} | 2Y预期 ${fmt(yieldPart.score, 0)} | CPI趋势 ${fmt(cpiPart.score, 0)} | 失业率 ${fmt(unratePart.score, 0)}<br/>
|
||||
数据时间:${capturedAt}(FRED)
|
||||
</div>
|
||||
`
|
||||
|
||||
const explain = [
|
||||
`当前阶段:${marketPhase}。`,
|
||||
`当前市场由【${dominant.dominant_factor_full}】主导,偏向${dominant.dominant_bias}。`,
|
||||
`${indexView.nasdaq} ${indexView.sp500}。`,
|
||||
`核心结论:${coreConclusion}`,
|
||||
`当前结构:${structureSummary}。`,
|
||||
`纳指影响:${state.state === '宽松' || state.state === '宽松初期' ? '长久期资产受益,估值扩张概率更高。' : state.state === '中性' ? '以交易驱动为主,业绩与预期差决定方向。' : '高利率压制估值,回撤风险先体现在纳指。'}`,
|
||||
`标普影响:${state.state === '宽松' || state.state === '宽松初期' ? '风险偏好抬升,周期与成长轮动走强。' : state.state === '中性' ? '盈利韧性与宏观数据反复博弈。' : '防御板块相对占优,指数上行空间受限。'}`,
|
||||
`风险提示:${riskHints.join(';')}。`,
|
||||
`分项得分:利率方向 ${fmt(ratePart.score, 1)}|2Y预期 ${fmt(yieldPart.score, 1)}|CPI趋势 ${fmt(cpiPart.score, 1)}|失业率 ${fmt(unratePart.score, 1)}`
|
||||
].join(' ')
|
||||
|
||||
const data = [{
|
||||
label: config.name,
|
||||
symbol: config.symbol,
|
||||
source: config.source || 'fred',
|
||||
name: `${state.emoji} ${state.state}`,
|
||||
price: totalScoreRounded,
|
||||
prevClose: 50,
|
||||
change: round(totalScoreRounded - 50, 2),
|
||||
change_percent: round(((totalScoreRounded - 50) / 50) * 100, 2),
|
||||
captured_at: capturedAt,
|
||||
render_type: 'html',
|
||||
html,
|
||||
explain,
|
||||
market_status: config.market_status,
|
||||
liquidity_state: state.state,
|
||||
liquidity_score: totalScoreRounded,
|
||||
market_phase: marketPhase,
|
||||
signal_strength: signalStrength,
|
||||
core_conclusion: coreConclusion,
|
||||
market_structure_summary: structureSummary,
|
||||
dominant_factor: dominant.dominant_factor,
|
||||
dominant_factor_full: dominant.dominant_factor_full,
|
||||
dominant_bias: dominant.dominant_bias,
|
||||
index_view: {
|
||||
nasdaq: indexView.nasdaq,
|
||||
sp500: indexView.sp500,
|
||||
nasdaq_bias: indexView.nasdaq_bias,
|
||||
sp500_bias: indexView.sp500_bias
|
||||
},
|
||||
risk_hints: riskHints,
|
||||
date: capturedAt.slice(0, 10),
|
||||
ffr: round(latestFfr.value, 4),
|
||||
us2y: round(latestUs2y.value, 4),
|
||||
us10y: round(latestUs10y.value, 4),
|
||||
cpi: round(latestCpiYoY && latestCpiYoY.value, 4),
|
||||
unemployment: round(latestUnrate.value, 4),
|
||||
trend_arrow: {
|
||||
ffr: ffrArrow,
|
||||
us2y: us2yArrow,
|
||||
cpi: cpiArrow,
|
||||
unemployment: unrateArrow
|
||||
},
|
||||
series_date: {
|
||||
ffr: latestFfr.date,
|
||||
us2y: latestUs2y.date,
|
||||
us10y: latestUs10y.date,
|
||||
cpi: latestCpiYoY ? latestCpiYoY.date : null,
|
||||
unemployment: latestUnrate.date
|
||||
}
|
||||
}]
|
||||
|
||||
return { data, alerts: [] }
|
||||
} catch (e) {
|
||||
logJSON('skill.liquidity.run.error', { error: String(e.message || e) }, 'market_skills')
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user