chore: 初始提交 - 移除所有硬编码凭据,统一从环境变量读取

This commit is contained in:
yangxiangyuan
2026-06-11 17:10:58 +08:00
commit c223c79b36
1699 changed files with 313940 additions and 0 deletions
@@ -0,0 +1,77 @@
{
"name_comment": "显示的��,
"name": "中国神�-299�,
"card_order": 1,
"card_type": "299",
"enabled_comment": "是��用该监�(true/false)",
"enabled": true,
"display_comment": "是�显示该��(false 则�拉�数�也��存)",
"display": false,
"schedule_comment": "刷新频率 (�10m 表示10分钟)",
"schedule": "10m",
"symbol_comment": "监控的代�(腾讯财�或其他�)",
"symbol": "sh601088_299",
"source_symbol_comment": "用于拉�K线的真实代�",
"source_symbol": "sh601088",
"url_comment": "点击�片�上角房�图标跳转的地�",
"url": "https://quote.eastmoney.com/sh601088.html",
"source_comment": "数��� (�tencent)",
"source": "tencent",
"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",
\"key\": \"\"
},
{
"url_comment": "推�接�地�",
"url": "http://localhost:{{PORT}}/api/calendar/events",
"enabled_comment": "是��用",
"enabled": true
}
],
"market_status": {
"timezone": "Asia/Shanghai",
"sessions": [
{
"start": "09:30",
"end": "11:30"
},
{
"start": "13:00",
"end": "15:00"
}
],
"display_label": "中国A�
},
"market_status_comment": "Defines the source market timezone and trading sessions for status display."
}
@@ -0,0 +1,201 @@
const calculateMA = (data, period) => {
if (!data || data.length < period) return null
const subset = data.slice(-period)
const sum = subset.reduce((a, b) => a + b, 0)
return sum / period
}
const fetchKLine = async (axios, symbol) => {
const url = `http://web.ifzq.gtimg.cn/appstock/app/fqkline/get?param=${symbol},day,,,300,qfq`
try {
const res = await axios.get(url, { timeout: 10000 })
if (!res || !res.data || res.data.code !== 0) return null
const d = res.data.data[symbol]
if (!d) return null
const rawData = d.qfqday || d.day
if (!rawData || !Array.isArray(rawData)) return null
return rawData.map(item => ({
date: item[0],
close: parseFloat(item[2])
}))
} catch (e) {
return null
}
}
const buildAlerts = ({
latestClose,
prevClose,
ma10,
ma20,
ma60,
ma90,
ma250,
prevMA20,
prevMA60,
prevMA90,
prevMA250
}) => {
const alerts = []
const addAlert = (id, title, message, level = 'warning') => {
alerts.push({ id, title, message, level })
}
const isCrossDown = (prevC, prevMa, currC, currMa) => prevC >= prevMa && currC < currMa
const isCrossUp = (prevC, prevMa, currC, currMa) => prevC <= prevMa && currC > currMa
const isBelowMA250 = latestClose < ma250
const isCrossDownMA250 = isCrossDown(prevClose, prevMA250, latestClose, ma250)
const isCrossUpMA250 = isCrossUp(prevClose, prevMA250, latestClose, ma250)
if (isCrossDownMA250) {
addAlert('below_ma250', '中国神华跌破年线!立即停止加仓,建议减仓', '中国神华跌破年线!立即停止加仓,建议减仓', 'error')
}
if (isCrossUpMA250) {
addAlert('above_ma250', '中国神华回到年线之上,可恢复加仓策略', '中国神华回到年线之上,可恢复加仓策略')
}
if (!isBelowMA250) {
if (isCrossDown(prevClose, prevMA20, latestClose, ma20)) {
addAlert('below_ma20', '中国神华-跌破20日线,需加仓 20%', '中国神华-跌破20日线,需加仓 20%')
}
if (isCrossDown(prevClose, prevMA60, latestClose, ma60)) {
addAlert('below_ma60', '中国神华-跌破60日线,需加仓 40%', '中国神华-跌破60日线,需加仓 40%')
}
if (isCrossDown(prevClose, prevMA90, latestClose, ma90)) {
addAlert('below_ma90', '中国神华-跌破90日线,需加仓剩余 40%', '中国神华-跌破90日线,需加仓剩余 40%')
}
}
if (isCrossUp(prevClose, prevMA20, latestClose, ma20)) {
addAlert('above_ma20', '中国神华-站上20日线,需卖出 20%', '中国神华-站上20日线,需卖出 20%')
}
if (isCrossUp(prevClose, prevMA60, latestClose, ma60)) {
addAlert('above_ma60', '中国神华-站上60日线,需卖出 40%', '中国神华-站上60日线,需卖出 40%')
}
if (isCrossUp(prevClose, prevMA90, latestClose, ma90)) {
addAlert('above_ma90', '中国神华-站上90日线,需卖出剩余 40%', '中国神华-站上90日线,需卖出剩余 40%')
}
if (ma10 > ma20) {
addAlert('trend_bull', '趋势提示', '中国神华处于上涨趋势,建议持有底仓 + 已建波段仓,不做 T')
}
return alerts
}
module.exports = {
run: async (context, config) => {
const { axios, logJSON } = context
const symbol = config.symbol
const sourceSymbol = config.source_symbol || 'sh601088'
const klineData = await fetchKLine(axios, sourceSymbol)
if (!klineData || klineData.length < 250) {
logJSON('skill.shenhua_299.error', { message: 'Insufficient data', length: klineData ? klineData.length : 0 }, 'market_skills')
return null
}
const closes = klineData.map(d => d.close)
const latestClose = closes[closes.length - 1]
const prevClose = closes[closes.length - 2]
const latestDate = klineData[klineData.length - 1].date
const ma10 = calculateMA(closes, 10)
const ma20 = calculateMA(closes, 20)
const ma60 = calculateMA(closes, 60)
const ma90 = calculateMA(closes, 90)
const ma250 = calculateMA(closes, 250)
const closesPrev = closes.slice(0, -1)
const prevMA20 = calculateMA(closesPrev, 20)
const prevMA60 = calculateMA(closesPrev, 60)
const prevMA90 = calculateMA(closesPrev, 90)
const prevMA250 = calculateMA(closesPrev, 250)
if (!ma10 || !ma20 || !ma60 || !ma90 || !ma250 || !prevMA20 || !prevMA60 || !prevMA90 || !prevMA250) {
logJSON('skill.shenhua_299.error', { message: 'MA calculation failed' }, 'market_skills')
return null
}
const change = latestClose - prevClose
const changePercent = (change / prevClose) * 100
const trendText = ma10 > ma20 ? '多头排列' : '非多头'
const yearLineText = latestClose < ma250 ? '年线下方' : '年线之上'
const isCrossDown = (prevC, prevMa, currC, currMa) => prevC >= prevMa && currC < currMa
const isCrossUp = (prevC, prevMa, currC, currMa) => prevC <= prevMa && currC > currMa
const isBelowMA250 = latestClose < ma250
const crossDownMA20 = isCrossDown(prevClose, prevMA20, latestClose, ma20)
const crossDownMA60 = isCrossDown(prevClose, prevMA60, latestClose, ma60)
const crossDownMA90 = isCrossDown(prevClose, prevMA90, latestClose, ma90)
const crossUpMA20 = isCrossUp(prevClose, prevMA20, latestClose, ma20)
const crossUpMA60 = isCrossUp(prevClose, prevMA60, latestClose, ma60)
const crossUpMA90 = isCrossUp(prevClose, prevMA90, latestClose, ma90)
const crossUpMA250 = isCrossUp(prevClose, prevMA250, latestClose, ma250)
const statusDownMA20 = isBelowMA250 ? '暂停(年线下方)' : (crossDownMA20 ? '已触发(加仓20%)' : '未触发')
const statusDownMA60 = isBelowMA250 ? '暂停(年线下方)' : (crossDownMA60 ? '已触发(加仓40%)' : '未触发')
const statusDownMA90 = isBelowMA250 ? '暂停(年线下方)' : (crossDownMA90 ? '已触发(加仓剩余40%)' : '未触发')
const statusUpMA20 = crossUpMA20 ? '已触发(卖出20%)' : '未触发'
const statusUpMA60 = crossUpMA60 ? '已触发(卖出40%)' : '未触发'
const statusUpMA90 = crossUpMA90 ? '已触发(卖出剩余40%)' : '未触发'
const statusTrend = ma10 > ma20 ? '已满足(上涨趋势)' : '未满足'
const statusMA250 = isBelowMA250 ? '已触发(停止加仓/建议减仓)' : (crossUpMA250 ? '已触发(恢复加仓)' : '未触发')
const htmlPayload = `
<div style="font-size: 14px; margin-top: 8px;">
价格:${latestClose.toFixed(2)} (${change >= 0 ? '+' : ''}${change.toFixed(2)} / ${changePercent.toFixed(2)}%)
</div>
<div style="margin: 8px 0; border-top: 1px solid #eee; width: 100%;"></div>
<div style="font-size: 13px; color: #666; line-height: 1.6;">
MA10:${ma10.toFixed(2)} | MA20:${ma20.toFixed(2)} | MA60:${ma60.toFixed(2)}<br/>
MA90:${ma90.toFixed(2)} | MA250:${ma250.toFixed(2)}<br/>
趋势:${trendText} | 年线状态:${yearLineText}
</div>
<div style="margin: 8px 0; border-top: 1px solid #eee; width: 100%;"></div>
<div style="font-size: 13px; color: #666; line-height: 1.6;">
20日线跌破:${statusDownMA20} | 站上:${statusUpMA20}<br/>
60日线跌破:${statusDownMA60} | 站上:${statusUpMA60}<br/>
90日线跌破:${statusDownMA90} | 站上:${statusUpMA90}<br/>
MA10>MA20:${statusTrend} | MA250:${statusMA250}
</div>
`
const data = [{
label: config.name,
symbol: symbol,
displaySymbol: sourceSymbol,
isMaCard: true,
render_type: 'html',
html: htmlPayload,
ma10: ma10.toFixed(2),
ma20: ma20.toFixed(2),
ma60: ma60.toFixed(2),
ma90: ma90.toFixed(2),
ma250: ma250.toFixed(2),
source: config.source || 'tencent',
name: config.name,
price: latestClose,
prevClose: prevClose,
change: change,
change_percent: changePercent,
market_status: `${trendText} (${latestDate})`
}]
const alerts = buildAlerts({
latestClose,
prevClose,
ma10,
ma20,
ma60,
ma90,
ma250,
prevMA20,
prevMA60,
prevMA90,
prevMA250
})
return { data, alerts }
}
}