9
Follow
17
Followers
Tôi đang gặp khó khăn với độ chính xác và việc thực thi các lệnh đối với hợp đồng lưới vĩnh cửu không giới hạn (vị thế mua và bán). Tôi đã làm việc với vấn đề này nửa tháng nay và hoàn toàn bế tắc. Tô...
Created 2025-12-08 11:00:34 Updated 2025-12-08 11:27:50
11
733
javascript
/*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);
lines.push('字段 | 值');
lines.push('---- | ----');
for (var i = 0; i < rows.length; i++) {
lines.push(rows[i][0] + ' | ' + rows[i][1]);
}
lines.push(''); // 空行分隔
};
addTextSection('📊 基础', [
['初始化时间', initTimeStr],
['运行时长', runTime],
['当前价格', ticker ? ticker.Last.toFixed(2) : '-'],
['做多爆仓价', liquidationPrices.long ? liquidationPrices.long.toFixed(2) : '-'],
['做空爆仓价', liquidationPrices.short ? liquidationPrices.short.toFixed(2) : '-'],
['参数迁移', isMigrating ? '迁移中' : '正常'],
isMigrating && oldConfig ? ['旧多头间距', oldConfig.longGridSpacing + '%'] : null,
isMigrating && oldConfig ? ['旧空头间距', oldConfig.shortGridSpacing + '%'] : null
].filter(Boolean));
addTextSection('💰 账户', [
['初始余额', INITIAL_BALANCE.toFixed(2)],
['钱包余额', (account.Balance || 0).toFixed(2)],
['保证金余额', (account.Margin || 0).toFixed(2)],
['可用保证金', (account.Available || 0).toFixed(2)],
['已用杠杆', leverage + 'x']
]);
addTextSection('📈 收益', [
['总收益', profitInfo.totalProfit.toFixed(2)],
['总收益率', (profitInfo.totalProfitRate * 100).toFixed(2) + '%'],
['当日收益', profitInfo.dailyProfit.toFixed(2)],
['平均日化', (profitInfo.avgDailyReturn * 100).toFixed(2) + '%'],
['预估月化', (profitInfo.estimatedMonthlyReturn * 100).toFixed(2) + '%'],
['预估年化', (profitInfo.estimatedYearlyReturn * 100).toFixed(2) + '%']
]);
addTextSection('🔲 网格', [
['多头持仓数', MANY_ORDER_LIST.length],
['下一个买入价', MANY_NEXT_BUY_PRICE > 0 ? MANY_NEXT_BUY_PRICE.toFixed(2) : '-'],
['多头间距', config.longGridSpacing + '%'],
['空头持仓数', SHORT_ORDER_LIST.length],
['下一个卖出价', SHORT_NEXT_BUY_PRICE > 0 ? SHORT_NEXT_BUY_PRICE.toFixed(2) : '-'],
['空头间距', config.shortGridSpacing + '%'],
['单网格金额', config.orderAmount.toFixed(2)]
]);
var posRows = [];
if (positionInfo.length > 0) {
for (var i = 0; i < positionInfo.length; i++) {
var pos = positionInfo[i];
posRows.push(['持仓' + (i + 1) + ' (' + (pos.direction === 'long' ? '多' : '空') + ')', '']);
posRows.push(['数量', pos.quantity.toFixed(2)]);
posRows.push(['持仓价', pos.positionPrice.toFixed(2)]);
posRows.push(['当前价', pos.currentPrice.toFixed(2)]);
posRows.push(['持仓价值', pos.positionValue.toFixed(2)]);
posRows.push(['未实现盈亏', pos.unrealizedPnl.toFixed(2)]);
if (i < positionInfo.length - 1) {
posRows.push(['---', '---']);
}
}
} else {
posRows.push(['暂无持仓', '']);
}
addTextSection('📦 持仓', posRows);
addTextSection('⚙️ 系统', [
['循环延时', cycleDelay.toFixed(0) + 'ms'],
['状态保存', LAST_SAVE_TIME > 0 ? new Date(LAST_SAVE_TIME * 1000).toLocaleTimeString() : '-'],
['收益记录数', PROFIT_HISTORY.length]
]);
LogStatus(lines.join('\n'));
// 记录收益曲线(使用LogProfit,FMZ平台会自动显示收益曲线)
var currentProfit = profitInfo.totalProfit;
LogProfit(currentProfit);
// 保存收益历史
PROFIT_HISTORY.push({
time: Unix(),
profit: currentProfit
});
// 只保留最近1000条记录
if (PROFIT_HISTORY.length > 1000) {
PROFIT_HISTORY.shift();
}
// 定期保存状态(每5分钟)
if (Unix() - LAST_SAVE_TIME >= SAVE_INTERVAL) {
safeSaveState();
}
} catch (e) {
Log("更新状态信息失败:", e);
}
}
// ==================== 主循环 ====================
function onTick() {
var ticker = exchange.GetTicker();
if (!ticker) return;
// 定期持仓对账,防止本地列表与实际仓位不一致(尤其防止空头被抵消后仍显示)
if (Unix() - LAST_RECONCILE_TIME >= RECONCILE_INTERVAL) {
reconcilePositions();
LAST_RECONCILE_TIME = Unix();
}
// 检查价格边界
if (!checkPriceBounds()) {
Log("价格超出网格边界,暂停交易");
return;
}
// 检查参数迁移是否完成
checkMigrationComplete();
// 如果多头持仓为空,首次买入
if (MANY_ORDER_LIST.length === 0) {
firstManyBuy();
} else {
// 多头加仓和平仓
manyBuy();
manySell();
}
// 如果空头持仓为空,首次卖出
if (SHORT_ORDER_LIST.length === 0) {
firstShortBuy();
} else {
// 空头加仓和平仓
shortBuy();
shortSell();
}
// 更新状态信息
updateStatusInfo();
}
// ==================== 主函数 ====================
function main() {
// 设置交易所类型(OKX)
exchange.SetContractType("swap"); // 或 "spot" 根据需求
// 尝试设置双向持仓(对冲)模式,若不支持则忽略,避免 TypeError
if (typeof exchange.SetPositionMode === 'function') {
try {
exchange.SetPositionMode(1);
Log("已尝试设置对冲模式 (SetPositionMode=1)");
} catch (e) {
Log("设置对冲模式失败,继续运行(可能交易所/托管者不支持):", e);
}
} else {
Log("SetPositionMode 不可用,跳过对冲模式设置");
}
// ✅ 从FMZ界面获取配置参数(禁止硬编码)
// 注意:FMZ平台中,界面设置的参数名称会自动成为JavaScript全局变量
config = getConfigParams();
// 尝试获取交易规则(tickSz/lotSz),以便自动设置步长与精度
loadInstrumentSpec();
// 应用用户手动覆盖(如有)
applyForceLot();
// 最终将精度写回 precision 供后续使用
precision = getPrecision();
// 验证配置
if (!config.symbol) {
throw new Error("请设置币种选择(界面参数名:symbol)");
}
if (!config.upperPrice || !config.lowerPrice) {
throw new Error("请设置网格上边界价格和下边界价格(界面参数名:upperPrice, lowerPrice)");
}
if (config.upperPrice <= config.lowerPrice) {
throw new Error("上边界价格必须大于下边界价格");
}
if (!config.orderAmount || config.orderAmount <= 0) {
throw new Error("单个网格下单金额必须大于0(界面参数名:orderAmount)");
}
if (!config.longGridSpacing || config.longGridSpacing <= 0) {
throw new Error("多头网格间距必须大于0(界面参数名:longGridSpacing)");
}
if (!config.shortGridSpacing || config.shortGridSpacing <= 0) {
throw new Error("空头网格间距必须大于0(界面参数名:shortGridSpacing)");
}
// 尝试恢复状态
Log("========== 策略启动 ==========");
var stateRestored = loadState();
if (stateRestored) {
Log("✅ 状态恢复成功");
} else {
Log("ℹ️ 首次运行,使用初始状态");
}
// 检查配置变化(启动参数迁移检测)
checkConfigChange(config);
// 获取精度
precision = getPrecision();
Log("========== 策略配置 ==========");
Log("币种选择:", config.symbol);
Log("多头网格间距:", config.longGridSpacing, "%");
Log("空头网格间距:", config.shortGridSpacing, "%");
Log("单个网格下单金额:", config.orderAmount);
Log("网格上边界价格:", config.upperPrice);
Log("网格下边界价格:", config.lowerPrice);
Log("价格精度:", precision.pricePrecision);
Log("数量精度:", precision.amountPrecision);
Log("合约最小单位: 0.01 (ETH永续合约)");
Log("============================");
// 初始化价格(如果状态未恢复)
if (!stateRestored) {
var ticker = exchange.GetTicker();
if (!ticker || !ticker.Last || ticker.Last <= 0) {
throw new Error("无法获取初始价格");
}
var initPrice = ticker.Last;
// 初始化多头和空头的下一个买入/卖出价格
MANY_NEXT_BUY_PRICE = formatPrice(
initPrice * (1 - config.longGridSpacing / 100),
precision.pricePrecision
);
SHORT_NEXT_BUY_PRICE = formatPrice(
initPrice * (1 + config.shortGridSpacing / 100),
precision.pricePrecision
);
Log("初始价格:", initPrice);
Log("多头下一个买入价格:", MANY_NEXT_BUY_PRICE);
Log("空头下一个卖出价格:", SHORT_NEXT_BUY_PRICE);
// 初始化状态信息
INIT_TIME = Unix();
var account = exchange.GetAccount();
if (account) {
INITIAL_BALANCE = account.Balance || 0;
Log("初始余额:", INITIAL_BALANCE);
}
// 初始化收益记录
LogProfit(0);
} else {
Log("使用恢复的状态继续运行");
}
CYCLE_START_TIME = Unix();
LAST_SAVE_TIME = Unix();
// 保存初始状态
safeSaveState();
Log("========== 策略运行中 ==========");
Log("状态将每5分钟自动保存,支持关机/重启后恢复");
// 主循环
while (true) {
try {
onTick();
} catch (e) {
if (isEOFError(e)) {
Log("检测到 EOF,可能是回测数据结束或连接被关闭,保存状态后退出主循环");
safeSaveState();
break;
}
Log("策略执行错误:", e);
}
Sleep(3000); // 每3秒执行一次
}
}
Related Recommendations
Inventor Quant Workflow FAQ (Continuously Updated)Financial Magic Zone Global KOL RecruitmentFAQ Summary (Updating...)PINE Language Introductory Tutorial of FMZ QuantPrimary Tutorial of Strategy Writing with FMZ Quant Trading Platform (Must Read)Getting Started with FMZ Quant Trading Platform (Must Read)
Comment
All comments (11)
javascript
function main(){
const marketinfo = exchange.GetMarkets()
const symbolinfo = marketinfo['BTC_USDT.swap']
Log(symbolinfo) //返回币种的各种信息 AmountPrecision 为3
let quantity = 5.776123
quantity = _N(quantity, symbolinfo.AmountPrecision)
Log(quantity) //返回数量精度处理后的 5.776
}
7 months ago
javascript
let statusDisplay = '`' + JSON.stringify(accountTable) + '`\n\n';
statusDisplay += '`' + JSON.stringify(positionTable) + '`\n\n';
statusDisplay += '`' + JSON.stringify(analysisTable) + '`\n\n';
statusDisplay += '`' + JSON.stringify(executionTable) + '`';
// 添加更新时间提示
if (lastUpdateTime) {
const timeSinceUpdate = Math.floor((Date.now() - lastUpdateTime) / 1000);
statusDisplay += `\n\n⏱️ 上次决策: ${timeSinceUpdate}秒前`;
}
// 显示状态
LogStatus(statusDisplay);
参考一下
6 months ago
精度计算中的变量作用域问题
javascript
function calculateOrderAmount(price, orderValue, amountPrecision) {
// ...
Log("计算订单数量 - 对齐到步长: " + amount.toFixed(amtPrecision));
// ↑ amtPrecision 在这行之后才定义
var amtPrecision = Math.max(amountPrecision || ...);
}
amtPrecision 在使用前未定义,JavaScript会变量提升但值为 undefined。
7 months ago
- 1

