”`js /*backtest start: 2024-01-01 00:00:00 end: 2024-12-31 23:59:00 period: 1m basePeriod: 1m exchanges: [{“eid”:“OKX”,“currency”:“BTC_USDT”,“balance”:10000}] */
// ==================== FMZ平台参数配置(从界面获取,禁止硬编码)==================== // 注意:FMZ平台中,界面设置的参数名称会自动成为JavaScript全局变量 // 直接使用变量名即可访问参数值,不需要使用GetConfig()函数 // 界面参数名必须与代码中的变量名一致 function getConfigParams() { var config = { // 币种选择(从FMZ界面选择,界面参数名:symbol) symbol: symbol || “BTC/USDT”,
// 网格参数(全部从界面输入,禁止硬编码)
longGridSpacing: parseFloat(longGridSpacing) || 1.0, // 多头网格间距(%),界面参数名:longGridSpacing
shortGridSpacing: parseFloat(shortGridSpacing) || 1.0, // 空头网格间距(%),界面参数名:shortGridSpacing
orderAmount: parseFloat(orderAmount) || 100, // 单个网格下单金额,界面参数名:orderAmount
upperPrice: parseFloat(upperPrice), // 网格上边界价格,界面参数名:upperPrice
lowerPrice: parseFloat(lowerPrice), // 网格下边界价格,界面参数名:lowerPrice
// 可选:手动覆盖最小步长/数量精度(界面参数名:forceMinLotSize, forceAmountPrecision)
forceMinLotSize: forceMinLotSize ? parseFloat(forceMinLotSize) : null,
forceAmountPrecision: forceAmountPrecision ? parseInt(forceAmountPrecision, 10) : null,
};
// ❌ 禁止硬编码示例:
// config.longGridSpacing = 1.0; // 错误!
// config.orderAmount = 100; // 错误!
return config;
}
// ==================== 状态持久化修正版 ==================== // 使用FMZ平台的_G函数实现状态持久化 // 关机或重启后可以自动恢复状态
function saveState() { /** 保存策略状态到全局存储 */ try { var state = { // 多头相关 MANY_NEXT_BUY_PRICE: MANY_NEXT_BUY_PRICE, MANY_ORDER_LIST: MANY_ORDER_LIST,
// 空头相关
SHORT_NEXT_BUY_PRICE: SHORT_NEXT_BUY_PRICE,
SHORT_ORDER_LIST: SHORT_ORDER_LIST,
// 状态信息相关
INIT_TIME: INIT_TIME,
INITIAL_BALANCE: INITIAL_BALANCE,
// 配置信息(用于参数迁移检测)
config: config,
// 收益历史(保留最近1000条)
PROFIT_HISTORY: PROFIT_HISTORY.slice(-1000),
// 参数迁移相关
oldConfig: oldConfig,
isMigrating: isMigrating,
// 时间戳
lastSaveTime: Unix()
};
// ✅ 修正:使用正确的FMZ全局存储函数
_G("strategy_state", JSON.stringify(state));
return true;
} catch (e) {
Log("保存状态失败:", e);
return false;
}
}
function loadState() { /** 从全局存储恢复策略状态 */ try { // ✅ 修正:使用正确的FMZ全局存储函数 var stateStr = _G(“strategy_state”); if (!stateStr) { Log(“未找到保存的状态,使用初始状态”); return false; }
var state = JSON.parse(stateStr);
// 恢复多头相关
if (state.MANY_NEXT_BUY_PRICE !== undefined) {
MANY_NEXT_BUY_PRICE = state.MANY_NEXT_BUY_PRICE;
}
if (state.MANY_ORDER_LIST && Array.isArray(state.MANY_ORDER_LIST)) {
MANY_ORDER_LIST = state.MANY_ORDER_LIST;
}
// 恢复空头相关
if (state.SHORT_NEXT_BUY_PRICE !== undefined) {
SHORT_NEXT_BUY_PRICE = state.SHORT_NEXT_BUY_PRICE;
}
if (state.SHORT_ORDER_LIST && Array.isArray(state.SHORT_ORDER_LIST)) {
SHORT_ORDER_LIST = state.SHORT_ORDER_LIST;
}
// 恢复状态信息
if (state.INIT_TIME) {
INIT_TIME = state.INIT_TIME;
}
if (state.INITIAL_BALANCE) {
INITIAL_BALANCE = state.INITIAL_BALANCE;
}
// 恢复收益历史
if (state.PROFIT_HISTORY && Array.isArray(state.PROFIT_HISTORY)) {
PROFIT_HISTORY = state.PROFIT_HISTORY;
}
// 恢复参数迁移状态
if (state.oldConfig) {
oldConfig = state.oldConfig;
}
if (state.isMigrating !== undefined) {
isMigrating = state.isMigrating;
}
Log("状态恢复成功 - 多头持仓数:", MANY_ORDER_LIST.length, "空头持仓数:", SHORT_ORDER_LIST.length);
if (state.lastSaveTime) {
var downtime = Unix() - state.lastSaveTime;
Log("上次保存时间:", new Date(state.lastSaveTime * 1000).toLocaleString(), "停机时长:", Math.floor(downtime / 60), "分钟");
}
return true;
} catch (e) {
Log("恢复状态失败:", e);
return false;
}
}
function saveHistory(record) { /** 保存历史记录到全局存储 */ try { var historyKey = “strategy_history”; var historyStr = _G(historyKey); var history = [];
if (historyStr) {
try {
history = JSON.parse(historyStr);
} catch (e) {
history = [];
}
}
// 添加新记录
record.timestamp = Unix();
history.push(record);
// 只保留最近5000条记录
if (history.length > 5000) {
history = history.slice(-5000);
}
_G(historyKey, JSON.stringify(history));
return true;
} catch (e) {
Log("保存历史记录失败:", e);
return false;
}
}
function getHistory(limit) { /** 获取历史记录 */ try { var historyKey = “strategy_history”; var historyStr = _G(historyKey); if (!historyStr) { return []; }
var history = JSON.parse(historyStr);
limit = limit || 100;
// 返回最近的记录
return history.slice(-limit);
} catch (e) {
Log("获取历史记录失败:", e);
return [];
}
}
// ==================== 安全的状态保存 ==================== function safeSaveState() { /** 安全保存状态,带错误处理 */ try { if (saveState()) { LAST_SAVE_TIME = Unix(); return true; } return false; } catch (e) { Log(“安全保存状态失败:”, e); return false; } }
// ==================== 持仓对账 ==================== function reconcilePositions() { try { var positions = exchange.GetPosition(); if (!positions) { return; } var hasLong = false; var hasShort = false; for (var i = 0; i < positions.length; i++) { if (positions[i].Amount > 0) { if (positions[i].Type === 0) hasLong = true; if (positions[i].Type === 1) hasShort = true; } } // 防抖:连续多次空才清空本地 if (!hasLong && !hasShort) { RECONCILE_EMPTY_STREAK += 1; } else { RECONCILE_EMPTY_STREAK = 0; } var shouldClear = RECONCILE_EMPTY_STREAK >= RECONCILE_EMPTY_THRESHOLD;
if (shouldClear) {
if (!hasLong && MANY_ORDER_LIST.length > 0) {
Log("对账:连续空结果,实际多头为空,清空本地多头持仓列表");
MANY_ORDER_LIST = [];
MANY_BUYING = false;
}
if (!hasShort && SHORT_ORDER_LIST.length > 0) {
Log("对账:连续空结果,实际空头为空,清空本地空头持仓列表");
SHORT_ORDER_LIST = [];
SHORT_BUYING = false;
}
if ((!hasLong && MANY_ORDER_LIST.length === 0) || (!hasShort && SHORT_ORDER_LIST.length === 0)) {
safeSaveState();
}
}
} catch (e) {
Log("持仓对账失败:", e);
}
}
// ==================== 双轨制参数迁移 ==================== // 实现渐进式参数迁移,保持原有持仓的平仓逻辑,新开仓使用新参数
// ==================== 增强的参数迁移检测 ==================== function checkConfigChange(newConfig) { /** 检查配置是否发生变化,如果变化则启动参数迁移 */ if (!oldConfig) { // 首次运行,保存当前配置 oldConfig = JSON.parse(JSON.stringify(newConfig)); isMigrating = false; return false; }
// 详细检查参数变化
var configChanged = false;
var changes = [];
if (oldConfig.longGridSpacing !== newConfig.longGridSpacing) {
configChanged = true;
changes.push("多头间距: " + oldConfig.longGridSpacing + "% → " + newConfig.longGridSpacing + "%");
}
if (oldConfig.shortGridSpacing !== newConfig.shortGridSpacing) {
configChanged = true;
changes.push("空头间距: " + oldConfig.shortGridSpacing + "% → " + newConfig.shortGridSpacing + "%");
}
if (oldConfig.orderAmount !== newConfig.orderAmount) {
configChanged = true;
changes.push("下单金额: " + oldConfig.orderAmount + " → " + newConfig.orderAmount);
}
if (oldConfig.upperPrice !== newConfig.upperPrice) {
configChanged = true;
changes.push("上边界: " + oldConfig.upperPrice + " → " + newConfig.upperPrice);
}
if (oldConfig.lowerPrice !== newConfig.lowerPrice) {
configChanged = true;
changes.push("下边界: " + oldConfig.lowerPrice + " → " + newConfig.lowerPrice);
}
if (configChanged && !isMigrating) {
Log("========== 🚀 检测到参数变化,启动双轨制迁移 ==========");
changes.forEach(function(change) {
Log("📝 " + change);
});
Log("🔄 迁移模式:旧持仓使用旧参数平仓,新开仓使用新参数");
Log("📊 当前持仓 - 多头:", MANY_ORDER_LIST.length, "空头:", SHORT_ORDER_LIST.length);
isMigrating = true;
safeSaveState(); // 立即保存迁移状态
}
return configChanged;
}
function shouldUseOldConfig(side) { /** 判断是否应该使用旧配置(用于平仓逻辑) */ if (!isMigrating || !oldConfig) { return false; }
// 如果还有旧持仓,使用旧配置平仓
if (side === LONG_SIDE) {
return MANY_ORDER_LIST.length > 0;
} else if (side === SHORT_SIDE) {
return SHORT_ORDER_LIST.length > 0;
}
return false;
}
function getGridSpacing(side, isClose) { /** 获取网格间距(支持双轨制) */ if (isClose && shouldUseOldConfig(side)) { // 平仓时,如果有旧持仓,使用旧参数 return side === LONG_SIDE ? oldConfig.longGridSpacing : oldConfig.shortGridSpacing; } else { // 开仓时,或旧持仓已平完,使用新参数 return side === LONG_SIDE ? config.longGridSpacing : config.shortGridSpacing; } }
function getOrderAmount(isClose) { /** 获取下单金额(支持双轨制) */ if (isClose && (shouldUseOldConfig(LONG_SIDE) || shouldUseOldConfig(SHORT_SIDE))) { // 平仓时,如果有旧持仓,使用旧参数 return oldConfig.orderAmount; } else { // 开仓时,使用新参数 return config.orderAmount; } }
function checkMigrationComplete() { /** 检查参数迁移是否完成 */ if (!isMigrating) { return true; }
// 如果旧持仓已全部平完,迁移完成
var oldLongPositions = 0;
var oldShortPositions = 0;
// 这里需要根据实际持仓判断,简化处理:如果持仓列表为空,认为迁移完成
// 实际应该检查持仓是否是用旧参数开的
if (MANY_ORDER_LIST.length === 0 && SHORT_ORDER_LIST.length === 0) {
Log("========== ✅ 参数迁移完成 ==========");
oldConfig = null;
isMigrating = false;
safeSaveState(); // 保存迁移完成状态
return true;
}
return false;
}
// ==================== 精度管理 ==================== function getDecimalPlaces(num) { /** 获取数字的小数位数 */ var str = num.toString(); if (str.indexOf(‘.’) > -1) { return str.split(‘.’)[1].length; } return 0; }
function getPrecision() { /** * 获取下单精度 * 1) 若已从规则获取 tickSz/lotSz,则直接使用 DEFAULT_PRICE_PREC / MIN_LOT_DECIMALS * 2) 否则从深度数据推断 * 3) 否则使用默认 */ if (DEFAULT_PRICE_PREC && MIN_LOT_DECIMALS) { return { pricePrecision: DEFAULT_PRICE_PREC, amountPrecision: forceAmountPrecision || MIN_LOT_DECIMALS }; } try { var depth = exchange.GetDepth(); if (depth && depth.Bids && depth.Bids.length > 0) { var bidPrice = depth.Bids[0].Price; var bidAmount = depth.Bids[0].Amount;
var pricePrecision = getDecimalPlaces(bidPrice);
var amountPrecision = getDecimalPlaces(bidAmount);
Log("从深度数据获取精度 - 买一价格:", bidPrice, "价格精度:", pricePrecision, "买一数量:", bidAmount, "数量精度:", amountPrecision);
return {
pricePrecision: Math.max(1, pricePrecision || DEFAULT_PRICE_PREC || 1),
amountPrecision: Math.max(MIN_LOT_DECIMALS, amountPrecision || MIN_LOT_DECIMALS)
};
}
} catch (e) {
Log("从深度数据获取精度失败:", e);
}
Log("使用默认精度 - 价格精度:", DEFAULT_PRICE_PREC, "数量精度:", MIN_LOT_DECIMALS);
return {
pricePrecision: DEFAULT_PRICE_PREC,
amountPrecision: forceAmountPrecision || MIN_LOT_DECIMALS
};
}
function formatPrice(price, precision) { return parseFloat(price.toFixed(precision)); }
function formatAmount(amount, precision) { return parseFloat(amount.toFixed(precision)); }
function calculateOrderAmount(price, orderValue, amountPrecision) { /** * 计算订单数量 * 确保数量是合约最小单位的整数倍 * ETH永续合约最小单位:0.01 */ var minLotSize = MIN_LOT_SIZE; // ETH永续合约最小单位
// 计算基础数量
var amount = orderValue / price;
Log("计算订单数量 - 基础计算: 目标金额=" + orderValue + ", 价格=" + price + ", 基础数量=" + amount.toFixed(6));
// 确保数量是minLotSize的整数倍(向上取整)
amount = Math.round(amount / minLotSize) * minLotSize;
if (amount < minLotSize) {
amount = minLotSize;
}
Log("计算订单数量 - 对齐到步长: " + amount.toFixed(amtPrecision));
// 应用精度格式化(数量精度改为2位)
var amtPrecision = Math.max(amountPrecision || (forceAmountPrecision || MIN_LOT_DECIMALS), MIN_LOT_DECIMALS);
amount = formatAmount(amount, amtPrecision); // 按传入精度格式化
// 再次确保是minLotSize的整数倍(防止精度格式化后丢失)
amount = Math.ceil(amount / minLotSize) * minLotSize;
amount = parseFloat(amount.toFixed(amtPrecision)); // 确保符合数量精度
// 计算实际订单金额
var actualValue = amount * price;
// 容错处理:允许订单金额与目标金额有较大偏差(允许10-20%的偏差)
// 如果实际金额太小(小于目标金额的80%),则增加一个最小单位
if (actualValue < orderValue * 0.8) {
amount = amount + minLotSize;
amount = parseFloat(amount.toFixed(amtPrecision));
actualValue = amount * price;
Log("计算订单数量 - 金额太小,增加一个最小单位: " + amount.toFixed(amtPrecision));
}
var deviation = ((actualValue - orderValue) / orderValue * 100).toFixed(2);
Log("计算订单数量 - 最终结果: 目标金额=" + orderValue + ", 价格=" + price + ", 计算数量=" + amount.toFixed(2) + ", 实际金额=" + actualValue.toFixed(2) + ", 偏差=" + deviation + "%");
return amount;
}
// ==================== 工具函数 ==================== function safeAmount(raw) { var val = Number(raw); return (isNaN(val) || val <= 0) ? 0.001 : Math.max(val, 0.001); }
function safePrice(raw) { var val = Number(raw); return (isNaN(val) || val <= 0) ? 1 : val; }
// 统一判断EOF错误,便于回测数据结束或连接断开时优雅退出 function isEOFError(e) { var msg = “; try { msg = (e && e.toString) ? e.toString() : String(e); } catch (_) { msg = “; } return msg && msg.indexOf(‘EOF’) !== -1; }
// ==================== 仓位记录 ==================== const LONG_SIDE = 1; const SHORT_SIDE = -1;
function addPosition(list, price, amount, side) { list.push({ price: price, amount: amount, side: side }); }
function removePosition(list, idx) { list.splice(idx, 1); }
// ==================== 全局变量 ==================== var config = null; var precision = null; // OKX ETH USDT 永续合约最小数量单位 0.01 var MIN_LOT_SIZE = 0.01; // 默认最小数量步长(OKX ETH 永续) var MIN_LOT_DECIMALS = 2; // 默认数量小数位(与步长匹配) var DEFAULT_PRICE_PREC = 1; // 默认价格精度 var forceMinLotSize = null; // 可选覆盖:最小步长 var forceAmountPrecision = null; // 可选覆盖:数量精度
function decimalPlaces(n) { var s = n.toString(); return s.indexOf(‘.’) > -1 ? s.split(‘.’)[1].length : 0; }
// ==================== 交易规则获取(OKX API) ==================== function loadInstrumentSpec() { try { // 仅当交易所支持 IO 方式直连 OKX V5 时生效 var symbolParts = (config && config.symbol ? config.symbol : “ETH/USDT”).replace(“_”, “/”).split(“/”); var base = symbolParts[0]; var quote = symbolParts[1]; var uly = base + “-” + quote; var params = “instType=SWAP&uly=” + encodeURIComponent(uly); var res = exchange.IO(“api”, “GET”, “/api/v5/public/instruments”, params); if (res && res.data && res.data.length > 0) { var inst = res.data[0]; if (inst.lotSz) { MIN_LOT_SIZE = parseFloat(inst.lotSz); MIN_LOT_DECIMALS = decimalPlaces(inst.lotSz); } if (inst.tickSz) { DEFAULT_PRICE_PREC = decimalPlaces(inst.tickSz); } Log(“获取规则成功 lotSz:”, inst.lotSz, “tickSz:”, inst.tickSz); return true; } else { Log(“获取规则失败:无数据返回”); } } catch (e) { Log(“获取规则异常(可能托管者/交易所不支持 IO):”, e); } return false; }
// 应用用户覆盖的步长/精度(若提供)。若已获取到交易所规则,则忽略覆盖以防违规。 function applyForceLot() { if (config && config.forceMinLotSize && !isNaN(config.forceMinLotSize)) { var f = parseFloat(config.forceMinLotSize); // 只有在未成功获取交易所规则时才采用用户覆盖 if (!DEFAULT_PRICE_PREC || DEFAULT_PRICE_PREC === 1) { MIN_LOT_SIZE = f; MIN_LOT_DECIMALS = decimalPlaces(MIN_LOT_SIZE); Log(“使用手动覆盖的最小步长:”, MIN_LOT_SIZE, “小数位:”, MIN_LOT_DECIMALS); } else { Log(“已从交易所规则获取 lotSz,忽略手动最小步长覆盖”); } } if (config && config.forceAmountPrecision && !isNaN(config.forceAmountPrecision)) { forceAmountPrecision = parseInt(config.forceAmountPrecision, 10); Log(“使用手动覆盖的数量精度:”, forceAmountPrecision); } }
// 多头相关 var MANY_BUYING = false; var MANY_NEXT_BUY_PRICE = 0; var MANY_ORDER_LIST = [];
// 空头相关 var SHORT_BUYING = false; var SHORT_NEXT_BUY_PRICE = 0; var SHORT_ORDER_LIST = [];
// 状态信息相关 var INIT_TIME = null; var INITIAL_BALANCE = 0; var LAST_PROFIT_TIME = null; var PROFIT_HISTORY = []; var CYCLE_START_TIME = 0;
// 参数迁移相关 var oldConfig = null; var isMigrating = false;
// 状态保存间隔(每5分钟保存一次) var LAST_SAVE_TIME = 0; var SAVE_INTERVAL = 300; // 5分钟 var LAST_RECONCILE_TIME = 0; var RECONCILE_INTERVAL = 60; // 每60秒对账一次 var RECONCILE_EMPTY_STREAK = 0; // 连续空结果计数,用于防抖 var RECONCILE_EMPTY_THRESHOLD = 2; // 连续2次空才清空本地持仓
// ==================== 下单函数 ==================== function placeOrder(dir, price, amount) { /** 下单函数,带错误处理和重试机制 */ try { exchange.SetDirection(dir); var finalPrice = safePrice(price); var finalAmount = safeAmount(amount);
// 按交易所精度要求格式化价格与数量
var pricePrecision = (precision && precision.pricePrecision) ? precision.pricePrecision : 2;
var amountPrecision = (precision && precision.amountPrecision) ? Math.max(precision.amountPrecision, MIN_LOT_DECIMALS) : (forceAmountPrecision || MIN_LOT_DECIMALS);
finalPrice = formatPrice(finalPrice, pricePrecision);
// 确保数量是0.01的整数倍(ETH永续合约最小单位)
var minLotSize = MIN_LOT_SIZE;
finalAmount = Math.round(finalAmount / minLotSize) * minLotSize;
if (finalAmount < minLotSize) {
finalAmount = minLotSize;
}
finalAmount = parseFloat(finalAmount.toFixed(amountPrecision)); // 按数量精度保留小数
// 确保数量至少是最小单位
if (finalAmount < minLotSize) {
finalAmount = minLotSize;
}
var orderValue = finalPrice * finalAmount;
Log("下单 - 方向:", dir, "价格:", finalPrice, "数量:", finalAmount, "金额:", orderValue.toFixed(2), "数量验证: 是0.01的", (finalAmount / minLotSize).toFixed(0), "倍");
var orderId = dir === 'buy' || dir === 'closebuy'
? exchange.Buy(finalPrice, finalAmount)
: exchange.Sell(finalPrice, finalAmount);
if (!orderId) {
Log("下单失败 - 方向:", dir, "价格:", finalPrice, "数量:", finalAmount, "返回订单ID为空");
// 重试一次(如果数量不是最小单位的整数倍,尝试调整)
if (finalAmount % minLotSize !== 0) {
finalAmount = Math.ceil(finalAmount / minLotSize) * minLotSize;
finalAmount = parseFloat(finalAmount.toFixed(2));
Log("下单重试 - 调整数量为:", finalAmount);
orderId = dir === 'buy' || dir === 'closebuy'
? exchange.Buy(finalPrice, finalAmount)
: exchange.Sell(finalPrice, finalAmount);
}
} else {
Log("下单成功 - 方向:", dir, "订单ID:", orderId, "价格:", finalPrice, "数量:", finalAmount);
}
return orderId;
} catch (e) {
Log("下单异常 - 方向:", dir, "价格:", price, "数量:", amount, "错误:", e);
return null;
}
}
// 轮询订单成交,带超时和自定义回调,避免重复代码 function waitOrderFilled(orderId, timeoutSec, onFilled, onTimeout) { var deadline = Unix() + timeoutSec; while (true) { if (Unix() > deadline) { if (onTimeout) onTimeout(orderId); return false; } var order = exchange.GetOrder(orderId); if (order && (order.Status === 1 || order.Status === 2)) { onFilled(order); return true; } Sleep(100); } }
// ==================== 多头网格策略 ==================== function firstManyBuy() { /** 首次多头买入,带超时和重试机制 */ if (MANY_BUYING) return;
var ticker = exchange.GetTicker();
if (!ticker) {
Log("首次多头买入失败:无法获取行情");
return;
}
var price = safePrice(ticker.Last);
var orderValue = getOrderAmount(false); // 使用新参数
Log("准备首次多头买入 - 当前价格:", price, "目标金额:", orderValue);
var amount = calculateOrderAmount(price, orderValue, precision.amountPrecision);
Log("首次多头买入 - 计算出的数量:", amount, "价格:", price);
var orderId = placeOrder('buy', price, amount);
if (!orderId) {
Log("首次多头买入失败:下单返回空,尝试重试...");
// 重试一次,使用调整后的数量
Sleep(1000);
amount = calculateOrderAmount(price, orderValue, precision.amountPrecision);
orderId = placeOrder('buy', price, amount);
if (!orderId) {
Log("首次多头买入重试失败:下单返回空");
return;
}
}
Log("首次多头买入 - 订单ID:", orderId, "价格:", price, "数量:", amount);
MANY_BUYING = true;
var timeoutSec = 60; // 60秒超时
var filled = waitOrderFilled(
orderId,
timeoutSec,
function(order) {
var gridSpacing = getGridSpacing(LONG_SIDE, false);
MANY_NEXT_BUY_PRICE = formatPrice(
order.Price * (1 - gridSpacing / 100),
precision.pricePrecision
);
MANY_BUYING = false;
addPosition(MANY_ORDER_LIST, order.Price, order.Amount, LONG_SIDE);
Log("多头首次买入成功,价格:", order.Price, "数量:", order.Amount, "下一个买入价格:", MANY_NEXT_BUY_PRICE);
saveHistory({
type: 'first_many_buy',
price: order.Price,
amount: order.Amount,
value: order.Price * order.Amount
});
safeSaveState();
},
function(orderIdTimeout) {
Log("首次多头买入超时,取消订单:", orderIdTimeout);
exchange.CancelOrder(orderIdTimeout);
MANY_BUYING = false;
}
);
if (!filled) {
return;
}
}
function manyBuy() { /** 多头加仓(网格买入) */ if (MANY_BUYING) return;
var ticker = exchange.GetTicker();
if (!ticker) {
Log("多头加仓失败:无法获取行情");
return;
}
if (ticker.Last > MANY_NEXT_BUY_PRICE) {
Log("多头加仓跳过:当前价格", ticker.Last, "高于下一个买入价格", MANY_NEXT_BUY_PRICE);
return;
}
var price = safePrice(ticker.Last);
var orderValue = getOrderAmount(false); // 使用新参数
Log("准备多头加仓 - 当前价格:", price, "下一个买入价格:", MANY_NEXT_BUY_PRICE, "目标金额:", orderValue);
var amount = calculateOrderAmount(price, orderValue, precision.amountPrecision);
Log("多头加仓 - 计算出的数量:", amount, "价格:", price);
var orderId = placeOrder('buy', price, amount);
if (!orderId) {
Log("多头加仓失败:下单返回空");
return;
}
Log("多头加仓 - 订单ID:", orderId, "价格:", price, "数量:", amount);
MANY_BUYING = true;
var timeoutSec = 60 * 30; // 30分钟超时
var filled = waitOrderFilled(
orderId,
timeoutSec,
function(order) {
var gridSpacing = getGridSpacing(LONG_SIDE, false);
MANY_NEXT_BUY_PRICE = formatPrice(
order.Price * (1 - gridSpacing / 100),
precision.pricePrecision
);
MANY_BUYING = false;
addPosition(MANY_ORDER_LIST, order.Price, order.Amount, LONG_SIDE);
Log("多头加仓成功,价格:", order.Price, "数量:", order.Amount, "持仓数:", MANY_ORDER_LIST.length, "下一个买入价格:", MANY_NEXT_BUY_PRICE);
saveHistory({
type: 'many_buy',
price: order.Price,
amount: order.Amount,
value: order.Price * order.Amount
});
safeSaveState();
},
function(orderIdTimeout) {
exchange.CancelOrder(orderIdTimeout);
MANY_BUYING = false;
Log("多头加仓订单超时,已取消订单:", orderIdTimeout);
}
);
if (!filled) {
return;
}
}
function manySell() { /** 多头平仓(网格卖出)- 支持双轨制 */ var ticker = exchange.GetTicker(); if (!ticker) return;
// 从最新持仓开始检查(倒序)
for (var i = MANY_ORDER_LIST.length - 1; i >= 0; i--) {
var pos = MANY_ORDER_LIST[i];
// 计算盈利价格(支持双轨制:旧持仓用旧参数,新持仓用新参数)
var gridSpacing = getGridSpacing(LONG_SIDE, true); // 平仓时检查是否用旧参数
var profitPrice = formatPrice(
pos.price * (1 + gridSpacing / 100),
precision.pricePrecision
);
// 如果当前价格达到盈利价格,平仓
if (ticker.Last >= profitPrice) {
var position = exchange.GetPosition();
var longPos = null;
if (position && position.length > 0) {
for (var j = 0; j < position.length; j++) {
if (position[j].Type === 0 && position[j].Amount > 0) {
longPos = position[j];
break;
}
}
}
if (!longPos || longPos.Amount <= 0) break;
var qty = safeAmount(Math.min(pos.amount, longPos.Amount));
// 确保数量是0.01的整数倍(ETH永续合约最小单位)
var minLotSize = MIN_LOT_SIZE;
qty = Math.floor(qty / minLotSize) * minLotSize;
qty = parseFloat(qty.toFixed(2)); // 保留2位小数
if (qty < minLotSize) {
Log("多头平仓跳过:数量太小", qty, "最小单位:", minLotSize);
break;
}
Log("准备多头平仓 - 持仓价格:", pos.price, "当前价格:", ticker.Last, "盈利价格:", profitPrice, "平仓数量:", qty, "使用参数:", shouldUseOldConfig(LONG_SIDE) ? "旧参数" : "新参数");
// 多头平仓应使用 closesell(卖出平多)
var orderId = placeOrder('closesell', ticker.Last, qty);
if (!orderId) continue;
while (true) {
var order = exchange.GetOrder(orderId);
if (!order) {
Sleep(100);
continue;
}
if (order.Status === 1 || order.Status === 2) {
// 计算下一个买入价格(基于新参数的多头网格间距)
var nextGridSpacing = getGridSpacing(LONG_SIDE, false);
MANY_NEXT_BUY_PRICE = formatPrice(
ticker.Last * (1 - nextGridSpacing / 100),
precision.pricePrecision
);
removePosition(MANY_ORDER_LIST, i);
Log("多头平仓成功,价格:", order.Price, "数量:", order.Amount, "剩余持仓:", MANY_ORDER_LIST.length, "下一个买入价格:", MANY_NEXT_BUY_PRICE);
// 保存历史记录
var profit = (order.Price - pos.price) * order.Amount;
saveHistory({
type: 'many_sell',
price: order.Price,
amount: order.Amount,
value: order.Price * order.Amount,
entryPrice: pos.price,
profit: profit
});
// ✅ 成交后立即保存状态
safeSaveState();
// 检查迁移是否完成
checkMigrationComplete();
break;
}
Sleep(100);
}
}
}
}
// ==================== 空头网格策略 ==================== function firstShortBuy() { /** 首次空头卖出,带超时和重试机制 */ if (SHORT_BUYING) return;
var ticker = exchange.GetTicker();
if (!ticker) {
Log("首次空头卖出失败:无法获取行情");
return;
}
var price = safePrice(ticker.Last);
var orderValue = getOrderAmount(false); // 使用新参数
Log("准备首次空头卖出 - 当前价格:", price, "目标金额:", orderValue);
var amount = calculateOrderAmount(price, orderValue, precision.amountPrecision);
Log("首次空头卖出 - 计算出的数量:", amount, "价格:", price);
var orderId = placeOrder('sell', price, amount);
if (!orderId) {
Log("首次空头卖出失败:下单返回空,尝试重试...");
// 重试一次,使用调整后的数量
Sleep(1000);
amount = calculateOrderAmount(price, orderValue, precision.amountPrecision);
orderId = placeOrder('sell', price, amount);
if (!orderId) {
Log("首次空头卖出重试失败:下单返回空");
return;
}
}
Log("首次空头卖出 - 订单ID:", orderId, "价格:", price, "数量:", amount);
SHORT_BUYING = true;
var timeoutSec = 60; // 60秒超时
var filled = waitOrderFilled(
orderId,
timeoutSec,
function(order) {
var gridSpacing = getGridSpacing(SHORT_SIDE, false);
SHORT_NEXT_BUY_PRICE = formatPrice(
order.Price * (1 + gridSpacing / 100),
precision.pricePrecision
);
SHORT_BUYING = false;
addPosition(SHORT_ORDER_LIST, order.Price, order.Amount, SHORT_SIDE);
Log("空头首次卖出成功,价格:", order.Price, "数量:", order.Amount, "下一个卖出价格:", SHORT_NEXT_BUY_PRICE);
saveHistory({
type: 'first_short_sell',
price: order.Price,
amount: order.Amount,
value: order.Price * order.Amount
});
safeSaveState();
},
function(orderIdTimeout) {
Log("首次空头卖出超时,取消订单:", orderIdTimeout);
exchange.CancelOrder(orderIdTimeout);
SHORT_BUYING = false;
}
);
if (!filled) {
return;
}
}
function shortBuy() { /** 空头加仓(网格卖出) */ if (SHORT_BUYING) return;
var ticker = exchange.GetTicker();
if (!ticker) {
Log("空头加仓失败:无法获取行情");
return;
}
if (ticker.Last < SHORT_NEXT_BUY_PRICE) {
Log("空头加仓跳过:当前价格", ticker.Last, "低于下一个卖出价格", SHORT_NEXT_BUY_PRICE);
return;
}
var price = safePrice(ticker.Last);
var orderValue = getOrderAmount(false); // 使用新参数
Log("准备空头加仓 - 当前价格:", price, "下一个卖出价格:", SHORT_NEXT_BUY_PRICE, "目标金额:", orderValue);
var amount = calculateOrderAmount(price, orderValue, precision.amountPrecision);
Log("空头加仓 - 计算出的数量:", amount, "价格:", price);
var orderId = placeOrder('sell', price, amount);
if (!orderId) {
Log("空头加仓失败:下单返回空");
return;
}
Log("空头加仓 - 订单ID:", orderId, "价格:", price, "数量:", amount);
SHORT_BUYING = true;
var timeoutSec = 60 * 30; // 30分钟超时
var filled = waitOrderFilled(
orderId,
timeoutSec,
function(order) {
var gridSpacing = getGridSpacing(SHORT_SIDE, false);
SHORT_NEXT_BUY_PRICE = formatPrice(
order.Price * (1 + gridSpacing / 100),
precision.pricePrecision
);
SHORT_BUYING = false;
addPosition(SHORT_ORDER_LIST, order.Price, order.Amount, SHORT_SIDE);
Log("空头加仓成功,价格:", order.Price, "数量:", order.Amount, "持仓数:", SHORT_ORDER_LIST.length, "下一个卖出价格:", SHORT_NEXT_BUY_PRICE);
saveHistory({
type: 'short_sell',
price: order.Price,
amount: order.Amount,
value: order.Price * order.Amount
});
safeSaveState();
},
function(orderIdTimeout) {
exchange.CancelOrder(orderIdTimeout);
SHORT_BUYING = false;
Log("空头加仓订单超时,已取消订单:", orderIdTimeout);
}
);
if (!filled) {
return;
}
}
function shortSell() { /** 空头平仓(网格买入)- 支持双轨制 */ var ticker = exchange.GetTicker(); if (!ticker) return;
// 从最新持仓开始检查(倒序)
for (var i = SHORT_ORDER_LIST.length - 1; i >= 0; i--) {
var pos = SHORT_ORDER_LIST[i];
// 计算盈利价格(支持双轨制:旧持仓用旧参数,新持仓用新参数)
var gridSpacing = getGridSpacing(SHORT_SIDE, true); // 平仓时检查是否用旧参数
var profitPrice = formatPrice(
pos.price * (1 - gridSpacing / 100),
precision.pricePrecision
);
// 如果当前价格达到盈利价格,平仓
if (ticker.Last <= profitPrice) {
var position = exchange.GetPosition();
var shortPos = null;
if (position && position.length > 0) {
for (var j = 0; j < position.length; j++) {
if (position[j].Type === 1 && position[j].Amount > 0) {
shortPos = position[j];
break;
}
}
}
if (!shortPos || shortPos.Amount <= 0) break;
var qty = safeAmount(Math.min(pos.amount, shortPos.Amount));
// 确保数量是0.01的整数倍(ETH永续合约最小单位)
var minLotSize = MIN_LOT_SIZE;
qty = Math.floor(qty / minLotSize) * minLotSize;
qty = parseFloat(qty.toFixed(2)); // 保留2位小数
if (qty < minLotSize) {
Log("空头平仓跳过:数量太小", qty, "最小单位:", minLotSize);
break;
}
Log("准备空头平仓 - 持仓价格:", pos.price, "当前价格:", ticker.Last, "盈利价格:", profitPrice, "平仓数量:", qty, "使用参数:", shouldUseOldConfig(SHORT_SIDE) ? "旧参数" : "新参数");
// 空头平仓应使用 closebuy(买入平空)
var orderId = placeOrder('closebuy', ticker.Last, qty);
if (!orderId) continue;
while (true) {
var order = exchange.GetOrder(orderId);
if (!order) {
Sleep(100);
continue;
}
if (order.Status === 1 || order.Status === 2) {
// 计算下一个卖出价格(基于新参数的空头网格间距)
var nextGridSpacing = getGridSpacing(SHORT_SIDE, false);
SHORT_NEXT_BUY_PRICE = formatPrice(
ticker.Last * (1 + nextGridSpacing / 100),
precision.pricePrecision
);
removePosition(SHORT_ORDER_LIST, i);
Log("空头平仓成功,价格:", order.Price, "数量:", order.Amount, "剩余持仓:", SHORT_ORDER_LIST.length, "下一个卖出价格:", SHORT_NEXT_BUY_PRICE);
// 保存历史记录
var profit = (pos.price - order.Price) * order.Amount;
saveHistory({
type: 'short_buy',
price: order.Price,
amount: order.Amount,
value: order.Price * order.Amount,
entryPrice: pos.price,
profit: profit
});
// ✅ 成交后立即保存状态
safeSaveState();
// 检查迁移是否完成
checkMigrationComplete();
break;
}
Sleep(100);
}
}
}
}
// ==================== 边界检查 ==================== function checkPriceBounds() { /** 检查价格是否在网格边界内 */ var ticker = exchange.GetTicker(); if (!ticker) return true;
var currentPrice = ticker.Last;
// 如果价格超出上边界,暂停多头买入
if (currentPrice >= config.upperPrice) {
return false;
}
// 如果价格低于下边界,暂停空头卖出
if (currentPrice <= config.lowerPrice) {
return false;
}
return true;
}
// ==================== 状态信息计算 ==================== function calculateLiquidationPrice(account, positions, leverage) { /** 计算爆仓价 */ var longLiquidationPrice = null; var shortLiquidationPrice = null;
if (!account || !positions || positions.length === 0) {
return { long: longLiquidationPrice, short: shortLiquidationPrice };
}
var margin = account.Margin || 0;
var balance = account.Balance || 0;
var available = account.Available || 0;
// 计算多头和空头持仓
var longAmount = 0;
var longPrice = 0;
var shortAmount = 0;
var shortPrice = 0;
for (var i = 0; i < positions.length; i++) {
var pos = positions[i];
if (pos.Type === 0 && pos.Amount > 0) {
// 多头
longAmount += pos.Amount;
longPrice = pos.Price;
} else if (pos.Type === 1 && pos.Amount > 0) {
// 空头
shortAmount += pos.Amount;
shortPrice = pos.Price;
}
}
// 计算多头爆仓价(价格下跌导致爆仓)
if (longAmount > 0 && longPrice > 0 && leverage > 0) {
// 简化计算:爆仓价 = 持仓价 * (1 - 维持保证金率)
// 维持保证金率通常为 0.5% - 1%
var maintenanceMarginRate = 0.01; // 1%
longLiquidationPrice = longPrice * (1 - maintenanceMarginRate);
}
// 计算空头爆仓价(价格上涨导致爆仓)
if (shortAmount > 0 && shortPrice > 0 && leverage > 0) {
var maintenanceMarginRate = 0.01; // 1%
shortLiquidationPrice = shortPrice * (1 + maintenanceMarginRate);
}
return { long: longLiquidationPrice, short: shortLiquidationPrice };
}
function calculateProfit(account, initialBalance) { /** 计算收益信息 */ if (!account || !initialBalance) { return { totalProfit: 0, totalProfitRate: 0, dailyProfit: 0, avgDailyReturn: 0, estimatedMonthlyReturn: 0, estimatedYearlyReturn: 0 }; }
var currentBalance = account.Balance || 0;
var totalProfit = currentBalance - initialBalance;
var totalProfitRate = initialBalance > 0 ? (totalProfit / initialBalance) : 0;
// 计算当日收益(简化处理,使用最近一次收益记录)
var dailyProfit = 0;
if (PROFIT_HISTORY.length > 0) {
var today = new Date();
today.setHours(0, 0, 0, 0);
var todayTimestamp = Math.floor(today.getTime() / 1000);
var yesterdayProfit = 0;
for (var i = PROFIT_HISTORY.length - 1; i >= 0; i--) {
if (PROFIT_HISTORY[i].time < todayTimestamp) {
yesterdayProfit = PROFIT_HISTORY[i].profit;
break;
}
}
dailyProfit = totalProfit - yesterdayProfit;
}
// 计算平均日化收益率
var avgDailyReturn = 0;
var estimatedMonthlyReturn = 0;
var estimatedYearlyReturn = 0;
if (INIT_TIME && INIT_TIME > 0) {
var days = (Unix() - INIT_TIME) / (24 * 3600);
if (days > 0) {
avgDailyReturn = totalProfitRate / days;
estimatedMonthlyReturn = avgDailyReturn * 30;
estimatedYearlyReturn = avgDailyReturn * 365;
}
}
return {
totalProfit: totalProfit,
totalProfitRate: totalProfitRate,
dailyProfit: dailyProfit,
avgDailyReturn: avgDailyReturn,
estimatedMonthlyReturn: estimatedMonthlyReturn,
estimatedYearlyReturn: estimatedYearlyReturn
};
}
function getPositionInfo() { /** 获取持仓详细信息 */ var positions = []; var ticker = exchange.GetTicker(); var currentPrice = ticker ? ticker.Last : 0;
// 获取实际持仓
var exchangePositions = exchange.GetPosition();
if (!exchangePositions || exchangePositions.length === 0) {
return positions;
}
for (var i = 0; i < exchangePositions.length; i++) {
var pos = exchangePositions[i];
if (pos.Amount <= 0) continue;
var direction = pos.Type === 0 ? 'long' : 'short';
var positionValue = pos.Amount * pos.Price;
var unrealizedPnl = pos.Profit || 0;
// 计算预估回归收益(基于网格间距)
var gridSpacing = direction === 'long' ? config.longGridSpacing : config.shortGridSpacing;
var estimatedReturn = positionValue * (gridSpacing / 100);
var estimatedTotalReturn = estimatedReturn * (direction === 'long' ? MANY_ORDER_LIST.length : SHORT_ORDER_LIST.length);
// 网格初始价格和转向间距
var gridInitialPrice = currentPrice;
var turnSpacing = gridSpacing;
// 挂单价格
var buyOrderPrice = direction === 'long' ? MANY_NEXT_BUY_PRICE : 0;
var sellOrderPrice = direction === 'short' ? SHORT_NEXT_BUY_PRICE : 0;
// 计算成交额(简化处理)
var tradeVolume = positionValue;
positions.push({
symbol: config.symbol,
direction: direction,
quantity: pos.Amount,
positionPrice: pos.Price,
positionValue: positionValue,
estimatedReturn: estimatedReturn,
estimatedTotalReturn: estimatedTotalReturn,
gridInitialPrice: gridInitialPrice,
turnSpacing: turnSpacing,
currentPrice: currentPrice,
buyOrderPrice: buyOrderPrice,
sellOrderPrice: sellOrderPrice,
unrealizedPnl: unrealizedPnl,
tradeVolume: tradeVolume
});
}
return positions;
}
function updateStatusInfo() { /** 更新并输出状态信息(FMZ原生表格格式,框架式布局) */ try { var account = exchange.GetAccount(); var positions = exchange.GetPosition(); var ticker = exchange.GetTicker();
if (!account) return;
// 计算爆仓价
var leverage = 1;
var liquidationPrices = calculateLiquidationPrice(account, positions, leverage);
// 计算收益
var profitInfo = calculateProfit(account, INITIAL_BALANCE);
// 计算循环延时
var cycleDelay = 0;
if (CYCLE_START_TIME > 0) {
cycleDelay = (Unix() - CYCLE_START_TIME) * 1000;
}
CYCLE_START_TIME = Unix();
// 获取持仓信息
var positionInfo = getPositionInfo();
// 格式化时间
var initTimeStr = '-';
if (INIT_TIME) {
var initDate = new Date(INIT_TIME * 1000);
var pad = function(n) { return n < 10 ? '0' + n : n; };
initTimeStr = initDate.getFullYear() + '-' +
pad(initDate.getMonth() + 1) + '-' +
pad(initDate.getDate()) + ' ' +
pad(initDate.getHours()) + ':' +
pad(initDate.getMinutes()) + ':' +
pad(initDate.getSeconds());
}
// 计算运行时长
var runTime = '-';
if (INIT_TIME) {
var seconds = Unix() - INIT_TIME;
var days = Math.floor(seconds / 86400);
var hours = Math.floor((seconds % 86400) / 3600);
var minutes = Math.floor((seconds % 3600) / 60);
runTime = days + '天' + hours + '小时' + minutes + '分钟';
}
// 构造纯文本表格(按行输出),在移动端也能清晰显示
var lines = [];
var addTextSection = function(title, rows) {
lines.push(title);