realtime tracker
This commit is contained in:
@@ -18,39 +18,36 @@ CREATE TABLE IF NOT EXISTS wallets (
|
||||
INDEX idx_trade_count (trade_count)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Tracked wallet addresses from Hyperliquid trades';
|
||||
|
||||
-- Wallet PnL snapshots table (optional - for historical tracking)
|
||||
CREATE TABLE IF NOT EXISTS wallet_pnl_snapshots (
|
||||
-- Note: PnL is now calculated from trades table, so wallet_pnl_snapshots table is no longer used
|
||||
|
||||
-- Trades table to store individual trades for each wallet
|
||||
CREATE TABLE IF NOT EXISTS trades (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
wallet_id INT NOT NULL,
|
||||
wallet_address VARCHAR(66) NOT NULL,
|
||||
pnl DECIMAL(30, 8) COMMENT 'Profit and Loss value',
|
||||
account_value DECIMAL(30, 8) COMMENT 'Total account value',
|
||||
unrealized_pnl DECIMAL(30, 8) COMMENT 'Unrealized PnL',
|
||||
snapshot_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
entry_hash VARCHAR(66) NOT NULL UNIQUE COMMENT 'Unique identifier for the trade entry (transaction hash)',
|
||||
entry_date TIMESTAMP NULL COMMENT 'When the position was opened',
|
||||
close_date TIMESTAMP NULL COMMENT 'When the position was closed (NULL if still open)',
|
||||
coin VARCHAR(20) NOT NULL COMMENT 'Trading pair/coin symbol',
|
||||
amount DECIMAL(30, 8) NOT NULL COMMENT 'Trade size/amount',
|
||||
entry_price DECIMAL(30, 8) NOT NULL COMMENT 'Price when position was opened',
|
||||
close_price DECIMAL(30, 8) NULL COMMENT 'Price when position was closed (NULL if still open)',
|
||||
direction ENUM('buy', 'sell') NOT NULL DEFAULT 'buy' COMMENT 'Trade direction: buy or sell',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (wallet_id) REFERENCES wallets(id) ON DELETE CASCADE,
|
||||
INDEX idx_wallet_id (wallet_id),
|
||||
INDEX idx_wallet_address (wallet_address),
|
||||
INDEX idx_snapshot_at (snapshot_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Historical PnL snapshots for tracked wallets';
|
||||
INDEX idx_entry_hash (entry_hash),
|
||||
INDEX idx_coin (coin),
|
||||
INDEX idx_entry_date (entry_date),
|
||||
INDEX idx_close_date (close_date),
|
||||
INDEX idx_direction (direction)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Individual trades for tracked wallets';
|
||||
|
||||
-- View for wallets with latest PnL
|
||||
CREATE OR REPLACE VIEW wallets_with_latest_pnl AS
|
||||
SELECT
|
||||
w.id,
|
||||
w.address,
|
||||
w.first_seen_at,
|
||||
w.last_seen_at,
|
||||
w.trade_count,
|
||||
wps.pnl,
|
||||
wps.account_value,
|
||||
wps.unrealized_pnl,
|
||||
wps.snapshot_at as last_pnl_snapshot
|
||||
FROM wallets w
|
||||
LEFT JOIN wallet_pnl_snapshots wps ON w.id = wps.wallet_id
|
||||
LEFT JOIN (
|
||||
SELECT wallet_id, MAX(snapshot_at) as max_snapshot
|
||||
FROM wallet_pnl_snapshots
|
||||
GROUP BY wallet_id
|
||||
) latest ON w.id = latest.wallet_id AND wps.snapshot_at = latest.max_snapshot;
|
||||
-- Migration: Add direction column if it doesn't exist
|
||||
ALTER TABLE trades ADD COLUMN IF NOT EXISTS direction ENUM('buy', 'sell') NOT NULL DEFAULT 'buy' COMMENT 'Trade direction: buy or sell' AFTER close_price;
|
||||
ALTER TABLE trades ADD INDEX IF NOT EXISTS idx_direction (direction);
|
||||
|
||||
-- Note: PnL is now calculated on-demand from trades table, so the view is no longer needed
|
||||
|
||||
|
||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -665,6 +665,7 @@
|
||||
"integrity": "sha512-VyKBr25BuFDzBFCK5sUM6ZXiWfqgCTwTAOK8qzGV/m9FCirXYDlmczJ+d5dXBAQALGCdRRdbteKYfJ84NGEusw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
@@ -1875,6 +1876,7 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
||||
385
src/database.ts
385
src/database.ts
@@ -6,18 +6,45 @@ import * as path from 'path';
|
||||
const envLocalPath = path.join(process.cwd(), '.env.local');
|
||||
const envPath = path.join(process.cwd(), '.env');
|
||||
|
||||
dotenv.config({ path: envLocalPath });
|
||||
dotenv.config({ path: envPath }); // .env.local values will override .env
|
||||
const envLocalResult = dotenv.config({ path: envLocalPath });
|
||||
if (envLocalResult.error) {
|
||||
if (envLocalResult.error.message && !envLocalResult.error.message.includes('ENOENT')) {
|
||||
console.warn('[ENV] Warning loading .env.local:', envLocalResult.error.message);
|
||||
}
|
||||
} else {
|
||||
console.log('[ENV] ✅ Loaded .env.local from:', envLocalPath);
|
||||
}
|
||||
|
||||
const envResult = dotenv.config({ path: envPath });
|
||||
if (envResult.error) {
|
||||
if (envResult.error.message && !envResult.error.message.includes('ENOENT')) {
|
||||
console.warn('[ENV] Warning loading .env:', envResult.error.message);
|
||||
}
|
||||
} else {
|
||||
console.log('[ENV] ✅ Loaded .env from:', envPath);
|
||||
}
|
||||
|
||||
import mysql from 'mysql2/promise';
|
||||
|
||||
// Database configuration
|
||||
const dbConfig: mysql.PoolOptions = {
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: parseInt(process.env.DB_PORT || '3306'),
|
||||
user: process.env.DB_USER || 'root',
|
||||
password: process.env.DB_PASSWORD || '',
|
||||
database: process.env.DB_NAME || 'hyperliquid_tracker',
|
||||
// Helper to get env var with fallback, handling empty strings
|
||||
const getEnvVar = (key: string, defaultValue: string): string => {
|
||||
const value = process.env[key];
|
||||
if (!value || !value.trim()) {
|
||||
return defaultValue;
|
||||
}
|
||||
return value.trim();
|
||||
};
|
||||
|
||||
let dbConfig: mysql.PoolOptions = getDBConfig();
|
||||
|
||||
function getDBConfig(): mysql.PoolOptions {
|
||||
return {
|
||||
host: getEnvVar('DB_HOST', 'localhost'),
|
||||
port: parseInt(getEnvVar('DB_PORT', '3306')),
|
||||
user: getEnvVar('DB_USER', 'hypr'),
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: getEnvVar('DB_NAME', 'hyperliquid_tracker'),
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0,
|
||||
@@ -48,6 +75,7 @@ const dbConfig: mysql.PoolOptions = {
|
||||
'-SECURE_CONNECTION',
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
console.log('[DB] Database config loaded:', {
|
||||
host: dbConfig.host,
|
||||
@@ -55,8 +83,17 @@ console.log('[DB] Database config loaded:', {
|
||||
database: dbConfig.database,
|
||||
user: dbConfig.user,
|
||||
passwordSet: !!dbConfig.password,
|
||||
passwordLength: dbConfig.password ? dbConfig.password.length : 0,
|
||||
ssl: dbConfig.ssl ? 'enabled' : 'disabled',
|
||||
});
|
||||
console.log('[DB] Raw environment variables:', {
|
||||
DB_HOST: process.env.DB_HOST ? `"${process.env.DB_HOST}"` : '(not set)',
|
||||
DB_PORT: process.env.DB_PORT ? `"${process.env.DB_PORT}"` : '(not set)',
|
||||
DB_USER: process.env.DB_USER !== undefined ? `"${process.env.DB_USER}" (length: ${process.env.DB_USER.length})` : '(not set)',
|
||||
DB_NAME: process.env.DB_NAME ? `"${process.env.DB_NAME}"` : '(not set)',
|
||||
DB_PASSWORD: process.env.DB_PASSWORD ? `***${process.env.DB_PASSWORD.length} chars***` : '(not set)',
|
||||
});
|
||||
console.log('[DB] getEnvVar result for DB_USER:', getEnvVar('DB_USER', 'hypr'));
|
||||
|
||||
// Create connection pool
|
||||
let pool: mysql.Pool | null = null;
|
||||
@@ -65,15 +102,19 @@ let pool: mysql.Pool | null = null;
|
||||
* Initialize database connection pool
|
||||
*/
|
||||
export function initDatabase(): mysql.Pool {
|
||||
console.log('getting db config');
|
||||
|
||||
|
||||
if (!pool) {
|
||||
pool = mysql.createPool(dbConfig);
|
||||
console.log('[DB] ✅ Database connection pool created');
|
||||
console.log('[DB] Config:', {
|
||||
pool = mysql.createPool({
|
||||
host: dbConfig.host,
|
||||
port: dbConfig.port,
|
||||
database: dbConfig.database,
|
||||
user: dbConfig.user,
|
||||
password: dbConfig.password,
|
||||
database: dbConfig.database,
|
||||
connectTimeout: 10000,
|
||||
});
|
||||
console.log('[DB] ✅ Database connection pool created');
|
||||
|
||||
// Handle connection events
|
||||
pool.on('connection', (connection: any) => {
|
||||
@@ -110,15 +151,18 @@ export async function testConnection(maxRetries: number = 3): Promise<boolean> {
|
||||
try {
|
||||
// Create a direct connection for testing (not using pool)
|
||||
const directConnection = await mysql.createConnection({
|
||||
...dbConfig,
|
||||
// Reduce timeout for faster failure detection
|
||||
host: dbConfig.host,
|
||||
port: dbConfig.port,
|
||||
user: dbConfig.user,
|
||||
password: dbConfig.password,
|
||||
database: dbConfig.database,
|
||||
connectTimeout: 10000,
|
||||
});
|
||||
|
||||
console.log('[DB] 🔌 Direct connection established');
|
||||
|
||||
// Immediately test the connection
|
||||
const [rows] = await directConnection.query('SELECT 1 as test, DATABASE() as current_db, USER() as current_user, VERSION() as version');
|
||||
const [rows] = await directConnection.query('SELECT 1 as test, DATABASE() as current_db, USER() as `current_user`, VERSION() as version');
|
||||
console.log('[DB] ✅ Test query successful:', rows);
|
||||
|
||||
await directConnection.ping();
|
||||
@@ -150,6 +194,7 @@ export async function testConnection(maxRetries: number = 3): Promise<boolean> {
|
||||
|
||||
return true;
|
||||
} catch (poolError: any) {
|
||||
console.error('[DB] Pool connection error:', poolError);
|
||||
console.error(`[DB] ❌ Pool connection failed (attempt ${attempt}/${maxRetries}):`, poolError.message);
|
||||
|
||||
if (attempt < maxRetries) {
|
||||
@@ -186,8 +231,8 @@ export async function testConnection(maxRetries: number = 3): Promise<boolean> {
|
||||
connectTimeout: 10000,
|
||||
};
|
||||
|
||||
const testConnection = await mysql.createConnection(testConfig);
|
||||
const [testRows] = await testConnection.query('SELECT 1 as test, USER() as current_user');
|
||||
const testConnection = await mysql.createConnection(dbConfig);
|
||||
const [testRows] = await testConnection.query('SELECT 1 as test, USER() as `current_user`');
|
||||
console.log('[DB] ✅ Connection without database works:', testRows);
|
||||
console.log('[DB] 💡 Issue may be with database name or permissions on that database');
|
||||
|
||||
@@ -239,6 +284,12 @@ export async function loadWalletsFromDB(): Promise<string[]> {
|
||||
*/
|
||||
export async function addWalletToDB(wallet: string): Promise<boolean> {
|
||||
try {
|
||||
// Validate wallet address length (Ethereum addresses are exactly 42 characters: 0x + 40 hex chars)
|
||||
if (!wallet || wallet.length !== 42 || !wallet.startsWith('0x')) {
|
||||
console.log(`[DB] ⚠️ Invalid wallet format/length (expected 42 chars), skipping:`, wallet?.substring(0, 50));
|
||||
return false;
|
||||
}
|
||||
|
||||
const normalizedWallet = wallet.toLowerCase();
|
||||
const connectionPool = getPool();
|
||||
|
||||
@@ -258,6 +309,20 @@ export async function addWalletToDB(wallet: string): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete wallet from database (e.g. when invalid length like tx hash)
|
||||
*/
|
||||
export async function deleteWalletFromDB(wallet: string): Promise<boolean> {
|
||||
try {
|
||||
const connectionPool = getPool();
|
||||
await connectionPool.execute('DELETE FROM wallets WHERE address = ?', [wallet]);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[DB] Error deleting wallet:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get wallet by address
|
||||
*/
|
||||
@@ -355,6 +420,12 @@ export async function migrateWalletsFromJSON(wallets: string[]): Promise<number>
|
||||
const connectionPool = getPool();
|
||||
|
||||
for (const wallet of wallets) {
|
||||
// Validate wallet address length (Ethereum addresses are exactly 42 characters)
|
||||
if (!wallet || wallet.length !== 42 || !wallet.startsWith('0x')) {
|
||||
console.log(`[DB] ⚠️ Skipping invalid wallet during migration (length: ${wallet?.length}):`, wallet?.substring(0, 50));
|
||||
continue;
|
||||
}
|
||||
|
||||
const normalizedWallet = wallet.toLowerCase();
|
||||
try {
|
||||
await connectionPool.execute(
|
||||
@@ -377,3 +448,283 @@ export async function migrateWalletsFromJSON(wallets: string[]): Promise<number>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up invalid wallet entries (addresses that are not exactly 42 characters)
|
||||
* This removes transaction hashes and other invalid data that may have been stored
|
||||
*/
|
||||
export async function cleanupInvalidWallets(): Promise<number> {
|
||||
try {
|
||||
const connectionPool = getPool();
|
||||
|
||||
// Delete wallets that are not exactly 42 characters (Ethereum addresses are 0x + 40 hex = 42)
|
||||
const [result] = await connectionPool.execute<mysql.ResultSetHeader>(
|
||||
`DELETE FROM wallets WHERE LENGTH(address) != 42 OR address NOT LIKE '0x%'`
|
||||
);
|
||||
|
||||
const deletedCount = result.affectedRows || 0;
|
||||
if (deletedCount > 0) {
|
||||
console.log(`[DB] 🧹 Cleaned up ${deletedCount} invalid wallet entries (non-42-char addresses)`);
|
||||
} else {
|
||||
console.log(`[DB] ✅ No invalid wallet entries found`);
|
||||
}
|
||||
|
||||
return deletedCount;
|
||||
} catch (error) {
|
||||
console.error('[DB] Error cleaning up invalid wallets:', error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a trade to the database
|
||||
* Uses entry_hash as unique identifier - updates existing trade if entry_hash exists and close info is provided
|
||||
*/
|
||||
export async function saveTradeToDB(
|
||||
walletAddress: string,
|
||||
entryHash: string,
|
||||
coin: string,
|
||||
amount: string | number,
|
||||
entryPrice: string | number,
|
||||
entryDate?: Date | string | null,
|
||||
closePrice?: string | number | null,
|
||||
closeDate?: Date | string | null,
|
||||
direction: 'buy' | 'sell' = 'buy'
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const normalizedWallet = walletAddress.toLowerCase();
|
||||
const normalizedEntryHash = entryHash.toLowerCase();
|
||||
const connectionPool = getPool();
|
||||
|
||||
// Get wallet ID
|
||||
const wallet = await getWalletFromDB(normalizedWallet);
|
||||
if (!wallet) {
|
||||
console.error(`[DB] Wallet not found for trade: ${normalizedWallet}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we have close information, check if it's a zero-PnL trade
|
||||
if (closePrice !== null && closePrice !== undefined && closeDate) {
|
||||
const entryPriceNum = parseFloat(String(entryPrice)) || 0;
|
||||
const closePriceNum = parseFloat(String(closePrice)) || 0;
|
||||
|
||||
// If entry_price equals close_price (zero-PnL), delete the trade instead
|
||||
if (Math.abs(entryPriceNum - closePriceNum) < 0.00000001) {
|
||||
console.log(`[DB] Deleting zero-PnL trade: entry_hash ${normalizedEntryHash.substring(0, 16)}... (entry: ${entryPriceNum}, close: ${closePriceNum})`);
|
||||
await connectionPool.execute(
|
||||
'DELETE FROM trades WHERE entry_hash = ?',
|
||||
[normalizedEntryHash]
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Update existing trade with close information
|
||||
await connectionPool.execute(
|
||||
`UPDATE trades
|
||||
SET close_price = ?, close_date = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE entry_hash = ?`,
|
||||
[closePrice, closeDate, normalizedEntryHash]
|
||||
);
|
||||
} else {
|
||||
// Insert new trade or update if entry_hash exists (for open trades)
|
||||
await connectionPool.execute(
|
||||
`INSERT INTO trades (wallet_id, wallet_address, entry_hash, coin, amount, entry_price, entry_date, close_price, close_date, direction)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
amount = VALUES(amount),
|
||||
entry_price = VALUES(entry_price),
|
||||
entry_date = VALUES(entry_date),
|
||||
direction = VALUES(direction),
|
||||
updated_at = CURRENT_TIMESTAMP`,
|
||||
[
|
||||
wallet.id,
|
||||
normalizedWallet,
|
||||
normalizedEntryHash,
|
||||
coin,
|
||||
amount,
|
||||
entryPrice,
|
||||
entryDate || null,
|
||||
closePrice || null,
|
||||
closeDate || null,
|
||||
direction,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[DB] Error saving trade:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate PnL from stored trades for a wallet
|
||||
* Returns: { realizedPnl, unrealizedPnl, totalPnl, closedTradesCount, openTradesCount }
|
||||
*/
|
||||
export async function calculatePnLFromTrades(walletAddress: string): Promise<{
|
||||
realizedPnl: string;
|
||||
unrealizedPnl: string;
|
||||
totalPnl: string;
|
||||
closedTradesCount: number;
|
||||
openTradesCount: number;
|
||||
}> {
|
||||
try {
|
||||
const trades = await getTradesForWallet(walletAddress);
|
||||
|
||||
let realizedPnl = 0;
|
||||
let unrealizedPnl = 0;
|
||||
let closedTradesCount = 0;
|
||||
let openTradesCount = 0;
|
||||
|
||||
for (const trade of trades) {
|
||||
const amount = parseFloat(trade.amount) || 0;
|
||||
const entryPrice = parseFloat(trade.entry_price) || 0;
|
||||
|
||||
if (trade.close_date && trade.close_price) {
|
||||
// Closed trade - calculate realized PnL
|
||||
const closePrice = parseFloat(trade.close_price) || 0;
|
||||
const tradePnl = (closePrice - entryPrice) * amount;
|
||||
realizedPnl += tradePnl;
|
||||
closedTradesCount++;
|
||||
} else {
|
||||
// Open trade - we'll need current price for unrealized PnL
|
||||
// For now, we'll set it to 0 and it can be updated with current prices if needed
|
||||
openTradesCount++;
|
||||
// Note: Unrealized PnL requires current market price, which we'd need to fetch
|
||||
// For now, we'll leave it at 0 or calculate it separately if current prices are available
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
realizedPnl: realizedPnl.toFixed(8),
|
||||
unrealizedPnl: unrealizedPnl.toFixed(8),
|
||||
totalPnl: (realizedPnl + unrealizedPnl).toFixed(8),
|
||||
closedTradesCount,
|
||||
openTradesCount,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[DB] Error calculating PnL from trades:', error);
|
||||
return {
|
||||
realizedPnl: '0',
|
||||
unrealizedPnl: '0',
|
||||
totalPnl: '0',
|
||||
closedTradesCount: 0,
|
||||
openTradesCount: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all trades for a wallet
|
||||
*/
|
||||
export async function getTradesForWallet(walletAddress: string): Promise<any[]> {
|
||||
try {
|
||||
const normalizedWallet = walletAddress.toLowerCase();
|
||||
const connectionPool = getPool();
|
||||
|
||||
const [rows] = await connectionPool.execute<mysql.RowDataPacket[]>(
|
||||
`SELECT
|
||||
id,
|
||||
wallet_address,
|
||||
entry_hash,
|
||||
entry_date,
|
||||
close_date,
|
||||
coin,
|
||||
amount,
|
||||
entry_price,
|
||||
close_price,
|
||||
direction,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM trades
|
||||
WHERE wallet_address = ?
|
||||
ORDER BY entry_date DESC, created_at DESC`,
|
||||
[normalizedWallet]
|
||||
);
|
||||
|
||||
return rows as any[];
|
||||
} catch (error) {
|
||||
console.error('[DB] Error getting trades for wallet:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all open trades (where close_date IS NULL)
|
||||
*/
|
||||
export async function getOpenTrades(): Promise<any[]> {
|
||||
try {
|
||||
const connectionPool = getPool();
|
||||
|
||||
const [rows] = await connectionPool.execute<mysql.RowDataPacket[]>(
|
||||
`SELECT
|
||||
id,
|
||||
wallet_address,
|
||||
entry_hash,
|
||||
entry_date,
|
||||
coin,
|
||||
amount,
|
||||
entry_price
|
||||
FROM trades
|
||||
WHERE close_date IS NULL
|
||||
ORDER BY entry_date DESC`
|
||||
);
|
||||
|
||||
return rows as any[];
|
||||
} catch (error) {
|
||||
console.error('[DB] Error getting open trades:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update trade close information by entry_hash
|
||||
* If entry_price equals close_price (zero-PnL), deletes the trade instead
|
||||
*/
|
||||
export async function updateTradeClose(
|
||||
entryHash: string,
|
||||
closePrice: string | number,
|
||||
closeDate: Date | string
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const normalizedEntryHash = entryHash.toLowerCase();
|
||||
const connectionPool = getPool();
|
||||
|
||||
// First, get the trade to check entry_price
|
||||
const [tradeRows] = await connectionPool.execute<mysql.RowDataPacket[]>(
|
||||
'SELECT entry_price FROM trades WHERE entry_hash = ?',
|
||||
[normalizedEntryHash]
|
||||
);
|
||||
|
||||
if (tradeRows.length === 0) {
|
||||
return false; // Trade not found
|
||||
}
|
||||
|
||||
const entryPrice = parseFloat(tradeRows[0].entry_price) || 0;
|
||||
const closePriceNum = parseFloat(String(closePrice)) || 0;
|
||||
|
||||
// If entry_price equals close_price (zero-PnL), delete the trade instead
|
||||
if (Math.abs(entryPrice - closePriceNum) < 0.00000001) {
|
||||
console.log(`[TRADES] Deleting zero-PnL trade: entry_hash ${normalizedEntryHash.substring(0, 16)}... (entry: ${entryPrice}, close: ${closePriceNum})`);
|
||||
await connectionPool.execute(
|
||||
'DELETE FROM trades WHERE entry_hash = ?',
|
||||
[normalizedEntryHash]
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise, update with close information
|
||||
const [result] = await connectionPool.execute<mysql.ResultSetHeader>(
|
||||
`UPDATE trades
|
||||
SET close_price = ?, close_date = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE entry_hash = ? AND close_date IS NULL`,
|
||||
[closePrice, closeDate, normalizedEntryHash]
|
||||
);
|
||||
|
||||
return result.affectedRows > 0;
|
||||
} catch (error) {
|
||||
console.error('[DB] Error updating trade close:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
915
src/index.ts
915
src/index.ts
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user