chore: 初始提交 - 移除所有硬编码凭据,统一从环境变量读取
This commit is contained in:
@@ -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 }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user