Skip to content

Instantly share code, notes, and snippets.

@laxra1966
Created June 20, 2025 12:22
Show Gist options
  • Save laxra1966/b3f7f945e90028bd5d3f6d93038fdca5 to your computer and use it in GitHub Desktop.
Save laxra1966/b3f7f945e90028bd5d3f6d93038fdca5 to your computer and use it in GitHub Desktop.
Arb
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - Arbitrage Scanner</title>
<script src="https://accounts.google.com/gsi/client" async defer></script>
<style>
/* --- Base Styles --- */
body { background-color: #0d1117; color: #c9d1d9; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; padding: 20px; margin: 0; }
/* --- View Control Styles --- */
#mainAppView { display: none; }
#loginView { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 80vh; text-align: center; padding: 20px; }
#loginView h2 { color: #58a6ff; margin-bottom: 30px; }
#loginView .g_id_signin { margin-bottom: 15px; }
#loginView .login-text { font-size: 1em; color: #8b949e; margin-top: 10px; margin-bottom: 20px; }
#g_id_onload { display: none; } /* Hide config div */
#loginView .skip-btn { background-color: #30363d; color: #c9d1d9; margin-top: 15px; padding: 8px 16px; font-size: 0.9em; border: 1px solid #484f58; }
#loginView .skip-btn:hover { background-color: #484f58; }
body.state-logged-in #mainAppView { display: block; }
body.state-logged-in #loginView { display: none; }
/* --- End View Control Styles --- */
/* --- Header Styles --- */
.main-header { display: flex; justify-content: space-between; align-items: center; padding: 10px 0px; margin-bottom: 20px; border-bottom: 1px solid #30363d; flex-wrap: wrap; }
.main-header h1 { margin: 0; font-size: 1.6em; color: #58a6ff; text-align: left; flex-grow: 1; margin-right: 20px; margin-bottom: 5px; }
.header-actions { display: flex; align-items: center; gap: 10px; flex-shrink: 0; margin-bottom: 5px; }
.header-actions button { font-size: 0.9em; padding: 6px 12px;}
.user-auth-section { display: flex; align-items: center; flex-shrink: 0; margin-left: auto; }
.user-profile { display: flex; align-items: center; background-color: #161b22; padding: 6px 12px; border-radius: 20px; box-shadow: 0 2px 5px rgba(0,0,0,0.2); }
.user-profile img { width: 30px; height: 30px; border-radius: 50%; margin-right: 8px; }
.user-profile span { margin-right: 10px; font-size: 0.9em; max-width: 150px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#logoutBtn { padding: 4px 8px; font-size: 0.8em; }
/* --- End Header Styles --- */
.profit { color: #3fb950; font-weight: bold; } .loss { color: #f85149; } .error-value { color: orange; font-weight: bold; }
.api-settings { background-color: #161b22; padding: 15px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 2px 10px rgba(0,0,0,0.3); }
.api-settings h3 { margin-top: 0; margin-bottom: 15px; color: #8b949e; font-size: 1.1em; border-bottom: 1px solid #30363d; padding-bottom: 8px; }
.api-section { margin-bottom: 20px; padding-bottom: 15px; border-bottom: 1px dashed #30363d; }
.api-section:last-child { margin-bottom: 0; padding-bottom: 0; border-bottom: none; }
label { display: inline-flex; align-items: center; margin: 0; cursor: pointer; }
input[type="number"], input[type="text"], input[type="password"] { padding: 6px 10px; border-radius: 5px; border: 1px solid #30363d; background-color: #0d1117; color: #c9d1d9; margin-left: 5px; }
input[type="number"] { width: 80px; } input[type="text"], input[type="password"] { width: 150px; }
input[type="checkbox"] { margin-right: 5px; accent-color: #58a6ff; }
button { padding: 7px 15px; background-color: #238636; border: none; border-radius: 5px; color: white; cursor: pointer; font-weight: 600; transition: background-color 0.2s ease; }
button:hover:not(:disabled) { background-color: #2ea043; } button:disabled { background-color: #30363d; cursor: not-allowed; opacity: 0.6; }
button.trade-btn { background-color: #58a6ff; } button.trade-btn:hover:not(:disabled) { background-color: #79b8ff; }
button.danger { background-color: #da3633; } button.danger:hover:not(:disabled) { background-color: #f85149; }
button.connect-btn { background-color: #1f6feb; } button.connect-btn:hover:not(:disabled) { background-color: #388bfd; }
.api-form { display: flex; flex-direction: column; gap: 20px; margin-top: 10px; } .form-group { display: flex; flex-direction: column; gap: 5px; }
.form-group label { font-weight: 500; align-self: flex-start; } .form-group input[type="text"], .form-group input[type="password"] { width: 100%; max-width: 400px; box-sizing: border-box; margin-left: 0; }
.toggle-api-btn { background-color: #30363d; color: #c9d1d9; margin-bottom: 15px; } .toggle-api-btn:hover:not(:disabled) { background-color: #484f58; }
.status-indicator { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 8px; vertical-align: middle; } .status-connected { background-color: #3fb950; } .status-disconnected { background-color: #f85149; }
.api-status { display: flex; align-items: center; font-size: 0.9em; margin-bottom: 10px; } .hidden { display: none !important; }
.trade-log { background-color: #161b22; padding: 15px; border-radius: 8px; margin-top: 20px; max-height: 200px; overflow-y: auto; box-shadow: 0 2px 10px rgba(0,0,0,0.3); border: 1px solid #30363d; }
.trade-log-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;}
.trade-log-header h3 { margin-top: 0; margin-bottom: 0; color: #8b949e; font-size: 1em; }
.trade-log-header button {font-size: 0.85em; padding: 4px 8px;}
.log-entry { padding: 3px 0; border-bottom: 1px solid #30363d; font-family: 'Courier New', monospace; font-size: 0.85em; line-height: 1.4; } .log-entry:last-child { border-bottom: none; }
.log-time { color: #8b949e; margin-right: 8px; } .log-success { color: #3fb950; } .log-error { color: #f85149; } .log-info { color: #58a6ff; }
.modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(13, 17, 23, 0.8); align-items: center; justify-content: center;}
.modal-content { background-color: #161b22; margin: auto; padding: 25px; border-radius: 8px; box-shadow: 0 4px 20px rgba(0,0,0,0.5); width: 90%; max-width: 650px; border: 1px solid #30363d; max-height: 85vh; display: flex; flex-direction: column; }
.modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; border-bottom: 1px solid #30363d; padding-bottom: 10px; flex-shrink: 0; }
.modal-title { font-size: 1.3em; font-weight: bold; color: #58a6ff; } .close-modal { color: #8b949e; font-size: 1.8em; font-weight: bold; cursor: pointer; line-height: 1; } .close-modal:hover { color: #c9d1d9; }
.modal-body { margin-bottom: 25px; line-height: 1.6; overflow-y: auto; flex-grow: 1; }
.modal-body p { margin-bottom: 10px; } .modal-body ol { margin-left: 20px; padding-left: 10px; } .modal-body li { margin-bottom: 5px; font-family: 'Courier New', Courier, monospace; }
.modal-actions { display: flex; justify-content: flex-end; gap: 10px; padding-top: 15px; border-top: 1px solid #30363d; flex-shrink: 0;}
.scanner-status { font-size: 0.85em; color: #8b949e; margin-left: 8px; font-style: italic; }
.settings-modal-grid { display: grid; grid-template-columns: 1fr; gap: 20px; }
.settings-section { background-color: #0d1117; padding: 15px; border-radius: 6px; border: 1px solid #30363d;}
.settings-section h4 { margin-top: 0; margin-bottom: 15px; color: #8b949e; font-size: 1.1em; border-bottom: 1px solid #30363d; padding-bottom: 8px;}
.settings-control-group { display: flex; flex-direction: column; gap: 12px; margin-bottom: 15px; }
.settings-control-group > div { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 5px;}
.settings-control-group label { flex-basis: auto; text-align: left; margin-right: 10px; }
.settings-control-group input[type="number"] { width: 80px; margin-left: 0; }
.settings-control-group input[type="checkbox"] { margin-left: 0; }
.exchange-selection-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; }
.exchange-selection-grid label { background-color: #161b22; padding: 8px; border-radius: 4px; border: 1px solid #30363d; transition: background-color 0.2s; display: flex; align-items: center; }
.exchange-selection-grid label:hover { background-color: #21262d; }
.exchange-selection-grid input[type="checkbox"] { margin-right: 8px; }
.arb-container { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 1.5rem; margin: 1.5rem 0; }
.arb-card { background: #161b22; border: 1px solid #30363d; border-radius: 12px; padding: 1rem; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); transition: transform 0.2s ease, box-shadow 0.2s ease; display: flex; flex-direction: column; }
.arb-card:hover { transform: translateY(-3px); box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15); }
.arb-card.highlight { border-left: 3px solid #58a6ff; background-color: rgba(88, 166, 255, 0.07); box-shadow: 0 0 15px rgba(88, 166, 255, 0.2); }
.card-header { display: flex; justify-content: center; align-items: center; margin-bottom: 0.75rem; padding-bottom: 0.5rem; border-bottom: 1px solid #30363d; text-align: center; }
.card-path { font-family: 'Courier New', Courier, monospace; font-size: 1em; color: #c9d1d9; font-weight: 500; }
.exchange-logo-container { display: flex; justify-content: center; align-items: center; margin-bottom: 1rem; padding: 8px 0; }
.exchange-logo-container img { height: 32px; width: auto; max-width: 120px; object-fit: contain; }
.steps-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.75rem; margin-bottom: 1rem; }
.step-card { background: #21262d; padding: 0.75rem; border-radius: 8px; border: 1px solid #30363d; font-size: 0.85em; line-height: 1.4; display: flex; flex-direction: column; }
.step-card div:first-child { color: #8b949e; font-size: 0.9em; font-weight: 600; margin-bottom: 0.4rem; }
.step-card-pair-details { /* Applied to the div holding action + description */
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
word-break: break-word;
font-size: 0.98em;
line-height: 1.5;
color: #c9d1d9;
}
.step-action-buy {
color: #3fb950; /* Green for BUY */
font-weight: 600;
margin-right: 4px; /* Space between BUY/SELL and description */
}
.step-action-sell {
color: #f85149; /* Red for SELL */
font-weight: 600;
margin-right: 4px; /* Space between BUY/SELL and description */
}
.profit-section { display: flex; justify-content: space-between; align-items: center; padding-top: 1rem; border-top: 1px solid #30363d; margin-top: auto; }
.profit-details { font-size: 0.9em; line-height: 1.5; }
.profit-details .start-amount { color: #8b949e; }
.profit-value { font-size: 1.1em; font-weight: 600; display: block; margin-top: 4px; }
.profit-positive { color: #3fb950; } .profit-negative { color: #f85149; }
.loading-card { background: #161b22; border: 1px solid #30363d; border-radius: 12px; padding: 2rem; text-align: center; color: #8b949e; animation: pulse 1.5s infinite; margin: 2rem auto; width: 100%; max-width: 400px; grid-column: 1 / -1; }
.loading-card.error-value { color: orange; font-weight: bold; animation: none; }
@keyframes pulse { 0% { opacity: 0.6; } 50% { opacity: 1; } 100% { opacity: 0.6; } }
@media (max-width: 1023px) { .steps-grid { grid-template-columns: 1fr; } }
@media (max-width: 768px) { body { padding: 15px; } .main-header { flex-direction: column; align-items: flex-start; gap: 10px;} .main-header h1 { margin-bottom: 0; font-size: 1.4em; } .header-actions { width: 100%; justify-content: flex-start; } .user-auth-section { margin-left: 0; margin-top:10px; width:100%; justify-content: flex-end;} .user-profile span { max-width: 100px; } .arb-card { padding: 0.75rem; } .exchange-logo-container img { height: 28px; } .step-card { padding: 0.5rem; } .profit-section { flex-direction: column; align-items: flex-start; gap: 8px; } .profit-section .trade-btn { width: 100%; text-align: center; } .modal-content { width: 95%; margin: 5% auto;} .settings-control-group > div { flex-direction: column; align-items: flex-start; gap: 5px;} .settings-control-group input[type="number"] { width: 100px; margin-left:0;} }
@media (max-width: 480px) { body { padding: 10px; } .main-header h1 { font-size: 1.2em; } .header-actions button {padding: 5px 8px; font-size: 0.8em;} .exchange-logo-container img { height: 24px; } .card-path { font-size: 0.9em; } .step-card-pair-details { font-size: 0.9em; } .modal-title {font-size: 1.1em;}}
</style>
</head>
<body>
<!-- HTML structure identical to the previous 'full code' response with settings modal, etc. -->
<div id="loginView">
<h2>Arbitrage Scanner &amp; Trader</h2>
<div id="g_id_onload" data-client_id="485859398037-pcstgi0p5j5jqm3v5vm4fj0162to0qle.apps.googleusercontent.com" data-context="signin" data-ux_mode="popup" data-callback="handleCredentialResponse" data-auto_prompt="false"></div>
<div class="g_id_signin" data-type="standard" data-shape="rectangular" data-theme="outline" data-text="signin_with" data-size="large" data-logo_alignment="left"></div>
<p class="login-text">Please log in to save settings and API keys</p><button id="skipLoginBtn" class="skip-btn">Continue without Login</button>
</div>
<div id="mainAppView">
<header class="main-header">
<h1>Triangular Arbitrage Scanner &amp; Trader</h1>
<div class="header-actions">
<button id="openSettingsModalBtn">⚙️ Settings</button> <button id="pausePlayBtn">Pause</button> <span id="scannerStatusText" class="scanner-status">(Status: Stopped)</span>
</div>
<div class="user-auth-section">
<div id="userProfileContainer" class="hidden">
<div class="user-profile">
<img id="userProfilePic" src="" alt="Profile"> <span id="userDisplayName"></span> <button id="logoutBtn" class="danger">Logout</button>
</div>
</div>
</div>
</header>
<div class="api-settings">
<button id="toggleApiSettings" class="toggle-api-btn">Show API Connection Settings</button>
<div id="apiForm" class="api-form hidden">
<div class="api-section">
<h3>Binance API</h3>
<div class="api-status">
<span class="status-indicator status-disconnected" id="binanceApiStatusIndicator"></span> <span id="binanceApiStatusText">Binance API Disconnected</span>
</div>
<div class="form-group">
<label for="binanceApiKey">API Key:</label> <input type="text" id="binanceApiKey" placeholder="Enter Binance API Key" autocomplete="off">
</div>
<div class="form-group">
<label for="binanceApiSecret">API Secret:</label> <input type="password" id="binanceApiSecret" placeholder="Enter Binance API Secret" autocomplete="new-password">
</div>
<div class="form-group">
<label><input type="checkbox" id="binanceTestMode" checked> Test Mode</label>
</div>
<div>
<button id="connectBinanceApi" class="connect-btn">Connect Binance</button> <button id="disconnectBinanceApi" class="danger hidden">Disconnect Binance</button>
</div>
</div>
<div class="api-section">
<h3>Bybit API</h3>
<div class="api-status">
<span class="status-indicator status-disconnected" id="bybitApiStatusIndicator"></span> <span id="bybitApiStatusText">Bybit API Disconnected</span>
</div>
<div class="form-group">
<label for="bybitApiKey">API Key:</label> <input type="text" id="bybitApiKey" placeholder="Enter Bybit API Key" autocomplete="off">
</div>
<div class="form-group">
<label for="bybitApiSecret">API Secret:</label> <input type="password" id="bybitApiSecret" placeholder="Enter Bybit API Secret" autocomplete="new-password">
</div>
<div class="form-group">
<label><input type="checkbox" id="bybitTestMode" checked> Test Mode</label>
</div>
<div>
<button id="connectBybitApi" class="connect-btn">Connect Bybit</button> <button id="disconnectBybitApi" class="danger hidden">Disconnect Bybit</button>
</div>
</div>
</div>
</div>
<div id="arbCardContainer" class="arb-container">
<div class="loading-card">
Waiting for scanner to start...
</div>
</div>
<div class="trade-log">
<div class="trade-log-header">
<h3>Trade Log</h3><button id="downloadLogBtn">Download Log (CSV)</button>
</div>
<div id="logEntries">
<div class="log-entry">
<span class="log-time">[System]</span> <span class="log-info">Please log in or skip to initialize.</span>
</div>
</div>
</div>
<div id="confirmModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<span class="modal-title">Confirm Trade</span> <span class="close-modal" id="closeConfirmModalBtn">×</span>
</div>
<div class="modal-body" id="confirmModalBody">
Loading...
</div>
<div class="modal-actions">
<button id="cancelTradeBtn" class="danger">Cancel</button> <button id="confirmTradeBtn" class="trade-btn">Confirm Trade</button>
</div>
</div>
</div>
<div id="settingsModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<span class="modal-title">Scanner Settings</span> <span class="close-modal" id="closeSettingsModalBtn">×</span>
</div>
<div class="modal-body settings-modal-grid">
<div class="settings-section">
<h4>General Controls</h4>
<div class="settings-control-group">
<div>
<label for="refreshRate">Refresh (sec):</label> <input type="number" id="refreshRate" value="5" min="2" max="60">
</div>
<div>
<label for="tradeAmount">Approx. Trade Amount (USDT):</label> <input type="number" id="tradeAmount" value="10" min="1" step="1">
</div>
</div>
</div>
<div class="settings-section">
<h4>Filters</h4>
<div class="settings-control-group">
<div>
<label><input type="checkbox" id="filterProfitable" checked> Show only profitable</label>
</div>
<div>
<label for="minProfitPercent">Min Profit (%):</label> <input type="number" id="minProfitPercent" value="0.05" min="0" step="0.01">
</div>
<div>
<label for="maxProfitPercent">Max Profit (%):</label> <input type="number" id="maxProfitPercent" value="50" min="0.1" step="0.1" title="Filter out unrealistic profits">
</div>
<div>
<label><input type="checkbox" id="autoTrade" disabled title="Connect an API to enable Auto-Trade"> Auto-Trade Top Result</label>
</div>
</div>
</div>
<div class="settings-section">
<h4>Enabled Exchanges</h4>
<div id="exchangeSelectionContainer" class="exchange-selection-grid">
{/* Checkboxes will be populated by JavaScript */}
</div>
</div>
</div>
<div class="modal-actions">
<button id="applySettingsBtn" class="trade-btn">Apply &amp; Restart Scanner</button>
</div>
</div>
</div>
</div>
<script>
// --- State Variables ---
let autoRefreshInterval; let currentRefreshRate = 5;
let selectedExchanges = [];
const API_CONFIG = { /* From previous response */
Binance: { type: 'single', url: "https://api.binance.com/api/v3/ticker/bookTicker" },
Bybit: { type: 'single', url: "https://api.bybit.com/v5/market/tickers?category=spot" },
OKX: { type: 'single', url: "https://www.okx.com/api/v5/market/tickers?instType=SPOT" },
Bitget: { type: 'single', url: "https://api.bitget.com/api/v2/spot/market/tickers" },
MEXC: { type: 'single', url: "https://api.mexc.com/api/v3/ticker/bookTicker" },
Gate: { type: 'single', url: "https://api.gateio.ws/api/v4/spot/tickers" },
HTX: { type: 'single', url: "https://api.htx.com/market/tickers" },
KuCoin: { type: 'single', url: "https://api.kucoin.com/api/v1/market/allTickers" },
Bitfinex: { type: 'single', url: "https://api-pub.bitfinex.com/v2/tickers?symbols=ALL" },
BingX: { type: 'single', url: "https://open-api.bingx.com/openApi/spot/v1/market/ticker"},
Coinbase: {
type: 'multi_step_individual',
list_url: "https://api.exchange.coinbase.com/products",
ticker_url_template: "https://api.exchange.coinbase.com/products/{id}/book?level=1"
},
Upbit: {
type: 'multi_step_batch',
list_url: "https://api.upbit.com/v1/market/all?isDetails=false",
ticker_url_base: "https://api.upbit.com/v1/orderbook?markets="
},
CryptoCom: {
type: 'multi_step_individual',
list_url: "https://api.crypto.com/exchange/v1/public/get-instruments",
ticker_url_template: "https://api.crypto.com/exchange/v1/public/get-ticker?instrument_name={instrument_name}"
},
Kraken: {
type: 'multi_step_batch',
list_url: "https://api.kraken.com/0/public/AssetPairs",
ticker_url_base: "https://api.kraken.com/0/public/Ticker?pair="
}
};
const EXTREME_PROFIT_THRESHOLD = 1000; const AUTO_TRADE_MIN_PROFIT_PERCENT = 0.2;
let binanceApiConnected = false; let binanceApiKey = ''; let binanceApiSecret = ''; let binanceTestMode = true;
let bybitApiConnected = false; let bybitApiKey = ''; let bybitApiSecret = ''; let bybitTestMode = true;
let currentArbitrageOpportunities = []; let pendingTradeOp = null; let isTradeExecuting = false; let isScannerRunning = false;
let currentUser = null; const USER_STORAGE_PREFIX = 'arb_scanner_user_';
let mainAppView, loginView,
opportunitiesContainerElement, toggleApiBtn, apiForm,
binanceApiKeyInput, binanceApiSecretInput, binanceTestModeCheckbox, connectBinanceApiBtn, disconnectBinanceApiBtn, binanceApiStatusIndicator, binanceApiStatusText,
bybitApiKeyInput, bybitApiSecretInput, bybitTestModeCheckbox, connectBybitApiBtn, disconnectBybitApiBtn, bybitApiStatusIndicator, bybitApiStatusText,
logEntriesContainer, confirmModal, confirmModalBody, closeConfirmModalBtn, cancelTradeBtn, confirmTradeBtn,
pausePlayBtn, scannerStatusText,
userProfileContainer, userProfilePic, userDisplayName, logoutBtn, skipLoginBtn,
openSettingsModalBtn, settingsModal, closeSettingsModalBtn, applySettingsBtn, downloadLogBtn,
exchangeSelectionContainer,
refreshRateInput, tradeAmountInput, filterProfitableCheckbox,
minProfitPercentInput, maxProfitPercentInput, autoTradeCheckbox;
// --- Function Definitions ---
function saveSetting(key, value) { try { localStorage.setItem(key, JSON.stringify(value)); } catch (e) { console.error("Error saving setting:", key, e); if(logEntriesContainer) logToConsole(`Error saving setting ${key}`, "error"); } }
function loadSetting(key, defaultValue) { try { const v = localStorage.getItem(key); return v !== null ? JSON.parse(v) : defaultValue; } catch (e) { console.error("Error loading setting:", key, e); if(logEntriesContainer) logToConsole(`Error loading setting ${key}`, "error"); return defaultValue; } }
function updateProfileDisplay() {
if (!document.body.classList.contains('state-logged-in') || !userProfileContainer) return;
const currentLogoutBtn = document.getElementById('logoutBtn');
if (currentUser) {
if (userProfileContainer) userProfileContainer.classList.remove('hidden');
if (userProfilePic) userProfilePic.src = currentUser.picture;
if (userDisplayName) userDisplayName.textContent = currentUser.name;
if (currentLogoutBtn) currentLogoutBtn.classList.remove('hidden');
} else {
if (userProfileContainer) userProfileContainer.classList.add('hidden');
if (currentLogoutBtn) currentLogoutBtn.classList.add('hidden');
}
}
function stopScanner() { clearInterval(autoRefreshInterval); isScannerRunning = false; if(pausePlayBtn) pausePlayBtn.textContent = "Play"; if(scannerStatusText) scannerStatusText.textContent = "(Status: Stopped)"; logToConsole("Scanner stopped.", "info"); if(opportunitiesContainerElement && document.body.classList.contains('state-logged-in')) opportunitiesContainerElement.innerHTML = `<div class="loading-card">Scanner Stopped.</div>`; }
function updateViewStateOnLogout() {
const currentMainAppView = document.getElementById('mainAppView');
const currentUserProfileContainer = document.getElementById('userProfileContainer');
if (currentMainAppView) currentMainAppView.style.display = 'none';
if (currentLoginView) currentLoginView.style.display = 'flex';
document.body.classList.add('state-logged-out');
document.body.classList.remove('state-logged-in', 'app-active');
document.title = "Login - Arbitrage Scanner";
if (currentUserProfileContainer) currentUserProfileContainer.classList.add('hidden');
if (typeof stopScanner === 'function' && isScannerRunning) stopScanner();
binanceApiKey = ''; binanceApiSecret = ''; binanceApiConnected = false;
bybitApiKey = ''; bybitApiSecret = ''; bybitApiConnected = false;
const initialOpportunitiesContainer = document.getElementById("arbCardContainer");
if (initialOpportunitiesContainer) {
initialOpportunitiesContainer.innerHTML = `<div class="loading-card">Please log in or skip to continue.</div>`;
}
}
function initializeAppView() {
console.log("Initializing App View...");
mainAppView = document.getElementById('mainAppView');
loginView = document.getElementById('loginView');
if (!mainAppView || !loginView) { console.error("Core view elements not found!"); return; }
opportunitiesContainerElement = document.getElementById("arbCardContainer");
toggleApiBtn = document.getElementById("toggleApiSettings");
apiForm = document.getElementById("apiForm");
binanceApiKeyInput = document.getElementById("binanceApiKey");
binanceApiSecretInput = document.getElementById("binanceApiSecret");
binanceTestModeCheckbox = document.getElementById("binanceTestMode");
connectBinanceApiBtn = document.getElementById("connectBinanceApi");
disconnectBinanceApiBtn = document.getElementById("disconnectBinanceApi");
binanceApiStatusIndicator = document.getElementById("binanceApiStatusIndicator");
binanceApiStatusText = document.getElementById("binanceApiStatusText");
bybitApiKeyInput = document.getElementById("bybitApiKey");
bybitApiSecretInput = document.getElementById("bybitApiSecret");
bybitTestModeCheckbox = document.getElementById("bybitTestMode");
connectBybitApiBtn = document.getElementById("connectBybitApi");
disconnectBybitApiBtn = document.getElementById("disconnectBybitApi");
bybitApiStatusIndicator = document.getElementById("bybitApiStatusIndicator");
bybitApiStatusText = document.getElementById("bybitApiStatusText");
logEntriesContainer = document.getElementById("logEntries");
confirmModal = document.getElementById("confirmModal");
confirmModalBody = document.getElementById("confirmModalBody");
closeConfirmModalBtn = document.getElementById("closeConfirmModalBtn");
cancelTradeBtn = document.getElementById("cancelTradeBtn");
confirmTradeBtn = document.getElementById("confirmTradeBtn");
pausePlayBtn = document.getElementById("pausePlayBtn");
scannerStatusText = document.getElementById("scannerStatusText");
userProfileContainer = document.getElementById('userProfileContainer');
userProfilePic = document.getElementById('userProfilePic');
userDisplayName = document.getElementById('userDisplayName');
logoutBtn = document.getElementById('logoutBtn');
openSettingsModalBtn = document.getElementById('openSettingsModalBtn');
settingsModal = document.getElementById('settingsModal');
closeSettingsModalBtn = document.getElementById('closeSettingsModalBtn');
applySettingsBtn = document.getElementById('applySettingsBtn');
downloadLogBtn = document.getElementById('downloadLogBtn');
exchangeSelectionContainer = document.getElementById('exchangeSelectionContainer');
refreshRateInput = document.getElementById("refreshRate");
tradeAmountInput = document.getElementById("tradeAmount");
filterProfitableCheckbox = document.getElementById("filterProfitable");
minProfitPercentInput = document.getElementById("minProfitPercent");
maxProfitPercentInput = document.getElementById("maxProfitPercent");
autoTradeCheckbox = document.getElementById("autoTrade");
mainAppView.style.display = 'block';
loginView.style.display = 'none';
document.body.classList.add('state-logged-in');
document.body.classList.remove('state-logged-out');
document.title = "Arbitrage Scanner & Trader";
if (logoutBtn) logoutBtn.addEventListener("click", handleLogout);
if (toggleApiBtn) toggleApiBtn.addEventListener("click", toggleApiSettings);
if (connectBinanceApiBtn) connectBinanceApiBtn.addEventListener("click", connectBinanceApi);
if (disconnectBinanceApiBtn) disconnectBinanceApiBtn.addEventListener("click", disconnectBinanceApi);
if (connectBybitApiBtn) connectBybitApiBtn.addEventListener("click", connectBybitApi);
if (disconnectBybitApiBtn) disconnectBybitApiBtn.addEventListener("click", disconnectBybitApi);
if (confirmTradeBtn) confirmTradeBtn.addEventListener("click", confirmTrade);
if (cancelTradeBtn) cancelTradeBtn.addEventListener("click", () => closeModalWindow(confirmModal));
if (closeConfirmModalBtn) closeConfirmModalBtn.addEventListener("click", () => closeModalWindow(confirmModal));
if (pausePlayBtn) pausePlayBtn.addEventListener("click", toggleScanner);
if (openSettingsModalBtn) openSettingsModalBtn.addEventListener("click", openSettingsModal);
if (closeSettingsModalBtn) closeSettingsModalBtn.addEventListener("click", () => closeModalWindow(settingsModal));
if (applySettingsBtn) applySettingsBtn.addEventListener("click", applyAllSettingsFromModal);
if (downloadLogBtn) downloadLogBtn.addEventListener("click", downloadTradeLog);
window.addEventListener("click", (event) => { if (event.target === confirmModal) closeModalWindow(confirmModal); if (event.target === settingsModal) closeModalWindow(settingsModal);});
if (binanceTestModeCheckbox) binanceTestModeCheckbox.addEventListener('change', () => { if (!binanceApiConnected) saveUserSetting('binanceTestModeDefault', binanceTestModeCheckbox.checked); else { binanceTestMode = binanceTestModeCheckbox.checked; updateApiStatusUI(); logToConsole(`Binance mode set to ${binanceTestMode ? 'Test' : 'Live'}.`, 'info'); } });
if (bybitTestModeCheckbox) bybitTestModeCheckbox.addEventListener('change', () => { if (!bybitApiConnected) saveUserSetting('bybitTestModeDefault', bybitTestModeCheckbox.checked); else { bybitTestMode = bybitTestModeCheckbox.checked; updateApiStatusUI(); logToConsole(`Bybit mode set to ${bybitTestMode ? 'Test' : 'Live'}.`, 'info'); } });
populateExchangeSelection();
updateProfileDisplay();
loadAllSettings();
startAutoRefresh();
}
function handleCredentialResponse(response) { const payload = parseJwt(response.credential); if (!payload) return; console.log("User signed in:", payload); currentUser = { id: payload.sub, email: payload.email, name: payload.name, picture: payload.picture, token: response.credential }; document.body.classList.add('app-active'); initializeAppView(); logToConsole("User signed in: " + currentUser.email, "success"); }
function parseJwt(token) { try { const b = token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/'); const j = decodeURIComponent(atob(b).split('').map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join('')); return JSON.parse(j); } catch (e) { console.error("JWT Error:", e); logToConsole("Login token error.", "error"); return null; } }
function handleLogout() { const email = currentUser?.email; currentUser = null; if (window.google?.accounts?.id) google.accounts.id.disableAutoSelect(); logToConsole("User logged out" + (email ? `: ${email}` : ''), "info"); updateViewStateOnLogout(); }
function handleSkipLogin() { console.log("User skipped login."); currentUser = null; document.body.classList.add('app-active'); initializeAppView(); logToConsole("Continued without login. Settings/API keys will not be saved to account.", "info"); }
function saveUserSetting(key, value) { if (currentUser) { saveSetting(USER_STORAGE_PREFIX + currentUser.id + '_' + key, value); } else { console.log("Skip login: Setting", key, "not saved to account."); } }
function loadUserSetting(key, defaultValue) { const uk = currentUser ? USER_STORAGE_PREFIX + currentUser.id + '_' + key : key; const uv = currentUser ? loadSetting(uk, null) : null; return uv !== null ? uv : loadSetting(key, defaultValue); }
function openSettingsModal() { if (settingsModal) settingsModal.style.display = 'flex'; }
function closeModalWindow(modalElement) { if (modalElement) modalElement.style.display = "none"; if (modalElement === confirmModal && isTradeExecuting && pendingTradeOp) { logToConsole(`Trade ID ${pendingTradeOp.id} cancelled by user.`, "warn"); resetTradeState(); } }
function populateExchangeSelection() {
if (!exchangeSelectionContainer) { console.error("exchangeSelectionContainer not found for population"); return; }
exchangeSelectionContainer.innerHTML = '';
Object.keys(API_CONFIG).forEach(exchangeName => {
const label = document.createElement('label');
const checkbox = document.createElement('input');
const checkbox = document.createElement('input');
checkbox.type = 'checkbox'; checkbox.value = exchangeName;
checkbox.id = `select-exchange-${exchangeName.toLowerCase().replace(/[^a-z0-9]/gi, '')}`;
checkbox.checked = selectedExchanges.includes(exchangeName);
checkbox.addEventListener('change', handleExchangeSelectionChange);
label.appendChild(checkbox); label.appendChild(document.createTextNode(` ${exchangeName}`));
label.htmlFor = checkbox.id;
exchangeSelectionContainer.appendChild(label);
});
}
function handleExchangeSelectionChange(event) {
const exchangeName = event.target.value;
if (event.target.checked) { if (!selectedExchanges.includes(exchangeName)) selectedExchanges.push(exchangeName); }
else { selectedExchanges = selectedExchanges.filter(ex => ex !== exchangeName); }
}
function applyAllSettingsFromModal() {
currentRefreshRate = parseInt(refreshRateInput.value, 10) || 5;
saveUserSetting('refreshRate', currentRefreshRate);
saveUserSetting('tradeAmount', tradeAmountInput.value);
saveUserSetting('filterProfitable', filterProfitableCheckbox.checked);
saveUserSetting('minProfitPercent', minProfitPercentInput.value);
saveUserSetting('maxProfitPercent', maxProfitPercentInput.value);
saveUserSetting('autoTrade', autoTradeCheckbox.checked);
saveUserSetting('selectedExchanges', selectedExchanges);
logToConsole("Settings applied. Restarting scanner...", "info");
closeModalWindow(settingsModal);
if (isScannerRunning) { stopScanner(); startAutoRefresh(); }
else { fetchAndDisplayData(); }
}
function downloadTradeLog() {
if (!logEntriesContainer) return; const entries = Array.from(logEntriesContainer.children);
if (entries.length === 0) { alert("Log is empty."); return; }
let csvContent = "Timestamp,Type,Message\n";
entries.reverse().forEach(entry => { const timeSpan = entry.querySelector(".log-time"); const msgSpan = entry.querySelector("span:not(.log-time)"); if (timeSpan && msgSpan) { const timestamp = timeSpan.textContent.replace(/[\[\]]/g, ''); const type = msgSpan.className.replace('log-', ''); const message = msgSpan.textContent.trim().replace(/"/g, '""'); csvContent += `"${timestamp}","${type}","${message}"\n`; } });
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement("a");
if (link.download !== undefined) { const url = URL.createObjectURL(blob); link.setAttribute("href", url); link.setAttribute("download", `arbitrage_scanner_log_${new Date().toISOString().slice(0,10)}.csv`); link.style.visibility = 'hidden'; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); } else { alert("Browser does not support automatic download."); }
}
function loadAllSettings() {
if (!document.body.classList.contains('state-logged-in')) { return; }
if (!refreshRateInput) { console.warn("Settings elements not ready, skipping load."); return; }
refreshRateInput.value = loadUserSetting('refreshRate', 5);
currentRefreshRate = parseInt(refreshRateInput.value, 10) || 5;
tradeAmountInput.value = loadUserSetting('tradeAmount', 10);
minProfitPercentInput.value = loadUserSetting('minProfitPercent', 0.05);
maxProfitPercentInput.value = loadUserSetting('maxProfitPercent', 50);
filterProfitableCheckbox.checked = loadUserSetting('filterProfitable', true);
autoTradeCheckbox.checked = loadUserSetting('autoTrade', false);
const savedExchanges = loadUserSetting('selectedExchanges', ['Binance', 'Bybit', 'KuCoin', 'OKX']);
selectedExchanges = Array.isArray(savedExchanges) ? savedExchanges : ['Binance', 'Bybit'];
populateExchangeSelection();
binanceApiConnected = false; bybitApiConnected = false;
const binanceKey = loadUserSetting('binanceApiKey', ''); const binanceSecret = loadUserSetting('binanceApiSecret', ''); const binanceTest = loadUserSetting('binanceTestMode', true);
if (currentUser && binanceKey && binanceSecret) { binanceApiKey = binanceKey; binanceApiSecret = binanceSecret; binanceTestMode = binanceTest; binanceApiConnected = true; logToConsole("Loaded Binance API credentials from account", "info"); } else { binanceApiKey = ''; binanceApiSecret = ''; binanceTestMode = loadUserSetting('binanceTestModeDefault', true); }
if(binanceTestModeCheckbox) binanceTestModeCheckbox.checked = binanceTestMode;
const bybitKey = loadUserSetting('bybitApiKey', ''); const bybitSecret = loadUserSetting('bybitApiSecret', ''); const bybitTest = loadUserSetting('bybitTestMode', true);
if (currentUser && bybitKey && bybitSecret) { bybitApiKey = bybitKey; bybitApiSecret = bybitSecret; bybitTestMode = bybitTest; bybitApiConnected = true; logToConsole("Loaded Bybit API credentials from account", "info"); } else { bybitApiKey = ''; bybitApiSecret = ''; bybitTestMode = loadUserSetting('bybitTestModeDefault', true); }
if(bybitTestModeCheckbox) bybitTestModeCheckbox.checked = bybitTestMode;
updateApiStatusUI(); checkAutoTradeStatus(); enableTradingButtons();
console.log("Settings loaded.");
}
function toggleScanner() { if (!document.body.classList.contains('state-logged-in')) { logToConsole("Please log in or skip to use the scanner.", "warn"); return; } if (isScannerRunning) { stopScanner(); } else { logToConsole("Scanner started/resumed.", "info"); startAutoRefresh(); } }
function startAutoRefresh() {
if (!document.body.classList.contains('state-logged-in')) return;
stopScanner(); const interval = currentRefreshRate * 1000; isScannerRunning = true;
if(pausePlayBtn) pausePlayBtn.textContent = "Pause"; if(scannerStatusText) scannerStatusText.textContent = "(Status: Running)";
fetchAndDisplayData(); autoRefreshInterval = setInterval(fetchAndDisplayData, interval); console.log(`Auto-refresh started: ${currentRefreshRate}s`);
}
function standardizeSymbol(symbol, exchangeName) { if (!symbol) return null; let s = symbol.toUpperCase(); s = s.replace(/[-_:\/\s]/g, ''); if (exchangeName === 'Kraken') { if (s.startsWith('XBT')) s = s.replace('XBT', 'BTC'); else if (s.startsWith('XDG')) s = s.replace('XDG', 'DOGE'); else if (s.startsWith('XETH')) s = s.replace('XETH', 'ETH'); else if (s.startsWith('XLTC')) s = s.replace('XLTC', 'LTC'); if (s.endsWith('ZUSD')) s = s.replace('ZUSD', 'USD'); else if (s.endsWith('ZEUR')) s = s.replace('ZEUR', 'EUR'); if (s.includes('XXBT')) s = s.replace('XXBT', 'BTC');} else if (exchangeName === 'Bitfinex') { if (s.startsWith('T')) s = s.substring(1); } if (s.endsWith("USD") && !s.endsWith("USDT") && !s.endsWith("USDC")) { s = s.substring(0, s.length - 3) + "USDT"; } if (s.endsWith("USDTUSDT")) s = s.substring(0, s.length - 4); if (s.endsWith("EURUSDT") && s.length > 7) s = s.replace("EURUSDT", "EUR"); return s; }
async function fetchExchangeData(exchangeName, config) { logToConsole(`Fetching from ${exchangeName}...`, 'info'); let rawData; try { if (config.type === 'single') { const response = await fetch(config.url); if (!response.ok) throw new Error(`Fetch failed: ${response.statusText} (${response.status}) for ${config.url}`); rawData = await response.json(); } else if (config.type === 'multi_step_batch') { const listResponse = await fetch(config.list_url); if (!listResponse.ok) throw new Error(`List fetch for ${exchangeName} failed: ${listResponse.statusText}`); const listData = await listResponse.json(); let itemIds = []; if (exchangeName === 'Upbit' && Array.isArray(listData)) { itemIds = listData.filter(m => m.market && (m.market.startsWith("KRW-") || m.market.startsWith("BTC-") || m.market.startsWith("USDT-"))).map(m => m.market).slice(0, 80); } else if (exchangeName === 'Kraken' && listData.result) { itemIds = Object.keys(listData.result).filter(pk => { const quote = listData.result[pk]?.quote; const standardizedQuote = standardizeSymbol(quote, exchangeName); return standardizedQuote === 'USDT' || standardizedQuote === 'USD'; }).slice(0, 20); } else if (exchangeName === 'Bitfinex' && Array.isArray(listData)) { itemIds = listData.filter(d => d.pair && parseFloat(d.last_price) > 0).map(d => `t${d.pair.toUpperCase()}`).slice(0,50); } if (itemIds.length > 0) { const tickerUrl = `${config.ticker_url_base}${itemIds.join(',')}`; const tickerResponse = await fetch(tickerUrl); if (!tickerResponse.ok) throw new Error(`Ticker batch fetch for ${exchangeName} failed: ${tickerResponse.statusText}`); rawData = await tickerResponse.json(); } else { logToConsole(`No relevant items for ${exchangeName} batch request.`, "warn"); return { exchangeName, error: `No items for ${exchangeName} batch` }; } } else if (config.type === 'multi_step_individual') { const listResponse = await fetch(config.list_url); if (!listResponse.ok) throw new Error(`List fetch for ${exchangeName} failed: ${listResponse.statusText}`); const listData = await listResponse.json(); let itemIds = []; if (exchangeName === 'Coinbase' && Array.isArray(listData)) { itemIds = listData.filter(p => (p.quote_currency === "USD" || p.quote_currency === "USDT" || p.quote_currency === "USDC") && !p.trading_disabled && p.status === 'online').map(p => p.id).slice(0, 10); } else if (exchangeName === 'CryptoCom' && listData.result && Array.isArray(listData.result.instruments) ) { itemIds = listData.result.instruments.filter(i => i.quote_currency === "USDT" || i.quote_currency === "USD").map(i => i.instrument_name).slice(0,10); } if (itemIds.length > 0) { rawData = []; for (const itemId of itemIds) { const tickerUrl = config.ticker_url_template.replace('{id}', itemId).replace('{instrument_name}', itemId).replace('{symbol}',itemId) ; try { const tickerResponse = await fetch(tickerUrl); if (tickerResponse.ok) { const tickerItem = await tickerResponse.json(); if (exchangeName === 'Coinbase' && !tickerItem.product_id) tickerItem.product_id = itemId; if (exchangeName === 'CryptoCom') { if(tickerItem.result && tickerItem.result.data) rawData.push(tickerItem.result.data); else logToConsole(`Unexpected ticker format for ${itemId} on Crypto.com`, 'warn'); } else { rawData.push(tickerItem); } } else { logToConsole(`Ticker for ${itemId} on ${exchangeName} failed: ${tickerResponse.statusText}`, 'warn'); } await new Promise(resolve => setTimeout(resolve, 300)); } catch(eInner) { logToConsole(`Error fetching individual ticker ${itemId} for ${exchangeName}: ${eInner.message}`, 'error'); } } } else { logToConsole(`No relevant items for ${exchangeName} individual requests.`, "warn"); return { exchangeName, error: `No items for ${exchangeName} individual` }; } } return { exchangeName, data: rawData }; } catch (error) { logToConsole(`Outer fetch/process for ${exchangeName} error: ${error.message}`, "error"); return { exchangeName, error: error.message }; } }
async function fetchAndDisplayData() { if (!isScannerRunning || isTradeExecuting) { return; } if (!opportunitiesContainerElement) { return; } if (!opportunitiesContainerElement.querySelector('.loading-card')?.textContent.startsWith('Fetching')) { opportunitiesContainerElement.innerHTML = `<div class="loading-card">Fetching latest prices... (this may take a moment for selected exchanges)</div>`; } const tradeAmt = parseFloat(tradeAmountInput.value) || 10; const filterProfitable = filterProfitableCheckbox.checked; const minProfit = parseFloat(minProfitPercentInput.value) || 0; const maxProfit = parseFloat(maxProfitPercentInput.value) || 50; const autoTrade = autoTradeCheckbox.checked; const activeApiConfig = Object.entries(API_CONFIG).filter(([name]) => selectedExchanges.includes(name)).reduce((obj, [name, config]) => { obj[name] = config; return obj; }, {}); if (Object.keys(activeApiConfig).length === 0) { logToConsole("No exchanges selected. Please select exchanges in Settings.", "warn"); opportunitiesContainerElement.innerHTML = `<div class="loading-card">No exchanges selected. Go to Settings to enable exchanges.</div>`; return; } try { const fetchPromises = Object.entries(activeApiConfig).map(([name, config]) => fetchExchangeData(name, config)); const results = await Promise.allSettled(fetchPromises); let allOps = []; results.forEach(result => { if (result.status === 'fulfilled' && result.value && !result.value.error) { const { exchangeName, data } = result.value; let map = {}; try { if (!data) { logToConsole(`No data received for ${exchangeName} post-process.`, "warn"); return; } if (exchangeName === 'Binance' && Array.isArray(data)) { map = data.reduce((m, t) => { const b = parseFloat(t.bidPrice), a = parseFloat(t.askPrice); const s = standardizeSymbol(t.symbol, exchangeName); if (s && b > 0 && a > 0) m[s] = { bidPrice: b, askPrice: a }; return m; }, {}); } else if (exchangeName === 'Bybit' && data.retCode === 0 && Array.isArray(data.result?.list)) { map = data.result.list.reduce((m, t) => { const b = parseFloat(t.bid1Price), a = parseFloat(t.ask1Price); const s = standardizeSymbol(t.symbol, exchangeName); if (s && b > 0 && a > 0) m[s] = { bidPrice: b, askPrice: a }; return m; }, {}); } else if (exchangeName === 'OKX' && data.code === "0" && Array.isArray(data.data)) { map = data.data.reduce((m, t) => { const b = parseFloat(t.bidPx), a = parseFloat(t.askPx); const s = standardizeSymbol(t.instId, exchangeName); if (s && b > 0 && a > 0) m[s] = { bidPrice: b, askPrice: a }; return m; }, {}); } else if (exchangeName === 'KuCoin' && data.code === "200000" && data.data && Array.isArray(data.data.ticker)) { map = data.data.ticker.reduce((m, t) => { const b = parseFloat(t.buy), a = parseFloat(t.sell); const s = standardizeSymbol(t.symbol, exchangeName); if (s && b > 0 && a > 0) m[s] = { bidPrice: b, askPrice: a }; return m; }, {}); } else if (exchangeName === 'Gate' && Array.isArray(data)) { map = data.reduce((m,t) => { const b = parseFloat(t.highest_bid), a = parseFloat(t.lowest_ask); const s = standardizeSymbol(t.currency_pair, exchangeName); if (s && b > 0 && a > 0) m[s] = { bidPrice: b, askPrice: a }; return m; }, {}); } else if (exchangeName === 'MEXC' && Array.isArray(data)) { map = data.reduce((m, t) => { const b = parseFloat(t.bidPrice), a = parseFloat(t.askPrice); const s = standardizeSymbol(t.symbol, exchangeName); if (s && b > 0 && a > 0) m[s] = { bidPrice: b, askPrice: a }; return m; }, {}); } else if (exchangeName === 'HTX' && data.status === 'ok' && Array.isArray(data.data)) { map = data.data.reduce((m, t) => { const b = parseFloat(t.bid), a = parseFloat(t.ask); const s = standardizeSymbol(t.symbol, exchangeName); if (s && b > 0 && a > 0) m[s] = { bidPrice: b, askPrice: a }; return m; }, {}); } else if (exchangeName === 'Bitget' && data.code === "00000" && Array.isArray(data.data)) { map = data.data.reduce((m,t) => { const b = parseFloat(t.buyOne), a = parseFloat(t.sellOne); const s = standardizeSymbol(t.symbolName || t.symbol, exchangeName); if(s && b > 0 && a > 0) m[s] = { bidPrice: b, askPrice: a}; return m;}, {}); } else if (exchangeName === 'CryptoCom' && Array.isArray(data)) { data.forEach(t => { if(t && t.i) { const b = parseFloat(t.b), a = parseFloat(t.k); const s = standardizeSymbol(t.i, exchangeName); if(s && b > 0 && a > 0) map[s] = {bidPrice: b, askPrice: a};}}); } else if (exchangeName === 'Bitfinex' && Array.isArray(data)) { map = data.reduce((m,t) => { if (Array.isArray(t) && t.length >= 7 && typeof t[0] === 'string' && t[0].startsWith('t')) { const s = standardizeSymbol(t[0].substring(1), exchangeName); const b = parseFloat(t[1]); const a = parseFloat(t[3]); if (s && b>0 && a>0) m[s] = {bidPrice: b, askPrice: a};} return m;}, {}); } else if (exchangeName === 'BingX' && data.code === 0 && data.data && (Array.isArray(data.data.tickers) || Array.isArray(data.data))) { const tickersArray = Array.isArray(data.data.tickers) ? data.data.tickers : (Array.isArray(data.data) ? data.data : []); map = tickersArray.reduce((m,t) => {const b = parseFloat(t.bidPrice), a = parseFloat(t.askPrice); const s = standardizeSymbol(t.symbol, exchangeName); if(s && b>0 && a>0) m[s] = {bidPrice:b, askPrice:a}; return m;}, {}); } else if (exchangeName === 'Kraken' && data.error && Array.isArray(data.error) && data.error.length === 0 && data.result) { for (const krakenPair in data.result) { const ticker = data.result[krakenPair]; const b = parseFloat(ticker.b?.[0]); const a = parseFloat(ticker.a?.[0]); const s = standardizeSymbol(krakenPair, exchangeName); if (s && b > 0 && a > 0) map[s] = { bidPrice: b, askPrice: a }; } } else if (exchangeName === 'Upbit' && Array.isArray(data)) { map = data.reduce((m,t) => { const orderbook_units = t.orderbook_units; if(orderbook_units && orderbook_units.length > 0){ const b = parseFloat(orderbook_units[0].bid_price); const a = parseFloat(orderbook_units[0].ask_price); const s = standardizeSymbol(t.market, exchangeName); if (s && b>0 && a>0) m[s] = {bidPrice: b, askPrice: a};} return m;}, {}); } else if (exchangeName === 'Coinbase' && Array.isArray(data)) { data.forEach(book => { if(book && book.product_id && Array.isArray(book.bids) && book.bids.length > 0 && Array.isArray(book.asks) && book.asks.length > 0) { const b = parseFloat(book.bids[0][0]); const a = parseFloat(book.asks[0][0]); const s = standardizeSymbol(book.product_id, exchangeName); if(s && b>0 && a>0) map[s] = {bidPrice:b, askPrice:a}; }}); } else { logToConsole(`No parsing logic matched for ${exchangeName}. Data:`, "warn"); console.log(data); } if (Object.keys(map).length > 0) { const ops = findTriangularArbitrage(map, "USDT", tradeAmt); ops.forEach(op => { op.exchange = exchangeName; op.id = `${exchangeName.substring(0,3).toUpperCase()}_${op.path.join('_')}_${Date.now()}`; }); allOps.push(...ops); logToConsole(`${exchangeName}: Found ${ops.length} potential opportunities from ${Object.keys(map).length} tickers.`, 'info'); } } catch (parseError) { logToConsole(`Error parsing ${exchangeName} data post-fetch: ${parseError.message}`, "error"); console.error(`Data for ${exchangeName} that failed parsing:`, data, parseError); } } else if (result.status === 'fulfilled' && result.value && result.value.error) { logToConsole(`Error from ${result.value.exchangeName} during fetch: ${result.value.error}`, "error"); } else if (result.status === 'rejected') { logToConsole(`Promise rejected for an exchange fetch (see network tab or previous logs): ${result.reason}`, "error"); } }); const filtered = allOps.filter(op => op.profitPercent <= maxProfit && op.profitPercent >= minProfit && (!filterProfitable || op.profit > 0)); const sorted = filtered.sort((a, b) => b.profitPercent - a.profitPercent); currentArbitrageOpportunities = sorted; if (isScannerRunning && autoTrade && !isTradeExecuting && sorted.length > 0) { const topOp = sorted[0]; const isConnected = (topOp.exchange === 'Binance' && binanceApiConnected) || (topOp.exchange === 'Bybit' && bybitApiConnected); const isTest = (topOp.exchange === 'Binance' && binanceTestMode) || (topOp.exchange === 'Bybit' && bybitTestMode); if (isConnected && topOp.profitPercent >= AUTO_TRADE_MIN_PROFIT_PERCENT) { logToConsole(`Auto-Trade Triggered (${topOp.exchange} ${isTest ? 'Test':'Live'}): ${topOp.path.join('->')} (${topOp.profitPercent.toFixed(4)}% >= ${AUTO_TRADE_MIN_PROFIT_PERCENT}%). Executing...`, "info"); executeTrade(0, true); } } if (isScannerRunning) renderTable(sorted, minProfit); } catch (err) { console.error("Overall fetch/process error:", err); logToConsole(`Error in fetchAndDisplayData: ${err.message}`, "error"); if (isScannerR
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment