Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
a567587
Revert "Merge dev to main: Login fix for default admin password"
CryptoGnome Oct 12, 2025
e1013f4
feat(auth): skip password requirement for default "admin" password
CryptoGnome Oct 12, 2025
4ed3f50
feat: add TradingView charts with liquidation database management
birdbathd Nov 18, 2025
bb0a7b6
feat: add infinite historical data loading to chart
birdbathd Nov 18, 2025
0046305
fix: remove duplicate prependHistoricalKlines function
birdbathd Nov 18, 2025
f0b82f3
fix: resolve chart initialization issues with historical data loading
birdbathd Nov 18, 2025
b6299c0
fix: preserve chart view position after user interaction
birdbathd Nov 18, 2025
5c6973e
fix: Add WebSocket keepalive and inactivity monitoring to Hunter
birdbathd Nov 18, 2025
e6aa8c7
feat: Add TP/SL toggle and reorganize chart controls
birdbathd Nov 19, 2025
3a21847
Merge branch 'dev' into feature/tradingview-charts
birdbathd Nov 19, 2025
8d9aad1
fix: prevent duplicate liquidations from accumulated event listeners
birdbathd Nov 19, 2025
57ebca2
feat: add configurable auto-refresh interval for chart (5s to 5min op…
birdbathd Nov 19, 2025
c09577f
perf: optimize auto-refresh to fetch only latest 2 candles instead of…
birdbathd Nov 19, 2025
3aeb2a5
Add volume histogram to TradingView chart with toggle
birdbathd Nov 19, 2025
0164ead
Fix ReferenceError and reduce WebSocket console spam
birdbathd Nov 19, 2025
42657ff
Add debug logging to track Hunter event listener accumulation
birdbathd Nov 19, 2025
de4a230
Revert "Add volume histogram to TradingView chart with toggle"
birdbathd Nov 19, 2025
56556b4
Reduce WebSocket broadcast spam and add eventTime logging
birdbathd Nov 19, 2025
bcc201c
fix: prevent duplicate liquidations with database UNIQUE constraint
birdbathd Nov 20, 2025
87ea43c
chore: restore tranche implementation and docs accidentally deleted b…
birdbathd Nov 20, 2025
6bb2ce2
docs: merge back with dev - restore tranche documentation
birdbathd Nov 20, 2025
2159acc
fix: prevent duplicate liquidation events from inflating threshold co…
birdbathd Nov 21, 2025
0303219
refactor: improve WebSocket reconnection logic to prevent reconnectio…
birdbathd Nov 21, 2025
703fe2f
feat: enable tranche management UI configuration
birdbathd Nov 23, 2025
f045004
feat: enable tranche management system (untested)
birdbathd Nov 23, 2025
c527762
feat: implement protective orders system
birdbathd Nov 23, 2025
2d97d31
Add timestamps to logs UI and initialize ProtectiveOrderService
birdbathd Nov 23, 2025
6882f5b
Remove per-symbol protective order config and fix duplicate service s…
birdbathd Nov 23, 2025
218f606
Fix ProtectiveOrderService initialization check in API
birdbathd Nov 23, 2025
0e07ed4
feat: Transform trailing stop to trailing TP with break-even protecti…
birdbathd Nov 23, 2025
3d6cf0f
fix: trailing take profit activation and minimum profit logic
birdbathd Nov 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,10 @@ data/optimizer-jobs.json
# claude code local settings and agents
.claude/settings.local.json
.claude/agents/

# local development files (not for commit)
[WEB]
ecosystem.config.js
scripts/aster-notifier.cjs
*.swp
.*.swp
Empty file added [WEB]
Empty file.
4 changes: 4 additions & 0 deletions config.default.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@
"deduplicationWindowMs": 1000,
"parallelProcessing": true,
"maxConcurrentRequests": 3
},
"liquidationDatabase": {
"retentionDays": 90,
"cleanupIntervalHours": 24
}
},
"version": "1.1.0"
Expand Down
4,320 changes: 2,757 additions & 1,563 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 1 addition & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,6 @@
"test:ws": "tsx tests/core/websocket.test.ts",
"test:errors": "tsx tests/core/error-logging.test.ts",
"test:integration": "tsx tests/integration/trading-flow.test.ts",
"test:tranche": "tsx tests/tranche-system-test.ts",
"test:tranche:integration": "tsx tests/tranche-integration-test.ts",
"test:tranche:all": "tsx tests/tranche-system-test.ts && tsx tests/tranche-integration-test.ts",
"test:watch": "tsx watch tests/**/*.test.ts",
"optimize:ui": "node optimize-config.js"
},
Expand Down Expand Up @@ -55,7 +52,7 @@
"clsx": "^2.1.1",
"ethers": "^6.15.0",
"gsap": "^3.13.0",
"html-to-image": "^1.11.13",
"lightweight-charts": "^4.1.3",
"lucide-react": "^0.544.0",
"next": "15.5.4",
"next-auth": "^4.24.11",
Expand Down
9 changes: 6 additions & 3 deletions src/app/api/bot/control/route.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { NextRequest, NextResponse } from 'next/server';
import { withAuth } from '@/lib/auth/with-auth';
import WebSocket from 'ws';
import { configLoader } from '@/lib/config/configLoader';

// Helper to send control command via WebSocket
async function sendBotCommand(action: string): Promise<{ success: boolean; error?: string }> {
return new Promise((resolve) => {
const ws = new WebSocket('ws://localhost:8080');
const config = configLoader.getConfig();
const wsPort = config?.global?.server?.websocketPort || 8081;
const ws = new WebSocket(`ws://localhost:${wsPort}`);
const timeout = setTimeout(() => {
ws.close();
resolve({ success: false, error: 'Connection timeout' });
Expand Down Expand Up @@ -37,9 +40,9 @@ export const POST = withAuth(async (request: NextRequest, _user) => {
const body = await request.json();
const { action } = body;

if (!action || !['pause', 'resume'].includes(action)) {
if (!action || !['pause', 'resume', 'stop'].includes(action)) {
return NextResponse.json(
{ error: 'Invalid action. Must be one of: pause, resume' },
{ error: 'Invalid action. Must be one of: pause, resume, stop' },
{ status: 400 }
);
}
Expand Down
77 changes: 77 additions & 0 deletions src/app/api/klines/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { NextRequest, NextResponse } from 'next/server';
import { getKlines } from '@/lib/api/market';
import { getCandlesFor7Days } from '@/lib/klineCache';

export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams;

const symbol = searchParams.get('symbol');
if (!symbol) {
return NextResponse.json(
{ success: false, error: 'Symbol parameter is required' },
{ status: 400 }
);
}

const interval = searchParams.get('interval') || '5m';
const requestedLimit = parseInt(searchParams.get('limit') || '0');
const since = searchParams.get('since');
const endTime = searchParams.get('endTime');

// Validate interval
const validIntervals = ['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', '6h', '8h', '12h', '1d', '3d', '1w', '1M'];
if (!validIntervals.includes(interval)) {
return NextResponse.json(
{ success: false, error: `Invalid interval. Must be one of: ${validIntervals.join(', ')}` },
{ status: 400 }
);
}

// Calculate limit: use 7-day calculation if no specific limit requested
const limit = requestedLimit > 0
? Math.min(requestedLimit, 1500)
: getCandlesFor7Days(interval);

console.log(`[Klines API] Fetching ${limit} candles for ${symbol} ${interval} (7-day optimized: ${getCandlesFor7Days(interval)})`);

const klines = await getKlines(symbol, interval, limit, endTime ? parseInt(endTime) : undefined);

// Transform to lightweight-charts format: [timestamp, open, high, low, close, volume]
const chartData = klines.map(kline => [
Math.floor(kline.openTime / 1000), // Convert to seconds for TradingView
parseFloat(kline.open),
parseFloat(kline.high),
parseFloat(kline.low),
parseFloat(kline.close),
parseFloat(kline.volume)
]);

// Filter by since parameter if provided
const filteredData = since
? chartData.filter(([timestamp]) => timestamp >= parseInt(since) / 1000)
: chartData;

return NextResponse.json({
success: true,
data: filteredData,
symbol,
interval,
count: filteredData.length,
requestedLimit,
calculatedLimit: limit,
sevenDayOptimal: getCandlesFor7Days(interval)
});

} catch (error) {
console.error('API error - get klines:', error);
return NextResponse.json(
{
success: false,
error: 'Failed to fetch klines data',
details: error instanceof Error ? error.message : 'Unknown error'
},
{ status: 500 }
);
}
}
23 changes: 23 additions & 0 deletions src/app/api/liquidations/symbols/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { NextResponse } from 'next/server';
import { liquidationStorage } from '@/lib/services/liquidationStorage';

export async function GET() {
try {
const symbols = await liquidationStorage.getUniqueSymbols();

return NextResponse.json({
success: true,
symbols: symbols || []
});
} catch (error) {
console.error('[API] Error fetching liquidation symbols:', error);
return NextResponse.json(
{
success: false,
error: 'Failed to fetch liquidation symbols',
symbols: []
},
{ status: 500 }
);
}
}
153 changes: 153 additions & 0 deletions src/app/api/logs/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { NextRequest, NextResponse } from 'next/server';
import { exec } from 'child_process';
import { promisify } from 'util';

const execAsync = promisify(exec);

export const dynamic = 'force-dynamic';

interface LogEntry {
id: string;
timestamp: number;
timestampFormatted: string;
level: 'info' | 'warn' | 'error';
component: string;
message: string;
}

/**
* Parse PM2 log line into structured format
*/
function parseLogLine(line: string): LogEntry | null {
// Skip empty lines and web server logs
if (!line.trim() || line.includes('[WEB]')) return null;

// Extract timestamp: [HH:MM:SS.mmm]
const timestampMatch = line.match(/\[(\d{2}:\d{2}:\d{2}\.\d{3})\]/);
if (!timestampMatch) return null;

const timeStr = timestampMatch[1];
const now = new Date();
const [hours, minutes, secondsMs] = timeStr.split(':');
const [seconds, milliseconds] = secondsMs.split('.');

// Create a date object for today with the extracted time
const timestamp = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
parseInt(hours),
parseInt(minutes),
parseInt(seconds),
parseInt(milliseconds)
);

// Extract component from patterns like "ComponentName: message"
let component = 'System';
let message = line;

const componentMatch = line.match(/\[BOT\].*?\](.+)/);
if (componentMatch) {
message = componentMatch[1].trim();
const nameMatch = message.match(/^(\w+(?:Manager|Service)?)\s*:/);
if (nameMatch) {
component = nameMatch[1];
}
}

// Determine log level
let level: 'info' | 'warn' | 'error' = 'info';
if (message.toLowerCase().includes('error') || message.toLowerCase().includes('failed')) {
level = 'error';
} else if (message.toLowerCase().includes('warn')) {
level = 'warn';
}

// Generate a unique ID
const id = `${timestamp.getTime()}_${Math.random().toString(36).substr(2, 9)}`;

return {
id,
timestamp: timestamp.getTime(),
timestampFormatted: timeStr,
level,
component,
message
};
}

/**
* GET /api/logs
* Fetch logs from PM2 with optional filtering
*/
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams;
const component = searchParams.get('component') || undefined;
const level = searchParams.get('level') as 'info' | 'warn' | 'error' | undefined;
const limit = searchParams.get('limit') ? parseInt(searchParams.get('limit')!, 10) : 500;

// Get PM2 logs
const { stdout } = await execAsync(`pm2 logs aster --lines ${limit} --nostream --raw 2>&1 | grep "\\[BOT\\]" || true`);

const lines = stdout.split('\n').filter(l => l.trim());
const parsedLogs = lines
.map(parseLogLine)
.filter((log): log is LogEntry => log !== null);

// Filter by component
let filteredLogs = parsedLogs;
if (component && component !== 'all') {
filteredLogs = filteredLogs.filter(log => log.component === component);
}

// Filter by level
if (level) {
filteredLogs = filteredLogs.filter(log => log.level === level);
}

// Get unique components
const components = Array.from(new Set(parsedLogs.map(log => log.component))).sort();

return NextResponse.json({
success: true,
logs: filteredLogs.reverse(), // Most recent first
components,
count: filteredLogs.length,
});
} catch (error) {
console.error('[API] Error fetching logs:', error);
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Failed to fetch logs',
logs: [],
components: [],
},
{ status: 500 }
);
}
}

/**
* DELETE /api/logs
* Clear PM2 logs
*/
export async function DELETE() {
try {
await execAsync('pm2 flush aster');
return NextResponse.json({
success: true,
message: 'PM2 logs cleared',
});
} catch (error) {
console.error('[API] Error clearing logs:', error);
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Failed to clear logs',
},
{ status: 500 }
);
}
}
10 changes: 4 additions & 6 deletions src/app/api/orders/all/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,11 @@ export async function GET(request: NextRequest) {
);
allOrders = orders;
} else if (configuredSymbols.length > 0) {
// Fetch for all configured/active symbols
console.log(`[Orders API] Fetching orders from ${configuredSymbols.length} configured symbols...`);

// Fetch for all configured/active symbols (when symbol is undefined or 'ALL')
// Fetch generous amount per symbol to ensure we get enough orders
// The limit will be applied AFTER filtering and sorting all orders from all symbols
const perSymbolLimit = Math.max(200, limit * 2);
// If filtering by FILLED status, we need to fetch more because many orders might not be filled
const perSymbolLimit = status === 'FILLED' ? Math.max(500, limit * 10) : Math.max(200, limit * 2);

for (const sym of configuredSymbols) {
try {
Expand All @@ -88,9 +87,8 @@ export async function GET(request: NextRequest) {
config.api,
startTime ? parseInt(startTime) : undefined,
endTime ? parseInt(endTime) : undefined,
Math.min(perSymbolLimit, 500)
Math.min(perSymbolLimit, 1000)
);
console.log(`[Orders API] Fetched ${orders.length} orders from ${sym}`);
allOrders = allOrders.concat(orders);
} catch (err) {
console.error(`Failed to fetch orders for ${sym}:`, err);
Expand Down
23 changes: 4 additions & 19 deletions src/app/api/positions/[symbol]/[side]/close/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { loadConfig } from '@/lib/bot/config';
import { symbolPrecision } from '@/lib/utils/symbolPrecision';
import { getExchangeInfo } from '@/lib/api/market';
import { invalidateIncomeCache } from '@/lib/api/income';
import { paperModeSimulator } from '@/lib/services/paperModeSimulator';

export async function POST(
request: NextRequest,
Expand Down Expand Up @@ -88,27 +87,13 @@ export async function POST(

// Check if we're in paper mode (simulation)
if (config.global.paperMode) {
console.log(`PAPER MODE: Closing simulated position for ${symbol} ${side}`);

// Close the simulated position via paper mode simulator
const closed = await paperModeSimulator.closePosition(symbol, side, 'Manual close via UI');

if (!closed) {
return NextResponse.json(
{
error: `No simulated position found for ${symbol} ${side}`,
success: false,
simulated: true
},
{ status: 404 }
);
}

console.log(`PAPER MODE: Would close position for ${symbol} ${side} with quantity ${quantity}`);
return NextResponse.json({
success: true,
message: `Paper mode: Successfully closed simulated ${symbol} ${side} position`,
message: `Paper mode: Simulated closing ${symbol} ${side} position of ${quantity} units`,
simulated: true,
order_side: orderSide
order_side: orderSide,
quantity: quantity
});
}

Expand Down
Loading
Loading