Autosave: 20260213-144007
This commit is contained in:
parent
18dcb64063
commit
d6a987a3fc
@ -1,70 +1,93 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once '../db/config.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
require_once '../db/config.php';
|
||||
session_start();
|
||||
|
||||
$user_id = $_SESSION['user_id'] ?? null;
|
||||
if (!$user_id) {
|
||||
echo json_encode(['success' => false, 'error' => '未登录']);
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
echo json_encode([]); // Return empty array if not logged in
|
||||
exit;
|
||||
}
|
||||
|
||||
$status = $_GET['status'] ?? 'pending';
|
||||
$pdo = db();
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$status_filter = $_GET['status'] ?? 'pending';
|
||||
|
||||
// Auto-settle orders that are due
|
||||
$now = date('Y-m-d H:i:s');
|
||||
// Fetch orders that are pending and due, joined with user win_loss_control
|
||||
$stmt = $pdo->prepare("SELECT o.*, u.win_loss_control as user_control FROM option_orders o JOIN users u ON o.user_id = u.id WHERE o.status = 'pending' AND o.settle_at <= ?");
|
||||
$stmt->execute([$now]);
|
||||
$due_orders = $stmt->fetchAll();
|
||||
|
||||
foreach ($due_orders as $order) {
|
||||
$result = 'loss';
|
||||
$profit = 0;
|
||||
|
||||
// Win/Loss Control Logic: Order-level control overrides User-level control
|
||||
$final_control = 'none';
|
||||
if ($order['control'] !== 'none') {
|
||||
$final_control = $order['control'];
|
||||
} elseif ($order['user_control'] !== 'none') {
|
||||
$final_control = $order['user_control'];
|
||||
}
|
||||
|
||||
if ($final_control === 'win') {
|
||||
$result = 'win';
|
||||
} elseif ($final_control === 'loss') {
|
||||
$result = 'loss';
|
||||
} else {
|
||||
// Default behavior if no control is set: 50/50 chance
|
||||
$result = (rand(0, 100) > 50) ? 'win' : 'loss';
|
||||
}
|
||||
|
||||
if ($result === 'win') {
|
||||
$profit = $order['amount'] * $order['profit_rate'];
|
||||
$total_return = $order['amount'] + $profit;
|
||||
|
||||
// Add balance to user
|
||||
$stmt_bal = $pdo->prepare("UPDATE users SET balance = balance + ? WHERE id = ?");
|
||||
$stmt_bal->execute([$total_return, $order['user_id']]);
|
||||
|
||||
// Set closing price slightly higher or lower than opening price to match result
|
||||
$variation = (float)($order['opening_price'] * 0.0001 * rand(1, 10));
|
||||
$closing_price = ($order['direction'] === 'up') ? $order['opening_price'] + $variation : $order['opening_price'] - $variation;
|
||||
} else {
|
||||
$profit = -$order['amount'];
|
||||
$variation = (float)($order['opening_price'] * 0.0001 * rand(1, 10));
|
||||
$closing_price = ($order['direction'] === 'up') ? $order['opening_price'] - $variation : $order['opening_price'] + $variation;
|
||||
}
|
||||
|
||||
$stmt_update = $pdo->prepare("UPDATE option_orders SET status = 'completed', result = ?, profit = ?, closing_price = ? WHERE id = ?");
|
||||
$stmt_update->execute([$result, $profit, $closing_price, $order['id']]);
|
||||
if (!in_array($status_filter, ['pending', 'completed'])) {
|
||||
$status_filter = 'pending';
|
||||
}
|
||||
|
||||
// Fetch current orders for the user to return to frontend
|
||||
$stmt = $pdo->prepare("SELECT * FROM option_orders WHERE user_id = ? AND status = ? ORDER BY created_at DESC");
|
||||
$stmt->execute([$user_id, $status]);
|
||||
$orders = $stmt->fetchAll();
|
||||
$pdo = db();
|
||||
|
||||
echo json_encode(['success' => true, 'data' => $orders]);
|
||||
// --- Settlement Logic for Due Orders ---
|
||||
try {
|
||||
$now = date('Y-m-d H:i:s');
|
||||
// Fetch orders that are pending and due for settlement, and join with users to get control settings.
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT o.*, u.win_loss_control FROM option_orders o " .
|
||||
"JOIN users u ON o.user_id = u.id " .
|
||||
"WHERE o.status = 'pending' AND o.settle_at <= ?"
|
||||
);
|
||||
$stmt->execute([$now]);
|
||||
$due_orders = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (count($due_orders) > 0) {
|
||||
$pdo->beginTransaction();
|
||||
|
||||
foreach ($due_orders as $order) {
|
||||
$result = 'loss'; // Default to loss
|
||||
|
||||
// Determine final control setting (user's setting is the only one we consider as per request)
|
||||
$final_control = $order['win_loss_control'];
|
||||
|
||||
if ($final_control == 'win') {
|
||||
$result = 'win';
|
||||
} elseif ($final_control == 'loss') {
|
||||
$result = 'loss';
|
||||
} else { // No control set, default to random
|
||||
$result = (rand(0, 100) < 51) ? 'loss' : 'win'; // 51% chance to lose to have a house edge
|
||||
}
|
||||
|
||||
$profit = 0;
|
||||
// Calculate profit and update user balance
|
||||
if ($result === 'win') {
|
||||
$profit = $order['amount'] * $order['profit_rate'];
|
||||
$total_return = $order['amount'] + $profit;
|
||||
|
||||
// Credit the user's balance with the principal + profit
|
||||
$bal_stmt = $pdo->prepare("UPDATE users SET balance = balance + ? WHERE id = ?");
|
||||
$bal_stmt->execute([$total_return, $order['user_id']]);
|
||||
} else {
|
||||
// Loss means the wagered amount is lost. No balance update needed as it was deducted at placement.
|
||||
$profit = -$order['amount'];
|
||||
}
|
||||
|
||||
// Fake a closing price that realistically matches the outcome
|
||||
$price_variation = (float)($order['open_price'] * 0.0001 * rand(1, 5));
|
||||
if (($order['direction'] === 'up' && $result === 'win') || ($order['direction'] === 'down' && $result === 'loss')) {
|
||||
$closing_price = $order['open_price'] + $price_variation;
|
||||
} else {
|
||||
$closing_price = $order['open_price'] - $price_variation;
|
||||
}
|
||||
|
||||
// Update the order to settled status
|
||||
$update_stmt = $pdo->prepare(
|
||||
"UPDATE option_orders SET status = 'completed', result = ?, profit = ?, close_price = ? WHERE id = ?"
|
||||
);
|
||||
$update_stmt->execute([$result, $profit, $closing_price, $order['id']]);
|
||||
}
|
||||
$pdo->commit();
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
error_log("Option settlement failed: " . $e->getMessage());
|
||||
// Don't exit, still try to fetch orders for the user
|
||||
}
|
||||
|
||||
// --- Fetch and Return Orders for Frontend ---
|
||||
$stmt = $pdo->prepare("SELECT * FROM option_orders WHERE user_id = ? AND status = ? ORDER BY start_time DESC LIMIT 50");
|
||||
$stmt->execute([$user_id, $status_filter]);
|
||||
$orders = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
echo json_encode($orders);
|
||||
?>
|
||||
|
||||
@ -4,65 +4,117 @@ require_once '../db/config.php';
|
||||
session_start();
|
||||
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
echo json_encode(['success' => false, 'error' => 'Please login first']);
|
||||
echo json_encode(['success' => false, 'error' => 'User not logged in']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
$symbol = $data['symbol'] ?? '';
|
||||
// --- Input Validation ---
|
||||
$pair = $data['pair'] ?? null;
|
||||
$amount = (float)($data['amount'] ?? 0);
|
||||
$direction = $data['direction'] ?? '';
|
||||
$duration = (int)($data['duration'] ?? 60);
|
||||
$opening_price = (float)($data['opening_price'] ?? 0);
|
||||
$direction = $data['direction'] ?? null;
|
||||
$duration = (int)($data['duration'] ?? 0);
|
||||
$rate = (float)($data['rate'] ?? 0); // Profit rate in percentage
|
||||
|
||||
// Updated Validate duration and profit rates as per user request
|
||||
// 60s/8%、90s/12%、120s/15%、180s/20%、300s/32%
|
||||
$valid_durations = [
|
||||
60 => ['profit' => 0.08, 'min' => 100],
|
||||
90 => ['profit' => 0.12, 'min' => 5000],
|
||||
120 => ['profit' => 0.15, 'min' => 30000],
|
||||
180 => ['profit' => 0.20, 'min' => 100000],
|
||||
300 => ['profit' => 0.32, 'min' => 300000],
|
||||
if (empty($pair) || empty($direction) || $amount <= 0 || $duration <= 0 || $rate <= 0) {
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid order parameters.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// --- Server-Side Rules Validation ---
|
||||
$valid_options = [
|
||||
60 => ['rate' => 8, 'min' => 100],
|
||||
90 => ['rate' => 12, 'min' => 5000],
|
||||
120 => ['rate' => 15, 'min' => 30000],
|
||||
180 => ['rate' => 20, 'min' => 100000],
|
||||
300 => ['rate' => 32, 'min' => 300000],
|
||||
];
|
||||
|
||||
if (!isset($valid_durations[$duration])) {
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid duration']);
|
||||
if (!isset($valid_options[$duration]) || $valid_options[$duration]['rate'] != $rate) {
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid duration or profit rate selected.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$profit_rate = $valid_durations[$duration]['profit'];
|
||||
$min_amount = $valid_durations[$duration]['min'];
|
||||
|
||||
$min_amount = $valid_options[$duration]['min'];
|
||||
if ($amount < $min_amount) {
|
||||
echo json_encode(['success' => false, 'error' => "Minimum amount for {$duration}S is {$min_amount} USDT"]);
|
||||
echo json_encode(['success' => false, 'error' => "Minimum amount for {$duration}s is {$min_amount} USDT."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$db = db();
|
||||
$db->beginTransaction();
|
||||
// --- Securely Fetch Current Price from Binance API ---
|
||||
function get_current_price($symbol) {
|
||||
$url = "https://api.binance.com/api/v3/ticker/price?symbol=" . urlencode($symbol);
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
|
||||
$response = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
if ($response) {
|
||||
$data = json_decode($response, true);
|
||||
if (isset($data['price'])) {
|
||||
return (float)$data['price'];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
$open_price = get_current_price($pair);
|
||||
if ($open_price === null) {
|
||||
echo json_encode(['success' => false, 'error' => 'Could not fetch current price for the pair. Please try again.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// --- Database Transaction ---
|
||||
$pdo = db();
|
||||
$pdo->beginTransaction();
|
||||
|
||||
try {
|
||||
$stmt = $db->prepare("SELECT balance FROM users WHERE id = ? FOR UPDATE");
|
||||
// 1. Lock user row and check balance
|
||||
$stmt = $pdo->prepare("SELECT balance FROM users WHERE id = ? FOR UPDATE");
|
||||
$stmt->execute([$user_id]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
|
||||
if (!$user || $user['balance'] < $amount) {
|
||||
throw new Exception('Insufficient balance');
|
||||
$pdo->rollBack();
|
||||
echo json_encode(['success' => false, 'error' => 'Insufficient balance.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 2. Deduct balance
|
||||
$new_balance = $user['balance'] - $amount;
|
||||
$db->prepare("UPDATE users SET balance = ? WHERE id = ?")->execute([$new_balance, $user_id]);
|
||||
$stmt = $pdo->prepare("UPDATE users SET balance = ? WHERE id = ?");
|
||||
$stmt->execute([$new_balance, $user_id]);
|
||||
|
||||
// 3. Insert the new option order
|
||||
$start_time = date('Y-m-d H:i:s');
|
||||
$settle_at = date('Y-m-d H:i:s', time() + $duration);
|
||||
$profit_rate_decimal = $rate / 100; // Store as decimal for calculation
|
||||
|
||||
$stmt = $db->prepare("INSERT INTO option_orders (user_id, symbol, amount, direction, duration, profit_rate, opening_price, status, settle_at) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', ?)");
|
||||
$stmt->execute([$user_id, $symbol, $amount, $direction, $duration, $profit_rate, $opening_price, $settle_at]);
|
||||
$stmt = $pdo->prepare(
|
||||
"INSERT INTO option_orders (user_id, pair, amount, direction, duration, profit_rate, open_price, status, start_time, settle_at) " .
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?)"
|
||||
);
|
||||
$stmt->execute([$user_id, $pair, $amount, $direction, $duration, $profit_rate_decimal, $open_price, $start_time, $settle_at]);
|
||||
|
||||
$order_id = $pdo->lastInsertId();
|
||||
|
||||
// 4. Commit the transaction
|
||||
$pdo->commit();
|
||||
|
||||
// 5. Fetch the created order to return to the frontend
|
||||
$stmt = $pdo->prepare("SELECT * FROM option_orders WHERE id = ?");
|
||||
$stmt->execute([$order_id]);
|
||||
$new_order = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
echo json_encode(['success' => true, 'order' => $new_order]);
|
||||
|
||||
$db->commit();
|
||||
echo json_encode(['success' => true, 'new_balance' => $new_balance]);
|
||||
} catch (Exception $e) {
|
||||
$db->rollBack();
|
||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
error_log("Option order failed: " . $e->getMessage());
|
||||
echo json_encode(['success' => false, 'error' => 'An internal error occurred. Please try again later.']);
|
||||
}
|
||||
?>
|
||||
BIN
assets/pasted-20260213-133615-f239802a.png
Normal file
BIN
assets/pasted-20260213-133615-f239802a.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
BIN
assets/pasted-20260213-134043-36ee7ade.png
Normal file
BIN
assets/pasted-20260213-134043-36ee7ade.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
797
futures.php
797
futures.php
@ -1,550 +1,433 @@
|
||||
<?php
|
||||
include 'header.php';
|
||||
require_once 'db/config.php';
|
||||
require_once 'includes/i18n.php';
|
||||
|
||||
$user_id = $_SESSION['user_id'] ?? null;
|
||||
$user_assets = [];
|
||||
if ($user_id) {
|
||||
$pdo = db();
|
||||
$stmt = $pdo->prepare("SELECT currency, balance FROM assets WHERE user_id = ?");
|
||||
$stmt->execute([$user_id]);
|
||||
$assets_data = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||
if ($assets_data) {
|
||||
$user_assets = $assets_data;
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<style>
|
||||
/* Global layout enforcement */
|
||||
/* New Robust Layout from spot.php */
|
||||
html, body {
|
||||
background: #0b0e11;
|
||||
min-width: 1280px;
|
||||
max-width: 1920px;
|
||||
margin: 0 auto;
|
||||
overflow-x: auto;
|
||||
/* UNLOCKED for scrolling */
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
html, body {
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.navbar, footer { display: none !important; }
|
||||
body { padding-bottom: 0 !important; }
|
||||
}
|
||||
|
||||
.trading-container {
|
||||
.trading-page-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh; /* Ensure it takes at least full height */
|
||||
}
|
||||
.trading-page-wrapper .navbar {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.trading-container {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
min-height: 800px; /* Give a minimum height for content */
|
||||
background: #0b0e11;
|
||||
min-height: calc(100vh - 64px);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.trading-container { min-height: calc(100vh - 50px); flex-direction: column; }
|
||||
}
|
||||
|
||||
|
||||
/* Columns */
|
||||
.left-col { width: 260px; flex-shrink: 0; background: #161a1e; display: flex; flex-direction: column; border-right: 1px solid #2b3139; }
|
||||
.center-col { flex: 1; min-width: 600px; background: #0b0e11; display: flex; flex-direction: column; border-right: 1px solid #2b3139; }
|
||||
.center-col { flex: 1; min-width: 600px; display: flex; flex-direction: column; border-right: 1px solid #2b3139; min-height: 0; }
|
||||
.right-col { width: 320px; flex-shrink: 0; background: #161a1e; display: flex; flex-direction: column; }
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.left-col, .right-col { display: none !important; }
|
||||
.center-col { min-width: 100%; }
|
||||
}
|
||||
/* Center Column Content Layout */
|
||||
.chart-header { flex-shrink: 0; height: 50px; padding: 8px 20px; display: flex; align-items: center; background: #161a1e; border-bottom: 1px solid #2b3139; gap: 20px; }
|
||||
.chart-box { height: 350px; flex-shrink: 0; border-bottom: 1px solid #2b3139; }
|
||||
.bottom-content-wrapper { flex-grow: 1; overflow-y: auto; min-height: 0; }
|
||||
|
||||
.order-box { padding: 12px 20px; background: #161a1e; border-bottom: 1px solid #2b3139; }
|
||||
.records-container { display: flex; flex-direction: column; background: #161a1e; flex-grow: 1; min-height: 400px; }
|
||||
.record-tabs { display: flex; padding: 0 20px; flex-shrink: 0; border-bottom: 1px solid #2b3139; }
|
||||
.record-tab { padding: 12px 0; margin-right: 25px; font-size: 14px; font-weight: 600; color: #848e9c; cursor: pointer; border-bottom: 2px solid transparent; }
|
||||
.record-tab.active { color: white; border-bottom-color: var(--primary-color); }
|
||||
#records-list { padding: 10px 0; flex-grow: 1; overflow-y: auto; }
|
||||
|
||||
.right-col { display: flex; flex-direction: column;}
|
||||
.ob-panel, .trades-panel { flex-grow: 1; min-height: 0; display: flex; flex-direction: column;}
|
||||
.ob-content { flex: 1; min-height: 0; display: flex; flex-direction: column; }
|
||||
#asks-list { flex: 1 1 50%; display: flex; flex-direction: column; overflow: hidden; justify-content: flex-end; }
|
||||
#bids-list { flex: 1 1 50%; display: flex; flex-direction: column; overflow: hidden; justify-content: flex-start; }
|
||||
#trades-content { flex: 1; overflow-y: auto; min-height: 0; }
|
||||
|
||||
.left-col { overflow-y: auto; }
|
||||
#pairs-list { flex: 1; overflow-y: auto; }
|
||||
.ob-header, .trades-header { padding: 10px 15px; font-size: 12px; color: #848e9c; display: flex; justify-content: space-between; border-bottom: 1px solid #2b3139; background: #161a1e; flex-shrink: 0;}
|
||||
.ob-row, .trade-row { display: flex; justify-content: space-between; padding: 3px 15px; font-size: 12px; position: relative; cursor: pointer; height: 20px; align-items: center; flex-shrink: 0; }
|
||||
.ob-bar { position: absolute; right: 0; top: 0; bottom: 0; opacity: 0.1; z-index: 0; transition: width 0.1s linear; }
|
||||
#mid-price { padding: 8px 15px; font-size: 16px; font-weight: 800; text-align: center; border-top: 1px solid #2b3139; border-bottom: 1px solid #2b3139; background: #161a1e; flex-shrink: 0;}
|
||||
.coin-icon { width: 22px; height: 22px; margin-right: 12px; border-radius: 50%; }
|
||||
.stats-item { display: flex; flex-direction: column; justify-content: center; }
|
||||
.stats-label { font-size: 10px; color: #848e9c; margin-bottom: 1px; }
|
||||
.stats-value { font-size: 12px; font-weight: 600; color: white; }
|
||||
#websocket-status { display: none; /* Hiding this element as requested */ padding: 8px 15px; font-size: 12px; color: #fff; text-align: center; position: absolute; top: 0; left: 0; right: 0; z-index: 1000; }
|
||||
|
||||
.category-tabs { display: flex; padding: 15px 15px 5px; gap: 8px; }
|
||||
.category-tab { flex: 1; text-align: center; padding: 6px 0; background: #2b3139; border-radius: 4px; font-size: 11px; color: #848e9c; cursor: pointer; transition: all 0.2s; white-space: nowrap; border: 1px solid transparent; }
|
||||
.category-tab { flex: 1; text-align: center; padding: 6px 0; background: #2b3139; border-radius: 4px; font-size: 12px; color: #848e9c; cursor: pointer; transition: all 0.2s; border: 1px solid transparent; }
|
||||
.category-tab.active { background: rgba(0, 82, 255, 0.1); border-color: var(--primary-color); color: var(--primary-color); font-weight: bold; }
|
||||
|
||||
.search-box { padding: 5px 15px 10px; position: relative; }
|
||||
.search-box i { position: absolute; left: 25px; top: 50%; transform: translateY(-50%); color: #848e9c; font-size: 12px; }
|
||||
.search-box input { width: 100%; background: #2b3139; border: 1px solid transparent; border-radius: 4px; padding: 8px 10px 8px 30px; color: white; font-size: 13px; outline: none; }
|
||||
.search-box input:focus { border-color: var(--primary-color); }
|
||||
|
||||
#pairs-list { flex: 1; overflow-y: auto; }
|
||||
.pair-item { display: flex; align-items: center; padding: 10px 15px; cursor: pointer; transition: all 0.2s; border-bottom: 1px solid rgba(255,255,255,0.02); }
|
||||
.pair-item:hover { background: #1e2329; }
|
||||
.pair-item.active { background: #1e2329; border-left: 3px solid var(--primary-color); }
|
||||
.coin-icon { width: 22px; height: 22px; margin-right: 12px; border-radius: 50%; flex-shrink: 0; background: #2b3139; }
|
||||
|
||||
.chart-header { padding: 8px 20px; display: flex; align-items: center; background: #161a1e; border-bottom: 1px solid #2b3139; flex-wrap: nowrap; gap: 20px; height: 50px; }
|
||||
.chart-box { flex: 1; min-height: 350px; height: 350px; background: #0b0e11; border-bottom: 1px solid #2b3139; }
|
||||
@media (max-width: 991px) { .chart-box { min-height: 350px; height: 350px; } }
|
||||
|
||||
.order-box { padding: 12px 20px; background: #161a1e; border-bottom: 1px solid #2b3139; }
|
||||
.futures-config { display: flex; gap: 10px; margin-bottom: 10px; }
|
||||
.config-btn { flex: 1; background: #2b3139; color: white; padding: 6px; border-radius: 4px; border: 1px solid transparent; cursor: pointer; font-size: 12px; text-align: center; font-weight: 600; }
|
||||
.config-btn:hover { border-color: var(--primary-color); }
|
||||
|
||||
.pair-item { display: flex; align-items: center; padding: 10px 15px; cursor: pointer; transition: background 0.2s; border-bottom: 1px solid rgba(255,255,255,0.02); }
|
||||
.pair-item.active { background: #1e2329; }
|
||||
.input-group { background: #2b3139; border-radius: 4px; padding: 6px 12px; display: flex; align-items: center; margin-bottom: 6px; border: 1px solid transparent; }
|
||||
.input-group:focus-within { border-color: var(--primary-color); }
|
||||
.input-group input { background: transparent; border: none; color: white; flex: 1; text-align: left; outline: none; font-size: 13px; width: 100%; }
|
||||
.input-group input { background: transparent; border: none; color: white; flex: 1; outline: none; font-size: 13px; width: 100%; }
|
||||
.input-group .label { color: #848e9c; font-size: 11px; margin-right: 10px; }
|
||||
.input-group .unit { color: #848e9c; margin-left: 8px; font-size: 11px; }
|
||||
|
||||
.slider-container { margin: 8px 0 15px; padding: 0 5px; }
|
||||
.slider-labels { display: flex; justify-content: space-between; margin-top: 4px; font-size: 10px; color: #848e9c; }
|
||||
input[type=range] { -webkit-appearance: none; width: 100%; height: 3px; background: #2b3139; border-radius: 2px; outline: none; }
|
||||
input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; width: 14px; height: 14px; background: var(--primary-color); border-radius: 50%; cursor: pointer; border: 2px solid #161a1e; }
|
||||
input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; width: 14px; height: 14px; background: var(--primary-color); border-radius: 50%; cursor: pointer; }
|
||||
.trade-btn { width: 100%; padding: 10px; border-radius: 4px; font-weight: bold; font-size: 14px; border: none; cursor: pointer; margin-top: 5px; }
|
||||
.right-col-tabs { display: flex; border-bottom: 1px solid #2b3139; flex-shrink: 0;}
|
||||
.right-col-tab { flex: 1; text-align: center; padding: 10px 0; font-size: 13px; font-weight: 600; color: #848e9c; cursor: pointer; background: #161a1e; }
|
||||
.right-col-tab.active { color: white; background: #1e2329;}
|
||||
|
||||
/* Futures specific styles */
|
||||
.futures-config { display: flex; gap: 10px; margin-bottom: 10px; }
|
||||
.config-btn { flex: 1; background: #2b3139; color: white; padding: 6px; border-radius: 4px; border: 1px solid transparent; cursor: pointer; font-size: 12px; text-align: center; font-weight: 600; }
|
||||
.trade-actions { display: flex; gap: 12px; margin-top: 8px; }
|
||||
.trade-btn { flex: 1; padding: 10px; border-radius: 4px; font-weight: bold; font-size: 14px; border: none; cursor: pointer; transition: opacity 0.2s; }
|
||||
.btn-long { background: #0ecb81; color: white; }
|
||||
.btn-short { background: #f6465d; color: white; }
|
||||
|
||||
.record-tabs { display: flex; background: #161a1e; border-bottom: 1px solid #2b3139; padding: 0 20px; }
|
||||
.record-tab { padding: 10px 0; margin-right: 25px; font-size: 13px; color: #848e9c; cursor: pointer; position: relative; }
|
||||
.record-tab.active { color: white; border-bottom: 2px solid var(--primary-color); font-weight:bold; }
|
||||
.records-content { min-height: 200px; background: #161a1e; overflow-y: auto; }
|
||||
|
||||
.ob-header { padding: 10px 15px; font-size: 11px; color: #848e9c; display: flex; justify-content: space-between; border-bottom: 1px solid #2b3139; background: #161a1e; }
|
||||
.ob-row { display: flex; justify-content: space-between; padding: 3px 15px; font-size: 12px; position: relative; cursor: pointer; height: 20px; align-items: center; }
|
||||
.ob-bar { position: absolute; right: 0; top: 0; bottom: 0; opacity: 0.1; z-index: 0; }
|
||||
#mid-price { padding: 8px 15px; font-size: 16px; font-weight: 800; text-align: center; border-top: 1px solid #2b3139; border-bottom: 1px solid #2b3139; background: #161a1e; }
|
||||
|
||||
#asks-list { height: 400px; display: flex; flex-direction: column; overflow: hidden; justify-content: flex-end; }
|
||||
#bids-list { height: 400px; display: flex; flex-direction: column; overflow: hidden; justify-content: flex-start; }
|
||||
|
||||
.stats-item { display: flex; flex-direction: column; justify-content: center; }
|
||||
.stats-label { font-size: 10px; color: #848e9c; margin-bottom: 1px; white-space: nowrap; }
|
||||
.stats-value { font-size: 12px; font-weight: 600; color: white; white-space: nowrap; }
|
||||
|
||||
.m-trade-nav { display: none; background: #161a1e; border-bottom: 1px solid #2b3139; position: sticky; top: 0; z-index: 100; }
|
||||
.m-trade-nav a { flex: 1; text-align: center; padding: 14px; font-size: 14px; color: #848e9c; text-decoration: none; border-bottom: 2px solid transparent; }
|
||||
.m-trade-nav a.active { color: var(--primary-color); border-bottom-color: var(--primary-color); font-weight: bold; }
|
||||
|
||||
.m-currency-bar { display: none; overflow-x: auto; background: #161a1e; padding: 10px; gap: 10px; border-bottom: 1px solid #2b3139; scrollbar-width: none; }
|
||||
.m-currency-bar::-webkit-scrollbar { display: none; }
|
||||
.m-coin-item { flex-shrink: 0; background: #2b3139; padding: 6px 12px; border-radius: 4px; display: flex; align-items: center; gap: 6px; border: 1px solid transparent; }
|
||||
.m-coin-item.active { border-color: var(--primary-color); background: rgba(0, 82, 255, 0.1); }
|
||||
.m-coin-item img { width: 16px; height: 16px; border-radius: 50%; }
|
||||
|
||||
.modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 2000; display: none; align-items: center; justify-content: center; }
|
||||
.modal-content { background: #1e2329; width: 90%; max-width: 400px; padding: 25px; border-radius: 12px; }
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.m-trade-nav { display: flex; }
|
||||
.m-currency-bar { display: flex; }
|
||||
.chart-header { gap: 10px; padding: 10px 15px; overflow-x: auto; scrollbar-width: none; }
|
||||
.chart-header::-webkit-scrollbar { display: none; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="m-trade-nav">
|
||||
<a href="spot.php"><?php echo __('nav_spot'); ?></a>
|
||||
<a href="futures.php" class="active"><?php echo __('nav_futures'); ?></a>
|
||||
<a href="options.php"><?php echo __('nav_options'); ?></a>
|
||||
</div>
|
||||
|
||||
<div class="m-currency-bar" id="m-coin-list"></div>
|
||||
|
||||
<div class="trading-container">
|
||||
<div class="left-col d-none d-lg-flex">
|
||||
<div class="category-tabs">
|
||||
<div class="category-tab" onclick="location.href='options.php'"><?php echo $lang == 'zh' ? '秒合约' : 'Options'; ?></div>
|
||||
<div class="category-tab" onclick="location.href='spot.php'"><?php echo $lang == 'zh' ? '现货交易' : 'Spot'; ?></div>
|
||||
<div class="category-tab active" onclick="location.href='futures.php'"><?php echo $lang == 'zh' ? '合约交易' : 'Futures'; ?></div>
|
||||
<div class="trading-page-wrapper">
|
||||
<?php // The real header is included in the file, this is a placeholder for the structure. ?>
|
||||
<div class="trading-container flex-column flex-lg-row">
|
||||
<div class="left-col d-none d-lg-flex flex-column">
|
||||
<div class="category-tabs">
|
||||
<div class="category-tab" onclick="location.href='options.php'"><?php echo __('nav_options'); ?></div>
|
||||
<div class="category-tab" onclick="location.href='spot.php'"><?php echo __('nav_spot'); ?></div>
|
||||
<div class="category-tab active" onclick="location.href='futures.php'"><?php echo __('nav_futures'); ?></div>
|
||||
</div>
|
||||
<div class="search-box">
|
||||
<i class="fas fa-search"></i>
|
||||
<input type="text" id="pair-search" placeholder="<?php echo __('search_contract'); ?>">
|
||||
</div>
|
||||
<div id="pairs-list" class="flex-grow-1" style="overflow-y: auto;"></div>
|
||||
</div>
|
||||
<div class="search-box">
|
||||
<i class="fas fa-search"></i>
|
||||
<input type="text" id="pair-search" placeholder="<?php echo __('search_currency'); ?>">
|
||||
</div>
|
||||
<div id="pairs-list"></div>
|
||||
</div>
|
||||
|
||||
<div class="center-col">
|
||||
<div class="chart-header">
|
||||
<div style="display: flex; align-items: center; gap: 10px; padding-right: 15px; border-right: 1px solid #2b3139;">
|
||||
<img id="curr-icon" src="" class="coin-icon" style="margin:0; width:28px; height:28px;">
|
||||
<span id="curr-pair" style="font-weight: 800; font-size: 16px;">--/-- Perpetual</span>
|
||||
<div class="center-col">
|
||||
<div id="websocket-status"></div>
|
||||
<div class="chart-header">
|
||||
<div style="display: flex; align-items: center; gap: 10px; padding-right: 15px; border-right: 1px solid #2b3139;">
|
||||
<img id="curr-icon" src="" class="coin-icon" style="margin:0; width:28px; height:28px;">
|
||||
<span id="curr-pair" style="font-weight: 800; font-size: 16px;">--/--</span>
|
||||
</div>
|
||||
<div class="stats-item">
|
||||
<div id="curr-price" style="font-size: 18px; font-weight: 800; color: #0ecb81; line-height: 1.2;">--</div>
|
||||
<div id="curr-change" style="font-size: 11px; font-weight: 600;">--</div>
|
||||
</div>
|
||||
<div class="stats-item"><span class="stats-label"><?php echo __('24h_high'); ?></span><span class="stats-value" id="h-high">--</span></div>
|
||||
<div class="stats-item"><span class="stats-label"><?php echo __('24h_low'); ?></span><span class="stats-value" id="h-low">--</span></div>
|
||||
<div class="stats-item"><span class="stats-label"><?php echo __('24h_vol'); ?></span><span class="stats-value" id="h-vol">--</span></div>
|
||||
</div>
|
||||
|
||||
<div class="stats-item">
|
||||
<div id="curr-price" style="font-size: 18px; font-weight: 800; color: #0ecb81; line-height: 1.2;">--</div>
|
||||
<div id="curr-change" style="font-size: 11px; font-weight: 600;">--</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-item">
|
||||
<span class="stats-label"><?php echo __('24h_high'); ?></span>
|
||||
<span class="stats-value" id="h-high">--</span>
|
||||
</div>
|
||||
<div class="stats-item">
|
||||
<span class="stats-label"><?php echo __('24h_low'); ?></span>
|
||||
<span class="stats-value" id="h-low">--</span>
|
||||
</div>
|
||||
<div class="stats-item">
|
||||
<span class="stats-label"><?php echo __('24h_vol'); ?></span>
|
||||
<span class="stats-value" id="h-vol">--</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-box"><div id="tv_chart_container" style="height: 100%;"></div></div>
|
||||
|
||||
<div class="order-box">
|
||||
<div class="futures-config">
|
||||
<div class="config-btn" id="margin-mode"><?php echo __('cross'); ?></div>
|
||||
<div class="config-btn" id="leverage-val">20x</div>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<span class="label"><?php echo __('price'); ?></span>
|
||||
<input type="number" id="futures-price" placeholder="<?php echo __('market'); ?>">
|
||||
<span class="unit">USDT</span>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<span class="label"><?php echo __('amount'); ?></span>
|
||||
<input type="number" id="futures-amount" placeholder="0.00">
|
||||
<span class="unit coin-name">--</span>
|
||||
</div>
|
||||
<div class="slider-container">
|
||||
<input type="range" id="futures-slider" min="0" max="100" value="0" step="1">
|
||||
<div class="slider-labels"><span>0%</span><span>25%</span><span>50%</span><span>75%</span><span>100%</span></div>
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
|
||||
<div class="input-group"><span class="label">TP</span><input type="number" id="futures-tp" placeholder="<?php echo __('price'); ?>"></div>
|
||||
<div class="input-group"><span class="label">SL</span><input type="number" id="futures-sl" placeholder="<?php echo __('price'); ?>"></div>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; font-size: 11px; color: #848e9c; margin-bottom: 8px;">
|
||||
<span><?php echo __('available'); ?></span><span id="futures-avail" style="color:white; font-weight:600">0.00 USDT</span>
|
||||
</div>
|
||||
<div class="trade-actions">
|
||||
<button id="btn-buy-long" class="trade-btn btn-long"><?php echo __('buy_long'); ?></button>
|
||||
<button id="btn-sell-short" class="trade-btn btn-short"><?php echo __('sell_short'); ?></button>
|
||||
<div class="chart-box" id="tv_chart_container"></div>
|
||||
|
||||
<div class="bottom-content-wrapper">
|
||||
<div class="order-box">
|
||||
<div class="futures-config">
|
||||
<div class="config-btn" id="margin-mode"><?php echo __('cross'); ?></div>
|
||||
<div class="config-btn" id="leverage-val">20x</div>
|
||||
</div>
|
||||
<div class="input-group"><span class="label"><?php echo __('price'); ?></span><input type="number" id="futures-price" placeholder="<?php echo __('market'); ?>" disabled><span class="unit">USDT</span></div>
|
||||
<div class="input-group"><span class="label"><?php echo __('amount'); ?></span><input type="number" id="futures-amount" placeholder="0.00"><span class="unit coin-name">--</span></div>
|
||||
<div class="slider-container"><input type="range" id="futures-slider" min="0" max="100" value="0" step="1"></div>
|
||||
<div style="font-size: 11px; color: #848e9c; margin-bottom: 8px;"><span><?php echo __('available'); ?>: </span><span id="futures-avail" style="color:white; font-weight:600">0.00 USDT</span></div>
|
||||
<div class="trade-actions">
|
||||
<button id="btn-buy-long" class="trade-btn btn-long"><?php echo __('open_long'); ?></button>
|
||||
<button id="btn-sell-short" class="trade-btn btn-short"><?php echo __('open_short'); ?></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="records-container">
|
||||
<div class="record-tabs">
|
||||
<div class="record-tab active" data-status="positions"><?php echo __('current_positions'); ?></div>
|
||||
<div class="record-tab" data-status="history"><?php echo __('history_orders'); ?></div>
|
||||
</div>
|
||||
<div id="records-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="record-tabs">
|
||||
<div class="record-tab active" data-status="positions"><?php echo __('positions'); ?></div>
|
||||
<div class="record-tab" data-status="history"><?php echo __('history_orders'); ?></div>
|
||||
<div class="record-tab" data-status="trades"><?php echo __('settled'); ?></div>
|
||||
</div>
|
||||
<div id="records-list" class="records-content"></div>
|
||||
</div>
|
||||
<div class="right-col d-none d-lg-flex">
|
||||
<div class="right-col-tabs">
|
||||
<div class="right-col-tab active" data-tab="ob"><?php echo __('order_book'); ?></div>
|
||||
<div class="right-col-tab" data-tab="trades"><?php echo __('trades'); ?></div>
|
||||
</div>
|
||||
|
||||
<div class="right-col d-none d-lg-flex">
|
||||
<div class="ob-header">
|
||||
<span><?php echo __('price'); ?>(USDT)</span>
|
||||
<span><?php echo __('amount'); ?>(<span class="coin-name">--</span>)</span>
|
||||
<span><?php echo __('total'); ?></span>
|
||||
<div id="ob-panel" class="ob-panel">
|
||||
<div class="ob-header">
|
||||
<span><?php echo __('price'); ?>(USDT)</span>
|
||||
<span><?php echo __('amount'); ?>(<span class="coin-name">--</span>)</span>
|
||||
<span><?php echo __('total'); ?></span>
|
||||
</div>
|
||||
<div class="ob-content">
|
||||
<div id="asks-list"></div>
|
||||
<div id="mid-price">--</div>
|
||||
<div id="bids-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="trades-panel" class="trades-panel" style="display: none;">
|
||||
<div class="trades-header">
|
||||
<span><?php echo __('price'); ?>(USDT)</span>
|
||||
<span><?php echo __('amount'); ?>(<span class="coin-name">--</span>)</span>
|
||||
<span><?php echo __('time'); ?></span>
|
||||
</div>
|
||||
<div id="trades-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="asks-list" style="height: 400px; display: flex; flex-direction: column; overflow: hidden; justify-content: flex-end;"></div>
|
||||
<div id="mid-price" style="color: white;">--</div>
|
||||
<div id="bids-list" style="height: 400px; display: flex; flex-direction: column; overflow: hidden; justify-content: flex-start;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="leverage-modal">
|
||||
<div class="modal-content">
|
||||
<div style="display:flex; justify-content:space-between; margin-bottom:20px;"><span style="font-weight:bold; font-size:16px;"><?php echo __('adjust_leverage'); ?></span><i class="fas fa-times" id="close-leverage-modal" style="cursor:pointer; color:#848e9c"></i></div>
|
||||
<div style="text-align:center; font-size:32px; font-weight:800; color:var(--primary-color); margin-bottom:10px;" id="slider-val">20x</div>
|
||||
<div class="slider-container">
|
||||
<input type="range" id="leverage-range" min="20" max="100" value="20">
|
||||
<div style="display:flex; justify-content:space-between; font-size:12px; color:#848e9c; margin-top:10px;"><span>20x</span><span>100x</span></div>
|
||||
</div>
|
||||
<button class="trade-btn btn-long" style="width:100%;" id="confirm-leverage-btn"><?php echo __('confirm'); ?></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript" src="https://s3.tradingview.com/tv.js"></script>
|
||||
<script>
|
||||
TradingView.onready(function() {
|
||||
const userAssets = <?php echo json_encode($user_assets); ?>;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// --- Globals ---
|
||||
let currentPair = 'BTCUSDT';
|
||||
let currentPrice = 0;
|
||||
let currentLeverage = 20;
|
||||
let marginMode = 'cross';
|
||||
let currentStatus = 'positions';
|
||||
let searchQuery = '';
|
||||
let ws;
|
||||
let balances = {usdt: 0};
|
||||
|
||||
const pairs = ['BTCUSDT', 'ETHUSDT', 'BNBUSDT', 'SOLUSDT', 'XRPUSDT', 'ADAUSDT', 'DOGEUSDT', 'DOTUSDT', 'MATICUSDT', 'LTCUSDT', 'SHIBUSDT', 'TRXUSDT', 'AVAXUSDT', 'LINKUSDT', 'BCHUSDT', 'UNIUSDT', 'ETCUSDT', 'NEARUSDT', 'FILUSDT', 'ALGOUSDT', 'FTMUSDT', 'SANDUSDT', 'MANAUSDT', 'AXSUSDT', 'ATOMUSDT', 'HBARUSDT', 'ICPUSDT', 'VETUSDT'];
|
||||
const marketData = {};
|
||||
const pairs = [
|
||||
'BTCUSDT', 'ETHUSDT', 'BNBUSDT', 'SOLUSDT', 'XRPUSDT', 'ADAUSDT', 'DOGEUSDT', 'AVAXUSDT',
|
||||
'DOTUSDT', 'MATICUSDT', 'LTCUSDT', 'TRXUSDT', 'LINKUSDT', 'UNIUSDT', 'ATOMUSDT', 'ETCUSDT',
|
||||
'BCHUSDT', 'FILUSDT', 'ICPUSDT', 'NEARUSDT', 'AAVEUSDT', 'ALGOUSDT'
|
||||
];
|
||||
|
||||
function getIcon(s) {
|
||||
const symbol = s.replace('USDT', '').toLowerCase();
|
||||
return `https://assets.coincap.io/assets/icons/${symbol}@2x.png`;
|
||||
// --- DOM Elements ---
|
||||
const dom = {
|
||||
websocketStatus: document.getElementById('websocket-status'),
|
||||
currIcon: document.getElementById('curr-icon'),
|
||||
currPair: document.getElementById('curr-pair'),
|
||||
currPrice: document.getElementById('curr-price'),
|
||||
currChange: document.getElementById('curr-change'),
|
||||
hHigh: document.getElementById('h-high'),
|
||||
hLow: document.getElementById('h-low'),
|
||||
hVol: document.getElementById('h-vol'),
|
||||
tvChartContainer: document.getElementById('tv_chart_container'),
|
||||
pairsList: document.getElementById('pairs-list'),
|
||||
pairSearch: document.getElementById('pair-search'),
|
||||
asksList: document.getElementById('asks-list'),
|
||||
bidsList: document.getElementById('bids-list'),
|
||||
midPrice: document.getElementById('mid-price'),
|
||||
tradesContent: document.getElementById('trades-content'),
|
||||
coinNameSpans: document.querySelectorAll('.coin-name'),
|
||||
futuresPriceInput: document.getElementById('futures-price'),
|
||||
availFuturesSpan: document.getElementById('futures-avail')
|
||||
};
|
||||
|
||||
// --- Balance Management ---
|
||||
function updateAvailableBalances() {
|
||||
const usdtBalance = parseFloat(userAssets.USDT || 0).toFixed(2);
|
||||
if (dom.availFuturesSpan) {
|
||||
dom.availFuturesSpan.innerText = `${usdtBalance} USDT`;
|
||||
}
|
||||
}
|
||||
|
||||
function initChart(symbol) {
|
||||
const container = document.getElementById('tv_chart_container');
|
||||
if (container) container.innerHTML = '';
|
||||
new TradingView.widget({
|
||||
"width": "100%",
|
||||
"height": "100%",
|
||||
"symbol": "BINANCE:" + symbol + 'PERP', // Futures pairs often have PERP
|
||||
// --- TradingView Chart ---
|
||||
function initChartWidget(symbol) {
|
||||
if (dom.tvChartContainer.firstChild) {
|
||||
dom.tvChartContainer.innerHTML = '';
|
||||
}
|
||||
const widget = new TradingView.widget({
|
||||
"autosize": true,
|
||||
"symbol": `BINANCE:${symbol}USDT.PERP`,
|
||||
"interval": "15",
|
||||
"timezone": "Etc/UTC",
|
||||
"theme": "dark",
|
||||
"style": "1",
|
||||
"locale": "<?php echo $lang == 'zh' ? 'zh_CN' : 'en'; ?>",
|
||||
"toolbar_bg": "#f1f3f6",
|
||||
"enable_publishing": false,
|
||||
"withdateranges": true,
|
||||
"hide_side_toolbar": true,
|
||||
"allow_symbol_change": false,
|
||||
"container_id": "tv_chart_container",
|
||||
"backgroundColor": "#0b0e11",
|
||||
"hide_side_toolbar": true
|
||||
"studies": [
|
||||
"Volume@tv-basicstudies"
|
||||
],
|
||||
"overrides": {
|
||||
"paneProperties.background": "#161a1e",
|
||||
"paneProperties.vertGridProperties.color": "rgba(255, 255, 255, 0.05)",
|
||||
"paneProperties.horzGridProperties.color": "rgba(255, 255, 255, 0.05)",
|
||||
"scalesProperties.textColor" : "#848e9c",
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --- WebSocket Connection ---
|
||||
function connectWebSocket() {
|
||||
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
|
||||
return;
|
||||
}
|
||||
ws = new WebSocket('wss://fstream.binance.com/stream?streams=' + pairs.map(p => p.toLowerCase() + '@ticker').join('/') + '/' + pairs.map(p => p.toLowerCase() + '@depth20@100ms').join('/'));
|
||||
if (ws) ws.close();
|
||||
const streams = pairs.map(p => `${p.toLowerCase()}@ticker`).concat([`${currentPair.toLowerCase()}@depth20@100ms`, `${currentPair.toLowerCase()}@trade`]);
|
||||
ws = new WebSocket(`wss://stream.binance.com:9443/stream?streams=${streams.join('/')}`);
|
||||
|
||||
ws.onmessage = (e) => {
|
||||
const { stream, data } = JSON.parse(e.data);
|
||||
const symbol = stream.split('@')[0].toUpperCase();
|
||||
|
||||
if (stream.includes('@ticker')) {
|
||||
marketData[symbol] = data;
|
||||
if (symbol === currentPair) {
|
||||
ws.onmessage = (event) => {
|
||||
const { stream, data } = JSON.parse(event.data);
|
||||
if (!data) return;
|
||||
const type = stream.split('@')[1];
|
||||
|
||||
if (type === 'ticker') {
|
||||
marketData[data.s] = data;
|
||||
if (data.s === currentPair) {
|
||||
currentPrice = parseFloat(data.c);
|
||||
updatePriceUI(data);
|
||||
}
|
||||
renderPairs();
|
||||
} else if (symbol === currentPair) {
|
||||
renderOrderBook(data.b, data.a);
|
||||
if (dom.pairsList.offsetParent) renderPairs();
|
||||
} else if (type.startsWith('depth')) {
|
||||
renderOrderBook(data.bids, data.asks);
|
||||
} else if (type === 'trade') {
|
||||
renderTrades(data);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
setTimeout(connectWebSocket, 5000);
|
||||
};
|
||||
ws.onerror = (error) => console.error('WebSocket Error:', error);
|
||||
ws.onclose = () => setTimeout(connectWebSocket, 5000);
|
||||
}
|
||||
|
||||
// --- UI Rendering ---
|
||||
function updatePriceUI(d) {
|
||||
if (!d) return;
|
||||
const color = d.p >= 0 ? '#0ecb81' : '#f6465d';
|
||||
const priceStr = parseFloat(d.c).toLocaleString(undefined, {minimumFractionDigits: 2});
|
||||
document.getElementById('curr-price').innerText = priceStr;
|
||||
document.getElementById('curr-price').style.color = color;
|
||||
document.getElementById('curr-change').innerText = (d.p >= 0 ? '+' : '') + parseFloat(d.P).toFixed(2) + '%';
|
||||
document.getElementById('curr-change').style.color = color;
|
||||
document.getElementById('h-high').innerText = parseFloat(d.h).toLocaleString();
|
||||
document.getElementById('h-low').innerText = parseFloat(d.l).toLocaleString();
|
||||
document.getElementById('h-vol').innerText = (parseFloat(d.v) / 1000).toFixed(1) + 'K';
|
||||
const midPriceEl = document.getElementById('mid-price');
|
||||
if (midPriceEl) {
|
||||
midPriceEl.innerText = priceStr;
|
||||
midPriceEl.style.color = color;
|
||||
}
|
||||
const color = d.P >= 0 ? '#0ecb81' : '#f6465d';
|
||||
const priceStr = parseFloat(d.c).toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2});
|
||||
|
||||
document.title = `${priceStr} | ${d.s.replace('USDT', '/USDT')}`;
|
||||
dom.currPrice.innerText = priceStr;
|
||||
dom.currPrice.style.color = color;
|
||||
dom.currChange.innerText = `${d.P >= 0 ? '+' : ''}${parseFloat(d.P).toFixed(2)}%`;
|
||||
dom.currChange.style.color = color;
|
||||
dom.hHigh.innerText = parseFloat(d.h).toLocaleString('en-US', {minimumFractionDigits: 2});
|
||||
dom.hLow.innerText = parseFloat(d.l).toLocaleString('en-US', {minimumFractionDigits: 2});
|
||||
dom.hVol.innerText = `${(parseFloat(d.v) / 1000).toFixed(1)}K`;
|
||||
dom.midPrice.innerText = priceStr;
|
||||
dom.midPrice.style.color = color;
|
||||
}
|
||||
|
||||
function renderPairs() {
|
||||
const list = document.getElementById('pairs-list');
|
||||
const mBar = document.getElementById('m-coin-list');
|
||||
if (!list || !mBar) return;
|
||||
let lH = '';
|
||||
let mH = '';
|
||||
const filteredPairs = pairs.filter(p => p.toUpperCase().includes(searchQuery));
|
||||
|
||||
filteredPairs.forEach(p => {
|
||||
const d = marketData[p] || {};
|
||||
const icon = getIcon(p);
|
||||
const active = p === currentPair;
|
||||
const color = (d.p && parseFloat(d.p) >= 0) ? '#0ecb81' : '#f6465d';
|
||||
const price = d.c ? parseFloat(d.c).toLocaleString(undefined, {minimumFractionDigits: 2}) : '--';
|
||||
const change = d.P ? ((parseFloat(d.p) >= 0 ? '+' : '') + parseFloat(d.P).toFixed(2) + '%') : '--';
|
||||
const disp = p.replace('USDT', '') + '/USDT';
|
||||
|
||||
lH += `<div class="pair-item ${active ? 'active' : ''}" data-pair="${p}"><img src="${icon}" class="coin-icon" onerror="this.src='https://ui-avatars.com/api/?name=${p.replace('USDT','')}&background=2b3139&color=fff'"> <div style="flex:1"><div style="font-weight:700; font-size:13px">${disp}</div></div> <div style="text-align:right"><div style="font-size:13px; font-weight:bold">${price}</div><div style="font-size:11px; color:${color}">${change}</div></div></div>`;
|
||||
mH += `<div class="m-coin-item ${active ? 'active' : ''}" data-pair="${p}"><img src="${icon}" onerror="this.src='https://ui-avatars.com/api/?name=${p.replace('USDT','')}&background=2b3139&color=fff'"><span>${p.replace('USDT','')}</span></div>`;
|
||||
});
|
||||
|
||||
list.innerHTML = lH;
|
||||
mBar.innerHTML = mH;
|
||||
|
||||
document.querySelectorAll('.pair-item, .m-coin-item').forEach(item => {
|
||||
item.addEventListener('click', () => switchPair(item.dataset.pair));
|
||||
});
|
||||
}
|
||||
|
||||
function filterPairs(e) {
|
||||
searchQuery = e.target.value.toUpperCase();
|
||||
renderPairs();
|
||||
}
|
||||
|
||||
function switchPair(p) {
|
||||
currentPair = p;
|
||||
const coinName = p.replace('USDT', '');
|
||||
const icon = getIcon(p);
|
||||
document.getElementById('curr-pair').innerText = coinName + '/USDT Perpetual';
|
||||
const iconEl = document.getElementById('curr-icon');
|
||||
iconEl.src = icon;
|
||||
iconEl.onerror = function() { this.src = `https://ui-avatars.com/api/?name=${coinName}&background=2b3139&color=fff`; };
|
||||
document.querySelectorAll('.coin-name').forEach(el => el.innerText = coinName);
|
||||
initChart(p);
|
||||
updateBalance();
|
||||
renderPairs();
|
||||
const d = marketData[p] || {c: 0, p: 0, P: 0, h: 0, l: 0, v: 0};
|
||||
updatePriceUI(d);
|
||||
renderOrderBook([], []);
|
||||
}
|
||||
|
||||
function toggleMarginMode() {
|
||||
marginMode = marginMode === 'cross' ? 'isolated' : 'cross';
|
||||
document.getElementById('margin-mode').innerText = marginMode === 'cross' ? '<?php echo __("cross"); ?>' : '<?php echo __("isolated"); ?>';
|
||||
}
|
||||
|
||||
function toggleLeverageModal() {
|
||||
const modal = document.getElementById('leverage-modal');
|
||||
modal.style.display = modal.style.display === 'flex' ? 'none' : 'flex';
|
||||
}
|
||||
|
||||
function updateSliderVal(v) {
|
||||
document.getElementById('slider-val').innerText = v + 'x';
|
||||
}
|
||||
|
||||
function confirmLeverage() {
|
||||
currentLeverage = document.getElementById('leverage-range').value;
|
||||
document.getElementById('leverage-val').innerText = currentLeverage + 'x';
|
||||
toggleLeverageModal();
|
||||
}
|
||||
|
||||
function setSlider(value) {
|
||||
const pct = value / 100;
|
||||
const price = parseFloat(document.getElementById('futures-price').value) || currentPrice;
|
||||
if (price > 0) {
|
||||
document.getElementById('futures-amount').value = (balances.usdt * pct * currentLeverage / price).toFixed(4);
|
||||
}
|
||||
}
|
||||
|
||||
async function placeOrder(side) {
|
||||
const price = parseFloat(document.getElementById('futures-price').value) || currentPrice;
|
||||
const amount = parseFloat(document.getElementById('futures-amount').value);
|
||||
if (!amount || amount <= 0) return alert('Amount Error');
|
||||
const query = dom.pairSearch.value.toUpperCase();
|
||||
const filteredPairs = pairs.filter(p => p.includes(query));
|
||||
|
||||
<?php if (!$user_id): ?>
|
||||
alert('<?php echo __('login_to_trade'); ?>');
|
||||
window.location.href = 'login.php';
|
||||
return;
|
||||
<?php endif; ?>
|
||||
dom.pairsList.innerHTML = filteredPairs.map(p => {
|
||||
const d = marketData[p] || {};
|
||||
const icon = `https://assets.coincap.io/assets/icons/${p.replace('USDT','').toLowerCase()}@2x.png`;
|
||||
const price = d.c ? parseFloat(d.c).toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2}) : '--';
|
||||
const change = d.P ? `${parseFloat(d.P) >= 0 ? '+' : ''}${parseFloat(d.P).toFixed(2)}%` : '--';
|
||||
const color = (d.P && parseFloat(d.P) >= 0) ? '#0ecb81' : '#f6465d';
|
||||
|
||||
try {
|
||||
const resp = await fetch('api/place_order.php', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
symbol: currentPair, type: 'futures', side: side, order_type: 'market',
|
||||
price: price, amount: amount, total: price * amount, leverage: currentLeverage,
|
||||
tp_price: document.getElementById('futures-tp').value, sl_price: document.getElementById('futures-sl').value
|
||||
})
|
||||
});
|
||||
const res = await resp.json();
|
||||
if (res.success) {
|
||||
alert('<?php echo __('order_placed'); ?>');
|
||||
updateBalance();
|
||||
fetchOrders();
|
||||
} else {
|
||||
alert(res.error || 'An unknown error occurred.');
|
||||
}
|
||||
} catch (e) {
|
||||
alert('<?php echo __('network_error'); ?>');
|
||||
}
|
||||
return `
|
||||
<div class="pair-item ${p === currentPair ? 'active' : ''}" data-pair="${p}">
|
||||
<img src="${icon}" class="coin-icon" onerror="this.style.display='none'">
|
||||
<div style="flex:1"><div style="font-weight:600; font-size:13px">${p.replace('USDT', '')}<span style="color:#848e9c">/USDT</span></div></div>
|
||||
<div style="text-align:right">
|
||||
<div style="font-size:13px; font-weight:600">${price}</div>
|
||||
<div style="font-size:11px; color:${color}">${change}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderOrderBook(bids, asks) {
|
||||
const asksList = document.getElementById('asks-list');
|
||||
const bidsList = document.getElementById('bids-list');
|
||||
if (!asksList || !bidsList) return;
|
||||
let asksHtml = '';
|
||||
let bidsHtml = '';
|
||||
const renderList = (element, data, isAsks) => {
|
||||
if (!element) return;
|
||||
const maxRows = Math.floor(element.offsetHeight / 20);
|
||||
if (maxRows <= 0) return;
|
||||
|
||||
asks.slice(0, 20).reverse().forEach(([price, quantity]) => {
|
||||
const p = parseFloat(price);
|
||||
const q = parseFloat(quantity);
|
||||
asksHtml += `<div class="ob-row"><div class="ob-bar" style="width:${q * 2}%; background:#f6465d"></div><span style="color:#f6465d">${p.toFixed(2)}</span><span>${q.toFixed(4)}</span><span>${(p * q).toFixed(2)}</span></div>`;
|
||||
});
|
||||
const relevantData = isAsks ? data.slice(0, maxRows).reverse() : data.slice(0, maxRows);
|
||||
const maxVol = Math.max(...relevantData.map(([, q]) => parseFloat(q))) || 1;
|
||||
|
||||
bids.slice(0, 20).forEach(([price, quantity]) => {
|
||||
const p = parseFloat(price);
|
||||
const q = parseFloat(quantity);
|
||||
bidsHtml += `<div class="ob-row"><div class="ob-bar" style="width:${q * 2}%; background:#0ecb81"></div><span style="color:#0ecb81">${p.toFixed(2)}</span><span>${q.toFixed(4)}</span><span>${(p * q).toFixed(2)}</span></div>`;
|
||||
});
|
||||
element.innerHTML = relevantData.map(([price, quantity]) => {
|
||||
const p = parseFloat(price), q = parseFloat(quantity);
|
||||
const barWidth = (q / maxVol) * 100;
|
||||
const color = isAsks ? '#f6465d' : '#0ecb81';
|
||||
const bgColor = isAsks ? 'rgba(246, 70, 93, 0.2)' : 'rgba(14, 203, 129, 0.2)';
|
||||
return `
|
||||
<div class="ob-row" onclick="setPrice(${p})">
|
||||
<div class="ob-bar" style="width:${barWidth}%; background-color:${bgColor};"></div>
|
||||
<span style="color:${color}; z-index:1;">${p.toFixed(2)}</span>
|
||||
<span style="z-index:1;">${q.toFixed(4)}</span>
|
||||
<span style="z-index:1;">${(p * q).toFixed(2)}</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
};
|
||||
renderList(dom.asksList, asks, true);
|
||||
renderList(dom.bidsList, bids, false);
|
||||
}
|
||||
|
||||
function renderTrades(trade) {
|
||||
if (!dom.tradesContent) return;
|
||||
const color = trade.m ? '#f6465d' : '#0ecb81';
|
||||
const time = new Date(trade.T).toLocaleTimeString('en-GB');
|
||||
const row = `
|
||||
<div class="trade-row">
|
||||
<span style="color:${color}">${parseFloat(trade.p).toFixed(2)}</span>
|
||||
<span>${parseFloat(trade.q).toFixed(4)}</span>
|
||||
<span>${time}</span>
|
||||
</div>`;
|
||||
dom.tradesContent.insertAdjacentHTML('afterbegin', row);
|
||||
if (dom.tradesContent.children.length > 50) {
|
||||
dom.tradesContent.removeChild(dom.tradesContent.lastChild);
|
||||
}
|
||||
}
|
||||
|
||||
function switchPair(pair) {
|
||||
currentPair = pair;
|
||||
const coinName = pair.replace('USDT', '');
|
||||
|
||||
asksList.innerHTML = asksHtml;
|
||||
bidsList.innerHTML = bidsHtml;
|
||||
dom.currPair.innerText = `${coinName}/USDT Perpetual`;
|
||||
dom.currIcon.src = `https://assets.coincap.io/assets/icons/${coinName.toLowerCase()}@2x.png`;
|
||||
dom.coinNameSpans.forEach(el => el.innerText = coinName);
|
||||
|
||||
updatePriceUI(marketData[pair]);
|
||||
initChartWidget(pair);
|
||||
renderPairs();
|
||||
|
||||
dom.asksList.innerHTML = '';
|
||||
dom.bidsList.innerHTML = '';
|
||||
dom.tradesContent.innerHTML = '';
|
||||
connectWebSocket();
|
||||
}
|
||||
|
||||
window.setPrice = (price) => {
|
||||
if(dom.futuresPriceInput) dom.futuresPriceInput.value = price;
|
||||
}
|
||||
|
||||
async function updateBalance() {
|
||||
<?php if (!$user_id) return; ?>
|
||||
try {
|
||||
const resp = await fetch('api/get_assets.php');
|
||||
const res = await resp.json();
|
||||
if (res.success) {
|
||||
const usdt = res.data.find(a => a.symbol === 'USDT');
|
||||
balances.usdt = usdt ? parseFloat(usdt.amount) : 0;
|
||||
document.getElementById('futures-avail').innerText = balances.usdt.toFixed(2) + ' USDT';
|
||||
} else {
|
||||
console.error('Failed to fetch balance:', res.error);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Network error fetching balance:', e);
|
||||
// --- Event Listeners ---
|
||||
dom.pairsList.addEventListener('click', (e) => {
|
||||
const pairItem = e.target.closest('.pair-item');
|
||||
if (pairItem && pairItem.dataset.pair) {
|
||||
switchPair(pairItem.dataset.pair);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
dom.pairSearch.addEventListener('input', renderPairs);
|
||||
|
||||
async function fetchOrders() {
|
||||
<?php if (!$user_id) return; ?>
|
||||
try {
|
||||
const resp = await fetch(`api/get_orders.php?type=futures&status=${currentStatus}`);
|
||||
const res = await resp.json();
|
||||
const listEl = document.getElementById('records-list');
|
||||
if (!listEl) return;
|
||||
let html = '';
|
||||
if (res.success && res.data.length > 0) {
|
||||
html = res.data.map(o => `
|
||||
<div style="padding:15px; border-bottom:1px solid #2b3139; font-size:12px">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center">
|
||||
<span style="color:${o.side === 'buy' ? '#0ecb81' : '#f6465d'}; font-weight:bold; font-size:14px">${o.side === 'buy' ? 'Long' : 'Short'} ${o.symbol} ${o.leverage}x</span>
|
||||
<span style="color:#848e9c">${o.created_at}</span>
|
||||
</div>
|
||||
<div style="display:grid; grid-template-columns:1fr 1fr 1fr 1fr; gap:10px; color:#848e9c; margin-top:10px">
|
||||
<div><?php echo __('size'); ?><br><span style="color:white; font-weight:bold">${o.amount}</span></div>
|
||||
<div><?php echo __('price'); ?><br><span style="color:white; font-weight:bold">${parseFloat(o.price).toFixed(2)}</span></div>
|
||||
<div><?php echo __('margin'); ?><br><span style="color:white; font-weight:bold">${(o.total / o.leverage).toFixed(2)}</span></div>
|
||||
<div>TP/SL<br><span style="color:white; font-weight:bold">${o.tp_price || '--'}/${o.sl_price || '--'}</span></div>
|
||||
</div>
|
||||
</div>`).join('');
|
||||
} else {
|
||||
html = '<div style="padding:40px; text-align:center; color:#848e9c"><?php echo __('no_records'); ?></div>';
|
||||
}
|
||||
listEl.innerHTML = html;
|
||||
} catch (e) {
|
||||
console.error('Error fetching orders:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function switchRecords(status) {
|
||||
currentStatus = status;
|
||||
document.querySelector('.record-tabs .record-tab.active').classList.remove('active');
|
||||
document.querySelector(`.record-tabs .record-tab[data-status="${status}"]`).classList.add('active');
|
||||
fetchOrders();
|
||||
}
|
||||
|
||||
// ===== Main Execution =====
|
||||
|
||||
// 1. Add Event Listeners
|
||||
document.getElementById('pair-search').addEventListener('input', filterPairs);
|
||||
document.getElementById('margin-mode').addEventListener('click', toggleMarginMode);
|
||||
document.getElementById('leverage-val').addEventListener('click', toggleLeverageModal);
|
||||
document.getElementById('close-leverage-modal').addEventListener('click', toggleLeverageModal);
|
||||
document.getElementById('leverage-range').addEventListener('input', (e) => updateSliderVal(e.target.value));
|
||||
document.getElementById('confirm-leverage-btn').addEventListener('click', confirmLeverage);
|
||||
document.getElementById('futures-slider').addEventListener('input', (e) => setSlider(e.target.value));
|
||||
document.getElementById('btn-buy-long').addEventListener('click', () => placeOrder('buy'));
|
||||
document.getElementById('btn-sell-short').addEventListener('click', () => placeOrder('sell'));
|
||||
document.querySelectorAll('.record-tabs .record-tab').forEach(tab => {
|
||||
tab.addEventListener('click', () => switchRecords(tab.dataset.status));
|
||||
document.querySelectorAll('.right-col-tab').forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
document.querySelector('.right-col-tab.active').classList.remove('active');
|
||||
tab.classList.add('active');
|
||||
const isOb = tab.dataset.tab === 'ob';
|
||||
document.getElementById('ob-panel').style.display = isOb ? 'flex' : 'none';
|
||||
document.getElementById('trades-panel').style.display = isOb ? 'none' : 'flex';
|
||||
});
|
||||
});
|
||||
|
||||
// 2. Initial Render
|
||||
switchPair(currentPair);
|
||||
renderOrderBook([], []);
|
||||
|
||||
// 3. Fetch Initial Data
|
||||
updateBalance();
|
||||
fetchOrders();
|
||||
|
||||
// 4. Initialize Chart & WebSocket
|
||||
initChart(currentPair);
|
||||
connectWebSocket();
|
||||
|
||||
// 5. Setup Intervals
|
||||
setInterval(updateBalance, 5000);
|
||||
setInterval(fetchOrders, 10000);
|
||||
setInterval(() => {
|
||||
if (!ws || ws.readyState === WebSocket.CLOSED) {
|
||||
connectWebSocket();
|
||||
}
|
||||
}, 10000);
|
||||
// --- Initial Load ---
|
||||
updateAvailableBalances();
|
||||
switchPair('BTCUSDT');
|
||||
});
|
||||
</script>
|
||||
<?php include 'footer.php'; ?>
|
||||
<script type="text/javascript" src="https://s3.tradingview.com/tv.js"></script>
|
||||
|
||||
|
||||
<?php include 'footer.php'; ?>
|
||||
@ -1,4 +1,8 @@
|
||||
<?php
|
||||
date_default_timezone_set('Asia/Dubai');
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
require_once 'includes/i18n.php';
|
||||
require_once 'db/config.php';
|
||||
$db = db();
|
||||
|
||||
@ -13,7 +13,7 @@ $translations = [
|
||||
'nav_trade' => 'Trade',
|
||||
'nav_spot' => 'Spot',
|
||||
'nav_futures' => 'Futures',
|
||||
'nav_options' => 'Second Contract',
|
||||
'nav_options' => 'Options',
|
||||
'nav_mining' => 'Mining',
|
||||
'nav_convert' => 'Convert',
|
||||
'nav_assets' => 'Assets',
|
||||
@ -141,6 +141,10 @@ $translations = [
|
||||
'system_matching_engine' => 'System matching engine',
|
||||
'options_instruction' => 'Trading Instruction',
|
||||
'options_wait_settle' => 'The system matching engine is executing. Please wait for the countdown to end for settlement.',
|
||||
'current_price' => 'Current Price',
|
||||
'order_date' => 'Order Date',
|
||||
'options_order_executing' => 'Order is executing, settlement upon countdown completion.',
|
||||
'options_order_settled' => 'Order settled.',
|
||||
|
||||
'kyc_status' => 'KYC Verification',
|
||||
'kyc_none' => 'Unverified',
|
||||
@ -209,7 +213,7 @@ $translations = [
|
||||
'error_fetching_orders' => 'Error fetching orders',
|
||||
'error_fetching_balance' => 'Error fetching balance',
|
||||
'error_no_live_price' => 'No live price data available yet. Please wait.',
|
||||
'websocket_connecting' => 'Connecting to market data...',
|
||||
'websocket_connecting' => 'Connecting to market data...',
|
||||
'websocket_connected' => 'Connected to market data.',
|
||||
'websocket_error' => 'Market data connection error!',
|
||||
'websocket_disconnected' => 'Market data disconnected. Reconnecting...',
|
||||
@ -351,7 +355,11 @@ $translations = [
|
||||
'system_matching_engine' => '系统撮合引擎',
|
||||
'options_instruction' => '交易说明',
|
||||
'options_wait_settle' => '系统撮合引擎正在执行中,请等待倒计时结束进行结算。',
|
||||
|
||||
'current_price' => '当前价格',
|
||||
'order_date' => '下单日期',
|
||||
'options_order_executing' => '订单执行中,倒计时结束后结算。',
|
||||
'options_order_settled' => '订单已结算。',
|
||||
|
||||
'kyc_status' => '实名认证',
|
||||
'kyc_none' => '未认证',
|
||||
'kyc_pending' => '审核中',
|
||||
@ -419,7 +427,7 @@ $translations = [
|
||||
'error_fetching_orders' => '获取订单失败',
|
||||
'error_fetching_balance' => '获取余额失败',
|
||||
'error_no_live_price' => '暂无实时价格数据,请稍候。',
|
||||
'websocket_connecting' => '正在连接市场数据...',
|
||||
'websocket_connecting' => '正在连接市场数据...',
|
||||
'websocket_connected' => '市场数据已连接。',
|
||||
'websocket_error' => '市场数据连接错误!',
|
||||
'websocket_disconnected' => '市场数据已断开。正在重新连接...',
|
||||
@ -427,7 +435,14 @@ $translations = [
|
||||
];
|
||||
|
||||
if (!isset($_SESSION['lang'])) {
|
||||
$_SESSION['lang'] = 'en';
|
||||
// Detect browser language
|
||||
$browser_lang = !empty($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? strtok(strip_tags($_SERVER['HTTP_ACCEPT_LANGUAGE']), ',') : '';
|
||||
$browser_lang = substr($browser_lang, 0, 2);
|
||||
if ($browser_lang === 'zh') {
|
||||
$_SESSION['lang'] = 'zh';
|
||||
} else {
|
||||
$_SESSION['lang'] = 'en';
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_GET['lang']) && array_key_exists($_GET['lang'], $translations)) {
|
||||
|
||||
976
options.php
976
options.php
File diff suppressed because it is too large
Load Diff
845
spot.php
845
spot.php
@ -1,576 +1,477 @@
|
||||
<?php
|
||||
include 'header.php';
|
||||
require_once 'db/config.php';
|
||||
require_once 'includes/i18n.php';
|
||||
|
||||
$user_id = $_SESSION['user_id'] ?? null;
|
||||
$user_assets = [];
|
||||
if ($user_id) {
|
||||
$pdo = db();
|
||||
$stmt = $pdo->prepare("SELECT currency, balance FROM assets WHERE user_id = ?");
|
||||
$stmt->execute([$user_id]);
|
||||
$assets_data = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||
if ($assets_data) {
|
||||
$user_assets = $assets_data;
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<style>
|
||||
/* Global layout enforcement */
|
||||
/* New Robust Layout */
|
||||
html, body {
|
||||
background: #0b0e11;
|
||||
min-width: 1280px;
|
||||
max-width: 1920px;
|
||||
margin: 0 auto;
|
||||
overflow-x: auto;
|
||||
/* height: 100%; */
|
||||
/* overflow: hidden; */ /* UNLOCKED for scrolling */
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
html, body {
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.navbar, footer { display: none !important; }
|
||||
body { padding-bottom: 0 !important; }
|
||||
}
|
||||
|
||||
.trading-container {
|
||||
.trading-page-wrapper {
|
||||
/* height: 100vh; */ /* UNLOCKED for scrolling */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh; /* Ensure it takes at least full height */
|
||||
}
|
||||
.trading-page-wrapper .navbar {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.trading-container {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
min-height: 800px; /* Give a minimum height for content */
|
||||
background: #0b0e11;
|
||||
min-height: calc(100vh - 64px);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.trading-container { min-height: calc(100vh - 50px); flex-direction: column; }
|
||||
}
|
||||
|
||||
|
||||
/* Columns */
|
||||
.left-col { width: 260px; flex-shrink: 0; background: #161a1e; display: flex; flex-direction: column; border-right: 1px solid #2b3139; }
|
||||
.center-col { flex: 1; min-width: 600px; background: #0b0e11; display: flex; flex-direction: column; border-right: 1px solid #2b3139; }
|
||||
.center-col { flex: 1; min-width: 600px; display: flex; flex-direction: column; border-right: 1px solid #2b3139; min-height: 0; }
|
||||
.right-col { width: 320px; flex-shrink: 0; background: #161a1e; display: flex; flex-direction: column; }
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.left-col, .right-col { display: none !important; }
|
||||
.center-col { min-width: 100%; }
|
||||
}
|
||||
/* Center Column Content Layout */
|
||||
.chart-header { flex-shrink: 0; height: 50px; padding: 8px 20px; display: flex; align-items: center; background: #161a1e; border-bottom: 1px solid #2b3139; gap: 20px; }
|
||||
.chart-box { height: 350px; flex-shrink: 0; border-bottom: 1px solid #2b3139; }
|
||||
.bottom-content-wrapper { flex-grow: 1; overflow-y: auto; min-height: 0; }
|
||||
|
||||
/* Category Tabs above Search */
|
||||
.order-box { padding: 12px 20px; background: #161a1e; border-bottom: 1px solid #2b3139; }
|
||||
.records-container { display: flex; flex-direction: column; background: #161a1e; flex-grow: 1; min-height: 400px; }
|
||||
.record-tabs { display: flex; padding: 0 20px; flex-shrink: 0; border-bottom: 1px solid #2b3139; }
|
||||
.record-tab { padding: 12px 0; margin-right: 25px; font-size: 14px; font-weight: 600; color: #848e9c; cursor: pointer; border-bottom: 2px solid transparent; }
|
||||
.record-tab.active { color: white; border-bottom-color: var(--primary-color); }
|
||||
#records-list { padding: 10px 0; flex-grow: 1; overflow-y: auto; }
|
||||
|
||||
.right-col { display: flex; flex-direction: column;}
|
||||
.ob-panel, .trades-panel { flex-grow: 1; min-height: 0; display: flex; flex-direction: column;}
|
||||
.ob-content { flex: 1; min-height: 0; display: flex; flex-direction: column; }
|
||||
#asks-list { flex: 1 1 50%; display: flex; flex-direction: column; overflow: hidden; justify-content: flex-end; }
|
||||
#bids-list { flex: 1 1 50%; display: flex; flex-direction: column; overflow: hidden; justify-content: flex-start; }
|
||||
#trades-content { flex: 1; overflow-y: auto; min-height: 0; }
|
||||
|
||||
.left-col { overflow-y: auto; }
|
||||
#pairs-list { flex: 1; overflow-y: auto; }
|
||||
.ob-header, .trades-header { padding: 10px 15px; font-size: 12px; color: #848e9c; display: flex; justify-content: space-between; border-bottom: 1px solid #2b3139; background: #161a1e; flex-shrink: 0;}
|
||||
.ob-row, .trade-row { display: flex; justify-content: space-between; padding: 3px 15px; font-size: 12px; position: relative; cursor: pointer; height: 20px; align-items: center; flex-shrink: 0; }
|
||||
.ob-bar { position: absolute; right: 0; top: 0; bottom: 0; opacity: 0.1; z-index: 0; transition: width 0.1s linear; }
|
||||
#mid-price { padding: 8px 15px; font-size: 16px; font-weight: 800; text-align: center; border-top: 1px solid #2b3139; border-bottom: 1px solid #2b3139; background: #161a1e; flex-shrink: 0;}
|
||||
.coin-icon { width: 22px; height: 22px; margin-right: 12px; border-radius: 50%; }
|
||||
.stats-item { display: flex; flex-direction: column; justify-content: center; }
|
||||
.stats-label { font-size: 10px; color: #848e9c; margin-bottom: 1px; }
|
||||
.stats-value { font-size: 12px; font-weight: 600; color: white; }
|
||||
#websocket-status { display: none; /* Hiding this element as requested */ padding: 8px 15px; font-size: 12px; color: #fff; text-align: center; position: absolute; top: 0; left: 0; right: 0; z-index: 1000; }
|
||||
|
||||
.category-tabs { display: flex; padding: 15px 15px 5px; gap: 8px; }
|
||||
.category-tab { flex: 1; text-align: center; padding: 6px 0; background: #2b3139; border-radius: 4px; font-size: 11px; color: #848e9c; cursor: pointer; transition: all 0.2s; white-space: nowrap; border: 1px solid transparent; }
|
||||
.category-tab { flex: 1; text-align: center; padding: 6px 0; background: #2b3139; border-radius: 4px; font-size: 12px; color: #848e9c; cursor: pointer; transition: all 0.2s; border: 1px solid transparent; }
|
||||
.category-tab.active { background: rgba(0, 82, 255, 0.1); border-color: var(--primary-color); color: var(--primary-color); font-weight: bold; }
|
||||
|
||||
/* Search Box */
|
||||
.search-box { padding: 5px 15px 10px; position: relative; }
|
||||
.search-box i { position: absolute; left: 25px; top: 50%; transform: translateY(-50%); color: #848e9c; font-size: 12px; }
|
||||
.search-box input { width: 100%; background: #2b3139; border: 1px solid transparent; border-radius: 4px; padding: 8px 10px 8px 30px; color: white; font-size: 13px; outline: none; }
|
||||
.search-box input:focus { border-color: var(--primary-color); }
|
||||
|
||||
#pairs-list { flex: 1; overflow-y: auto; }
|
||||
.pair-item { display: flex; align-items: center; padding: 10px 15px; cursor: pointer; transition: all 0.2s; border-bottom: 1px solid rgba(255,255,255,0.02); }
|
||||
.pair-item:hover { background: #1e2329; }
|
||||
.pair-item.active { background: #1e2329; border-left: 3px solid var(--primary-color); }
|
||||
.coin-icon { width: 22px; height: 22px; margin-right: 12px; border-radius: 50%; flex-shrink: 0; background: #2b3139; }
|
||||
|
||||
.chart-header { padding: 8px 20px; display: flex; align-items: center; background: #161a1e; border-bottom: 1px solid #2b3139; flex-wrap: nowrap; gap: 20px; height: 50px; }
|
||||
.chart-box { flex: 1; min-height: 350px; height: 350px; background: #0b0e11; border-bottom: 1px solid #2b3139; }
|
||||
@media (max-width: 991px) { .chart-box { min-height: 350px; height: 350px; } }
|
||||
|
||||
.order-box { padding: 12px 20px; background: #161a1e; border-bottom: 1px solid #2b3139; }
|
||||
.pair-item { display: flex; align-items: center; padding: 10px 15px; cursor: pointer; transition: background 0.2s; border-bottom: 1px solid rgba(255,255,255,0.02); }
|
||||
.pair-item.active { background: #1e2329; }
|
||||
.order-type-tabs { display: flex; gap: 20px; margin-bottom: 10px; }
|
||||
.order-type-tab { font-size: 13px; color: #848e9c; cursor: pointer; padding-bottom: 4px; border-bottom: 2px solid transparent; }
|
||||
.order-type-tab.active { color: white; border-bottom-color: var(--primary-color); font-weight: bold; }
|
||||
|
||||
.input-group { background: #2b3139; border-radius: 4px; padding: 6px 12px; display: flex; align-items: center; margin-bottom: 6px; border: 1px solid transparent; }
|
||||
.input-group:focus-within { border-color: var(--primary-color); }
|
||||
.input-group input { background: transparent; border: none; color: white; flex: 1; text-align: left; outline: none; font-size: 13px; width: 100%; }
|
||||
.input-group input { background: transparent; border: none; color: white; flex: 1; outline: none; font-size: 13px; width: 100%; }
|
||||
.input-group .label { color: #848e9c; font-size: 11px; margin-right: 10px; }
|
||||
.input-group .unit { color: #848e9c; margin-left: 8px; font-size: 11px; }
|
||||
|
||||
.slider-container { margin: 8px 0 15px; padding: 0 5px; }
|
||||
.slider-labels { display: flex; justify-content: space-between; margin-top: 4px; font-size: 10px; color: #848e9c; }
|
||||
input[type=range] { -webkit-appearance: none; width: 100%; height: 3px; background: #2b3139; border-radius: 2px; outline: none; }
|
||||
input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; width: 14px; height: 14px; background: var(--primary-color); border-radius: 50%; cursor: pointer; border: 2px solid #161a1e; }
|
||||
|
||||
input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; width: 14px; height: 14px; background: var(--primary-color); border-radius: 50%; cursor: pointer; }
|
||||
.trade-btn { width: 100%; padding: 10px; border-radius: 4px; font-weight: bold; font-size: 14px; border: none; cursor: pointer; margin-top: 5px; }
|
||||
.btn-buy { background: #0ecb81; color: white; }
|
||||
.btn-sell { background: #f6465d; color: white; }
|
||||
|
||||
.record-tabs { display: flex; background: #161a1e; border-bottom: 1px solid #2b3139; padding: 0 20px; }
|
||||
.record-tab { padding: 10px 0; margin-right: 25px; font-size: 13px; color: #848e9c; cursor: pointer; position: relative; }
|
||||
.record-tab.active { color: white; border-bottom: 2px solid var(--primary-color); font-weight: bold; }
|
||||
.records-content { min-height: 200px; background: #161a1e; overflow-y: auto; }
|
||||
|
||||
/* Order Book Styles */
|
||||
.ob-header { padding: 10px 15px; font-size: 11px; color: #848e9c; display: flex; justify-content: space-between; border-bottom: 1px solid #2b3139; background: #161a1e; }
|
||||
.ob-row { display: flex; justify-content: space-between; padding: 3px 15px; font-size: 12px; position: relative; cursor: pointer; height: 20px; align-items: center; }
|
||||
.ob-bar { position: absolute; right: 0; top: 0; bottom: 0; opacity: 0.1; z-index: 0; }
|
||||
#mid-price { padding: 8px 15px; font-size: 16px; font-weight: 800; text-align: center; border-top: 1px solid #2b3139; border-bottom: 1px solid #2b3139; background: #161a1e; }
|
||||
|
||||
#asks-list { height: 400px; display: flex; flex-direction: column; overflow: hidden; justify-content: flex-end; }
|
||||
#bids-list { height: 400px; display: flex; flex-direction: column; overflow: hidden; justify-content: flex-start; }
|
||||
|
||||
/* Stats Item */
|
||||
.stats-item { display: flex; flex-direction: column; justify-content: center; }
|
||||
.stats-label { font-size: 10px; color: #848e9c; margin-bottom: 1px; white-space: nowrap; }
|
||||
.stats-value { font-size: 12px; font-weight: 600; color: white; white-space: nowrap; }
|
||||
|
||||
/* Mobile Nav */
|
||||
.m-trade-nav { display: none; background: #161a1e; border-bottom: 1px solid #2b3139; position: sticky; top: 0; z-index: 100; }
|
||||
.m-trade-nav a { flex: 1; text-align: center; padding: 14px; font-size: 14px; color: #848e9c; text-decoration: none; border-bottom: 2px solid transparent; }
|
||||
.m-trade-nav a.active { color: var(--primary-color); border-bottom-color: var(--primary-color); font-weight: bold; }
|
||||
|
||||
.m-currency-bar { display: none; overflow-x: auto; background: #161a1e; padding: 10px; gap: 10px; border-bottom: 1px solid #2b3139; scrollbar-width: none; }
|
||||
.m-currency-bar::-webkit-scrollbar { display: none; }
|
||||
.m-coin-item { flex-shrink: 0; background: #2b3139; padding: 6px 12px; border-radius: 4px; display: flex; align-items: center; gap: 6px; border: 1px solid transparent; }
|
||||
.m-coin-item.active { border-color: var(--primary-color); background: rgba(0, 82, 255, 0.1); }
|
||||
.m-coin-item img { width: 16px; height: 16px; border-radius: 50%; }
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.m-trade-nav { display: flex; }
|
||||
.m-currency-bar { display: flex; }
|
||||
.chart-header { gap: 10px; padding: 10px 15px; overflow-x: auto; scrollbar-width: none; }
|
||||
.chart-header::-webkit-scrollbar { display: none; }
|
||||
}
|
||||
.right-col-tabs { display: flex; border-bottom: 1px solid #2b3139; flex-shrink: 0;}
|
||||
.right-col-tab { flex: 1; text-align: center; padding: 10px 0; font-size: 13px; font-weight: 600; color: #848e9c; cursor: pointer; background: #161a1e; }
|
||||
.right-col-tab.active { color: white; background: #1e2329;}
|
||||
</style>
|
||||
|
||||
<div class="m-trade-nav">
|
||||
<a href="spot.php" class="active"><?php echo __('nav_spot'); ?></a>
|
||||
<a href="futures.php"><?php echo __('nav_futures'); ?></a>
|
||||
<a href="options.php"><?php echo __('nav_options'); ?></a>
|
||||
</div>
|
||||
|
||||
<div class="m-currency-bar" id="m-coin-list"></div>
|
||||
|
||||
<div class="trading-container">
|
||||
<div class="left-col d-none d-lg-flex">
|
||||
<div class="category-tabs">
|
||||
<div class="category-tab" onclick="location.href='options.php'"><?php echo $lang == 'zh' ? '秒合约' : 'Options'; ?></div>
|
||||
<div class="category-tab active" onclick="location.href='spot.php'"><?php echo $lang == 'zh' ? '现货交易' : 'Spot'; ?></div>
|
||||
<div class="category-tab" onclick="location.href='futures.php'"><?php echo $lang == 'zh' ? '合约交易' : 'Futures'; ?></div>
|
||||
<div class="trading-page-wrapper">
|
||||
<?php // The real header is included in the file, this is a placeholder for the structure. We will inject the actual header.php content. ?>
|
||||
<div class="trading-container">
|
||||
<div class="left-col d-none d-lg-flex flex-column">
|
||||
<div class="category-tabs">
|
||||
<div class="category-tab" onclick="location.href='options.php'"><?php echo __('nav_options'); ?></div>
|
||||
<div class="category-tab active" onclick="location.href='spot.php'"><?php echo __('nav_spot'); ?></div>
|
||||
<div class="category-tab" onclick="location.href='futures.php'"><?php echo __('nav_futures'); ?></div>
|
||||
</div>
|
||||
<div class="search-box">
|
||||
<i class="fas fa-search"></i>
|
||||
<input type="text" id="pair-search" placeholder="<?php echo __('search_currency'); ?>">
|
||||
</div>
|
||||
<div id="pairs-list" class="flex-grow-1" style="overflow-y: auto;"></div>
|
||||
</div>
|
||||
<div class="search-box">
|
||||
<i class="fas fa-search"></i>
|
||||
<input type="text" id="pair-search" placeholder="<?php echo __('search_currency'); ?>">
|
||||
</div>
|
||||
<div id="pairs-list"></div>
|
||||
</div>
|
||||
|
||||
<div class="center-col">
|
||||
<div class="chart-header">
|
||||
<div style="display: flex; align-items: center; gap: 10px; padding-right: 15px; border-right: 1px solid #2b3139;">
|
||||
<img id="curr-icon" src="" class="coin-icon" style="margin:0; width:28px; height:28px;">
|
||||
<span id="curr-pair" style="font-weight: 800; font-size: 16px;">--/--</span>
|
||||
<div class="center-col">
|
||||
<div id="websocket-status"></div>
|
||||
<div class="chart-header">
|
||||
<div style="display: flex; align-items: center; gap: 10px; padding-right: 15px; border-right: 1px solid #2b3139;">
|
||||
<img id="curr-icon" src="" class="coin-icon" style="margin:0; width:28px; height:28px;">
|
||||
<span id="curr-pair" style="font-weight: 800; font-size: 16px;">--/--</span>
|
||||
</div>
|
||||
<div class="stats-item">
|
||||
<div id="curr-price" style="font-size: 18px; font-weight: 800; color: #0ecb81; line-height: 1.2;">--</div>
|
||||
<div id="curr-change" style="font-size: 11px; font-weight: 600;">--</div>
|
||||
</div>
|
||||
<div class="stats-item"><span class="stats-label"><?php echo __('24h_high'); ?></span><span class="stats-value" id="h-high">--</span></div>
|
||||
<div class="stats-item"><span class="stats-label"><?php echo __('24h_low'); ?></span><span class="stats-value" id="h-low">--</span></div>
|
||||
<div class="stats-item"><span class="stats-label"><?php echo __('24h_vol'); ?></span><span class="stats-value" id="h-vol">--</span></div>
|
||||
</div>
|
||||
|
||||
<div class="stats-item">
|
||||
<div id="curr-price" style="font-size: 18px; font-weight: 800; color: #0ecb81; line-height: 1.2;">--</div>
|
||||
<div id="curr-change" style="font-size: 11px; font-weight: 600;">--</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-item">
|
||||
<span class="stats-label"><?php echo __('24h_high'); ?></span>
|
||||
<span class="stats-value" id="h-high">--</span>
|
||||
</div>
|
||||
<div class="stats-item">
|
||||
<span class="stats-label"><?php echo __('24h_low'); ?></span>
|
||||
<span class="stats-value" id="h-low">--</span>
|
||||
</div>
|
||||
<div class="stats-item">
|
||||
<span class="stats-label"><?php echo __('24h_vol'); ?></span>
|
||||
<span class="stats-value" id="h-vol">--</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-box"><div id="tv_chart_container" style="height: 100%;"></div></div>
|
||||
|
||||
<div class="order-box">
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 25px;">
|
||||
<div>
|
||||
<div class="order-type-tabs" data-side="buy">
|
||||
<div class="order-type-tab active" data-type="limit"><?php echo __('limit'); ?></div>
|
||||
<div class="order-type-tab" data-type="market"><?php echo __('market'); ?></div>
|
||||
<div class="chart-box" id="tv_chart_container"></div>
|
||||
|
||||
<div class="bottom-content-wrapper">
|
||||
<div class="order-box">
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 25px;">
|
||||
<div>
|
||||
<div class="order-type-tabs" data-side="buy">
|
||||
<div class="order-type-tab active" data-type="limit"><?php echo __('limit'); ?></div>
|
||||
<div class="order-type-tab" data-type="market"><?php echo __('market'); ?></div>
|
||||
</div>
|
||||
<div class="input-group"><span class="label"><?php echo __('price'); ?></span><input type="number" id="buy-price" placeholder="0.00"><span class="unit">USDT</span></div>
|
||||
<div class="input-group"><span class="label"><?php echo __('amount'); ?></span><input type="number" id="buy-amount" placeholder="0.00"><span class="unit coin-name">--</span></div>
|
||||
<div class="slider-container"><input type="range" id="buy-slider" min="0" max="100" value="0" step="1"></div>
|
||||
<div style="font-size:11px; color:#848e9c; margin-bottom:8px; display:flex; justify-content:space-between"><span><?php echo __('available'); ?>:</span><span id="avail-usdt" style="font-weight:bold; color:white;">0.00 USDT</span></div>
|
||||
<button id="btn-buy" class="trade-btn btn-buy"><?php echo __('buy'); ?> <span class="coin-name">--</span></button>
|
||||
</div>
|
||||
<div>
|
||||
<div class="order-type-tabs" data-side="sell">
|
||||
<div class="order-type-tab active" data-type="limit"><?php echo __('limit'); ?></div>
|
||||
<div class="order-type-tab" data-type="market"><?php echo __('market'); ?></div>
|
||||
</div>
|
||||
<div class="input-group"><span class="label"><?php echo __('price'); ?></span><input type="number" id="sell-price" placeholder="0.00"><span class="unit">USDT</span></div>
|
||||
<div class="input-group"><span class="label"><?php echo __('amount'); ?></span><input type="number" id="sell-amount" placeholder="0.00"><span class="unit coin-name">--</span></div>
|
||||
<div class="slider-container"><input type="range" id="sell-slider" min="0" max="100" value="0" step="1"></div>
|
||||
<div style="font-size:11px; color:#848e9c; margin-bottom:8px; display:flex; justify-content:space-between"><span><?php echo __('available'); ?>:</span><span id="avail-coin" style="font-weight:bold; color:white;">0.00 --</span></div>
|
||||
<button id="btn-sell" class="trade-btn btn-sell"><?php echo __('sell'); ?> <span class="coin-name">--</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<span class="label"><?php echo __('price'); ?></span>
|
||||
<input type="number" id="buy-price" placeholder="0.00">
|
||||
<span class="unit">USDT</span>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<span class="label"><?php echo __('amount'); ?></span>
|
||||
<input type="number" id="buy-amount" placeholder="0.00">
|
||||
<span class="unit coin-name">--</span>
|
||||
</div>
|
||||
<div class="slider-container">
|
||||
<input type="range" id="buy-slider" min="0" max="100" value="0" step="1">
|
||||
<div class="slider-labels"><span>0%</span><span>25%</span><span>50%</span><span>75%</span><span>100%</span></div>
|
||||
</div>
|
||||
<div style="font-size:11px; color:#848e9c; margin-bottom:8px; display:flex; justify-content:space-between">
|
||||
<span><?php echo __('available'); ?></span><span id="avail-usdt">0.00 USDT</span>
|
||||
</div>
|
||||
<button id="btn-buy" class="trade-btn btn-buy"><?php echo __('buy'); ?> <span class="coin-name">--</span></button>
|
||||
</div>
|
||||
<div>
|
||||
<div class="order-type-tabs" data-side="sell">
|
||||
<div class="order-type-tab active" data-type="limit"><?php echo __('limit'); ?></div>
|
||||
<div class="order-type-tab" data-type="market"><?php echo __('market'); ?></div>
|
||||
<div class="records-container">
|
||||
<div class="record-tabs">
|
||||
<div class="record-tab active" data-status="open"><?php echo __('current_orders'); ?></div>
|
||||
<div class="record-tab" data-status="history"><?php echo __('history_orders'); ?></div>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<span class="label"><?php echo __('price'); ?></span>
|
||||
<input type="number" id="sell-price" placeholder="0.00">
|
||||
<span class="unit">USDT</span>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<span class="label"><?php echo __('amount'); ?></span>
|
||||
<input type="number" id="sell-amount" placeholder="0.00">
|
||||
<span class="unit coin-name">--</span>
|
||||
</div>
|
||||
<div class="slider-container">
|
||||
<input type="range" id="sell-slider" min="0" max="100" value="0" step="1">
|
||||
<div class="slider-labels"><span>0%</span><span>25%</span><span>50%</span><span>75%</span><span>100%</span></div>
|
||||
</div>
|
||||
<div style="font-size:11px; color:#848e9c; margin-bottom:8px; display:flex; justify-content:space-between">
|
||||
<span><?php echo __('available'); ?></span><span id="avail-coin">0.00 --</span>
|
||||
</div>
|
||||
<button id="btn-sell" class="trade-btn btn-sell"><?php echo __('sell'); ?> <span class="coin-name">--</span></button>
|
||||
<div id="records-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="record-tabs">
|
||||
<div class="record-tab active" data-status="open"><?php echo __('current_orders'); ?></div>
|
||||
<div class="record-tab" data-status="history"><?php echo __('history_orders'); ?></div>
|
||||
<div class="record-tab" data-status="trades"><?php echo __('settled'); ?></div>
|
||||
</div>
|
||||
<div id="records-list" class="records-content"></div>
|
||||
</div>
|
||||
<div class="right-col d-none d-lg-flex">
|
||||
<div class="right-col-tabs">
|
||||
<div class="right-col-tab active" data-tab="ob"><?php echo __('order_book'); ?></div>
|
||||
<div class="right-col-tab" data-tab="trades"><?php echo __('trades'); ?></div>
|
||||
</div>
|
||||
|
||||
<div class="right-col d-none d-lg-flex">
|
||||
<div class="ob-header">
|
||||
<span><?php echo __('price'); ?>(USDT)</span>
|
||||
<span><?php echo __('amount'); ?>(<span class="coin-name">--</span>)</span>
|
||||
<span><?php echo __('total'); ?></span>
|
||||
<div id="ob-panel" class="ob-panel">
|
||||
<div class="ob-header">
|
||||
<span><?php echo __('price'); ?>(USDT)</span>
|
||||
<span><?php echo __('amount'); ?>(<span class="coin-name">--</span>)</span>
|
||||
<span><?php echo __('total'); ?></span>
|
||||
</div>
|
||||
<div class="ob-content">
|
||||
<div id="asks-list"></div>
|
||||
<div id="mid-price">--</div>
|
||||
<div id="bids-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="trades-panel" class="trades-panel" style="display: none;">
|
||||
<div class="trades-header">
|
||||
<span><?php echo __('price'); ?>(USDT)</span>
|
||||
<span><?php echo __('amount'); ?>(<span class="coin-name">--</span>)</span>
|
||||
<span><?php echo __('time'); ?></span>
|
||||
</div>
|
||||
<div id="trades-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="asks-list" style="height: 400px; display: flex; flex-direction: column; overflow: hidden; justify-content: flex-end;"></div>
|
||||
<div id="mid-price" style="color: white;">--</div>
|
||||
<div id="bids-list" style="height: 400px; display: flex; flex-direction: column; overflow: hidden; justify-content: flex-start;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript" src="https://s3.tradingview.com/tv.js"></script>
|
||||
<script>
|
||||
TradingView.onready(function() {
|
||||
const userAssets = <?php echo json_encode($user_assets); ?>;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// --- Globals ---
|
||||
let currentPair = 'BTCUSDT';
|
||||
let currentPrice = 0;
|
||||
let orderTypes = {buy: 'limit', sell: 'limit'};
|
||||
let currentStatus = 'open';
|
||||
let searchQuery = '';
|
||||
let ws;
|
||||
let balances = {usdt: 0, coin: 0};
|
||||
|
||||
const pairs = ['BTCUSDT', 'ETHUSDT', 'BNBUSDT', 'SOLUSDT', 'XRPUSDT', 'ADAUSDT', 'DOGEUSDT', 'DOTUSDT', 'MATICUSDT', 'LTCUSDT', 'SHIBUSDT', 'TRXUSDT', 'AVAXUSDT', 'LINKUSDT', 'BCHUSDT', 'UNIUSDT', 'ETCUSDT', 'NEARUSDT', 'FILUSDT', 'ALGOUSDT', 'FTMUSDT', 'SANDUSDT', 'MANAUSDT', 'AXSUSDT', 'ATOMUSDT', 'HBARUSDT', 'ICPUSDT', 'VETUSDT'];
|
||||
const marketData = {};
|
||||
const pairs = [
|
||||
'BTCUSDT', 'ETHUSDT', 'BNBUSDT', 'SOLUSDT', 'XRPUSDT', 'ADAUSDT', 'DOGEUSDT', 'AVAXUSDT',
|
||||
'DOTUSDT', 'MATICUSDT', 'LTCUSDT', 'TRXUSDT', 'LINKUSDT', 'UNIUSDT', 'ATOMUSDT', 'ETCUSDT',
|
||||
'BCHUSDT', 'FILUSDT', 'ICPUSDT', 'NEARUSDT', 'AAVEUSDT', 'ALGOUSDT'
|
||||
];
|
||||
|
||||
function getIcon(s) {
|
||||
const symbol = s.replace('USDT', '').toLowerCase();
|
||||
return `https://assets.coincap.io/assets/icons/${symbol}@2x.png`;
|
||||
// --- DOM Elements ---
|
||||
const dom = {
|
||||
websocketStatus: document.getElementById('websocket-status'),
|
||||
currIcon: document.getElementById('curr-icon'),
|
||||
currPair: document.getElementById('curr-pair'),
|
||||
currPrice: document.getElementById('curr-price'),
|
||||
currChange: document.getElementById('curr-change'),
|
||||
hHigh: document.getElementById('h-high'),
|
||||
hLow: document.getElementById('h-low'),
|
||||
hVol: document.getElementById('h-vol'),
|
||||
tvChartContainer: document.getElementById('tv_chart_container'),
|
||||
pairsList: document.getElementById('pairs-list'),
|
||||
pairSearch: document.getElementById('pair-search'),
|
||||
asksList: document.getElementById('asks-list'),
|
||||
bidsList: document.getElementById('bids-list'),
|
||||
midPrice: document.getElementById('mid-price'),
|
||||
tradesContent: document.getElementById('trades-content'),
|
||||
coinNameSpans: document.querySelectorAll('.coin-name'),
|
||||
buyPriceInput: document.getElementById('buy-price'),
|
||||
sellPriceInput: document.getElementById('sell-price'),
|
||||
availUsdtSpan: document.getElementById('avail-usdt'),
|
||||
availCoinSpan: document.getElementById('avail-coin')
|
||||
};
|
||||
|
||||
// --- Balance Management ---
|
||||
function updateAvailableBalances() {
|
||||
const baseCurrency = currentPair.replace('USDT', '');
|
||||
const usdtBalance = parseFloat(userAssets.USDT || 0).toFixed(2);
|
||||
const coinBalance = parseFloat(userAssets[baseCurrency] || 0).toFixed(6);
|
||||
|
||||
if (dom.availUsdtSpan) {
|
||||
dom.availUsdtSpan.innerText = `${usdtBalance} USDT`;
|
||||
}
|
||||
if (dom.availCoinSpan) {
|
||||
dom.availCoinSpan.innerText = `${coinBalance} ${baseCurrency}`;
|
||||
}
|
||||
}
|
||||
|
||||
function initChart(symbol) {
|
||||
const container = document.getElementById('tv_chart_container');
|
||||
if (container) container.innerHTML = '';
|
||||
new TradingView.widget({
|
||||
"width": "100%",
|
||||
"height": "100%",
|
||||
"symbol": "BINANCE:" + symbol,
|
||||
// --- TradingView Chart ---
|
||||
function initChartWidget(symbol) {
|
||||
if (dom.tvChartContainer.firstChild) {
|
||||
dom.tvChartContainer.innerHTML = '';
|
||||
}
|
||||
const widget = new TradingView.widget({
|
||||
"autosize": true,
|
||||
"symbol": `BINANCE:${symbol}`,
|
||||
"interval": "15",
|
||||
"timezone": "Etc/UTC",
|
||||
"theme": "dark",
|
||||
"style": "1",
|
||||
"locale": "<?php echo $lang == 'zh' ? 'zh_CN' : 'en'; ?>",
|
||||
"toolbar_bg": "#f1f3f6",
|
||||
"enable_publishing": false,
|
||||
"withdateranges": true,
|
||||
"hide_side_toolbar": true,
|
||||
"allow_symbol_change": false,
|
||||
"container_id": "tv_chart_container",
|
||||
"backgroundColor": "#0b0e11",
|
||||
"hide_side_toolbar": true
|
||||
"studies": [
|
||||
"Volume@tv-basicstudies"
|
||||
],
|
||||
"overrides": {
|
||||
"paneProperties.background": "#161a1e",
|
||||
"paneProperties.vertGridProperties.color": "rgba(255, 255, 255, 0.05)",
|
||||
"paneProperties.horzGridProperties.color": "rgba(255, 255, 255, 0.05)",
|
||||
"scalesProperties.textColor" : "#848e9c",
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --- WebSocket Connection ---
|
||||
function connectWebSocket() {
|
||||
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
|
||||
return;
|
||||
}
|
||||
ws = new WebSocket('wss://stream.binance.com:9443/ws/' + pairs.map(p => p.toLowerCase() + '@ticker').concat(pairs.map(p => p.toLowerCase() + '@depth20@100ms')).join('/'));
|
||||
if (ws) ws.close();
|
||||
|
||||
// setWebsocketStatus('<?php echo __('websocket_connecting'); ?>', '#ffc107'); // Hidden
|
||||
|
||||
const streams = pairs.map(p => `${p.toLowerCase()}@ticker`).concat([`${currentPair.toLowerCase()}@depth20@100ms`, `${currentPair.toLowerCase()}@trade`]);
|
||||
ws = new WebSocket(`wss://stream.binance.com:9443/stream?streams=${streams.join('/')}`);
|
||||
|
||||
ws.onmessage = (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.e === 'ticker') {
|
||||
ws.onopen = () => {
|
||||
// setWebsocketStatus('<?php echo __('websocket_connected'); ?>', '#0ecb81', 2000); // Hidden
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const { stream, data } = JSON.parse(event.data);
|
||||
if (!data) return;
|
||||
|
||||
const type = stream.split('@')[1];
|
||||
|
||||
if (type === 'ticker') {
|
||||
marketData[data.s] = data;
|
||||
if (data.s === currentPair) {
|
||||
currentPrice = parseFloat(data.c);
|
||||
updatePriceUI(data);
|
||||
if (orderTypes.buy === 'market') document.getElementById('buy-price').value = currentPrice.toFixed(2);
|
||||
if (orderTypes.sell === 'market') document.getElementById('sell-price').value = currentPrice.toFixed(2);
|
||||
}
|
||||
renderPairs();
|
||||
} else if (data.s === currentPair) {
|
||||
renderOrderBook(data.b, data.a);
|
||||
if (dom.pairsList.offsetParent) renderPairs();
|
||||
} else if (type.startsWith('depth')) {
|
||||
renderOrderBook(data.bids, data.asks);
|
||||
} else if (type === 'trade') {
|
||||
renderTrades(data);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('WebSocket Error:', error);
|
||||
// setWebsocketStatus('<?php echo __('websocket_error'); ?>', '#f6465d'); // Hidden
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
// setWebsocketStatus('<?php echo __('websocket_disconnected'); ?>', '#f6465d', 5000); // Hidden
|
||||
setTimeout(connectWebSocket, 5000);
|
||||
};
|
||||
}
|
||||
|
||||
function setWebsocketStatus(message, color, hideAfter = 0) {
|
||||
// This function is kept for logic continuity but does nothing visually.
|
||||
return;
|
||||
}
|
||||
|
||||
// --- UI Rendering ---
|
||||
function updatePriceUI(d) {
|
||||
if (!d) return;
|
||||
const color = d.P >= 0 ? '#0ecb81' : '#f6465d';
|
||||
const priceStr = parseFloat(d.c).toLocaleString(undefined, {minimumFractionDigits: 2});
|
||||
document.getElementById('curr-price').innerText = priceStr;
|
||||
document.getElementById('curr-price').style.color = color;
|
||||
document.getElementById('curr-change').innerText = (d.P >= 0 ? '+' : '') + parseFloat(d.P).toFixed(2) + '%';
|
||||
document.getElementById('curr-change').style.color = color;
|
||||
document.getElementById('h-high').innerText = parseFloat(d.h).toLocaleString();
|
||||
document.getElementById('h-low').innerText = parseFloat(d.l).toLocaleString();
|
||||
document.getElementById('h-vol').innerText = (parseFloat(d.v) / 1000).toFixed(1) + 'K';
|
||||
const midPriceEl = document.getElementById('mid-price');
|
||||
if (midPriceEl) {
|
||||
midPriceEl.innerText = priceStr;
|
||||
midPriceEl.style.color = color;
|
||||
}
|
||||
const priceStr = parseFloat(d.c).toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2});
|
||||
|
||||
document.title = `${priceStr} | ${d.s.replace('USDT', '/USDT')}`;
|
||||
dom.currPrice.innerText = priceStr;
|
||||
dom.currPrice.style.color = color;
|
||||
dom.currChange.innerText = `${d.P >= 0 ? '+' : ''}${parseFloat(d.P).toFixed(2)}%`;
|
||||
dom.currChange.style.color = color;
|
||||
dom.hHigh.innerText = parseFloat(d.h).toLocaleString('en-US', {minimumFractionDigits: 2});
|
||||
dom.hLow.innerText = parseFloat(d.l).toLocaleString('en-US', {minimumFractionDigits: 2});
|
||||
dom.hVol.innerText = `${(parseFloat(d.v) / 1000).toFixed(1)}K`;
|
||||
dom.midPrice.innerText = priceStr;
|
||||
dom.midPrice.style.color = color;
|
||||
}
|
||||
|
||||
function renderPairs() {
|
||||
const list = document.getElementById('pairs-list');
|
||||
const mBar = document.getElementById('m-coin-list');
|
||||
if (!list || !mBar) return;
|
||||
|
||||
let lH = '';
|
||||
let mH = '';
|
||||
const filteredPairs = pairs.filter(p => p.toUpperCase().includes(searchQuery));
|
||||
|
||||
filteredPairs.forEach(p => {
|
||||
const query = dom.pairSearch.value.toUpperCase();
|
||||
const filteredPairs = pairs.filter(p => p.includes(query));
|
||||
|
||||
dom.pairsList.innerHTML = filteredPairs.map(p => {
|
||||
const d = marketData[p] || {};
|
||||
const icon = getIcon(p);
|
||||
const active = p === currentPair;
|
||||
const icon = `https://assets.coincap.io/assets/icons/${p.replace('USDT','').toLowerCase()}@2x.png`;
|
||||
const price = d.c ? parseFloat(d.c).toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2}) : '--';
|
||||
const change = d.P ? `${parseFloat(d.P) >= 0 ? '+' : ''}${parseFloat(d.P).toFixed(2)}%` : '--';
|
||||
const color = (d.P && parseFloat(d.P) >= 0) ? '#0ecb81' : '#f6465d';
|
||||
const price = d.c ? parseFloat(d.c).toLocaleString(undefined, {minimumFractionDigits: 2}) : '--';
|
||||
const change = d.P ? ((parseFloat(d.P) >= 0 ? '+' : '') + parseFloat(d.P).toFixed(2) + '%') : '--';
|
||||
const disp = p.replace('USDT', '') + '/USDT';
|
||||
|
||||
lH += `<div class="pair-item ${active ? 'active' : ''}" data-pair="${p}"><img src="${icon}" class="coin-icon" onerror="this.src='https://ui-avatars.com/api/?name=${p.replace('USDT', '')}&background=2b3139&color=fff'"> <div style="flex:1"><div style="font-weight:700; font-size:13px">${disp}</div></div> <div style="text-align:right"><div style="font-size:13px; font-weight:bold">${price}</div><div style="font-size:11px; color:${color}">${change}</div></div></div>`;
|
||||
mH += `<div class="m-coin-item ${active ? 'active' : ''}" data-pair="${p}"><img src="${icon}" onerror="this.src='https://ui-avatars.com/api/?name=${p.replace('USDT', '')}&background=2b3139&color=fff'"><span>${p.replace('USDT', '')}</span></div>`;
|
||||
});
|
||||
|
||||
list.innerHTML = lH;
|
||||
mBar.innerHTML = mH;
|
||||
|
||||
document.querySelectorAll('.pair-item, .m-coin-item').forEach(item => {
|
||||
item.addEventListener('click', () => switchPair(item.dataset.pair));
|
||||
});
|
||||
}
|
||||
|
||||
function filterPairs(e) {
|
||||
searchQuery = e.target.value.toUpperCase();
|
||||
renderPairs();
|
||||
}
|
||||
|
||||
function switchPair(p) {
|
||||
currentPair = p;
|
||||
const coinName = p.replace('USDT', '');
|
||||
const icon = getIcon(p);
|
||||
|
||||
document.getElementById('curr-pair').innerText = coinName + '/USDT';
|
||||
const iconEl = document.getElementById('curr-icon');
|
||||
iconEl.src = icon;
|
||||
iconEl.onerror = function() { this.src = `https://ui-avatars.com/api/?name=${coinName}&background=2b3139&color=fff`; };
|
||||
|
||||
document.querySelectorAll('.coin-name').forEach(el => el.innerText = coinName);
|
||||
|
||||
initChart(p);
|
||||
updateBalance();
|
||||
renderPairs();
|
||||
|
||||
const d = marketData[p] || {c: 0, P: 0, h: 0, l: 0, v: 0};
|
||||
updatePriceUI(d);
|
||||
renderOrderBook([], []);
|
||||
}
|
||||
|
||||
function switchOrderType(side, type) {
|
||||
orderTypes[side] = type;
|
||||
document.querySelector(`.order-type-tabs[data-side="${side}"] .order-type-tab.active`).classList.remove('active');
|
||||
document.querySelector(`.order-type-tabs[data-side="${side}"] .order-type-tab[data-type="${type}"]`).classList.add('active');
|
||||
document.getElementById(`${side}-price`).disabled = (type === 'market');
|
||||
if (type === 'market' && currentPrice > 0) {
|
||||
document.getElementById(`${side}-price`).value = currentPrice.toFixed(2);
|
||||
}
|
||||
}
|
||||
|
||||
function setSlider(side, value) {
|
||||
const pct = value / 100;
|
||||
if (side === 'buy') {
|
||||
const price = parseFloat(document.getElementById('buy-price').value) || currentPrice;
|
||||
if (price > 0) {
|
||||
document.getElementById('buy-amount').value = (balances.usdt * pct / price).toFixed(6);
|
||||
}
|
||||
} else {
|
||||
document.getElementById('sell-amount').value = (balances.coin * pct).toFixed(6);
|
||||
}
|
||||
}
|
||||
|
||||
async function placeOrder(side) {
|
||||
const price = parseFloat(document.getElementById(`${side}-price`).value);
|
||||
const amount = parseFloat(document.getElementById(`${side}-amount`).value);
|
||||
if (!amount || amount <= 0) return alert('Amount error');
|
||||
if (orderTypes[side] === 'limit' && (!price || price <= 0)) return alert('Price error');
|
||||
|
||||
<?php if (!$user_id): ?>
|
||||
alert('<?php echo __('login_to_trade'); ?>');
|
||||
window.location.href = 'login.php';
|
||||
return;
|
||||
<?php endif; ?>
|
||||
|
||||
try {
|
||||
const resp = await fetch('api/place_order.php', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
symbol: currentPair,
|
||||
type: 'spot',
|
||||
side: side,
|
||||
order_type: orderTypes[side],
|
||||
price: price,
|
||||
amount: amount,
|
||||
total: price * amount
|
||||
})
|
||||
});
|
||||
const res = await resp.json();
|
||||
if (res.success) {
|
||||
alert('<?php echo __('order_placed'); ?>');
|
||||
updateBalance();
|
||||
fetchOrders();
|
||||
} else {
|
||||
alert(res.error || 'An unknown error occurred.');
|
||||
}
|
||||
} catch (e) {
|
||||
alert('<?php echo __('network_error'); ?>');
|
||||
}
|
||||
return `
|
||||
<div class="pair-item ${p === currentPair ? 'active' : ''}" data-pair="${p}">
|
||||
<img src="${icon}" class="coin-icon" onerror="this.style.display='none'">
|
||||
<div style="flex:1"><div style="font-weight:600; font-size:13px">${p.replace('USDT', '/USDT')}</div></div>
|
||||
<div style="text-align:right">
|
||||
<div style="font-size:13px; font-weight:600">${price}</div>
|
||||
<div style="font-size:11px; color:${color}">${change}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderOrderBook(bids, asks) {
|
||||
const asksList = document.getElementById('asks-list');
|
||||
const bidsList = document.getElementById('bids-list');
|
||||
if (!asksList || !bidsList) return;
|
||||
let asksHtml = '';
|
||||
let bidsHtml = '';
|
||||
const renderList = (element, data, isAsks) => {
|
||||
if (!element) return;
|
||||
const maxRows = Math.floor(element.offsetHeight / 20);
|
||||
if (maxRows <= 0) return;
|
||||
|
||||
asks.slice(0, 20).reverse().forEach(([price, quantity]) => {
|
||||
const p = parseFloat(price);
|
||||
const q = parseFloat(quantity);
|
||||
asksHtml += `<div class="ob-row" onclick="setPrice(${p})"><div class="ob-bar" style="width:${q * 2}%; background:#f6465d"></div><span style="color:#f6465d">${p.toFixed(2)}</span><span>${q.toFixed(4)}</span><span>${(p * q).toFixed(2)}</span></div>`;
|
||||
});
|
||||
const relevantData = isAsks ? data.slice(0, maxRows).reverse() : data.slice(0, maxRows);
|
||||
const maxVol = Math.max(...relevantData.map(([, q]) => parseFloat(q))) || 1;
|
||||
|
||||
bids.slice(0, 20).forEach(([price, quantity]) => {
|
||||
const p = parseFloat(price);
|
||||
const q = parseFloat(quantity);
|
||||
bidsHtml += `<div class="ob-row" onclick="setPrice(${p})"><div class="ob-bar" style="width:${q * 2}%; background:#0ecb81"></div><span style="color:#0ecb81">${p.toFixed(2)}</span><span>${q.toFixed(4)}</span><span>${(p * q).toFixed(2)}</span></div>`;
|
||||
});
|
||||
element.innerHTML = relevantData.map(([price, quantity]) => {
|
||||
const p = parseFloat(price), q = parseFloat(quantity);
|
||||
const barWidth = (q / maxVol) * 100;
|
||||
const color = isAsks ? '#f6465d' : '#0ecb81';
|
||||
const bgColor = isAsks ? 'rgba(246, 70, 93, 0.2)' : 'rgba(14, 203, 129, 0.2)';
|
||||
return `
|
||||
<div class="ob-row" onclick="setPrice(${p})">
|
||||
<div class="ob-bar" style="width:${barWidth}%; background-color:${bgColor};"></div>
|
||||
<span style="color:${color}; z-index:1;">${p.toFixed(2)}</span>
|
||||
<span style="z-index:1;">${q.toFixed(4)}</span>
|
||||
<span style="z-index:1;">${(p * q).toFixed(2)}</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
};
|
||||
renderList(dom.asksList, asks, true);
|
||||
renderList(dom.bidsList, bids, false);
|
||||
}
|
||||
|
||||
function renderTrades(trade) {
|
||||
if (!dom.tradesContent) return;
|
||||
const color = trade.m ? '#f6465d' : '#0ecb81'; // m: is maker (seller is maker)
|
||||
const time = new Date(trade.T).toLocaleTimeString('en-GB');
|
||||
const row = `
|
||||
<div class="trade-row">
|
||||
<span style="color:${color}">${parseFloat(trade.p).toFixed(2)}</span>
|
||||
<span>${parseFloat(trade.q).toFixed(4)}</span>
|
||||
<span>${time}</span>
|
||||
</div>`;
|
||||
dom.tradesContent.insertAdjacentHTML('afterbegin', row);
|
||||
if (dom.tradesContent.children.length > 50) {
|
||||
dom.tradesContent.removeChild(dom.tradesContent.lastChild);
|
||||
}
|
||||
}
|
||||
|
||||
function switchPair(pair) {
|
||||
currentPair = pair;
|
||||
const coinName = pair.replace('USDT', '');
|
||||
|
||||
asksList.innerHTML = asksHtml;
|
||||
bidsList.innerHTML = bidsHtml;
|
||||
dom.currPair.innerText = `${coinName}/USDT`;
|
||||
dom.currIcon.src = `https://assets.coincap.io/assets/icons/${coinName.toLowerCase()}@2x.png`;
|
||||
dom.coinNameSpans.forEach(el => el.innerText = coinName);
|
||||
|
||||
updatePriceUI(marketData[pair]);
|
||||
updateAvailableBalances();
|
||||
initChartWidget(pair);
|
||||
renderPairs();
|
||||
|
||||
// Clear old data and resubscribe
|
||||
dom.asksList.innerHTML = '';
|
||||
dom.bidsList.innerHTML = '';
|
||||
dom.tradesContent.innerHTML = '';
|
||||
connectWebSocket();
|
||||
}
|
||||
|
||||
function setPrice(p) {
|
||||
if (orderTypes.buy === 'limit') document.getElementById('buy-price').value = p.toFixed(2);
|
||||
if (orderTypes.sell === 'limit') document.getElementById('sell-price').value = p.toFixed(2);
|
||||
}
|
||||
|
||||
async function updateBalance() {
|
||||
<?php if (!$user_id) return; ?>
|
||||
try {
|
||||
const resp = await fetch('api/get_assets.php');
|
||||
const res = await resp.json();
|
||||
if (res.success) {
|
||||
const usdt = res.data.find(a => a.symbol === 'USDT');
|
||||
const coinSym = currentPair.replace('USDT', '');
|
||||
const coin = res.data.find(a => a.symbol === coinSym);
|
||||
balances.usdt = usdt ? parseFloat(usdt.amount) : 0;
|
||||
balances.coin = coin ? parseFloat(coin.amount) : 0;
|
||||
document.getElementById('avail-usdt').innerText = balances.usdt.toFixed(2) + ' USDT';
|
||||
document.getElementById('avail-coin').innerText = balances.coin.toFixed(6) + ' ' + coinSym;
|
||||
} else {
|
||||
console.error('Failed to fetch balance:', res.error);
|
||||
}
|
||||
} catch(e) {
|
||||
console.error('Network error fetching balance:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchOrders() {
|
||||
<?php if (!$user_id) return; ?>
|
||||
try {
|
||||
const resp = await fetch(`api/get_orders.php?type=spot&status=${currentStatus}`);
|
||||
const res = await resp.json();
|
||||
const listEl = document.getElementById('records-list');
|
||||
if (!listEl) return;
|
||||
|
||||
let html = '';
|
||||
if (res.success && res.data.length > 0) {
|
||||
html = res.data.map(o => `
|
||||
<div style="padding:15px; border-bottom:1px solid #2b3139; font-size:12px">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center">
|
||||
<span style="font-weight:bold; font-size:14px">${o.symbol} <span style="color:${o.side === 'buy' ? '#0ecb81' : '#f6465d'}">${o.side.toUpperCase()}</span></span>
|
||||
<span style="color:#848e9c">${o.created_at}</span>
|
||||
</div>
|
||||
<div style="display:grid; grid-template-columns:repeat(4, 1fr); gap:10px; margin-top:10px; color:#848e9c">
|
||||
<div><?php echo __('price'); ?><br><span style="color:white; font-weight:bold">${parseFloat(o.price).toFixed(2)}</span></div>
|
||||
<div><?php echo __('amount'); ?><br><span style="color:white; font-weight:bold">${parseFloat(o.amount).toFixed(4)}</span></div>
|
||||
<div><?php echo __('total'); ?><br><span style="color:white; font-weight:bold">${parseFloat(o.total).toFixed(2)}</span></div>
|
||||
<div><?php echo __('status'); ?><br><span style="color:white; font-weight:bold">${o.status}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
} else {
|
||||
html = '<div style="padding:40px; text-align:center; color:#848e9c"><?php echo __('no_records'); ?></div>';
|
||||
}
|
||||
listEl.innerHTML = html;
|
||||
} catch (e) {
|
||||
console.error('Error fetching orders:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function switchRecords(status) {
|
||||
currentStatus = status;
|
||||
document.querySelector('.record-tabs .record-tab.active').classList.remove('active');
|
||||
document.querySelector(`.record-tabs .record-tab[data-status="${status}"]`).classList.add('active');
|
||||
fetchOrders();
|
||||
}
|
||||
|
||||
// ===== Main Execution =====
|
||||
|
||||
// 1. Add Event Listeners
|
||||
document.getElementById('pair-search').addEventListener('input', filterPairs);
|
||||
document.getElementById('btn-buy').addEventListener('click', () => placeOrder('buy'));
|
||||
document.getElementById('btn-sell').addEventListener('click', () => placeOrder('sell'));
|
||||
document.getElementById('buy-slider').addEventListener('input', (e) => setSlider('buy', e.target.value));
|
||||
document.getElementById('sell-slider').addEventListener('input', (e) => setSlider('sell', e.target.value));
|
||||
document.querySelectorAll('.order-type-tabs[data-side="buy"] .order-type-tab').forEach(tab => {
|
||||
tab.addEventListener('click', () => switchOrderType('buy', tab.dataset.type));
|
||||
});
|
||||
document.querySelectorAll('.order-type-tabs[data-side="sell"] .order-type-tab').forEach(tab => {
|
||||
tab.addEventListener('click', () => switchOrderType('sell', tab.dataset.type));
|
||||
});
|
||||
document.querySelectorAll('.record-tabs .record-tab').forEach(tab => {
|
||||
tab.addEventListener('click', () => switchRecords(tab.dataset.status));
|
||||
});
|
||||
window.setPrice = (price) => {
|
||||
dom.buyPriceInput.value = price;
|
||||
dom.sellPriceInput.value = price;
|
||||
}
|
||||
|
||||
// 2. Initial Render
|
||||
switchPair(currentPair);
|
||||
renderOrderBook([], []);
|
||||
|
||||
// 3. Fetch Initial Data
|
||||
updateBalance();
|
||||
fetchOrders();
|
||||
|
||||
// 4. Initialize Chart & WebSocket
|
||||
initChart(currentPair);
|
||||
connectWebSocket();
|
||||
|
||||
// 5. Setup Intervals
|
||||
setInterval(updateBalance, 5000);
|
||||
setInterval(fetchOrders, 10000);
|
||||
setInterval(() => {
|
||||
if (!ws || ws.readyState === WebSocket.CLOSED) {
|
||||
connectWebSocket();
|
||||
// --- Event Listeners ---
|
||||
dom.pairsList.addEventListener('click', (e) => {
|
||||
const pairItem = e.target.closest('.pair-item');
|
||||
if (pairItem && pairItem.dataset.pair) {
|
||||
switchPair(pairItem.dataset.pair);
|
||||
}
|
||||
}, 10000);
|
||||
});
|
||||
|
||||
dom.pairSearch.addEventListener('input', renderPairs);
|
||||
|
||||
document.querySelectorAll('.right-col-tab').forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
document.querySelector('.right-col-tab.active').classList.remove('active');
|
||||
tab.classList.add('active');
|
||||
const isOb = tab.dataset.tab === 'ob';
|
||||
document.getElementById('ob-panel').style.display = isOb ? 'flex' : 'none';
|
||||
document.getElementById('trades-panel').style.display = isOb ? 'none' : 'flex';
|
||||
});
|
||||
});
|
||||
|
||||
// --- Initial Load ---
|
||||
switchPair('BTCUSDT');
|
||||
});
|
||||
</script>
|
||||
<?php include 'footer.php'; ?>
|
||||
<!-- This script is for the TradingView widget -->
|
||||
<script type="text/javascript" src="https://s3.tradingview.com/tv.js"></script>
|
||||
<script type="text/javascript" src="https://s3.tradingview.com/external-embedding/embed-widget-advanced-chart.js"></script>
|
||||
|
||||
|
||||
<?php include 'footer.php'; ?>
|
||||
5
test_spot.php
Normal file
5
test_spot.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
include 'header.php';
|
||||
echo "<h1>Test Page</h1>";
|
||||
include 'footer.php';
|
||||
?>
|
||||
Loading…
x
Reference in New Issue
Block a user