商品期货板块轮动策略


创建日期: 2026-04-24 09:24:39 最后修改: 2026-04-28 13:19:36
复制: 0 点击次数: 14
avatar of ianzeng123 ianzeng123
1
关注
179
关注者
策略源码
// ==================== 板块轮动策略(商品期货版)====================

// ==================== 全局配置参数 ====================
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);
    }
}