//歡迎加入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));
}
創作者介紹
創作者 田烽志的隨意手札 的頭像
烽志

田烽志的隨意手札

烽志 發表在 痞客邦 留言(0) 人氣( 5 )