//歡迎加入MAX交易所 https://max.maicoin.com/signup?r=6d8216c0
const MAX = require('max-exchange-api-node');
const sqlite3 = require('sqlite3').verbose();
const path = require('path');
const max = new MAX({
accessKey: "",
secretKey: "",
});
const rest = max.rest();
const market = 'dogetwd';
const MIN_VOLUME = 47;
// 建立 SQLite DB
const db = new sqlite3.Database('aaa.db', sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE, err => {
if (err) return console.error(err);
// 切換到 Write-Ahead Logging,減少寫鎖衝突
db.run("PRAGMA journal_mode = WAL;");
// 當遇到 busy(忙碌)狀態時,最多等待 5 秒再失敗
db.configure("busyTimeout", 5000);
// 把後續所有操作都排成序列執行,避免平行化寫入
db.serialize();
});
db.run(`CREATE TABLE IF NOT EXISTS trade_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
side TEXT,
volume REAL,
price REAL,
amount_twd REAL,
balance_before REAL,
balance_after REAL,
profit REAL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
)`);
startLoop();
async function startLoop() {
let lastPrice = null;
while (true) {
try {
const shouldBuy = await detectBuySignal();
const shouldSell = await detectSellSignal();
const tick = await rest.ticker({ market });
const dogeBal = parseFloat((await rest.account('doge')).balance);
const twdBal = parseFloat((await rest.account('twd')).balance);
const price = shouldBuy ? parseFloat(tick.sell) : parseFloat(tick.buy);
const totalTwd = price * MIN_VOLUME;
const before = twdBal + dogeBal * price;
const avgCost = await getAverageCost();
// ✅ 買入條件 A:有訊號、資金足夠、低於平均成本
const buyConditionA = shouldBuy && twdBal > totalTwd && (!avgCost || price <= avgCost);
// ✅ 買入條件 B:與上次價格相比下跌超過
const buyConditionB = lastPrice && price < lastPrice * 0.999 && twdBal > totalTwd;
// ✅ 買入條件 C:單邊上漲超過
const buyConditionC = lastPrice && price > lastPrice * 1.004 && twdBal > totalTwd;
if (buyConditionA || buyConditionB || buyConditionC) {
let reason = '';
if (buyConditionA) {
reason = `[🟢 BUY-A] 有訊號且價格 ${price.toFixed(4)} < 成本 ${avgCost?.toFixed(4) ?? 'N/A'}`;
} else if (buyConditionB) {
reason = `[📉 BUY-B] 價格從 ${lastPrice.toFixed(4)} 下跌超過 1% ➜ ${price.toFixed(4)}`;
} else if (buyConditionC) {
reason = `[📈 BUY-C] 價格從 ${lastPrice.toFixed(4)} 上漲超過 0.5% ➜ ${price.toFixed(4)}`;
}
console.log(`${reason},嘗試買入 ${MIN_VOLUME} DOGE`);
await rest.placeOrder({
market,
volume: MIN_VOLUME.toString(),
side: 'buy',
ordType: 'market',
});
await recordTrade('buy', MIN_VOLUME, price, before);
} else if (shouldSell && dogeBal >= MIN_VOLUME) {
// 🔴 賣出條件
if (avgCost && price >= avgCost + 0.1) {
console.log(`[🔴 SELL] 當前價格 ${price} > 成本 ${avgCost.toFixed(4)},賣出 ${MIN_VOLUME} DOGE`);
await rest.placeOrder({
market,
volume: MIN_VOLUME.toString(),
side: 'sell',
ordType: 'market',
});
await recordTrade('sell', MIN_VOLUME, price, before);
} else {
console.log(`[🟡 HOLD] 當前價格 ${price} <= 成本 ${avgCost?.toFixed(4) ?? 'N/A'},不賣出`);
}
} else {
console.log('[⏸️ HOLD] 無明確訊號、價格不合條件,或資產不足');
}
lastPrice = price;
} catch (err) {
console.error('❌ 錯誤:', err.message || err);
}
showTodayStats();
await sleep(5000);
}
}
async function recordTrade(side, volume, price, balanceBefore) {
await sleep(2000); // 等待資產更新
const dogeBal = parseFloat((await rest.account('doge')).balance);
const twdBal = parseFloat((await rest.account('twd')).balance);
const after = twdBal + dogeBal * price;
const profit = after - balanceBefore;
db.run(`INSERT INTO trade_log (side, volume, price, amount_twd, balance_before, balance_after, profit)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[side, volume, price, price * volume, balanceBefore, after, profit],
err => {
if (err) {
console.error("❌ 無法寫入交易記錄", err.message);
} else {
console.log(`✅ 已記錄 ${side} 成交: ${volume} DOGE @ ${price}, 盈虧: ${profit.toFixed(2)} TWD`);
showTodayStats();
}
});
}
function getAverageCost() {
return new Promise((resolve, reject) => {
db.all(`SELECT volume, amount_twd FROM trade_log WHERE side = 'buy'`, (err, rows) => {
if (err) {
console.error("❌ 無法計算平均成本", err.message);
return reject(err);
}
const totalVolume = rows.reduce((sum, r) => sum + r.volume, 0);
const totalAmount = rows.reduce((sum, r) => sum + r.amount_twd, 0);
const avgCost = totalVolume > 0 ? totalAmount / totalVolume : null;
resolve(avgCost);
});
});
}
function showTodayStats() {
db.all(
`SELECT side, volume, amount_twd, profit FROM trade_log`,
[],
(err, rows) => {
if (err) {
return console.error("❌ 統計失敗", err.message);
}
const totalTrades = rows.length;
const profitTrades = rows.filter(r => r.profit > 0).length;
const lossTrades = rows.filter(r => r.profit <= 0).length;
const totalProfit = rows.reduce((sum, r) => sum + r.profit, 0);
const winRate = totalTrades > 0
? (profitTrades / totalTrades * 100).toFixed(2) + '%'
: '0.00%';
// 計算所有買入的平均成本
const buyTrades = rows.filter(r => r.side === 'buy');
const totalBuyVolume = buyTrades.reduce((sum, r) => sum + r.volume, 0);
const totalBuyAmount = buyTrades.reduce((sum, r) => sum + r.amount_twd, 0);
const avgCost = totalBuyVolume > 0
? (totalBuyAmount / totalBuyVolume).toFixed(4)
: 'N/A';
}
);
}
async function detectBuySignal() {
const candles = await rest.k({ market, period: 1, limit: 30 });
const closes = candles.map(c => parseFloat(c[4])); // close
const rsi = calcRSI(closes, 14);
const { lower } = calcBollinger(closes, 20);
const last = closes.at(-1);
return rsi < 25 && last < lower;
}
async function detectSellSignal() {
const candles = await rest.k({ market, period: 1, limit: 30 });
const closes = candles.map(c => parseFloat(c[4])); // close
const rsi = calcRSI(closes, 14);
const { upper } = calcBollinger(closes, 20);
const last = closes.at(-1);
return rsi > 75 && last > upper;
}
function calcRSI(closes, period) {
if (closes.length < period + 1) return 50; // return neutral if not enough data
let gainSum = 0, lossSum = 0;
for (let i = closes.length - period - 1; i < closes.length - 1; i++) {
const change = closes[i + 1] - closes[i];
if (change > 0) gainSum += change;
else lossSum -= change;
}
const avgGain = gainSum / period;
const avgLoss = lossSum / period || 1;
const rs = avgGain / avgLoss;
const rsi = 100 - (100 / (1 + rs));
// 自動調整閾值範圍:根據波動性偏移 RSI 評分
const recentVolatility = Math.max(...closes.slice(-5)) - Math.min(...closes.slice(-5));
const baselineVolatility = Math.max(...closes.slice(-period)) - Math.min(...closes.slice(-period));
const volatilityFactor = recentVolatility / (baselineVolatility || 1);
// 假如波動大,代表趨勢強,允許 RSI 更極端
if (volatilityFactor > 1.3 && rsi < 50) {
return rsi - 5;
} else if (volatilityFactor > 1.3 && rsi > 50) {
return rsi + 5;
}
return rsi;
}
function calcBollinger(closes, period) {
const slice = closes.slice(-period);
const avg = slice.reduce((a, b) => a + b, 0) / slice.length;
const std = Math.sqrt(slice.reduce((sum, val) => sum + (val - avg) ** 2, 0) / period);
return { lower: avg - 2 * std, upper: avg + 2 * std };
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
請先 登入 以發表留言。