策略源码
// ==================== 板块轮动策略(商品期货版)====================
// ==================== 全局配置参数 ====================
var Amount = 1;
var klineLen = 300;
var topPerSector = 2;
var maxTotalPositions = 6;
var minSectorScore = 30;
var minWinRate = 0.40;
var minProfitFactor = 1.20;
var maxMDD = 25;
var allowShort = true;
var sectorScanInterval = 15 * 60 * 1000;
var signalCheckInterval = 5 * 60 * 1000;
var MAX_RETRY = 2;
var maParams = [[5, 20], [10, 30], [20, 60], [25, 99]];
// ==================== 板块定义 ====================
var SECTORS = {
"能源系": {
emoji: "🛢️",
codes: ["fu", "bu", "lu"],
maFast: 10, maSlow: 30
},
"有色金属系": {
emoji: "🔩",
codes: ["cu", "al", "zn", "pb", "ni", "sn", "ao"],
maFast: 10, maSlow: 30
},
"黑色金属系": {
emoji: "⚙️",
codes: ["rb", "hc", "ss", "i", "j", "jm", "SF", "SM", "FG", "SA"],
maFast: 10, maSlow: 30
},
"油脂系": {
emoji: "🫙",
codes: ["y", "p", "OI"],
maFast: 10, maSlow: 30
},
"农产品系": {
emoji: "🌽",
codes: ["c", "cs", "a", "b", "m", "jd", "lh"],
maFast: 10, maSlow: 30
},
"软商品系": {
emoji: "🍎",
codes: ["SR", "CF", "CY", "AP", "CJ", "PK"],
maFast: 10, maSlow: 30
},
"化工系": {
emoji: "🧪",
codes: ["l", "v", "pp", "eg", "eb", "pg", "TA", "MA", "PX", "PF", "SH", "UR"],
maFast: 10, maSlow: 30
}
};
// ==================== 安全请求封装 ====================
function safeGetRecords(contractId, period) {
for (var retry = 0; retry < MAX_RETRY; retry++) {
try {
exchange.SetContractType(contractId);
Sleep(50);
var records = exchange.GetRecords(period);
if (!records || !Array.isArray(records) || records.length < 10) {
Log('⚠️ K线数据无效:', contractId, '长度:', records ? records.length : 0);
Sleep(500);
continue;
}
return records;
} catch(e) {
Log('⚠️ GetRecords异常:', contractId, e.message);
Sleep(500);
}
}
Log('❌ 跳过品种(数据获取失败):', contractId);
return null;
}
function safeSetContract(code) {
try {
var info = exchange.SetContractType(code + '888');
Sleep(50);
if (info && info.InstrumentID) return info.InstrumentID;
return null;
} catch(e) {
Log('⚠️ SetContractType失败:', code, e.message);
return null;
}
}
// ==================== 工具函数 ====================
function isTradeTime() {
var t = new Date();
var hour = t.getHours();
var minute = t.getMinutes();
var day = t.getDay();
var curtime = hour * 100 + minute;
return (day >= 1 && day <= 5) &&
((curtime >= 900 && curtime <= 1130) ||
(curtime >= 1330 && curtime <= 1500) ||
(curtime >= 2100 && curtime <= 2300));
}
function calcMA(closes, period) {
if (!closes || closes.length < period) return null;
var sum = 0;
for (var i = closes.length - period; i < closes.length; i++) sum += closes[i];
return sum / period;
}
function calcMAArray(closes, period) {
var result = [];
var sum = 0;
for (var i = 0; i < closes.length; i++) {
sum += closes[i];
if (i >= period) sum -= closes[i - period];
result.push(i >= period - 1 ? sum / period : null);
}
return result;
}
function calcMomentum(records, lookback) {
var n = records.length;
if (n < lookback + 1) return 0;
var base = records[n - 1 - lookback].Close;
var cur = records[n - 1].Close;
if (!base || base === 0) return 0;
return (cur - base) / base * 100;
}
function calcVolPct(records) {
var n = records.length;
if (n < 20) return 0.5;
var atrs = [];
for (var i = 1; i < n; i++) {
var atr = Math.max(
records[i].High - records[i].Low,
Math.abs(records[i].High - records[i-1].Close),
Math.abs(records[i].Low - records[i-1].Close)
);
if (!isNaN(atr) && atr >= 0) atrs.push(atr);
}
if (atrs.length < 14) return 0.5;
var recent14 = atrs.slice(-14);
var avgATR = recent14.reduce(function(a, b) { return a + b; }, 0) / recent14.length;
var sorted = atrs.slice().sort(function(a, b) { return a - b; });
var idx = sorted.length - 1;
for (var k = 0; k < sorted.length; k++) {
if (sorted[k] >= avgATR) { idx = k; break; }
}
return idx / sorted.length;
}
function backtestMA(records, fast, slow) {
if (!records || records.length < slow + 10) return null;
var closes = records.map(function(r) { return r.Close; });
var fastMA = calcMAArray(closes, fast);
var slowMA = calcMAArray(closes, slow);
var n = closes.length;
var trades = [], position = null, equity = 1.0, peak = 1.0, maxDrawdown = 0;
for (var i = slow; i < n; i++) {
if (!fastMA[i] || !slowMA[i] || !fastMA[i-1] || !slowMA[i-1]) continue;
var crossUp = fastMA[i-1] <= slowMA[i-1] && fastMA[i] > slowMA[i];
var crossDown = fastMA[i-1] >= slowMA[i-1] && fastMA[i] < slowMA[i];
if (position !== null) {
var shouldClose = (position.side === 'long' && crossDown) ||
(position.side === 'short' && crossUp);
if (shouldClose) {
var ret = (closes[i] - position.entryPrice) / position.entryPrice;
if (position.side === 'short') ret = -ret;
equity *= (1 + ret);
if (equity > peak) peak = equity;
var dd = (peak - equity) / peak * 100;
if (dd > maxDrawdown) maxDrawdown = dd;
trades.push(ret);
position = null;
}
}
if (position === null) {
if (crossUp) position = { side: 'long', entryPrice: closes[i] };
else if (crossDown) position = { side: 'short', entryPrice: closes[i] };
}
}
if (position !== null) {
var ret2 = (closes[n-1] - position.entryPrice) / position.entryPrice;
if (position.side === 'short') ret2 = -ret2;
equity *= (1 + ret2);
if (equity > peak) peak = equity;
var dd2 = (peak - equity) / peak * 100;
if (dd2 > maxDrawdown) maxDrawdown = dd2;
trades.push(ret2);
}
if (trades.length === 0) return null;
var wins = trades.filter(function(t) { return t > 0; });
var losses = trades.filter(function(t) { return t <= 0; });
var winRate = wins.length / trades.length;
var avgWin = wins.length ? wins.reduce(function(a, b) { return a + b; }, 0) / wins.length : 0;
var avgLoss = losses.length ? losses.reduce(function(a, b) { return a + b; }, 0) / losses.length : 0;
avgLoss = Math.abs(avgLoss);
var profitFactor = avgLoss > 0 ? avgWin / avgLoss : (avgWin > 0 ? 99 : 0);
return { winRate: winRate, profitFactor: profitFactor, maxDrawdown: maxDrawdown, signalCount: trades.length };
}
function fmtPnl(pct) {
var v = parseFloat(pct);
if (v > 5) return '🚀 +' + v.toFixed(2) + '%';
if (v > 0) return '✅ +' + v.toFixed(2) + '%';
if (v > -3) return '🟡 ' + v.toFixed(2) + '%';
return '🔴 ' + v.toFixed(2) + '%';
}
// ==================== 主力合约管理 ====================
function getSectorContracts(sectorName) {
var codes = SECTORS[sectorName].codes;
var result = {};
for (var i = 0; i < codes.length; i++) {
var code = codes[i];
var contractId = safeSetContract(code);
if (contractId) {
result[code] = contractId;
} else {
Log('⚠️ 合约获取失败,跳过:', code);
}
Sleep(100);
}
return result;
}
function getAllContracts() {
var all = {};
for (var sName in SECTORS) {
Log('获取合约:', sName);
all[sName] = getSectorContracts(sName);
Sleep(200);
}
return all;
}
// ==================== 板块趋势得分 ====================
function calcSectorScore(sectorName, contracts) {
var codes = Object.keys(contracts);
if (codes.length === 0) return { score: 0, direction: 0, details: {} };
var memberScores = [], bullCount = 0, bearCount = 0, details = {};
var fast = SECTORS[sectorName].maFast;
var slow = SECTORS[sectorName].maSlow;
for (var i = 0; i < codes.length; i++) {
var code = codes[i];
var contractId = contracts[code];
try {
var records = safeGetRecords(contractId, PERIOD_H1);
if (!records || records.length < 40) continue;
if (records.length > klineLen) records = records.slice(-klineLen);
var closes = records.map(function(r) { return r.Close; });
var fastMA = calcMA(closes, fast);
var slowMA = calcMA(closes, slow);
if (!fastMA || !slowMA) continue;
var isBull = fastMA > slowMA;
var isBear = fastMA < slowMA;
var maScore = 0;
if (isBull) { maScore = 40; bullCount++; }
else if (isBear) { maScore = -40; bearCount++; }
var momentum = calcMomentum(records, 20);
var momScore = momentum > 0
? (30 + Math.min(momentum * 3, 20))
: -(30 + Math.min(Math.abs(momentum) * 3, 20));
var volPct = calcVolPct(records);
var volScore = volPct > 0.5 ? 10 : 0;
var rawScore = maScore + momScore + volScore;
memberScores.push(rawScore);
details[code] = { score: rawScore, isBull: isBull, momentum: momentum.toFixed(2) };
} catch(e) {
Log('⚠️ 板块评分单品种异常:', sectorName, code, e.message);
}
Sleep(100);
}
if (memberScores.length === 0) return { score: 0, direction: 0, details: details };
var total = bullCount + bearCount;
var direction = bullCount > bearCount ? 1 : (bearCount > bullCount ? -1 : 0);
var consistency = total > 0 ? Math.max(bullCount, bearCount) / total : 0;
var avgRaw = memberScores.reduce(function(a, b) { return a + b; }, 0) / memberScores.length;
var alignedScore = Math.min(Math.max(direction >= 0 ? avgRaw : -avgRaw, 0), 100);
var finalScore = alignedScore * (0.5 + consistency * 0.5);
return {
score: finalScore, direction: direction, consistency: consistency,
bullCount: bullCount, bearCount: bearCount,
memberCount: memberScores.length, details: details
};
}
// ==================== 品种回测筛选 ====================
function screenSymbolsInSector(sectorName, contracts) {
var results = [];
var codes = Object.keys(contracts);
var total = codes.length;
for (var i = 0; i < total; i++) {
var code = codes[i];
var contractId = contracts[code];
Log(' 品种筛选进度 [' + sectorName + ']:', (i + 1) + '/' + total, code.toUpperCase());
try {
var records = safeGetRecords(contractId, PERIOD_H1);
if (!records) {
Log(' ⏭️ 跳过', code, '原因: 数据获取失败');
continue;
}
if (records.length < 100) {
Log(' ⏭️ 跳过', code, '原因: 数据量不足', records.length, '< 100');
continue;
}
if (records.length > klineLen) records = records.slice(-klineLen);
var validCount = 0;
for (var vi = 0; vi < records.length; vi++) {
if (records[vi] && records[vi].Close > 0 &&
records[vi].High >= records[vi].Low &&
!isNaN(records[vi].Close)) {
validCount++;
}
}
if (validCount < records.length * 0.9) {
Log(' ⏭️ 跳过', code, '原因: 数据质量差,有效率:',
(validCount / records.length * 100).toFixed(1) + '%');
continue;
}
var bestScore = -999999, bestResult = null, bestParams = null;
for (var j = 0; j < maParams.length; j++) {
var fast = maParams[j][0], slow = maParams[j][1];
var bt = backtestMA(records, fast, slow);
if (!bt) continue;
if (bt.signalCount < 3) continue;
if (bt.winRate < minWinRate) continue;
if (bt.profitFactor < minProfitFactor) continue;
if (bt.maxDrawdown > maxMDD) continue;
var score = bt.winRate * 100 * 0.30
+ Math.min(bt.profitFactor * 20, 60) * 0.30
+ Math.max(0, 1 - bt.maxDrawdown / maxMDD) * 100 * 0.20
+ calcVolPct(records) * 10;
if (score > bestScore) {
bestScore = score;
bestResult = bt;
bestParams = { fast: fast, slow: slow };
}
}
if (bestResult && bestParams) {
Log(' ✅', code.toUpperCase(),
'MA' + bestParams.fast + '/' + bestParams.slow,
'胜率:' + (bestResult.winRate * 100).toFixed(1) + '%',
'盈亏比:' + bestResult.profitFactor.toFixed(2),
'MDD:' + bestResult.maxDrawdown.toFixed(2) + '%');
results.push({
code: code, contractId: contractId, score: bestScore,
winRate: bestResult.winRate, profitFactor: bestResult.profitFactor,
maxDrawdown: bestResult.maxDrawdown, signalCount: bestResult.signalCount,
bestFast: bestParams.fast, bestSlow: bestParams.slow
});
}
} catch(e) {
Log(' ❌ 品种筛选异常,跳过:', code, '|', e.name, e.message);
}
Sleep(150);
}
results.sort(function(a, b) { return b.score - a.score; });
return results;
}
// ==================== MA排列检查(退出判断)====================
function shouldKeepPosition(contractId, fast, slow, side) {
try {
var records = safeGetRecords(contractId, PERIOD_H1);
if (!records || records.length < slow + 1) return true;
var closes = records.map(function(r) { return r.Close; });
var fastMA = calcMA(closes, fast);
var slowMA = calcMA(closes, slow);
if (!fastMA || !slowMA) return true;
if (side === 'long') return fastMA > slowMA;
if (side === 'short') return fastMA < slowMA;
return true;
} catch(e) { return true; }
}
// ==================== 开仓信号检查 ====================
function calcSignal(contractId, fast, slow, direction) {
try {
var records = safeGetRecords(contractId, PERIOD_H1);
if (!records || records.length < slow + 3) return null;
var closes = records.map(function(r) { return r.Close; });
var n = closes.length;
var fastCur = calcMA(closes.slice(0, n), fast);
var fastPrev = calcMA(closes.slice(0, n - 1), fast);
var slowCur = calcMA(closes.slice(0, n), slow);
var slowPrev = calcMA(closes.slice(0, n - 1), slow);
if (!fastCur || !fastPrev || !slowCur || !slowPrev) return null;
var crossUp = fastPrev <= slowPrev && fastCur > slowCur;
var crossDown = fastPrev >= slowPrev && fastCur < slowCur;
if (crossUp && direction >= 0) return 'long';
if (crossDown && allowShort && direction <= 0) return 'short';
return null;
} catch(e) { return null; }
}
// ==================== 持仓 MA 反叉巡检 ====================
function checkExitSignals(p, candidateMap) {
var positions;
try { positions = exchange.GetPosition(); } catch(e) { return; }
if (!positions || positions.length === 0) return;
for (var i = 0; i < positions.length; i++) {
var pos = positions[i];
if (Math.abs(pos.Amount) === 0) continue;
var m = pos.ContractType.match(/[A-Za-z]+/g);
var code = m ? m[0].toLowerCase() : '';
var isLong = pos.Type === PD_LONG || pos.Type === PD_LONG_YD;
var side = isLong ? 'long' : 'short';
var sc = candidateMap[code];
if (!sc || !sc.bestFast || !sc.bestSlow) continue;
try {
if (!shouldKeepPosition(pos.ContractType, sc.bestFast, sc.bestSlow, side)) {
Log('🔄 MA反叉平仓 [' + code.toUpperCase() + ']',
side === 'long' ? '多头→死叉' : '空头→金叉',
'MA' + sc.bestFast + '/' + sc.bestSlow);
p.Cover(pos.ContractType);
Sleep(200);
}
} catch(e) {
Log('⚠️ 平仓检查异常:', code, e.message);
}
}
}
// ==================== 信号扫描并开仓 ====================
function scanAndOpen(p, sectorScores, candidateMap, allContracts) {
var positions;
try { positions = exchange.GetPosition() || []; } catch(e) { return; }
var holdingSet = {}, holdingCount = 0;
for (var i = 0; i < positions.length; i++) {
if (Math.abs(positions[i].Amount) > 0) {
holdingSet[positions[i].ContractType] = true;
holdingCount++;
}
}
if (holdingCount >= maxTotalPositions) {
Log('持仓已达上限(' + maxTotalPositions + '),跳过开仓扫描');
return;
}
for (var sName in sectorScores) {
var sc = sectorScores[sName];
if (sc.score < minSectorScore) continue;
var direction = sc.direction;
var sectorHoldCount = 0;
var candidates = sc.candidates || [];
for (var j = 0; j < candidates.length; j++) {
if (holdingCount >= maxTotalPositions) break;
var item = candidates[j];
var contractId = item.contractId;
if (holdingSet[contractId]) { sectorHoldCount++; continue; }
if (sectorHoldCount >= topPerSector) break;
try {
var sig = calcSignal(contractId, item.bestFast, item.bestSlow, direction);
if (sig === 'long') {
Log('📈 金叉开多 [' + item.code.toUpperCase() + ']',
sName, 'MA' + item.bestFast + '/' + item.bestSlow,
'板块得分:' + sc.score.toFixed(1));
p.OpenLong(contractId, Amount);
holdingSet[contractId] = true;
holdingCount++; sectorHoldCount++;
Sleep(200);
} else if (sig === 'short') {
Log('📉 死叉开空 [' + item.code.toUpperCase() + ']',
sName, 'MA' + item.bestFast + '/' + item.bestSlow,
'板块得分:' + sc.score.toFixed(1));
p.OpenShort(contractId, Amount);
holdingSet[contractId] = true;
holdingCount++; sectorHoldCount++;
Sleep(200);
}
} catch(e) {
Log('⚠️ 开仓信号检查异常:', item.code, e.message);
}
Sleep(150);
}
}
}
// ==================== 状态面板 ====================
function updateStatus(sectorScores, candidateMap, scanStatus) {
var account;
try { account = exchange.GetAccount(); } catch(e) { return; }
if (!account) return;
var initMoney = _G('sr_initmoney') || account.Equity;
var equity = account.Equity;
var profit = equity - initMoney;
var pct = (profit / initMoney * 100).toFixed(2);
var runCount = _G('sr_runCount') || 0;
var t0 = {
type: 'table', title: '📊 账户概览',
cols: ['💵 初始资金', '💰 当前权益', '📈 总收益', '📊 收益率', '🔄 运行次数', '📡 扫描状态'],
rows: [[
'¥' + initMoney.toFixed(2),
'¥' + equity.toFixed(2),
(profit >= 0 ? '💰 +¥' : '💸 -¥') + Math.abs(profit).toFixed(2),
(parseFloat(pct) >= 0 ? '🟢 +' : '🔴 ') + pct + '%',
'🔄 ' + runCount + '次',
scanStatus || '⏳ 等待扫描'
]]
};
var t1 = {
type: 'table', title: '🔄 板块得分总览',
cols: ['板块', '综合得分', '方向', '多头品种', '空头品种', '一致性', '信号监听'],
rows: []
};
var sectorList = Object.keys(sectorScores);
sectorList.sort(function(a, b) { return sectorScores[b].score - sectorScores[a].score; });
for (var i = 0; i < sectorList.length; i++) {
var sName = sectorList[i];
var sc = sectorScores[sName];
var scoreFmt = sc.score >= 60 ? '🟢 ' : sc.score >= 30 ? '🟡 ' : '🔴 ';
var listening = sc.score >= minSectorScore ? '✅ 监听中' : '⏸️ 得分不足';
t1.rows.push([
(SECTORS[sName] ? SECTORS[sName].emoji : '') + ' ' + sName,
scoreFmt + sc.score.toFixed(1),
sc.direction > 0 ? '📈 多头' : sc.direction < 0 ? '📉 空头' : '➖ 震荡',
sc.bullCount || 0,
sc.bearCount || 0,
sc.consistency !== undefined ? (sc.consistency * 100).toFixed(0) + '%' : 'N/A',
listening
]);
}
var t2 = {
type: 'table', title: '🏆 候选品种(回测通过,等待信号)',
cols: ['板块', '品种', '评分', '胜率', '盈亏比', '最大回撤', '最优MA'],
rows: []
};
var hasCandidates = false;
for (var sName in sectorScores) {
var sc = sectorScores[sName];
if (!sc.candidates || sc.candidates.length === 0 || sc.score < minSectorScore) continue;
for (var j = 0; j < sc.candidates.length; j++) {
hasCandidates = true;
var c = sc.candidates[j];
t2.rows.push([
(SECTORS[sName] ? SECTORS[sName].emoji : '') + ' ' + sName,
'🪙 ' + c.code.toUpperCase(),
c.score.toFixed(1),
(c.winRate * 100).toFixed(1) + '%',
c.profitFactor.toFixed(2),
c.maxDrawdown.toFixed(2) + '%',
'MA' + c.bestFast + '/' + c.bestSlow
]);
}
}
if (!hasCandidates) {
t2.rows.push(['—', '⏳ 等待首次板块扫描...', '—', '—', '—', '—', '—']);
}
// ★ 持仓表:非交易时间不调用 GetTicker,用持仓均价代替
var t3 = {
type: 'table', title: '📋 实时持仓(MA反叉退出)',
cols: ['品种', '方向', '入场价', '现价', '浮盈%', 'MA状态', '退出条件'],
rows: []
};
var positions;
try { positions = exchange.GetPosition() || []; } catch(e) { positions = []; }
var hasPos = false;
var inTrade = isTradeTime();
for (var i = 0; i < positions.length; i++) {
var pos = positions[i];
if (Math.abs(pos.Amount) === 0) continue;
hasPos = true;
var m = pos.ContractType.match(/[A-Za-z]+/g);
var code = m ? m[0].toLowerCase() : '';
var isLong = pos.Type === PD_LONG || pos.Type === PD_LONG_YD;
// ★ 只在交易时间获取最新价,非交易时间用持仓均价
var curPrice = pos.Price;
if (inTrade) {
try {
exchange.SetContractType(pos.ContractType);
var ticker = exchange.GetTicker();
if (ticker && ticker.Last > 0) curPrice = ticker.Last;
} catch(e) {
// 静默处理,保持使用持仓均价
}
}
var pnlPct = (curPrice - pos.Price) / pos.Price * 100;
if (!isLong) pnlPct = -pnlPct;
var sc = candidateMap[code] || {};
var maStatus = 'N/A';
if (sc.bestFast && sc.bestSlow) {
if (inTrade) {
// ★ 只在交易时间查询MA状态
try {
var keep = shouldKeepPosition(
pos.ContractType, sc.bestFast, sc.bestSlow, isLong ? 'long' : 'short'
);
maStatus = keep ? '✅ 顺势' : '⚠️ 反叉';
} catch(e) {
maStatus = 'N/A';
}
} else {
maStatus = '💤 休市';
}
}
t3.rows.push([
'🪙 ' + code.toUpperCase(),
isLong ? '🟢 多' : '🔴 空',
pos.Price.toFixed(2),
curPrice.toFixed(2) + (inTrade ? '' : ' 💤'),
fmtPnl(pnlPct),
maStatus,
sc.bestFast && sc.bestSlow ? (isLong ? 'MA死叉平多' : 'MA金叉平空') : '—'
]);
}
if (!hasPos) {
t3.rows.push(['😴 当前无持仓', '—', '—', '—', '—', '—', '等待信号']);
}
LogStatus('`' + JSON.stringify(t0) + '`\n\n`' + JSON.stringify(t1) + '`\n\n`' + JSON.stringify(t2) + '`\n\n`' + JSON.stringify(t3) + '`');
}
// ==================== 主函数 ====================
function main() {
// ★ 加入 market not ready 过滤
SetErrorFilter("502:|503:|tcp|character|unexpected|network|timeout|WSARecv|Connect|GetAddr|no such|reset|http|received|EOF|reused|market not ready|not ready");
var p = $.NewPositionManager();
if (_G('sr_initialized') === null) {
_G('sr_initialized', true);
_G('sr_sectorScores', JSON.stringify({}));
_G('sr_runCount', 0);
var account = _C(exchange.GetAccount);
_G('sr_initmoney', account.Equity);
Log('=== 板块轮动策略(多板块并行信号版)初始化完成 ===');
Log('初始资金:', account.Equity, 'CNY');
Log('模式: 全板块并行监听,信号触发即开仓,MA反叉即平仓');
Log('监控板块数:', Object.keys(SECTORS).length);
}
LogReset(0);
var lastCheckDate = null;
var allContracts = {};
var sectorScores = JSON.parse(_G('sr_sectorScores') || '{}');
var candidateMap = {};
var lastSectorScanTime = 0;
var lastSignalTime = 0;
var scanStatus = '⏳ 等待首次扫描';
while (true) {
try {
if (!exchange.IO("status")) { Sleep(3000); continue; }
var now = new Date();
var todayStr = now.getFullYear() + '-' + (now.getMonth() + 1) + '-' + now.getDate();
var nowMs = now.getTime();
_G('sr_runCount', (_G('sr_runCount') || 0) + 1);
// 每天刷新主力合约
if (lastCheckDate !== todayStr) {
lastCheckDate = todayStr;
Log('📅 更新全板块主力合约...');
try {
allContracts = getAllContracts();
var total = 0;
for (var s in allContracts) for (var c in allContracts[s]) total++;
Log('✅ 主力合约更新完成,共', total, '个品种');
} catch(e) {
Log('❌ 主力合约更新失败:', e.message);
allContracts = allContracts || {};
}
lastSectorScanTime = 0;
}
// ========== 板块评分 + 品种筛选 ==========
if (isTradeTime() && (nowMs - lastSectorScanTime) >= sectorScanInterval) {
lastSectorScanTime = nowMs;
var h = now.getHours();
var mi = now.getMinutes();
Log('═══ 板块评分+筛选 开始 [' + h + ':' + (mi < 10 ? '0' : '') + mi + '] ═══');
scanStatus = '🔄 扫描中... ' + h + ':' + (mi < 10 ? '0' : '') + mi;
var newCandidateMap = {};
var sectorsDone = 0;
var sectorsTotal = Object.keys(SECTORS).length;
for (var sName in SECTORS) {
if (!allContracts[sName]) { sectorsDone++; continue; }
Log('📊 板块评分 [' + SECTORS[sName].emoji + ' ' + sName + ']',
'(' + (sectorsDone + 1) + '/' + sectorsTotal + ')');
var sc;
try {
sc = calcSectorScore(sName, allContracts[sName]);
} catch(e) {
Log('❌ 板块评分失败:', sName, e.message);
sc = { score: 0, direction: 0, candidates: [], consistency: 0,
bullCount: 0, bearCount: 0, memberCount: 0, details: {} };
}
if (sc.score >= minSectorScore) {
Log('[' + SECTORS[sName].emoji + ' ' + sName + ']',
'得分:' + sc.score.toFixed(1),
'方向:' + (sc.direction > 0 ? '多' : sc.direction < 0 ? '空' : '震荡'),
'→ 开始品种筛选');
try {
var screenResults = screenSymbolsInSector(sName, allContracts[sName]);
sc.candidates = screenResults.slice(0, topPerSector);
} catch(e) {
Log('❌ 品种筛选失败:', sName, e.message);
sc.candidates = [];
}
for (var k = 0; k < sc.candidates.length; k++) {
newCandidateMap[sc.candidates[k].code] = sc.candidates[k];
}
Log(' → 候选品种数:', sc.candidates.length);
} else {
Log('[' + SECTORS[sName].emoji + ' ' + sName + ']',
'得分:' + sc.score.toFixed(1), '→ 低于阈值,跳过筛选');
sc.candidates = [];
}
sectorScores[sName] = sc;
sectorsDone++;
// 每完成一个板块立即保存,防止中途异常丢失数据
_G('sr_sectorScores', JSON.stringify(sectorScores));
}
for (var code in newCandidateMap) {
candidateMap[code] = newCandidateMap[code];
}
var endH = new Date().getHours();
var endMi = new Date().getMinutes();
Log('═══ 板块评分+筛选 完成 ═══');
Log('候选品种:', Object.keys(newCandidateMap).length,
'| 完成时间:', endH + ':' + (endMi < 10 ? '0' : '') + endMi);
scanStatus = '✅ 上次扫描: ' + endH + ':' + (endMi < 10 ? '0' : '') + endMi +
' | 候选品种: ' + Object.keys(newCandidateMap).length + '个';
}
// ========== 开仓信号扫描 ==========
if (isTradeTime() && (nowMs - lastSignalTime) >= signalCheckInterval) {
lastSignalTime = nowMs;
try {
scanAndOpen(p, sectorScores, candidateMap, allContracts);
} catch(e) {
Log('❌ 开仓扫描异常:', e.message);
}
}
// ========== MA反叉巡检(每个周期持续运行)==========
if (isTradeTime()) {
try {
checkExitSignals(p, candidateMap);
} catch(e) {
Log('❌ 退出检查异常:', e.message);
}
}
// 收益曲线
try {
var account = exchange.GetAccount();
if (account) {
var initMoney = _G('sr_initmoney') || account.Equity;
LogProfit(account.Equity - initMoney, '&');
}
} catch(e) {}
updateStatus(sectorScores, candidateMap, scanStatus);
} catch(e) {
Log('⚠️ 主循环异常:', e.name, '-', e.message);
Sleep(5000);
continue;
}
Sleep(5000);
}
}