From a56758771c57d51c140d018708beaeadf5022abd Mon Sep 17 00:00:00 2001 From: Crypto Gnome <33667144+CryptoGnome@users.noreply.github.com> Date: Sun, 12 Oct 2025 17:14:00 -0400 Subject: [PATCH 01/30] Revert "Merge dev to main: Login fix for default admin password" This reverts commit 74cea1aac92250b547d0bf664adfa74a599183f1, reversing changes made to 19214f7f434b32e659ed9121893dd2c5677dc46f. --- CLAUDE.md | 74 +- README.md | 124 - docs/TRANCHE_IMPLEMENTATION_PLAN.md | 2154 ----------------- docs/TRANCHE_TESTING.md | 433 ---- docs/TRANCHE_USER_GUIDE.md | 730 ------ package-lock.json | 7 - package.json | 4 - src/app/api/auth/check/route.ts | 3 +- src/app/api/bot/control/route.ts | 4 +- src/app/api/paper-mode/positions/route.ts | 53 - .../positions/[symbol]/[side]/close/route.ts | 23 +- src/app/api/tranches/route.ts | 89 - src/app/config/page.tsx | 59 +- src/app/login/page.tsx | 6 +- src/app/page.tsx | 104 +- src/app/tranches/page.tsx | 178 -- src/bot/index.ts | 196 -- src/bot/websocketServer.ts | 137 -- src/components/BotControlButtons.tsx | 129 +- src/components/LiquidationSidebar.tsx | 2 +- src/components/PerformanceCardInline.tsx | 43 +- src/components/PnLChart.tsx | 71 +- src/components/PositionTable.tsx | 85 +- src/components/SessionPerformanceCard.tsx | 61 +- src/components/ShareConfigModal.tsx | 236 -- src/components/SymbolConfigForm.tsx | 158 +- src/components/TrancheBreakdownCard.tsx | 336 --- src/components/TrancheSettingsSection.tsx | 251 -- src/components/TrancheTimeline.tsx | 218 -- src/components/dashboard-layout.tsx | 32 +- src/lib/bot/hunter.ts | 180 +- src/lib/bot/positionManager.ts | 233 +- src/lib/config/types.ts | 5 - src/lib/db/initDb.ts | 3 - src/lib/db/trancheDb.ts | 457 ---- src/lib/services/paperModeSimulator.ts | 335 --- src/lib/services/pnlService.ts | 18 +- src/lib/services/trancheManager.ts | 846 ------- src/lib/types.ts | 113 +- tests/tranche-integration-test.ts | 766 ------ tests/tranche-system-test.ts | 355 --- 41 files changed, 470 insertions(+), 8841 deletions(-) delete mode 100644 docs/TRANCHE_IMPLEMENTATION_PLAN.md delete mode 100644 docs/TRANCHE_TESTING.md delete mode 100644 docs/TRANCHE_USER_GUIDE.md delete mode 100644 src/app/api/paper-mode/positions/route.ts delete mode 100644 src/app/api/tranches/route.ts delete mode 100644 src/app/tranches/page.tsx delete mode 100644 src/components/ShareConfigModal.tsx delete mode 100644 src/components/TrancheBreakdownCard.tsx delete mode 100644 src/components/TrancheSettingsSection.tsx delete mode 100644 src/components/TrancheTimeline.tsx delete mode 100644 src/lib/db/trancheDb.ts delete mode 100644 src/lib/services/paperModeSimulator.ts delete mode 100644 src/lib/services/trancheManager.ts delete mode 100644 tests/tranche-integration-test.ts delete mode 100644 tests/tranche-system-test.ts diff --git a/CLAUDE.md b/CLAUDE.md index 77c59e3..6e4dc98 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -39,17 +39,14 @@ npm run lint # Run ESLint npx tsc --noEmit # Check TypeScript types # Testing -npm test # Run all tests -npm run test:hunter # Test Hunter component -npm run test:position # Test PositionManager -npm run test:rate # Test rate limiting -npm run test:ws # Test WebSocket functionality -npm run test:errors # Test error logging -npm run test:integration # Test trading flow integration -npm run test:tranche # Test tranche system (basic) -npm run test:tranche:integration # Test tranche integration (comprehensive) -npm run test:tranche:all # Run all tranche tests -npm run test:watch # Run tests in watch mode +npm test # Run all tests +npm run test:hunter # Test Hunter component +npm run test:position # Test PositionManager +npm run test:rate # Test rate limiting +npm run test:ws # Test WebSocket functionality +npm run test:errors # Test error logging +npm run test:integration # Test trading flow integration +npm run test:watch # Run tests in watch mode # Utilities npm run optimize:ui # Run configuration optimizer @@ -83,57 +80,10 @@ npm run optimize:ui # Run configuration optimizer |-----------|----------|---------| | **Hunter** | `src/lib/bot/hunter.ts` | Monitors liquidation streams, triggers trades | | **PositionManager** | `src/lib/bot/positionManager.ts` | Manages positions, SL/TP orders, user data streams | -| **TrancheManager** | `src/lib/services/trancheManager.ts` | Tracks multiple position entries (tranches) per symbol | | **AsterBot** | `src/bot/index.ts` | Main orchestrator coordinating Hunter and PositionManager | | **StatusBroadcaster** | `src/bot/websocketServer.ts` | WebSocket server for real-time UI updates | | **ProcessManager** | `scripts/process-manager.js` | Cross-platform process lifecycle management | -### Multi-Tranche Position Management - -The bot includes an advanced **multi-tranche system** that tracks multiple virtual position entries per symbol: - -**What are Tranches?** -- Virtual position entries tracked locally while exchange sees one combined position -- Allows isolation of underwater positions (>5% loss by default) -- Continue trading fresh positions without adding to losers -- Better margin utilization and risk management - -**Key Components:** -- **Database Layer** (`src/lib/db/trancheDb.ts`): Tranche and event storage with SQLite -- **TrancheManager Service** (`src/lib/services/trancheManager.ts`): Core tranche lifecycle management -- **Hunter Integration**: Pre-trade limit checks, post-order tranche creation -- **PositionManager Integration**: Tranche closing on SL/TP fills, exchange synchronization -- **UI Dashboard** (`/tranches`): Real-time tranche visualization and management - -**Configuration (per symbol):** -```json -{ - "enableTrancheManagement": true, - "trancheIsolationThreshold": 5, // % loss before isolation - "maxTranches": 3, // Max active tranches - "maxIsolatedTranches": 2, // Max isolated tranches - "trancheStrategy": { - "closingStrategy": "FIFO", // FIFO, LIFO, WORST_FIRST, BEST_FIRST - "slTpStrategy": "NEWEST", // NEWEST, OLDEST, BEST_ENTRY, AVERAGE - "isolationAction": "HOLD" // Action when isolated - }, - "allowTrancheWhileIsolated": true, // Continue trading with isolated tranches - "trancheAutoCloseIsolated": false // Auto-close when recovered -} -``` - -**Testing:** -```bash -npm run test:tranche # Basic system tests -npm run test:tranche:integration # Full integration tests (100% passing) -npm run test:tranche:all # Run all tranche tests -``` - -**Documentation:** -- Implementation Plan: `docs/TRANCHE_IMPLEMENTATION_PLAN.md` -- Testing Guide: `docs/TRANCHE_TESTING.md` -- User Guide: `docs/TRANCHE_USER_GUIDE.md` (for end users) - ### Services (`src/lib/services/`) - **balanceService.ts**: Real-time balance tracking via WebSocket @@ -143,7 +93,6 @@ npm run test:tranche:all # Run all tranche tests - **configManager.ts**: Hot-reload configuration management - **pnlService.ts**: Real-time P&L tracking and session metrics - **thresholdMonitor.ts**: 60-second rolling volume threshold tracking -- **trancheManager.ts**: Multi-tranche position tracking and lifecycle management ### API Layer (`src/lib/api/`) @@ -316,13 +265,6 @@ config.default.json # Default configuration template - Includes stack traces, timestamps, and trading data - Accessible via web UI at `/errors` -**Tranche Database** (`src/lib/db/trancheDb.ts`): -- Stores all tranche entries and lifecycle events -- Tracks active, isolated, and closed tranches -- Audit trail via `tranche_events` table -- Indexed for performance (symbol, side, status, entry_time) -- Automatic cleanup of old closed tranches - ## Error Handling ### Custom Error Types (`src/lib/errors/TradingErrors.ts`) diff --git a/README.md b/README.md index 849c2c9..076db4c 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,6 @@ A smart trading bot that monitors and trades liquidation events on Aster DEX. Fe - ๐Ÿ“ˆ **Real-time Liquidation Hunting** - Monitors and instantly trades liquidation events - ๐Ÿ’ฐ **Smart Position Management** - Automatic stop-loss and take-profit on every trade -- ๐ŸŽฏ **Multi-Tranche System** - Isolate losing positions while continuing to trade fresh entries - ๐Ÿงช **Paper Trading Mode** - Test strategies safely with simulated trades - ๐ŸŽจ **Beautiful Web Dashboard** - Monitor everything from a clean, modern UI - โšก **One-Click Setup** - Get running in under 2 minutes @@ -76,7 +75,6 @@ Access at http://localhost:3000 - **Dashboard** - Monitor positions and P&L - **Config** - Adjust all settings via UI -- **Tranches** - View and manage multi-tranche positions - **History** - View past trades ## โš™๏ธ Commands @@ -180,133 +178,11 @@ Found a bug in the dev branch? Help us improve! **Note**: Always start with paper mode when testing new beta features! -## ๐ŸŽฏ Advanced Features - -### Multi-Tranche Position Management - -The bot includes an intelligent **multi-tranche system** that dramatically improves trading performance when positions move against you: - -#### What are Tranches? - -Think of tranches as separate "sub-positions" within the same trading pair. Instead of one large position that you keep adding to, the bot tracks multiple independent entries: - -- **Position goes underwater (>5% loss)?** โ†’ Bot automatically **isolates** it -- **Continue trading?** โ†’ Bot opens **new tranches** without adding to the loser -- **Keep making profits?** โ†’ Trade fresh entries while holding positions recover -- **Better margin usage** โ†’ Don't let one bad position lock up all your capital - -#### Why Use Multi-Tranche? - -**Traditional Trading Problem:** -``` -Enter BTCUSDT LONG @ $50,000 -Price drops to $47,500 (-5%) -You're stuck: Can't trade more without adding to losing position -Miss opportunities while waiting for recovery -``` - -**With Multi-Tranche System:** -``` -Tranche #1: LONG @ $50,000 โ†’ Down 5% โ†’ ISOLATED (held separately) -Tranche #2: LONG @ $47,500 โ†’ Up 2% โ†’ CLOSE (+profit!) -Tranche #3: LONG @ $48,000 โ†’ Up 3% โ†’ CLOSE (+profit!) -Meanwhile, Tranche #1 recovers โ†’ Eventually closes at breakeven or profit -``` - -**Result:** You keep making money on new trades while bad positions recover naturally. - -#### Key Benefits - -โœ… **Isolate Losing Positions** - Underwater positions tracked separately -โœ… **Continue Trading** - Open fresh positions without adding to losers -โœ… **Better Margin Efficiency** - Don't lock up capital in losing trades -โœ… **Automatic Management** - Bot handles everything automatically -โœ… **Configurable Strategies** - Choose FIFO, LIFO, or close best/worst first -โœ… **Real-Time Monitoring** - Dashboard shows all tranches and their P&L - -#### How to Enable - -1. **Via Web UI** (Recommended): - - Go to http://localhost:3000/config - - Find your trading pair (e.g., BTCUSDT) - - Scroll to "Tranche Management Settings" - - Toggle "Enable Multi-Tranche Management" - - Configure settings: - - **Isolation Threshold**: When to isolate (default: 5% loss) - - **Max Tranches**: Max active positions (default: 3) - - **Max Isolated**: Max underwater positions before blocking new trades (default: 2) - - **Closing Strategy**: FIFO (oldest first), LIFO (newest first), WORST_FIRST, BEST_FIRST - - **SL/TP Strategy**: Which tranche's targets to use (NEWEST, OLDEST, BEST_ENTRY, AVERAGE) - -2. **Monitor Your Tranches**: - - Visit http://localhost:3000/tranches - - See all active, isolated, and closed tranches - - Real-time P&L tracking - - Event timeline showing tranche lifecycle - -#### Configuration Example - -```json -{ - "symbols": { - "BTCUSDT": { - "enableTrancheManagement": true, - "trancheIsolationThreshold": 5, - "maxTranches": 3, - "maxIsolatedTranches": 2, - "allowTrancheWhileIsolated": true, - "trancheStrategy": { - "closingStrategy": "FIFO", - "slTpStrategy": "NEWEST", - "isolationAction": "HOLD" - } - } - } -} -``` - -#### Safety & Risk Management - -The multi-tranche system includes built-in safety features: - -- **Position Limits**: Won't exceed max tranches per symbol -- **Isolation Blocking**: Stops new trades if too many positions are underwater -- **Exchange Sync**: Reconciles local tracking with exchange positions -- **Automatic Monitoring**: Checks every 10 seconds for positions needing isolation -- **Event Audit Trail**: Full history of every tranche action in database - -**โš ๏ธ Important Notes:** -- Start with **paper mode** to understand how tranches work -- Set conservative limits (3 max tranches, 2 max isolated is recommended) -- Higher isolation threshold (5-10%) prevents over-isolation -- Monitor the `/tranches` dashboard regularly - -#### Advanced Use Cases - -**Scalping Strategy:** -- Low isolation threshold (3%) -- High max tranches (5) -- LIFO closing (close newest first) -- Works great for quick in-and-out trades - -**Hold & Recover Strategy:** -- High isolation threshold (10%) -- Moderate max tranches (3) -- FIFO closing (close oldest first) -- Good for trending markets - -**Best Trade First:** -- BEST_FIRST closing strategy -- Take profits on winners quickly -- Hold losers for recovery -- Maximizes realized gains - ## ๐Ÿ›ก๏ธ Safety Features - Paper mode for testing - Automatic stop-loss/take-profit - Position size limits -- Multi-tranche isolation system - WebSocket auto-reconnection ## ๐ŸŒ Remote Access Configuration diff --git a/docs/TRANCHE_IMPLEMENTATION_PLAN.md b/docs/TRANCHE_IMPLEMENTATION_PLAN.md deleted file mode 100644 index 19ea89d..0000000 --- a/docs/TRANCHE_IMPLEMENTATION_PLAN.md +++ /dev/null @@ -1,2154 +0,0 @@ -# Multi-Tranche Position Management - Implementation Plan - -## โœ… IMPLEMENTATION COMPLETE - -**Status:** All 8 phases completed and tested -**Completion Date:** 2025-10-12 -**Branch:** `feature/tranche-management` -**Test Results:** 19/19 tests passing (100% pass rate) - -### Quick Summary - -The multi-tranche position management system has been successfully implemented with: -- โœ… Virtual tranche tracking layer with SQLite persistence -- โœ… Automatic isolation of underwater positions (>5% loss) -- โœ… Configurable closing strategies (FIFO/LIFO/WORST_FIRST/BEST_FIRST) -- โœ… Exchange synchronization and drift detection -- โœ… Real-time WebSocket updates and UI dashboard -- โœ… Comprehensive automated test suite -- โœ… Full documentation (user guide + technical docs) - -### Implementation Phases - -| Phase | Status | Tests | Notes | -|-------|--------|-------|-------| -| Phase 1: Foundation | โœ… Complete | N/A | Types, database schema, initialization | -| Phase 2: Core Service | โœ… Complete | 8/8 passing | TrancheManager with 700+ LOC | -| Phase 3: Hunter Integration | โœ… Complete | 2/2 passing | Pre-trade checks, post-order creation | -| Phase 4: Position Manager | โœ… Complete | 4/4 passing | Exit logic, SL/TP, exchange sync | -| Phase 5: Real-time Updates | โœ… Complete | 2/2 passing | WebSocket broadcasting, isolation monitoring | -| Phase 6: UI Dashboard | โœ… Complete | 1/1 passing | Tranche breakdown, timeline, config UI | -| Phase 7: Testing | โœ… Complete | 19/19 passing | System tests + integration tests | -| Phase 8: Documentation | โœ… Complete | N/A | README, CLAUDE.md, user guide | - ---- - -## Overview - -This document provides a step-by-step implementation plan for adding multi-tranche position management to the Aster Lick Hunter bot. The system will allow tracking multiple "virtual" position entries (tranches) while the exchange only sees a single combined position per symbol+side. - -### Core Problem -When a position goes underwater (>5% loss), we currently can't place new trades on the same symbol without adding to the losing position. This locks up margin and prevents us from taking advantage of new opportunities. - -### Solution Architecture -Implement a **virtual tranche tracking layer** that: -- Tracks multiple position entries locally as separate "tranches" -- Syncs with the single exchange position (reconciliation layer) -- Manages SL/TP orders intelligently across all tranches -- Allows isolation of underwater positions while opening fresh tranches - ---- - -## Phase 1: Foundation - Data Models & Database - -### 1.1 Type Definitions (`src/lib/types.ts`) - -- [ ] **Add Tranche Interface** - ```typescript - export interface Tranche { - // Identity - id: string; // UUID v4 - symbol: string; // e.g., "BTCUSDT" - side: 'LONG' | 'SHORT'; // Position direction - positionSide: 'LONG' | 'SHORT' | 'BOTH'; // Exchange position side - - // Entry details - entryPrice: number; // Average entry price for this tranche - quantity: number; // Position size in base asset (BTC, ETH, etc.) - marginUsed: number; // USDT margin allocated - leverage: number; // Leverage used (1-125) - entryTime: number; // Unix timestamp - entryOrderId?: string; // Exchange order ID that created this tranche - - // Exit details - exitPrice?: number; // Average exit price (when closed) - exitTime?: number; // Unix timestamp - exitOrderId?: string; // Exchange order ID that closed this tranche - - // P&L tracking - unrealizedPnl: number; // Current unrealized P&L (updated real-time) - realizedPnl: number; // Final realized P&L (on close) - - // Risk management (inherited from SymbolConfig at entry time) - tpPercent: number; // Take profit % - slPercent: number; // Stop loss % - tpPrice: number; // Calculated TP price - slPrice: number; // Calculated SL price - - // Status tracking - status: 'active' | 'closed' | 'liquidated'; - isolated: boolean; // True if underwater > isolation threshold - isolationTime?: number; // When it became isolated - isolationPrice?: number; // Price when isolated - - // Metadata - notes?: string; // Optional notes (e.g., "manual entry", "recovered from restart") - } - ``` - -- [ ] **Add TrancheGroup Interface** (manages all tranches for a symbol+side) - ```typescript - export interface TrancheGroup { - symbol: string; - side: 'LONG' | 'SHORT'; - positionSide: 'LONG' | 'SHORT' | 'BOTH'; - - // Tranche tracking - tranches: Tranche[]; // All tranches (active + closed) - activeTranches: Tranche[]; // Currently open tranches - isolatedTranches: Tranche[]; // Underwater tranches - - // Aggregated metrics (sum of active tranches) - totalQuantity: number; // Total position size - totalMarginUsed: number; // Total margin allocated - weightedAvgEntry: number; // Weighted average entry price - totalUnrealizedPnl: number; // Sum of all unrealized P&L - - // Exchange sync - lastExchangeQuantity: number; // Last known exchange position size - lastExchangeSync: number; // Last sync timestamp - syncStatus: 'synced' | 'drift' | 'conflict'; // Sync health - - // Order management - activeSlOrderId?: number; // Current exchange SL order - activeTpOrderId?: number; // Current exchange TP order - targetSlPrice?: number; // Target SL price - targetTpPrice?: number; // Target TP price - } - ``` - -- [ ] **Add TrancheStrategy Interface** (defines tranche behavior) - ```typescript - export interface TrancheStrategy { - // Closing priority when SL/TP hits - closingStrategy: 'FIFO' | 'LIFO' | 'WORST_FIRST' | 'BEST_FIRST'; - - // SL/TP calculation method - slTpStrategy: 'NEWEST' | 'OLDEST' | 'BEST_ENTRY' | 'AVERAGE'; - - // Isolation behavior - isolationAction: 'HOLD' | 'REDUCE_LEVERAGE' | 'PARTIAL_CLOSE'; - } - ``` - -- [ ] **Extend SymbolConfig Interface** - ```typescript - export interface SymbolConfig { - // ... existing fields ... - - // Tranche management settings - enableTrancheManagement?: boolean; // Enable multi-tranche system - trancheIsolationThreshold?: number; // % loss to isolate (default: 5) - maxTranches?: number; // Max active tranches (default: 3) - maxIsolatedTranches?: number; // Max isolated tranches before blocking (default: 2) - trancheAllocation?: 'equal' | 'dynamic'; // How to size new tranches - trancheStrategy?: TrancheStrategy; // Tranche behavior settings - - // Advanced tranche settings - allowTrancheWhileIsolated?: boolean; // Allow new tranches when some are isolated (default: true) - isolatedTrancheMinMargin?: number; // Min margin to keep in isolated tranches (USDT) - trancheAutoCloseIsolated?: boolean; // Auto-close isolated tranches at breakeven (default: false) - } - ``` - -### 1.2 Database Schema (`src/lib/db/trancheDb.ts`) - -- [ ] **Create Tranches Table** - ```sql - CREATE TABLE IF NOT EXISTS tranches ( - -- Identity - id TEXT PRIMARY KEY, - symbol TEXT NOT NULL, - side TEXT NOT NULL, -- 'LONG' | 'SHORT' - position_side TEXT NOT NULL, -- 'LONG' | 'SHORT' | 'BOTH' - - -- Entry details - entry_price REAL NOT NULL, - quantity REAL NOT NULL, - margin_used REAL NOT NULL, - leverage INTEGER NOT NULL, - entry_time INTEGER NOT NULL, - entry_order_id TEXT, - - -- Exit details - exit_price REAL, - exit_time INTEGER, - exit_order_id TEXT, - - -- P&L tracking - unrealized_pnl REAL DEFAULT 0, - realized_pnl REAL DEFAULT 0, - - -- Risk management - tp_percent REAL NOT NULL, - sl_percent REAL NOT NULL, - tp_price REAL NOT NULL, - sl_price REAL NOT NULL, - - -- Status - status TEXT DEFAULT 'active', -- 'active' | 'closed' | 'liquidated' - isolated BOOLEAN DEFAULT 0, - isolation_time INTEGER, - isolation_price REAL, - - -- Metadata - notes TEXT, - created_at INTEGER DEFAULT (strftime('%s', 'now')), - updated_at INTEGER DEFAULT (strftime('%s', 'now')) - ); - - -- Indexes for performance - CREATE INDEX IF NOT EXISTS idx_tranches_symbol_side_status - ON tranches(symbol, side, status); - CREATE INDEX IF NOT EXISTS idx_tranches_status - ON tranches(status); - CREATE INDEX IF NOT EXISTS idx_tranches_entry_time - ON tranches(entry_time DESC); - CREATE INDEX IF NOT EXISTS idx_tranches_isolated - ON tranches(isolated, status) WHERE isolated = 1; - ``` - -- [ ] **Create Tranche Events Table** (audit trail) - ```sql - CREATE TABLE IF NOT EXISTS tranche_events ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - tranche_id TEXT NOT NULL, - event_type TEXT NOT NULL, -- 'created' | 'isolated' | 'closed' | 'liquidated' | 'updated' - event_time INTEGER NOT NULL, - - -- Event details - price REAL, -- Price at event time - quantity REAL, -- Quantity affected - pnl REAL, -- P&L at event (if applicable) - - -- Context - trigger TEXT, -- What triggered the event - metadata TEXT, -- JSON with additional details - - FOREIGN KEY (tranche_id) REFERENCES tranches(id) - ); - - CREATE INDEX IF NOT EXISTS idx_tranche_events_tranche_id - ON tranche_events(tranche_id); - CREATE INDEX IF NOT EXISTS idx_tranche_events_time - ON tranche_events(event_time DESC); - ``` - -- [ ] **Implement Database Methods** - ```typescript - // Create - export async function createTranche(tranche: Tranche): Promise - - // Read - export async function getTranche(id: string): Promise - export async function getActiveTranches(symbol: string, side: string): Promise - export async function getIsolatedTranches(symbol: string, side: string): Promise - export async function getAllTranchesForSymbol(symbol: string): Promise - - // Update - export async function updateTranche(id: string, updates: Partial): Promise - export async function updateTrancheUnrealizedPnl(id: string, pnl: number): Promise - export async function isolateTranche(id: string, price: number): Promise - - // Delete/Close - export async function closeTranche(id: string, exitPrice: number, realizedPnl: number, orderId?: string): Promise - export async function liquidateTranche(id: string, liquidationPrice: number): Promise - - // Events - export async function logTrancheEvent(trancheId: string, eventType: string, data: any): Promise - export async function getTrancheHistory(trancheId: string): Promise - - // Cleanup - export async function cleanupOldTranches(daysToKeep: number = 30): Promise - ``` - -- [ ] **Add Database Initialization** to `src/lib/db/initDb.ts` - - Import and call tranche table creation - - Add to cleanup scheduler for old closed tranches - ---- - -## Phase 2: Core Service - Tranche Manager - -### 2.1 Tranche Manager Service (`src/lib/services/trancheManager.ts`) - -- [ ] **Service Structure** - ```typescript - class TrancheManagerService extends EventEmitter { - private trancheGroups: Map = new Map(); // key: "BTCUSDT_LONG" - private config: Config; - private priceService: any; // For real-time price updates - - constructor(config: Config) { - super(); - this.config = config; - } - } - ``` - -- [ ] **Initialization Methods** - ```typescript - // Initialize from database on startup - public async initialize(): Promise { - // Load all active tranches from DB - // Reconstruct TrancheGroups - // Subscribe to price updates - // Validate against exchange positions (sync check) - } - - // Check if tranche management is enabled for a symbol - public isEnabled(symbol: string): boolean { - return this.config.symbols[symbol]?.enableTrancheManagement === true; - } - ``` - -- [ ] **Tranche Creation Methods** - ```typescript - // Create a new tranche when opening a position - public async createTranche(params: { - symbol: string; - side: 'BUY' | 'SELL'; // Order side - positionSide: 'LONG' | 'SHORT' | 'BOTH'; - entryPrice: number; - quantity: number; - marginUsed: number; - leverage: number; - orderId?: string; - }): Promise { - const symbolConfig = this.config.symbols[params.symbol]; - const trancheSide = params.side === 'BUY' ? 'LONG' : 'SHORT'; - - // Calculate TP/SL prices - const tpPrice = this.calculateTpPrice(params.entryPrice, symbolConfig.tpPercent, trancheSide); - const slPrice = this.calculateSlPrice(params.entryPrice, symbolConfig.slPercent, trancheSide); - - const tranche: Tranche = { - id: uuidv4(), - symbol: params.symbol, - side: trancheSide, - positionSide: params.positionSide, - entryPrice: params.entryPrice, - quantity: params.quantity, - marginUsed: params.marginUsed, - leverage: params.leverage, - entryTime: Date.now(), - entryOrderId: params.orderId, - unrealizedPnl: 0, - realizedPnl: 0, - tpPercent: symbolConfig.tpPercent, - slPercent: symbolConfig.slPercent, - tpPrice, - slPrice, - status: 'active', - isolated: false, - }; - - // Save to database - await createTranche(tranche); - - // Add to in-memory tracking - const groupKey = this.getGroupKey(params.symbol, trancheSide); - let group = this.trancheGroups.get(groupKey); - if (!group) { - group = this.createTrancheGroup(params.symbol, trancheSide, params.positionSide); - this.trancheGroups.set(groupKey, group); - } - - group.tranches.push(tranche); - group.activeTranches.push(tranche); - this.recalculateGroupMetrics(group); - - // Log event - await logTrancheEvent(tranche.id, 'created', { - entryPrice: params.entryPrice, - quantity: params.quantity, - orderId: params.orderId, - }); - - // Emit event - this.emit('trancheCreated', tranche); - - return tranche; - } - ``` - -- [ ] **Tranche Isolation Methods** - ```typescript - // Check if a tranche should be isolated (P&L < threshold) - public shouldIsolateTranche(tranche: Tranche, currentPrice: number): boolean { - if (tranche.isolated || tranche.status !== 'active') { - return false; - } - - const symbolConfig = this.config.symbols[tranche.symbol]; - const threshold = symbolConfig?.trancheIsolationThreshold || 5; - - // Calculate unrealized P&L % - const pnlPercent = this.calculatePnlPercent( - tranche.entryPrice, - currentPrice, - tranche.side - ); - - return pnlPercent <= -threshold; // Negative = loss - } - - // Isolate a tranche (mark as underwater) - public async isolateTranche(trancheId: string, currentPrice?: number): Promise { - const tranche = await getTranche(trancheId); - if (!tranche || tranche.isolated) return; - - const price = currentPrice || await this.getCurrentPrice(tranche.symbol); - - await isolateTranche(trancheId, price); - - // Update in-memory - tranche.isolated = true; - tranche.isolationTime = Date.now(); - tranche.isolationPrice = price; - - const groupKey = this.getGroupKey(tranche.symbol, tranche.side); - const group = this.trancheGroups.get(groupKey); - if (group) { - // Move from active to isolated - group.activeTranches = group.activeTranches.filter(t => t.id !== trancheId); - group.isolatedTranches.push(tranche); - this.recalculateGroupMetrics(group); - } - - // Log event - await logTrancheEvent(trancheId, 'isolated', { - price, - unrealizedPnl: tranche.unrealizedPnl, - }); - - // Emit event - this.emit('trancheIsolated', tranche); - - logWithTimestamp(`TrancheManager: Isolated tranche ${trancheId.substring(0, 8)} for ${tranche.symbol} at ${price} (P&L: ${tranche.unrealizedPnl.toFixed(2)} USDT)`); - } - - // Monitor all active tranches and isolate if needed - public async checkIsolationConditions(): Promise { - for (const [_key, group] of this.trancheGroups) { - const currentPrice = await this.getCurrentPrice(group.symbol); - - for (const tranche of group.activeTranches) { - if (this.shouldIsolateTranche(tranche, currentPrice)) { - await this.isolateTranche(tranche.id, currentPrice); - } - } - } - } - ``` - -- [ ] **Tranche Closing Methods** - ```typescript - // Select which tranche(s) to close based on strategy - public selectTranchesToClose( - symbol: string, - side: 'LONG' | 'SHORT', - quantityToClose: number - ): Tranche[] { - const groupKey = this.getGroupKey(symbol, side); - const group = this.trancheGroups.get(groupKey); - if (!group) return []; - - const symbolConfig = this.config.symbols[symbol]; - const strategy = symbolConfig?.trancheStrategy?.closingStrategy || 'FIFO'; - - const tranchesToClose: Tranche[] = []; - let remainingQty = quantityToClose; - - // Sort tranches based on strategy - let sortedTranches = [...group.activeTranches]; - switch (strategy) { - case 'FIFO': - sortedTranches.sort((a, b) => a.entryTime - b.entryTime); // Oldest first - break; - case 'LIFO': - sortedTranches.sort((a, b) => b.entryTime - a.entryTime); // Newest first - break; - case 'WORST_FIRST': - sortedTranches.sort((a, b) => a.unrealizedPnl - b.unrealizedPnl); // Most negative first - break; - case 'BEST_FIRST': - sortedTranches.sort((a, b) => b.unrealizedPnl - a.unrealizedPnl); // Most positive first - break; - } - - // Select tranches until we have enough quantity - for (const tranche of sortedTranches) { - if (remainingQty <= 0) break; - - tranchesToClose.push(tranche); - remainingQty -= tranche.quantity; - } - - return tranchesToClose; - } - - // Close a tranche (fully or partially) - public async closeTranche(params: { - trancheId: string; - exitPrice: number; - quantityClosed?: number; // If partial close - realizedPnl: number; - orderId?: string; - }): Promise { - const tranche = await getTranche(params.trancheId); - if (!tranche) return; - - const isFullClose = !params.quantityClosed || params.quantityClosed >= tranche.quantity; - - if (isFullClose) { - // Full close - await closeTranche(params.trancheId, params.exitPrice, params.realizedPnl, params.orderId); - - // Update in-memory - tranche.status = 'closed'; - tranche.exitPrice = params.exitPrice; - tranche.exitTime = Date.now(); - tranche.exitOrderId = params.orderId; - tranche.realizedPnl = params.realizedPnl; - - const groupKey = this.getGroupKey(tranche.symbol, tranche.side); - const group = this.trancheGroups.get(groupKey); - if (group) { - group.activeTranches = group.activeTranches.filter(t => t.id !== params.trancheId); - group.isolatedTranches = group.isolatedTranches.filter(t => t.id !== params.trancheId); - this.recalculateGroupMetrics(group); - } - - await logTrancheEvent(params.trancheId, 'closed', { - exitPrice: params.exitPrice, - realizedPnl: params.realizedPnl, - orderId: params.orderId, - }); - - this.emit('trancheClosed', tranche); - - logWithTimestamp(`TrancheManager: Closed tranche ${params.trancheId.substring(0, 8)} for ${tranche.symbol} at ${params.exitPrice} (P&L: ${params.realizedPnl.toFixed(2)} USDT)`); - } else { - // Partial close - reduce quantity - const newQuantity = tranche.quantity - params.quantityClosed; - const proportionalPnl = params.realizedPnl * (params.quantityClosed / tranche.quantity); - - await updateTranche(params.trancheId, { - quantity: newQuantity, - realizedPnl: tranche.realizedPnl + proportionalPnl, - }); - - // Update in-memory - tranche.quantity = newQuantity; - tranche.realizedPnl += proportionalPnl; - - await logTrancheEvent(params.trancheId, 'updated', { - exitPrice: params.exitPrice, - quantityClosed: params.quantityClosed, - partialPnl: proportionalPnl, - }); - - this.emit('tranchePartialClose', tranche); - - logWithTimestamp(`TrancheManager: Partially closed tranche ${params.trancheId.substring(0, 8)} - ${params.quantityClosed} of ${tranche.quantity} (P&L: ${proportionalPnl.toFixed(2)} USDT)`); - } - } - - // Process order fill and close appropriate tranches - public async processOrderFill(params: { - symbol: string; - side: 'BUY' | 'SELL'; - positionSide: 'LONG' | 'SHORT' | 'BOTH'; - quantityFilled: number; - fillPrice: number; - realizedPnl: number; - orderId: string; - }): Promise { - const trancheSide = params.side === 'BUY' ? 'SHORT' : 'LONG'; // Closing side is opposite - - const tranchesToClose = this.selectTranchesToClose( - params.symbol, - trancheSide, - params.quantityFilled - ); - - let remainingQty = params.quantityFilled; - let remainingPnl = params.realizedPnl; - - for (const tranche of tranchesToClose) { - const qtyToClose = Math.min(remainingQty, tranche.quantity); - const proportionalPnl = remainingPnl * (qtyToClose / params.quantityFilled); - - await this.closeTranche({ - trancheId: tranche.id, - exitPrice: params.fillPrice, - quantityClosed: qtyToClose, - realizedPnl: proportionalPnl, - orderId: params.orderId, - }); - - remainingQty -= qtyToClose; - remainingPnl -= proportionalPnl; - - if (remainingQty <= 0) break; - } - } - ``` - -- [ ] **Exchange Sync Methods** - ```typescript - // Sync local tranches with exchange position - public async syncWithExchange( - symbol: string, - side: 'LONG' | 'SHORT', - exchangePosition: ExchangePosition - ): Promise { - const groupKey = this.getGroupKey(symbol, side); - const group = this.trancheGroups.get(groupKey); - - const exchangeQty = Math.abs(parseFloat(exchangePosition.positionAmt)); - - if (!group) { - if (exchangeQty > 0) { - // Exchange has position but we have no tranches - create "unknown" tranche - logWarnWithTimestamp(`TrancheManager: Found untracked position ${symbol} ${side}, creating recovery tranche`); - await this.createTranche({ - symbol, - side: side === 'LONG' ? 'BUY' : 'SELL', - positionSide: exchangePosition.positionSide as any, - entryPrice: parseFloat(exchangePosition.entryPrice), - quantity: exchangeQty, - marginUsed: exchangeQty * parseFloat(exchangePosition.entryPrice) / parseFloat(exchangePosition.leverage), - leverage: parseFloat(exchangePosition.leverage), - }); - } - return; - } - - // Compare quantities - const localQty = group.totalQuantity; - const drift = Math.abs(localQty - exchangeQty); - const driftPercent = (drift / Math.max(exchangeQty, 0.00001)) * 100; - - if (driftPercent > 1) { // More than 1% drift - logWarnWithTimestamp(`TrancheManager: Quantity drift detected for ${symbol} ${side} - Local: ${localQty}, Exchange: ${exchangeQty} (${driftPercent.toFixed(2)}% drift)`); - group.syncStatus = 'drift'; - - if (exchangeQty === 0 && localQty > 0) { - // Exchange position closed but we still have tranches - close all - logWarnWithTimestamp(`TrancheManager: Exchange position closed, closing all local tranches`); - for (const tranche of group.activeTranches) { - await this.closeTranche({ - trancheId: tranche.id, - exitPrice: parseFloat(exchangePosition.markPrice), - realizedPnl: 0, // Unknown - already realized on exchange - }); - } - } else if (exchangeQty > 0 && localQty === 0) { - // Exchange has position but we have no tranches - logWarnWithTimestamp(`TrancheManager: Creating recovery tranche for untracked position`); - await this.createTranche({ - symbol, - side: side === 'LONG' ? 'BUY' : 'SELL', - positionSide: exchangePosition.positionSide as any, - entryPrice: parseFloat(exchangePosition.entryPrice), - quantity: exchangeQty, - marginUsed: exchangeQty * parseFloat(exchangePosition.entryPrice) / parseFloat(exchangePosition.leverage), - leverage: parseFloat(exchangePosition.leverage), - }); - } else if (exchangeQty < localQty) { - // Partial close on exchange - close oldest tranches to match - const qtyToClose = localQty - exchangeQty; - const tranchesToClose = this.selectTranchesToClose(symbol, side, qtyToClose); - - for (const tranche of tranchesToClose) { - await this.closeTranche({ - trancheId: tranche.id, - exitPrice: parseFloat(exchangePosition.markPrice), - quantityClosed: Math.min(tranche.quantity, qtyToClose), - realizedPnl: 0, // Unknown - }); - } - } - } else { - group.syncStatus = 'synced'; - } - - group.lastExchangeQuantity = exchangeQty; - group.lastExchangeSync = Date.now(); - } - ``` - -- [ ] **Position Limit Checks** - ```typescript - // Check if we can open a new tranche - public canOpenNewTranche(symbol: string, side: 'LONG' | 'SHORT'): { - allowed: boolean; - reason?: string; - } { - const symbolConfig = this.config.symbols[symbol]; - if (!symbolConfig?.enableTrancheManagement) { - return { allowed: true }; // Not using tranche system - } - - const groupKey = this.getGroupKey(symbol, side); - const group = this.trancheGroups.get(groupKey); - - if (!group) { - return { allowed: true }; // First tranche - } - - // Check max active tranches - const maxTranches = symbolConfig.maxTranches || 3; - if (group.activeTranches.length >= maxTranches) { - return { - allowed: false, - reason: `Max active tranches (${maxTranches}) reached for ${symbol}`, - }; - } - - // Check max isolated tranches - const maxIsolated = symbolConfig.maxIsolatedTranches || 2; - if (group.isolatedTranches.length >= maxIsolated) { - if (!symbolConfig.allowTrancheWhileIsolated) { - return { - allowed: false, - reason: `Max isolated tranches (${maxIsolated}) reached for ${symbol}`, - }; - } - } - - return { allowed: true }; - } - ``` - -- [ ] **P&L Update Methods** - ```typescript - // Update unrealized P&L for all active tranches - public async updateUnrealizedPnl(symbol: string, currentPrice: number): Promise { - const groups = [ - this.trancheGroups.get(this.getGroupKey(symbol, 'LONG')), - this.trancheGroups.get(this.getGroupKey(symbol, 'SHORT')), - ]; - - for (const group of groups) { - if (!group) continue; - - for (const tranche of group.activeTranches) { - const pnl = this.calculateUnrealizedPnl( - tranche.entryPrice, - currentPrice, - tranche.quantity, - tranche.side - ); - - tranche.unrealizedPnl = pnl; - - // Update in DB (batch update for performance) - await updateTrancheUnrealizedPnl(tranche.id, pnl); - } - - this.recalculateGroupMetrics(group); - } - - // Check isolation conditions after P&L update - await this.checkIsolationConditions(); - } - - // Calculate unrealized P&L for a tranche - private calculateUnrealizedPnl( - entryPrice: number, - currentPrice: number, - quantity: number, - side: 'LONG' | 'SHORT' - ): number { - if (side === 'LONG') { - return (currentPrice - entryPrice) * quantity; - } else { - return (entryPrice - currentPrice) * quantity; - } - } - - // Calculate P&L percentage - private calculatePnlPercent( - entryPrice: number, - currentPrice: number, - side: 'LONG' | 'SHORT' - ): number { - if (side === 'LONG') { - return ((currentPrice - entryPrice) / entryPrice) * 100; - } else { - return ((entryPrice - currentPrice) / entryPrice) * 100; - } - } - ``` - -- [ ] **Helper Methods** - ```typescript - private getGroupKey(symbol: string, side: 'LONG' | 'SHORT'): string { - return `${symbol}_${side}`; - } - - private createTrancheGroup( - symbol: string, - side: 'LONG' | 'SHORT', - positionSide: 'LONG' | 'SHORT' | 'BOTH' - ): TrancheGroup { - return { - symbol, - side, - positionSide, - tranches: [], - activeTranches: [], - isolatedTranches: [], - totalQuantity: 0, - totalMarginUsed: 0, - weightedAvgEntry: 0, - totalUnrealizedPnl: 0, - lastExchangeQuantity: 0, - lastExchangeSync: Date.now(), - syncStatus: 'synced', - }; - } - - private recalculateGroupMetrics(group: TrancheGroup): void { - // Sum quantities and margins - let totalQty = 0; - let totalMargin = 0; - let weightedEntry = 0; - let totalPnl = 0; - - for (const tranche of group.activeTranches) { - totalQty += tranche.quantity; - totalMargin += tranche.marginUsed; - weightedEntry += tranche.entryPrice * tranche.quantity; - totalPnl += tranche.unrealizedPnl; - } - - group.totalQuantity = totalQty; - group.totalMarginUsed = totalMargin; - group.weightedAvgEntry = totalQty > 0 ? weightedEntry / totalQty : 0; - group.totalUnrealizedPnl = totalPnl; - } - - private async getCurrentPrice(symbol: string): Promise { - if (this.priceService) { - const price = this.priceService.getPrice(symbol); - if (price) return price; - } - - // Fallback to API - const markPriceData = await getMarkPrice(symbol); - return parseFloat(Array.isArray(markPriceData) ? markPriceData[0].markPrice : markPriceData.markPrice); - } - - private calculateTpPrice(entryPrice: number, tpPercent: number, side: 'LONG' | 'SHORT'): number { - if (side === 'LONG') { - return entryPrice * (1 + tpPercent / 100); - } else { - return entryPrice * (1 - tpPercent / 100); - } - } - - private calculateSlPrice(entryPrice: number, slPercent: number, side: 'LONG' | 'SHORT'): number { - if (side === 'LONG') { - return entryPrice * (1 - slPercent / 100); - } else { - return entryPrice * (1 + slPercent / 100); - } - } - - // Public getters - public getTranches(symbol: string, side: 'LONG' | 'SHORT'): Tranche[] { - const groupKey = this.getGroupKey(symbol, side); - return this.trancheGroups.get(groupKey)?.activeTranches || []; - } - - public getTrancheGroup(symbol: string, side: 'LONG' | 'SHORT'): TrancheGroup | undefined { - const groupKey = this.getGroupKey(symbol, side); - return this.trancheGroups.get(groupKey); - } - - public getAllTrancheGroups(): TrancheGroup[] { - return Array.from(this.trancheGroups.values()); - } - ``` - -- [ ] **Export Singleton Instance** - ```typescript - let trancheManager: TrancheManagerService | null = null; - - export function initializeTrancheManager(config: Config): TrancheManagerService { - trancheManager = new TrancheManagerService(config); - return trancheManager; - } - - export function getTrancheManager(): TrancheManagerService { - if (!trancheManager) { - throw new Error('TrancheManager not initialized'); - } - return trancheManager; - } - ``` - ---- - -## Phase 3: Hunter Integration (Entry Logic) - -### 3.1 Modify Hunter to Use Tranche Manager - -- [ ] **Import Tranche Manager in `src/lib/bot/hunter.ts`** - ```typescript - import { getTrancheManager } from '../services/trancheManager'; - ``` - -- [ ] **Update `placeTrade()` Method - Pre-Trade Checks** - ```typescript - // Add BEFORE existing position limit checks (around line 758) - - // Check tranche management - if (this.config.symbols[symbol]?.enableTrancheManagement) { - const trancheManager = getTrancheManager(); - const trancheSide = side === 'BUY' ? 'LONG' : 'SHORT'; - - // Update P&L and check isolation conditions - const currentPrice = await getMarkPrice(symbol); - const price = parseFloat(Array.isArray(currentPrice) ? currentPrice[0].markPrice : currentPrice.markPrice); - await trancheManager.updateUnrealizedPnl(symbol, price); - - // Check if we can open a new tranche - const canOpen = trancheManager.canOpenNewTranche(symbol, trancheSide); - if (!canOpen.allowed) { - logWithTimestamp(`Hunter: ${canOpen.reason}`); - - // Broadcast to UI - if (this.statusBroadcaster) { - this.statusBroadcaster.broadcastTradingError( - `Tranche Limit Reached - ${symbol}`, - canOpen.reason || 'Cannot open new tranche', - { - component: 'Hunter', - symbol, - details: { - activeTranches: trancheManager.getTranches(symbol, trancheSide).length, - maxTranches: this.config.symbols[symbol].maxTranches || 3, - } - } - ); - } - - return; // Block the trade - } - } - ``` - -- [ ] **Update `placeTrade()` Method - Post-Order Creation** - ```typescript - // Add AFTER order is successfully placed (around line 1151) - - // Only broadcast and emit if order was successfully placed - if (order && order.orderId) { - // Create tranche if tranche management is enabled - if (this.config.symbols[symbol]?.enableTrancheManagement) { - const trancheManager = getTrancheManager(); - const trancheSide = side === 'BUY' ? 'LONG' : 'SHORT'; - - try { - const tranche = await trancheManager.createTranche({ - symbol, - side, - positionSide: getPositionSide(this.isHedgeMode, side) as any, - entryPrice: orderType === 'LIMIT' ? orderPrice : entryPrice, - quantity, - marginUsed: tradeSizeUSDT, - leverage: symbolConfig.leverage, - orderId: order.orderId.toString(), - }); - - logWithTimestamp(`Hunter: Created tranche ${tranche.id.substring(0, 8)} for ${symbol} ${side}`); - } catch (error) { - logErrorWithTimestamp('Hunter: Failed to create tranche:', error); - // Don't fail the trade, just log the error - } - } - - // Existing broadcast and emit code... - } - ``` - ---- - -## Phase 4: Position Manager Integration (Exit Logic) - -### 4.1 Modify Position Manager for Tranche Tracking - -- [ ] **Import Tranche Manager in `src/lib/bot/positionManager.ts`** - ```typescript - import { getTrancheManager } from '../services/trancheManager'; - ``` - -- [ ] **Update `syncWithExchange()` Method** - ```typescript - // Add AFTER processing each position (around line 432) - - if (symbolConfig && symbolConfig.enableTrancheManagement) { - const trancheManager = getTrancheManager(); - const trancheSide = posAmt > 0 ? 'LONG' : 'SHORT'; - - try { - await trancheManager.syncWithExchange(symbol, trancheSide, position); - } catch (error) { - logErrorWithTimestamp(`PositionManager: Failed to sync tranches for ${symbol}:`, error); - } - } - ``` - -- [ ] **Update `handleOrderUpdate()` Method - Process Fills** - ```typescript - // Add when order fills with realized P&L (around line 997) - - if (orderStatus === 'FILLED' && order.rp) { - const symbol = order.s; - const symbolConfig = this.config.symbols[symbol]; - - // Check if tranche management is enabled - if (symbolConfig?.enableTrancheManagement) { - const trancheManager = getTrancheManager(); - const reduceOnlyFill = order.R === true || order.R === 'true'; - - if (reduceOnlyFill) { - // This is a closing order (SL or TP) - const quantityFilled = parseFloat(order.z); // Cumulative filled qty - const fillPrice = parseFloat(order.ap); // Average price - const realizedPnl = parseFloat(order.rp); // Realized profit - const orderId = order.i.toString(); - - try { - await trancheManager.processOrderFill({ - symbol, - side: order.S, - positionSide: order.ps || 'BOTH', - quantityFilled, - fillPrice, - realizedPnl, - orderId, - }); - - logWithTimestamp(`PositionManager: Processed tranche close for ${symbol}, qty: ${quantityFilled}, P&L: ${realizedPnl.toFixed(2)} USDT`); - } catch (error) { - logErrorWithTimestamp(`PositionManager: Failed to process tranche fill for ${symbol}:`, error); - } - } - } - } - ``` - -### 4.2 SL/TP Order Management Strategy - -**Critical Challenge**: The exchange only allows ONE SL and ONE TP order per position, but we have multiple tranches with different targets. - -**Solution Strategy**: Use the NEWEST (most favorable) tranche's TP/SL targets - -- [ ] **Create Helper Method for Tranche-Based SL/TP Calculation** - ```typescript - // Add to PositionManager class - - private async calculateTrancheBasedTargets( - symbol: string, - side: 'LONG' | 'SHORT', - totalQuantity: number - ): Promise<{ slPrice: number; tpPrice: number; targetTranche: Tranche } | null> { - const symbolConfig = this.config.symbols[symbol]; - if (!symbolConfig?.enableTrancheManagement) { - return null; - } - - const trancheManager = getTrancheManager(); - const activeTranches = trancheManager.getTranches(symbol, side); - - if (activeTranches.length === 0) { - return null; - } - - // Get strategy - const strategy = symbolConfig.trancheStrategy?.slTpStrategy || 'NEWEST'; - - let targetTranche: Tranche; - - switch (strategy) { - case 'NEWEST': - // Use newest tranche (most favorable entry) - targetTranche = activeTranches.sort((a, b) => b.entryTime - a.entryTime)[0]; - break; - - case 'OLDEST': - // Use oldest tranche - targetTranche = activeTranches.sort((a, b) => a.entryTime - b.entryTime)[0]; - break; - - case 'BEST_ENTRY': - // Use tranche with best entry price - if (side === 'LONG') { - targetTranche = activeTranches.sort((a, b) => a.entryPrice - b.entryPrice)[0]; // Lowest entry - } else { - targetTranche = activeTranches.sort((a, b) => b.entryPrice - a.entryPrice)[0]; // Highest entry - } - break; - - case 'AVERAGE': - // Use weighted average of all tranches - const group = trancheManager.getTrancheGroup(symbol, side); - if (!group) return null; - - const avgEntry = group.weightedAvgEntry; - const avgTpPercent = activeTranches.reduce((sum, t) => sum + t.tpPercent, 0) / activeTranches.length; - const avgSlPercent = activeTranches.reduce((sum, t) => sum + t.slPercent, 0) / activeTranches.length; - - const slPrice = side === 'LONG' - ? avgEntry * (1 - avgSlPercent / 100) - : avgEntry * (1 + avgSlPercent / 100); - - const tpPrice = side === 'LONG' - ? avgEntry * (1 + avgTpPercent / 100) - : avgEntry * (1 - avgTpPercent / 100); - - return { - slPrice: symbolPrecision.formatPrice(symbol, slPrice), - tpPrice: symbolPrecision.formatPrice(symbol, tpPrice), - targetTranche: activeTranches[0], // Use first tranche for reference - }; - - default: - targetTranche = activeTranches[0]; - } - - logWithTimestamp(`PositionManager: Using ${strategy} tranche for SL/TP - Entry: ${targetTranche.entryPrice}, SL: ${targetTranche.slPrice}, TP: ${targetTranche.tpPrice}`); - - return { - slPrice: targetTranche.slPrice, - tpPrice: targetTranche.tpPrice, - targetTranche, - }; - } - ``` - -- [ ] **Update `placeProtectiveOrdersWithLock()` Method** - ```typescript - // Modify around line 1000 (inside try block of placeProtectiveOrdersWithLock) - - // Calculate SL/TP prices - let slPrice: number; - let tpPrice: number; - - // Check if tranche management is enabled - const trancheTargets = await this.calculateTrancheBasedTargets( - position.symbol, - isLong ? 'LONG' : 'SHORT', - positionQty - ); - - if (trancheTargets) { - // Use tranche-based targets - slPrice = trancheTargets.slPrice; - tpPrice = trancheTargets.tpPrice; - - logWithTimestamp(`PositionManager: Using tranche-based targets for ${symbol} - SL: ${slPrice}, TP: ${tpPrice}`); - } else { - // Use traditional calculation (existing code) - const entryPrice = parseFloat(position.entryPrice); - const slPercent = symbolConfig.slPercent; - const tpPercent = symbolConfig.tpPercent; - - slPrice = isLong - ? entryPrice * (1 - slPercent / 100) - : entryPrice * (1 + slPercent / 100); - - tpPrice = isLong - ? entryPrice * (1 + tpPercent / 100) - : entryPrice * (1 - tpPercent / 100); - - // Format prices - slPrice = symbolPrecision.formatPrice(position.symbol, slPrice); - tpPrice = symbolPrecision.formatPrice(position.symbol, tpPrice); - } - - // Continue with existing order placement logic... - ``` - -- [ ] **Update `adjustProtectiveOrders()` Method** - ```typescript - // Add at the start of adjustProtectiveOrders method - - // Recalculate targets based on tranche strategy - const trancheTargets = await this.calculateTrancheBasedTargets( - position.symbol, - isLong ? 'LONG' : 'SHORT', - positionQty - ); - - if (trancheTargets) { - // Use tranche-based targets for adjustment - // (Update the calculation to use trancheTargets.slPrice and trancheTargets.tpPrice) - } - ``` - ---- - -## Phase 5: Real-Time Updates & Monitoring - -### 5.1 Price Update Integration - -- [ ] **Subscribe to Price Updates in Tranche Manager** - ```typescript - // In trancheManager.initialize() - - const priceService = getPriceService(); - if (priceService) { - // Subscribe to all symbols with active tranches - const symbols = new Set(); - for (const group of this.trancheGroups.values()) { - if (group.activeTranches.length > 0) { - symbols.add(group.symbol); - } - } - - if (symbols.size > 0) { - priceService.subscribeToSymbols(Array.from(symbols)); - } - - // Listen for price updates - priceService.on('priceUpdate', async (data: { symbol: string; price: number }) => { - await this.updateUnrealizedPnl(data.symbol, data.price); - }); - } - ``` - -- [ ] **Periodic Isolation Check** - ```typescript - // In trancheManager class - - private isolationCheckInterval?: NodeJS.Timeout; - - public startIsolationMonitoring(intervalMs: number = 10000): void { - this.stopIsolationMonitoring(); - - this.isolationCheckInterval = setInterval(async () => { - try { - await this.checkIsolationConditions(); - } catch (error) { - logErrorWithTimestamp('TrancheManager: Isolation check failed:', error); - } - }, intervalMs); - - logWithTimestamp(`TrancheManager: Started isolation monitoring (every ${intervalMs / 1000}s)`); - } - - public stopIsolationMonitoring(): void { - if (this.isolationCheckInterval) { - clearInterval(this.isolationCheckInterval); - this.isolationCheckInterval = undefined; - logWithTimestamp('TrancheManager: Stopped isolation monitoring'); - } - } - ``` - -### 5.2 WebSocket Event Broadcasting - -- [ ] **Add Tranche Events to Status Broadcaster** - ```typescript - // In src/bot/websocketServer.ts - - // Add new broadcast methods - public broadcastTrancheCreated(tranche: Tranche): void { - this.broadcast('tranche_created', { - id: tranche.id, - symbol: tranche.symbol, - side: tranche.side, - entryPrice: tranche.entryPrice, - quantity: tranche.quantity, - marginUsed: tranche.marginUsed, - leverage: tranche.leverage, - timestamp: tranche.entryTime, - }); - } - - public broadcastTrancheIsolated(tranche: Tranche): void { - this.broadcast('tranche_isolated', { - id: tranche.id, - symbol: tranche.symbol, - side: tranche.side, - isolationPrice: tranche.isolationPrice, - unrealizedPnl: tranche.unrealizedPnl, - timestamp: tranche.isolationTime, - }); - } - - public broadcastTrancheClosed(tranche: Tranche): void { - this.broadcast('tranche_closed', { - id: tranche.id, - symbol: tranche.symbol, - side: tranche.side, - exitPrice: tranche.exitPrice, - realizedPnl: tranche.realizedPnl, - timestamp: tranche.exitTime, - }); - } - - public broadcastTrancheUpdate(group: TrancheGroup): void { - this.broadcast('tranche_update', { - symbol: group.symbol, - side: group.side, - activeTranches: group.activeTranches.length, - isolatedTranches: group.isolatedTranches.length, - totalQuantity: group.totalQuantity, - totalMarginUsed: group.totalMarginUsed, - weightedAvgEntry: group.weightedAvgEntry, - totalUnrealizedPnl: group.totalUnrealizedPnl, - syncStatus: group.syncStatus, - }); - } - ``` - -- [ ] **Connect Tranche Manager Events to Broadcaster** - ```typescript - // In src/bot/index.ts (AsterBot initialization) - - // After initializing tranche manager - const trancheManager = getTrancheManager(); - - trancheManager.on('trancheCreated', (tranche) => { - this.statusBroadcaster.broadcastTrancheCreated(tranche); - }); - - trancheManager.on('trancheIsolated', (tranche) => { - this.statusBroadcaster.broadcastTrancheIsolated(tranche); - }); - - trancheManager.on('trancheClosed', (tranche) => { - this.statusBroadcaster.broadcastTrancheClosed(tranche); - }); - - trancheManager.on('tranchePartialClose', (tranche) => { - this.statusBroadcaster.broadcastTrancheUpdate( - trancheManager.getTrancheGroup(tranche.symbol, tranche.side)! - ); - }); - ``` - ---- - -## Phase 6: UI Dashboard Integration - -### 6.1 Tranche Breakdown Component - -- [ ] **Create `src/components/TrancheBreakdownCard.tsx`** - ```typescript - import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; - import { Badge } from '@/components/ui/badge'; - import { Button } from '@/components/ui/button'; - import { TrendingUp, TrendingDown, AlertTriangle } from 'lucide-react'; - - interface Tranche { - id: string; - side: 'LONG' | 'SHORT'; - entryPrice: number; - quantity: number; - marginUsed: number; - leverage: number; - unrealizedPnl: number; - isolated: boolean; - entryTime: number; - tpPrice: number; - slPrice: number; - } - - interface TrancheBreakdownProps { - symbol: string; - tranches: Tranche[]; - currentPrice: number; - onCloseTranche?: (trancheId: string) => void; - } - - export function TrancheBreakdownCard({ symbol, tranches, currentPrice, onCloseTranche }: TrancheBreakdownProps) { - const activeTranches = tranches.filter(t => !t.isolated); - const isolatedTranches = tranches.filter(t => t.isolated); - - const totalPnl = tranches.reduce((sum, t) => sum + t.unrealizedPnl, 0); - const totalMargin = tranches.reduce((sum, t) => sum + t.marginUsed, 0); - - return ( - - - - {symbol} Tranches -
- = 0 ? "success" : "destructive"}> - {totalPnl >= 0 ? '+' : ''}{totalPnl.toFixed(2)} USDT - - - {tranches.length} Total - -
-
-
- - {/* Active Tranches */} - {activeTranches.length > 0 && ( -
-

Active Tranches

-
- {activeTranches.map(tranche => ( - - ))} -
-
- )} - - {/* Isolated Tranches */} - {isolatedTranches.length > 0 && ( -
-

- - Isolated Tranches -

-
- {isolatedTranches.map(tranche => ( - - ))} -
-
- )} - - {/* Summary */} -
-
- Total Margin: - {totalMargin.toFixed(2)} USDT -
-
-
-
- ); - } - - function TrancheRow({ tranche, currentPrice, isolated, onClose }: { - tranche: Tranche; - currentPrice: number; - isolated?: boolean; - onClose?: (id: string) => void; - }) { - const pnlPercent = ((currentPrice - tranche.entryPrice) / tranche.entryPrice) * 100 * (tranche.side === 'LONG' ? 1 : -1); - const isProfitable = tranche.unrealizedPnl >= 0; - - return ( -
-
-
- {tranche.side === 'LONG' ? ( - - ) : ( - - )} - - {tranche.side} - - - {new Date(tranche.entryTime).toLocaleTimeString()} - -
- - {isProfitable ? '+' : ''}{pnlPercent.toFixed(2)}% - -
- -
-
- Entry: - ${tranche.entryPrice.toFixed(4)} -
-
- Size: - {tranche.quantity.toFixed(4)} -
-
- Margin: - {tranche.marginUsed.toFixed(2)} USDT -
-
- P&L: - - {isProfitable ? '+' : ''}{tranche.unrealizedPnl.toFixed(2)} USDT - -
-
- TP: - ${tranche.tpPrice.toFixed(4)} -
-
- SL: - ${tranche.slPrice.toFixed(4)} -
-
- - {onClose && ( - - )} -
- ); - } - ``` - -- [ ] **Create API Route for Tranche Data (`src/app/api/tranches/route.ts`)** - ```typescript - import { NextResponse } from 'next/server'; - import { getTrancheManager } from '@/lib/services/trancheManager'; - - export async function GET(request: Request) { - try { - const { searchParams } = new URL(request.url); - const symbol = searchParams.get('symbol'); - const side = searchParams.get('side') as 'LONG' | 'SHORT' | null; - - const trancheManager = getTrancheManager(); - - if (symbol && side) { - const tranches = trancheManager.getTranches(symbol, side); - return NextResponse.json({ tranches }); - } else if (symbol) { - const longTranches = trancheManager.getTranches(symbol, 'LONG'); - const shortTranches = trancheManager.getTranches(symbol, 'SHORT'); - return NextResponse.json({ - long: longTranches, - short: shortTranches, - }); - } else { - const allGroups = trancheManager.getAllTrancheGroups(); - return NextResponse.json({ groups: allGroups }); - } - } catch (error) { - return NextResponse.json({ error: 'Failed to fetch tranches' }, { status: 500 }); - } - } - - export async function POST(request: Request) { - try { - const { action, trancheId, price } = await request.json(); - const trancheManager = getTrancheManager(); - - if (action === 'isolate' && trancheId) { - await trancheManager.isolateTranche(trancheId, price); - return NextResponse.json({ success: true }); - } - - if (action === 'close' && trancheId && price) { - // Manual close - would need to place order on exchange - // For now, just return error - return NextResponse.json({ error: 'Manual close not implemented' }, { status: 501 }); - } - - return NextResponse.json({ error: 'Invalid action' }, { status: 400 }); - } catch (error) { - return NextResponse.json({ error: 'Action failed' }, { status: 500 }); - } - } - ``` - -- [ ] **Add Tranche Breakdown to Dashboard (`src/app/page.tsx`)** - ```typescript - // Import - import { TrancheBreakdownCard } from '@/components/TrancheBreakdownCard'; - - // Add WebSocket listener for tranche updates - useEffect(() => { - if (!ws) return; - - const handleTrancheUpdate = (data: any) => { - // Update tranche state - setTrancheGroups(prev => ({ - ...prev, - [`${data.symbol}_${data.side}`]: data, - })); - }; - - ws.addEventListener('message', (event) => { - const data = JSON.parse(event.data); - if (data.type === 'tranche_update') { - handleTrancheUpdate(data.data); - } - if (data.type === 'tranche_created') { - // Refresh tranche data - } - if (data.type === 'tranche_isolated') { - // Show notification - } - if (data.type === 'tranche_closed') { - // Show notification - } - }); - }, [ws]); - - // Render tranche cards for each symbol with active tranches - ``` - -### 6.2 Tranche Timeline Component - -- [ ] **Create `src/components/TrancheTimeline.tsx`** - ```typescript - import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; - import { Badge } from '@/components/ui/badge'; - - interface TrancheEvent { - id: string; - trancheId: string; - eventType: 'created' | 'isolated' | 'closed' | 'liquidated'; - eventTime: number; - price: number; - pnl?: number; - } - - interface TrancheTimelineProps { - symbol: string; - events: TrancheEvent[]; - } - - export function TrancheTimeline({ symbol, events }: TrancheTimelineProps) { - const sortedEvents = [...events].sort((a, b) => b.eventTime - a.eventTime); - - return ( - - - {symbol} Tranche History - - -
- {/* Timeline line */} -
- - {/* Events */} -
- {sortedEvents.map(event => ( -
- {/* Timeline dot */} -
- - {/* Event content */} -
-
- - {event.eventType.toUpperCase()} - - - {new Date(event.eventTime).toLocaleString()} - -
-
- Price: - ${event.price.toFixed(4)} - {event.pnl !== undefined && ( - <> - P&L: - = 0 ? 'text-green-600' : 'text-red-600'}`}> - {event.pnl >= 0 ? '+' : ''}{event.pnl.toFixed(2)} USDT - - - )} -
-
-
- ))} -
-
- - - ); - } - - function getEventColor(type: string): string { - switch (type) { - case 'created': return 'bg-blue-500'; - case 'isolated': return 'bg-yellow-500'; - case 'closed': return 'bg-green-500'; - case 'liquidated': return 'bg-red-500'; - default: return 'bg-gray-500'; - } - } - - function getEventVariant(type: string): 'default' | 'success' | 'destructive' | 'warning' { - switch (type) { - case 'created': return 'default'; - case 'isolated': return 'warning'; - case 'closed': return 'success'; - case 'liquidated': return 'destructive'; - default: return 'default'; - } - } - ``` - -### 6.3 Configuration UI Updates - -- [ ] **Add Tranche Settings to `src/components/SymbolConfigForm.tsx`** - ```typescript - // Add new section for tranche management -
-

Tranche Management

- -
- handleChange('enableTrancheManagement', e.target.checked)} - /> - -
- - {config.enableTrancheManagement && ( - <> -
-
- - handleChange('trancheIsolationThreshold', parseFloat(e.target.value))} - min={1} - max={50} - step={0.5} - /> -

% loss to isolate tranche

-
- -
- - handleChange('maxTranches', parseInt(e.target.value))} - min={1} - max={10} - /> -
- -
- - handleChange('maxIsolatedTranches', parseInt(e.target.value))} - min={0} - max={5} - /> -
- -
- - -
- -
- - -
-
- -
- handleChange('allowTrancheWhileIsolated', e.target.checked)} - /> - -
- - )} -
- ``` - ---- - -## Phase 7: Testing & Validation - -### 7.1 Unit Tests - -- [ ] **Create `tests/services/trancheManager.test.ts`** - ```typescript - import { describe, it, expect, beforeEach } from '@jest/globals'; - import { TrancheManagerService } from '@/lib/services/trancheManager'; - import { Config } from '@/lib/types'; - - describe('TrancheManager', () => { - let trancheManager: TrancheManagerService; - let config: Config; - - beforeEach(() => { - config = { - // Mock config - }; - trancheManager = new TrancheManagerService(config); - }); - - describe('Tranche Creation', () => { - it('should create a new tranche', async () => { - // Test tranche creation - }); - - it('should calculate correct TP/SL prices', async () => { - // Test TP/SL calculation - }); - - it('should enforce max tranche limits', async () => { - // Test limits - }); - }); - - describe('Tranche Isolation', () => { - it('should isolate tranche when P&L drops below threshold', async () => { - // Test isolation - }); - - it('should not isolate if already isolated', async () => { - // Test duplicate isolation prevention - }); - }); - - describe('Tranche Closing', () => { - it('should close tranche fully', async () => { - // Test full close - }); - - it('should close tranche partially', async () => { - // Test partial close - }); - - it('should select correct tranches based on strategy', async () => { - // Test FIFO, LIFO, etc. - }); - }); - - describe('Exchange Sync', () => { - it('should sync with exchange position', async () => { - // Test sync - }); - - it('should detect and handle drift', async () => { - // Test drift handling - }); - - it('should create recovery tranche for untracked positions', async () => { - // Test recovery - }); - }); - - describe('P&L Calculations', () => { - it('should calculate unrealized P&L correctly for LONG', async () => { - // Test LONG P&L - }); - - it('should calculate unrealized P&L correctly for SHORT', async () => { - // Test SHORT P&L - }); - - it('should update group metrics correctly', async () => { - // Test aggregation - }); - }); - }); - ``` - -- [ ] **Create `tests/db/trancheDb.test.ts`** - ```typescript - import { describe, it, expect, beforeEach } from '@jest/globals'; - import { - createTranche, - getTranche, - getActiveTranches, - closeTranche, - isolateTranche, - } from '@/lib/db/trancheDb'; - - describe('Tranche Database', () => { - beforeEach(async () => { - // Setup test database - }); - - it('should create and retrieve tranche', async () => { - // Test CRUD operations - }); - - it('should query active tranches', async () => { - // Test queries - }); - - it('should update tranche status', async () => { - // Test updates - }); - }); - ``` - -### 7.2 Integration Tests - -- [ ] **Create `tests/integration/tranche-flow.test.ts`** - ```typescript - import { describe, it, expect } from '@jest/globals'; - - describe('Tranche Flow Integration', () => { - it('should complete full tranche lifecycle', async () => { - // 1. Create tranche on entry - // 2. Update P&L - // 3. Isolate when underwater - // 4. Open new tranche - // 5. Close profitable tranche - // 6. Verify state - }); - - it('should sync with exchange correctly', async () => { - // Test sync scenarios - }); - - it('should handle SL/TP fills correctly', async () => { - // Test order fills - }); - }); - ``` - -### 7.3 Manual Testing Checklist - -- [ ] **Basic Tranche Operations** - - [ ] Open position with tranche management enabled - - [ ] Verify tranche created in database - - [ ] Check tranche appears in UI - - [ ] Update price and verify P&L calculation - - [ ] Trigger isolation by price drop >5% - - [ ] Verify isolated tranche shown separately in UI - -- [ ] **Multiple Tranches** - - [ ] Open 2nd tranche while 1st is active - - [ ] Verify both show in UI - - [ ] Check SL/TP orders use correct strategy (newest/oldest/etc) - - [ ] Trigger TP and verify correct tranche closes (FIFO/LIFO) - -- [ ] **Edge Cases** - - [ ] Restart bot with active tranches - - [ ] Verify tranches recovered from database - - [ ] Sync with exchange position - - [ ] Place manual trade on exchange - - [ ] Verify "unknown" tranche created - - [ ] Test with max tranches reached - -- [ ] **UI Testing** - - [ ] Check tranche breakdown card displays correctly - - [ ] Verify timeline shows events - - [ ] Test configuration settings save/load - - [ ] Check WebSocket updates in real-time - ---- - -## Phase 8: Documentation & Deployment - -### 8.1 Documentation - -- [ ] **Update `CLAUDE.md`** - - Add tranche management overview - - Document configuration options - - Add troubleshooting section - -- [ ] **Create `docs/TRANCHE_SYSTEM.md`** - - Detailed architecture explanation - - Usage guide - - FAQ section - -- [ ] **Update `README.md`** - - Add tranche management to features list - - Link to detailed documentation - -### 8.2 Configuration Defaults - -- [ ] **Update `config.default.json`** - ```json - { - "symbols": { - "BTCUSDT": { - "enableTrancheManagement": false, - "trancheIsolationThreshold": 5, - "maxTranches": 3, - "maxIsolatedTranches": 2, - "allowTrancheWhileIsolated": true, - "trancheStrategy": { - "closingStrategy": "FIFO", - "slTpStrategy": "NEWEST" - } - } - } - } - ``` - -### 8.3 Migration & Deployment - -- [ ] **Create Migration Script** (`scripts/migrate-to-tranches.js`) - - Scan existing positions - - Create "recovery" tranches for untracked positions - - Verify data integrity - -- [ ] **Deployment Checklist** - - [ ] Backup current database - - [ ] Run database migrations - - [ ] Test with paper mode first - - [ ] Gradually enable for live symbols - - [ ] Monitor for issues - ---- - -## Risk Mitigation & Monitoring - -### Known Risks - -1. **Exchange Sync Issues** - - **Risk**: Local tranches drift from exchange position - - **Mitigation**: Regular sync checks, drift detection, automatic reconciliation - - **Monitoring**: Log sync status, alert on drift >2% - -2. **SL/TP Order Coordination** - - **Risk**: Single exchange SL/TP doesn't protect all tranches optimally - - **Mitigation**: Use configurable strategy (NEWEST/AVERAGE/etc) - - **Monitoring**: Track which tranches hit SL/TP, adjust strategy if needed - -3. **Database Corruption** - - **Risk**: Tranche data lost or corrupted - - **Mitigation**: Regular backups, recovery from exchange state - - **Monitoring**: Validate data integrity on startup - -4. **Performance Impact** - - **Risk**: Tranche management adds processing overhead - - **Mitigation**: Efficient DB queries, in-memory caching, batch updates - - **Monitoring**: Track latency, optimize slow queries - -5. **Complexity Bugs** - - **Risk**: Edge cases cause unexpected behavior - - **Mitigation**: Comprehensive testing, logging, fail-safes - - **Monitoring**: Error tracking, user reports - -### Monitoring Dashboard - -- [ ] **Add Tranche Metrics to Dashboard** - - Total active tranches across all symbols - - Total isolated tranches - - Average tranche duration - - Sync health status - - P&L attribution accuracy - ---- - -## Success Criteria - -### Functional Requirements -- โœ… Create multiple virtual tranches per symbol+side -- โœ… Isolate underwater tranches automatically -- โœ… Allow new trades while holding isolated positions -- โœ… Sync virtual tranches with single exchange position -- โœ… Close tranches based on configurable strategy (FIFO/LIFO/etc) -- โœ… Calculate and display per-tranche P&L -- โœ… Persist tranches to database for recovery - -### Performance Requirements -- โœ… P&L updates complete in <100ms -- โœ… Tranche creation adds <50ms to trade execution -- โœ… UI updates render in <500ms -- โœ… Database queries return in <50ms - -### User Experience -- โœ… Clear visualization of all tranches -- โœ… Easy configuration in UI -- โœ… Helpful error messages and warnings -- โœ… Accurate real-time P&L tracking - ---- - -## Timeline Estimate - -| Phase | Estimated Time | Dependencies | -|-------|---------------|--------------| -| Phase 1: Foundation | 1-2 days | None | -| Phase 2: Core Service | 2-3 days | Phase 1 | -| Phase 3: Hunter Integration | 0.5 day | Phase 2 | -| Phase 4: Position Manager | 1 day | Phase 2 | -| Phase 5: Real-time Updates | 0.5 day | Phase 2-4 | -| Phase 6: UI Dashboard | 2 days | Phase 5 | -| Phase 7: Testing | 1-2 days | Phase 6 | -| Phase 8: Docs & Deploy | 0.5 day | Phase 7 | -| **Total** | **8-11 days** | | - ---- - -## Next Steps - -1. Review this plan and get approval -2. Set up development branch: `git checkout -b feature/tranche-management` -3. Start with Phase 1 (Foundation) -4. Implement incrementally with testing at each phase -5. Deploy to paper mode for validation -6. Gradual rollout to live trading - ---- - -## Questions & Decisions Needed - -- [ ] **Tranche Naming**: Should users be able to name/tag tranches? -- [ ] **Manual Tranche Management**: Allow manual tranche creation/closure via UI? -- [ ] **Tranche Limits**: Global max tranches across all symbols? -- [ ] **Isolation Actions**: What to do with isolated tranches? (Hold, reduce leverage, partial close?) -- [ ] **Reporting**: Export tranche history to CSV/JSON? -- [ ] **Advanced Features**: DCA into isolated tranches? Tranche merging? - ---- - -*This implementation plan provides a comprehensive roadmap for adding multi-tranche position management. Each checkbox represents a discrete, completable task. Follow the phases sequentially for best results.* diff --git a/docs/TRANCHE_TESTING.md b/docs/TRANCHE_TESTING.md deleted file mode 100644 index 90b6f6f..0000000 --- a/docs/TRANCHE_TESTING.md +++ /dev/null @@ -1,433 +0,0 @@ -# Multi-Tranche Position Management - Testing Guide - -## Overview - -This guide provides comprehensive testing procedures for the multi-tranche position management system. The system allows tracking multiple virtual position entries (tranches) per symbol while syncing with a single exchange position. - -## Prerequisites - -Before testing, ensure: -- [ ] TypeScript compilation passes: `npx tsc --noEmit` โœ… -- [ ] All Phase 1-5 code is committed to `feature/multi-tranche-management` branch -- [ ] Database is initialized with tranche tables -- [ ] Configuration includes tranche-enabled symbols - -## Test Environment Setup - -### 1. Configuration Setup - -Add tranche management settings to your test symbol in `config.user.json`: - -```json -{ - "symbols": { - "BTCUSDT": { - "enableTrancheManagement": true, - "trancheIsolationThreshold": 5, - "maxTranches": 3, - "maxIsolatedTranches": 2, - "trancheStrategy": { - "closingStrategy": "FIFO", - "slTpStrategy": "NEWEST", - "isolationAction": "HOLD" - }, - "allowTrancheWhileIsolated": true, - "trancheAutoCloseIsolated": false - } - }, - "global": { - "paperMode": true - } -} -``` - -### 2. Database Verification - -Check that tranche tables were created: - -```bash -# Open database -sqlite3 liquidations.db - -# Verify tables exist -.tables -# Should show: tranches, tranche_events - -# Check tranche table schema -.schema tranches - -# Check events table schema -.schema tranche_events - -# Exit -.exit -``` - -Expected `tranches` table columns: -- id, symbol, side, positionSide, entryPrice, quantity, marginUsed, leverage -- entryTime, entryOrderId, exitPrice, exitTime, exitOrderId -- unrealizedPnl, realizedPnl, tpPercent, slPercent, tpPrice, slPrice -- status, isolated, isolationTime, isolationPrice, notes - -## Manual Testing Checklist - -### Phase 1: Database Layer Tests - -#### Test 1.1: Database Initialization -- [ ] Start bot: `npm run dev:bot` -- [ ] Verify log: `โœ… Database initialized` -- [ ] Check for tranche table creation logs -- [ ] No database errors in console - -#### Test 1.2: Database CRUD Operations -```bash -# Test creating a tranche record directly -node -e " -const { createTranche } = require('./src/lib/db/trancheDb'); -createTranche({ - id: 'test-uuid-001', - symbol: 'BTCUSDT', - side: 'LONG', - positionSide: 'LONG', - entryPrice: 50000, - quantity: 0.001, - marginUsed: 5, - leverage: 10, - entryTime: Date.now(), - entryOrderId: '123456', - unrealizedPnl: 0, - realizedPnl: 0, - tpPercent: 5, - slPercent: 2, - tpPrice: 52500, - slPrice: 49000, - status: 'active', - isolated: false -}).then(() => console.log('โœ… Tranche created')).catch(e => console.error('โŒ Error:', e)); -" -``` - -Expected: `โœ… Tranche created` - -Verify in database: -```bash -sqlite3 liquidations.db "SELECT * FROM tranches WHERE id='test-uuid-001';" -``` - -### Phase 2: TrancheManager Service Tests - -#### Test 2.1: TrancheManager Initialization -- [ ] Enable tranche management for BTCUSDT in config -- [ ] Start bot: `npm run dev:bot` -- [ ] Look for log: `โœ… Tranche Manager initialized for 1 symbol(s): BTCUSDT` -- [ ] Verify no initialization errors - -#### Test 2.2: Tranche Creation via Manager -```bash -# Create test script -node -e " -const { loadConfig } = require('./src/lib/bot/config'); -const { initializeTrancheManager } = require('./src/lib/services/trancheManager'); - -(async () => { - const config = await loadConfig(); - const tm = initializeTrancheManager(config); - await tm.initialize(); - - const tranche = await tm.createTranche({ - symbol: 'BTCUSDT', - side: 'BUY', - positionSide: 'LONG', - entryPrice: 50000, - quantity: 0.001, - marginUsed: 5, - leverage: 10, - orderId: 'test-order-001' - }); - - console.log('โœ… Tranche created:', tranche.id.substring(0, 8)); - console.log('Entry:', tranche.entryPrice, 'TP:', tranche.tpPrice, 'SL:', tranche.slPrice); -})(); -" -``` - -Expected output: -- `โœ… Tranche created: xxxxxxxx` -- Entry, TP, and SL prices calculated correctly - -#### Test 2.3: Isolation Logic -```bash -# Test isolation threshold calculation -node -e " -const { loadConfig } = require('./src/lib/bot/config'); -const { initializeTrancheManager } = require('./src/lib/services/trancheManager'); - -(async () => { - const config = await loadConfig(); - const tm = initializeTrancheManager(config); - await tm.initialize(); - - // Create tranche at 50000 - const tranche = await tm.createTranche({ - symbol: 'BTCUSDT', - side: 'BUY', - positionSide: 'LONG', - entryPrice: 50000, - quantity: 0.001, - marginUsed: 5, - leverage: 10, - orderId: 'test-order-002' - }); - - console.log('Tranche created at entry:', tranche.entryPrice); - - // Test isolation at 47500 (5% loss) - const shouldIsolate = tm.shouldIsolateTranche(tranche, 47500); - console.log('Should isolate at 47500 (5% loss)?', shouldIsolate); - - // Test at 48000 (4% loss) - const shouldNotIsolate = tm.shouldIsolateTranche(tranche, 48000); - console.log('Should isolate at 48000 (4% loss)?', shouldNotIsolate); -})(); -" -``` - -Expected: -- Should isolate at 47500: `true` โœ… -- Should isolate at 48000: `false` โœ… - -### Phase 3: Hunter Integration Tests - -#### Test 3.1: Pre-Trade Tranche Checks -- [ ] Enable paper mode and tranche management -- [ ] Set `maxTranches: 2` for BTCUSDT -- [ ] Start bot and wait for liquidation opportunities -- [ ] Observe logs for tranche limit checks -- [ ] After 2 tranches created, verify 3rd trade is blocked - -Expected logs: -``` -Hunter: Tranche Limit Reached - BTCUSDT -Hunter: Active tranches (2) >= maxTranches (2) -``` - -#### Test 3.2: Tranche Creation on Order Fill -- [ ] Clear existing tranches from database -- [ ] Start bot with paper mode enabled -- [ ] Wait for a liquidation opportunity and order placement -- [ ] Check logs for: `Hunter: Created tranche xxxxxxxx for BTCUSDT BUY` -- [ ] Verify tranche in database: - -```bash -sqlite3 liquidations.db "SELECT id, symbol, side, entryPrice, quantity, status FROM tranches ORDER BY entryTime DESC LIMIT 1;" -``` - -Expected: New tranche record with correct details - -### Phase 4: PositionManager Integration Tests - -#### Test 4.1: Tranche Closing on SL/TP Fill -This test requires actual positions to be closed. Best tested in paper mode with mock fills: - -- [ ] Create 2 tranches for BTCUSDT LONG (via Hunter) -- [ ] Simulate SL/TP order fill (requires live trading or paper mode simulation) -- [ ] Check logs for: `PositionManager: Processed tranche close for BTCUSDT LONG` -- [ ] Verify tranches marked as closed in database - -```bash -sqlite3 liquidations.db "SELECT id, status, exitPrice, realizedPnl FROM tranches WHERE status='closed' ORDER BY exitTime DESC LIMIT 5;" -``` - -#### Test 4.2: Exchange Synchronization -- [ ] Create 2 tranches manually in database (total quantity 0.002 BTC) -- [ ] Open position on exchange with quantity 0.002 BTC -- [ ] Trigger ACCOUNT_UPDATE event -- [ ] Check logs for: `PositionManager: Synced tranches for BTCUSDT LONG with exchange` -- [ ] Verify sync status in TrancheGroup is 'synced' - -### Phase 5: Real-Time Broadcasting Tests - -#### Test 5.1: WebSocket Tranche Events -- [ ] Start bot: `npm run dev` -- [ ] Open dashboard: http://localhost:3000 -- [ ] Open browser console (F12) -- [ ] Look for WebSocket connection: `ws://localhost:8080` -- [ ] Create a tranche (via liquidation opportunity) -- [ ] Verify WebSocket messages received: - - `tranche_created` with tranche details - - `tranche_pnl_update` with P&L updates - -Expected WebSocket message format: -```json -{ - "type": "tranche_created", - "data": { - "trancheId": "uuid-here", - "symbol": "BTCUSDT", - "side": "LONG", - "entryPrice": 50000, - "quantity": 0.001, - "marginUsed": 5, - "leverage": 10, - "tpPrice": 52500, - "slPrice": 49000, - "timestamp": "2025-10-12T..." - } -} -``` - -#### Test 5.2: Isolation Broadcasting -- [ ] Create tranche at entry price (e.g., 50000) -- [ ] Wait for price to drop >5% OR manually trigger isolation -- [ ] Check browser console for `tranche_isolated` WebSocket event -- [ ] Verify log: `โš ๏ธ Tranche isolated: xxxxxxxx for BTCUSDT (-5.XX% loss)` - -#### Test 5.3: Closing Broadcasting -- [ ] Have active tranche -- [ ] Close position (SL/TP hit or manual close) -- [ ] Check browser console for `tranche_closed` WebSocket event -- [ ] Verify log: `๐Ÿ’ฐ Tranche closed: xxxxxxxx for BTCUSDT (PnL: $X.XX)` - -## Integration Testing Scenarios - -### Scenario 1: Full Lifecycle - Profitable Trade -1. Enable tranche management for BTCUSDT -2. Wait for liquidation opportunity (LONG) -3. Hunter places order โ†’ Tranche created -4. Price moves up 5% โ†’ TP hit -5. PositionManager closes tranche -6. Verify tranche status='closed' with positive realizedPnl - -### Scenario 2: Isolation Flow -1. Create tranche at entry 50000 (LONG) -2. Price drops to 47500 (5% loss) -3. Isolation monitor detects threshold breach -4. Tranche marked as isolated -5. New liquidation opportunity occurs -6. New tranche created (old one still isolated) -7. Price recovers to 51000 -8. Both tranches profitable, close together - -### Scenario 3: Multi-Tranche Position -1. Create 3 tranches for BTCUSDT LONG: - - Tranche 1: Entry 50000, qty 0.001 - - Tranche 2: Entry 49500, qty 0.001 - - Tranche 3: Entry 49000, qty 0.001 -2. Total exchange position: 0.003 BTC -3. Price moves to 52000 -4. Verify all tranches show unrealized profit -5. Close position (SL/TP or manual) -6. Verify FIFO closing: Tranche 1 closes first - -### Scenario 4: Exchange Sync with Drift -1. Create 2 tranches (total 0.002 BTC) -2. Manually close 0.001 BTC on exchange -3. Trigger ACCOUNT_UPDATE -4. Verify sync detects drift (>1%) -5. Check logs for quantity mismatch warning -6. Verify appropriate tranche closed - -## Performance Testing - -### Test 1: Database Performance -```bash -# Insert 100 tranches -for i in {1..100}; do - sqlite3 liquidations.db "INSERT INTO tranches (id, symbol, side, positionSide, entryPrice, quantity, marginUsed, leverage, entryTime, unrealizedPnl, realizedPnl, tpPercent, slPercent, tpPrice, slPrice, status, isolated) VALUES ('test-$i', 'BTCUSDT', 'LONG', 'LONG', 50000, 0.001, 5, 10, $(date +%s)000, 0, 0, 5, 2, 52500, 49000, 'active', 0);" -done - -# Query performance -time sqlite3 liquidations.db "SELECT * FROM tranches WHERE symbol='BTCUSDT' AND status='active';" -``` - -Expected: Query completes in <100ms - -### Test 2: Isolation Monitoring Performance -- [ ] Create 10 active tranches across multiple symbols -- [ ] Start isolation monitoring (10s interval) -- [ ] Monitor CPU usage during checks -- [ ] Verify no performance degradation - -### Test 3: Concurrent Tranche Operations -- [ ] Multiple trades happening simultaneously -- [ ] Verify no race conditions -- [ ] Check database locks handled correctly -- [ ] No duplicate tranches created - -## Error Handling Tests - -### Test 1: TrancheManager Not Initialized -- [ ] Disable tranche management in config -- [ ] Start bot -- [ ] Trigger trade -- [ ] Verify log: `TrancheManager check failed (not initialized?), continuing with trade` -- [ ] Trade completes normally - -### Test 2: Database Error Handling -- [ ] Corrupt database file -- [ ] Start bot -- [ ] Verify error logged but bot continues -- [ ] Database recreated on next start - -### Test 3: Invalid Configuration -- [ ] Set `maxTranches: 0` -- [ ] Start bot -- [ ] Verify validation error or warning -- [ ] Bot uses safe default (3) - -## Success Criteria - -The multi-tranche system passes testing if: -- โœ… All database operations complete without errors -- โœ… Tranches created automatically on order fills -- โœ… Isolation threshold correctly triggers at configured % -- โœ… Exchange synchronization detects and handles drift -- โœ… Position closes respect closing strategy (FIFO/LIFO/etc) -- โœ… WebSocket broadcasts all tranche events to UI -- โœ… No memory leaks or performance degradation -- โœ… Error handling gracefully degrades (continues trading) -- โœ… Database persists tranches across bot restarts -- โœ… All TypeScript compilation passes - -## Known Limitations & Edge Cases - -### Limitations: -1. Exchange only allows one SL/TP per position (handled via strategies) -2. Tranche tracking is local - not visible to exchange -3. Position mode must be HEDGE for best results -4. Requires paper mode for full testing without real funds - -### Edge Cases to Test: -- [ ] Position closed manually on exchange (not via bot) -- [ ] Network interruption during tranche creation -- [ ] Multiple tranches closing simultaneously -- [ ] Isolated tranche never recovers (stays isolated) -- [ ] Max tranches reached, then one closes, then new trade - -## Next Steps After Testing - -Once manual testing is complete: -1. Document any bugs found โ†’ create GitHub issues -2. Proceed to Phase 6: UI Dashboard Components -3. Create automated unit tests for critical paths -4. Prepare for merge to `dev` branch -5. Update user documentation - -## Test Execution Log - -Date: _____________ -Tester: _____________ - -| Test | Status | Notes | -|------|--------|-------| -| Database Init | โฌœ Pass / โฌœ Fail | | -| Tranche Creation | โฌœ Pass / โฌœ Fail | | -| Isolation Logic | โฌœ Pass / โฌœ Fail | | -| Exchange Sync | โฌœ Pass / โฌœ Fail | | -| WebSocket Events | โฌœ Pass / โฌœ Fail | | -| Full Lifecycle | โฌœ Pass / โฌœ Fail | | -| Error Handling | โฌœ Pass / โฌœ Fail | | - ---- - -**Important**: Always test in **paper mode** first before enabling live trading with tranche management! diff --git a/docs/TRANCHE_USER_GUIDE.md b/docs/TRANCHE_USER_GUIDE.md deleted file mode 100644 index c3afd8e..0000000 --- a/docs/TRANCHE_USER_GUIDE.md +++ /dev/null @@ -1,730 +0,0 @@ -# Multi-Tranche Position Management - User Guide - -## Table of Contents - -1. [Introduction](#introduction) -2. [What Are Tranches?](#what-are-tranches) -3. [Why Use Multi-Tranche Management?](#why-use-multi-tranche-management) -4. [Getting Started](#getting-started) -5. [Configuration Guide](#configuration-guide) -6. [Using the Tranche Dashboard](#using-the-tranche-dashboard) -7. [Trading Strategies](#trading-strategies) -8. [Monitoring & Troubleshooting](#monitoring--troubleshooting) -9. [Best Practices](#best-practices) -10. [FAQ](#faq) - ---- - -## Introduction - -The **Multi-Tranche Position Management System** is an advanced feature that allows the bot to track multiple independent position entries (tranches) within the same trading pair. This enables you to: - -- Isolate losing positions automatically -- Continue trading fresh entries without adding to underwater positions -- Generate consistent profits while bad positions recover -- Maximize margin efficiency and avoid locked capital - -This guide will help you understand, configure, and use the tranche system effectively. - ---- - -## What Are Tranches? - -Think of **tranches** as individual "sub-positions" within the same trading symbol. - -### Traditional Position Management - -Normally, when you trade a symbol multiple times, your positions stack together: - -``` -Entry #1: LONG BTCUSDT @ $50,000 (0.01 BTC) -Entry #2: LONG BTCUSDT @ $49,000 (0.01 BTC) -Combined Position: LONG BTCUSDT @ $49,500 (0.02 BTC) - Average entry -``` - -**Problem:** If the first entry is losing, you can't exit it without closing the entire combined position. - -### Multi-Tranche Management - -With tranches, each entry is tracked separately: - -``` -Tranche #1: LONG BTCUSDT @ $50,000 (0.01 BTC) โ†’ Down 5% โ†’ ISOLATED -Tranche #2: LONG BTCUSDT @ $49,000 (0.01 BTC) โ†’ Up 2% โ†’ CLOSE (+profit) -Tranche #3: LONG BTCUSDT @ $48,500 (0.01 BTC) โ†’ Up 3% โ†’ CLOSE (+profit) - -Exchange sees: One combined position (updated as tranches close) -Bot tracks: Three separate entries with independent P&L -``` - -**Solution:** You can close profitable tranches individually while holding losing tranches for recovery. - ---- - -## Why Use Multi-Tranche Management? - -### Key Benefits - -| Feature | Without Tranches | With Tranches | -|---------|-----------------|---------------| -| **Losing Position** | Must hold entire position or take full loss | Isolate loser, trade fresh entries | -| **Profit Opportunities** | Blocked until position recovers | Continue trading and profiting | -| **Margin Efficiency** | Capital locked in underwater position | Only isolated tranches locked | -| **Risk Management** | All-or-nothing closes | Granular control per entry | -| **Profitability** | Wait for breakeven/profit | Generate profits while holding losers | - -### Real-World Example - -**Scenario:** BTCUSDT liquidation hunting with 5% isolation threshold - -``` -09:00 - Enter LONG @ $50,000 (Tranche #1) -09:15 - Price drops to $47,500 (-5%) - โ†’ Tranche #1 ISOLATED automatically -09:30 - New liquidation spike - โ†’ Enter LONG @ $47,800 (Tranche #2) -09:45 - Price hits $48,700 (+1.8%) - โ†’ Close Tranche #2 for +1.8% profit -10:00 - Another liquidation spike - โ†’ Enter LONG @ $48,200 (Tranche #3) -10:15 - Price hits $49,300 (+2.3%) - โ†’ Close Tranche #3 for +2.3% profit -10:30 - Price recovers to $50,500 - โ†’ Close Tranche #1 for +1% profit - -Result: +5.1% total profit vs -5% loss without tranches -``` - ---- - -## Getting Started - -### Prerequisites - -1. Bot must be installed and running -2. Access to web dashboard at `http://localhost:3000` -3. At least one symbol configured in your config -4. Understanding of basic trading concepts (leverage, SL/TP) - -### Quick Setup (5 Minutes) - -1. **Enable Tranches:** - - Open http://localhost:3000/config - - Select your trading symbol (e.g., BTCUSDT) - - Find "Tranche Management Settings" - - Toggle **"Enable Multi-Tranche Management"** to ON - -2. **Start with Defaults:** - - Isolation Threshold: 5% - - Max Tranches: 3 - - Max Isolated: 2 - - Closing Strategy: FIFO (First In, First Out) - -3. **Test in Paper Mode:** - - Ensure "Paper Mode" is enabled - - Monitor the `/tranches` dashboard - - Watch how tranches are created and isolated - -4. **Go Live (When Ready):** - - Disable paper mode - - Start with small position sizes - - Monitor closely for the first few trades - ---- - -## Configuration Guide - -### Access Configuration - -**Via Web UI:** -1. Navigate to http://localhost:3000/config -2. Select your symbol from the list -3. Scroll to "Tranche Management Settings" - -### Core Settings - -#### 1. Enable Multi-Tranche Management -- **Type:** Toggle (ON/OFF) -- **Default:** OFF -- **Description:** Master switch for tranche system -- **Recommendation:** Start OFF in paper mode, enable after testing - -#### 2. Isolation Threshold -- **Type:** Percentage (0-100%) -- **Default:** 5% -- **Description:** Unrealized loss % that triggers automatic isolation -- **Examples:** - - **3%**: Aggressive isolation (more tranches, quicker isolation) - - **5%**: Balanced (recommended for most strategies) - - **10%**: Conservative (fewer isolations, higher tolerance) -- **Formula:** `(currentPrice - entryPrice) / entryPrice * 100` - -#### 3. Max Tranches -- **Type:** Number (1-10) -- **Default:** 3 -- **Description:** Maximum active tranches per symbol/side -- **Recommendations:** - - **1-2**: Conservative, minimal complexity - - **3-5**: Balanced, good for most strategies - - **6+**: Aggressive, requires more monitoring - -#### 4. Max Isolated Tranches -- **Type:** Number (1-10) -- **Default:** 2 -- **Description:** Max underwater tranches before blocking new trades -- **Safety:** Prevents accumulating too many losing positions -- **Formula:** `max_isolated = max_tranches - 1` (keep at least 1 slot for profitable trading) - -#### 5. Allow Tranche While Isolated -- **Type:** Toggle (ON/OFF) -- **Default:** ON -- **Description:** Allow new tranches even when some are isolated -- **Use Cases:** - - **ON**: Continue trading despite isolated tranches (recommended) - - **OFF**: Block all new trades until isolated tranches close - -### Strategy Settings - -The tranche system uses optimized strategies that are hardcoded for best performance: - -#### 1. Closing Strategy: LIFO (Last In, First Out) -**Automatically configured** - closes newest tranches first. - -**Why LIFO?** -- Perfect for liquidation hunting strategies -- Quick profit-taking on recent entries -- Keeps older positions for potential recovery -- Minimizes complexity - -**Example:** -``` -Tranches: -#1: LONG @ $50,000 โ†’ -5% (oldest, underwater) -#2: LONG @ $48,000 โ†’ +2% (middle, profitable) -#3: LONG @ $49,000 โ†’ +1% (newest, profitable) - -SL/TP triggers โ†’ LIFO closes #3 first, then #2, then #1 -``` - -#### 2. Best Entry Tracking -The bot tracks which tranche has the most favorable entry price: -- **For LONG positions:** Lowest entry price -- **For SHORT positions:** Highest entry price - -This is used for display purposes and P&L tracking to help you understand your best positions. - -#### 3. Isolation Action -Determines what happens when a tranche is isolated. - -| Action | Description | Status | -|--------|-------------|--------| -| **HOLD** | Keep position, wait for recovery | โœ… Implemented | -| **REDUCE_LEVERAGE** | Lower leverage to reduce risk | ๐Ÿ”œ Future | -| **PARTIAL_CLOSE** | Close portion to reduce exposure | ๐Ÿ”œ Future | - -**Currently:** Only HOLD is implemented. Future versions will add dynamic risk management. - ---- - -## Using the Tranche Dashboard - -Access the dashboard at **http://localhost:3000/tranches** - -### Dashboard Overview - -The tranche dashboard provides real-time visibility into all your tranches: - -1. **Symbol Selector** - - Choose which symbol to view - - Select side (LONG/SHORT) - - Auto-refreshes every 5 seconds - -2. **Summary Metrics** - - Total Active Tranches - - Total Isolated Tranches - - Total Closed Tranches - - Combined Unrealized P&L - - Combined Realized P&L - -3. **Tranche Breakdown Tab** - - **Active Tranches:** Currently open positions - - **Isolated Tranches:** Underwater positions (>threshold) - - **Closed Tranches:** Historical completed trades - - Color-coded status indicators - -4. **Event Timeline Tab** - - Real-time event stream - - Tranche creation notifications - - Isolation events - - Close events with P&L - - Sync updates from exchange - -### Reading Tranche Cards - -Each tranche displays: - -``` -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ Tranche #abc123 | LONG โ”‚ -โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ -โ”‚ Entry: $50,000.00 | Time: 10:30:15 AM โ”‚ -โ”‚ Quantity: 0.01 BTC | Margin: $100 USDT โ”‚ -โ”‚ Leverage: 10x | Unrealized P&L: -$5.00โ”‚ -โ”‚ TP: $50,500 (1%) | SL: $49,000 (2%) โ”‚ -โ”‚ Status: ๐Ÿ”ด ISOLATED โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -``` - -**Status Colors:** -- ๐ŸŸข **GREEN**: Active (profitable or within threshold) -- ๐Ÿ”ด **RED**: Isolated (underwater > threshold) -- โšซ **GRAY**: Closed (historical) - -### Timeline Events - -Events appear in real-time and show: -- โœ… **Tranche Created**: New entry opened -- โš ๏ธ **Tranche Isolated**: Position went underwater -- ๐Ÿ’ฐ **Tranche Closed**: Exit with P&L -- ๐Ÿ”„ **Exchange Sync**: Reconciliation with exchange -- ๐Ÿ“Š **P&L Update**: Unrealized P&L changed - ---- - -## Trading Strategies - -The tranche system automatically uses **LIFO closing** for all strategies. Configure these parameters to match your trading style: - -### Strategy 1: Aggressive Scalping - -**Goal:** Fast in-and-out trades with minimal isolation time - -**Configuration:** -```json -{ - "trancheIsolationThreshold": 3, - "maxTranches": 5, - "maxIsolatedTranches": 2, - "allowTrancheWhileIsolated": true -} -``` - -**Characteristics:** -- Low 3% isolation threshold โ†’ quick isolation -- High max tranches (5) โ†’ more opportunities -- LIFO automatically takes profits on newest entries -- Good for high-volatility, liquid pairs - -**Pros:** Maximum trading frequency, quick profit generation -**Cons:** More isolated tranches, requires active monitoring - ---- - -### Strategy 2: Hold & Recover - -**Goal:** Hold losing positions long-term while scalping profits - -**Configuration:** -```json -{ - "trancheIsolationThreshold": 10, - "maxTranches": 3, - "maxIsolatedTranches": 2, - "allowTrancheWhileIsolated": true -} -``` - -**Characteristics:** -- High 10% isolation threshold โ†’ rare isolation -- Moderate max tranches (3) โ†’ balanced -- LIFO lets profitable new entries close first -- Good for trending, less volatile pairs - -**Pros:** Fewer isolations, simpler management -**Cons:** Takes longer to recover underwater positions - ---- - -### Strategy 3: Balanced Approach - -**Goal:** Balance between quick profits and position recovery - -**Configuration:** -```json -{ - "trancheIsolationThreshold": 5, - "maxTranches": 4, - "maxIsolatedTranches": 2, - "allowTrancheWhileIsolated": true -} -``` - -**Characteristics:** -- Balanced 5% isolation threshold -- Moderate max tranches (4) -- LIFO closes newest (often most profitable) -- Good for mixed market conditions - -**Pros:** Good balance of profit-taking and recovery -**Cons:** Middle-ground complexity - ---- - -### Strategy 4: Conservative Risk Management - -**Goal:** Minimal complexity, tight risk control - -**Configuration:** -```json -{ - "trancheIsolationThreshold": 7, - "maxTranches": 2, - "maxIsolatedTranches": 1, - "allowTrancheWhileIsolated": false -} -``` - -**Characteristics:** -- Moderate 7% isolation threshold -- Low max tranches (2) โ†’ simple tracking -- Block new trades when isolated โ†’ no compounding losses -- LIFO minimizes exposure time - -**Pros:** Simple, controlled risk -**Cons:** Fewer trading opportunities - ---- - -## Monitoring & Troubleshooting - -### Normal Operation Indicators - -โœ… **Healthy Tranche System:** -- Active tranches cycling (opening/closing regularly) -- Isolated tranches recovering over time -- Positive net realized P&L trend -- Dashboard updates every 5 seconds -- Timeline shows regular events - -### Warning Signs - -โš ๏ธ **Potential Issues:** -- Max isolated tranches reached frequently -- Tranches not closing for extended periods -- Large negative unrealized P&L building up -- Sync status showing "drift" or "conflict" -- No new tranches being created - -### Common Issues & Solutions - -#### Issue 1: Too Many Isolated Tranches - -**Symptom:** Max isolated limit reached, new trades blocked - -**Causes:** -- Isolation threshold too low -- Market moving strongly against positions -- Max tranches set too high - -**Solutions:** -1. Increase isolation threshold (5% โ†’ 7% or 10%) -2. Reduce max tranches (5 โ†’ 3) -3. Wait for market recovery -4. Manually close worst tranches via exchange - ---- - -#### Issue 2: Tranches Not Being Created - -**Symptom:** No new tranches appearing despite liquidation signals - -**Causes:** -- `enableTrancheManagement` not enabled -- Max tranches limit reached -- Max isolated tranches blocking new entries -- TrancheManager initialization failed - -**Solutions:** -1. Check config UI: Tranche Management toggle ON -2. View current tranche count in dashboard -3. Check bot console for TrancheManager errors -4. Restart bot if initialization failed - ---- - -#### Issue 3: Sync Drift Detected - -**Symptom:** Timeline shows "Exchange sync drift detected" - -**Causes:** -- Manual trades made outside bot -- Partial fills not tracked correctly -- Database/memory state mismatch - -**Solutions:** -1. Let TrancheManager auto-reconcile (happens automatically) -2. Check exchange position size matches tranche totals -3. If persistent, restart bot to re-sync from exchange - ---- - -#### Issue 4: Unrealized P&L Not Updating - -**Symptom:** P&L values frozen or stale - -**Causes:** -- WebSocket connection lost -- Price service not updating -- Dashboard auto-refresh stopped - -**Solutions:** -1. Check WebSocket connection status (top of timeline tab) -2. Refresh browser page -3. Check bot console for WebSocket errors -4. Verify `priceService` is running - ---- - -### Logs to Check - -**Bot Console:** -``` -TrancheManager: Created tranche [ID] for BTCUSDT LONG -TrancheManager: Isolated tranche [ID] (P&L: -5.2%) -TrancheManager: Closed tranche [ID] with P&L: $12.50 -``` - -**Database Queries:** -```sql --- View all active tranches -SELECT * FROM tranches WHERE status = 'active'; - --- View isolated tranches -SELECT * FROM tranches WHERE isolated = 1; - --- View tranche events (audit trail) -SELECT * FROM tranche_events ORDER BY event_time DESC LIMIT 20; -``` - ---- - -## Best Practices - -### 1. Start in Paper Mode -- Enable tranches in paper mode first -- Monitor for at least 24 hours -- Understand how isolation/closing works -- Adjust settings based on simulated results - -### 2. Conservative Initial Settings -```json -{ - "trancheIsolationThreshold": 5, // Balanced threshold - "maxTranches": 3, // Moderate complexity - "maxIsolatedTranches": 2, // Safety buffer - "allowTrancheWhileIsolated": true // Continue trading -} -``` -Note: LIFO closing and best entry tracking are automatically configured. - -### 3. Monitor Regularly -- Check `/tranches` dashboard daily -- Review timeline events for patterns -- Watch for repeated isolations (adjust threshold) -- Track realized P&L trends - -### 4. Adjust Based on Market Conditions - -**Trending Market (Strong Direction):** -- Increase isolation threshold (7-10%) -- Use FIFO closing (ride trend) -- Higher max tranches (4-5) - -**Choppy Market (Range-Bound):** -- Decrease isolation threshold (3-5%) -- Use LIFO closing (quick exits) -- Moderate max tranches (3-4) - -**High Volatility:** -- Increase isolation threshold (8-12%) -- Reduce max tranches (2-3) -- Use WORST_FIRST closing (cut losses) - -### 5. Risk Management Rules - -**Position Sizing:** -- Each tranche should be manageable in isolation -- Total margin across all tranches โ‰ค max position margin -- Don't overleverage individual tranches - -**Isolation Management:** -- Don't let isolated tranches exceed 50% of total margin -- If >2 tranches isolated, reduce new trade frequency -- Consider manual intervention if isolation persists >24h - -**Leverage Control:** -- Lower leverage (5-10x) when using tranches -- Higher leverage increases isolation risk -- Balance between profit potential and safety - -### 6. Testing New Strategies - -Before deploying a new tranche strategy: - -1. **Backtest (Manual):** - - Review historical data - - Estimate isolation frequency - - Calculate expected P&L - -2. **Paper Trade (1-2 weeks):** - - Enable in paper mode - - Monitor actual isolation rate - - Adjust settings as needed - -3. **Small Live Test (1 week):** - - Start with minimal position sizes - - One symbol only - - Monitor closely - -4. **Full Deployment:** - - Increase position sizes gradually - - Add more symbols one at a time - - Maintain monitoring routine - ---- - -## FAQ - -### General Questions - -**Q: Do I need special API permissions for tranches?** -A: No, tranches are tracked locally by the bot. Standard trading API permissions are sufficient. - -**Q: Will tranches work with paper mode?** -A: Yes! Paper mode fully supports tranches with simulated fills and P&L. - -**Q: Can I use tranches on multiple symbols simultaneously?** -A: Yes, each symbol has independent tranche tracking and configuration. - -**Q: What happens if the bot restarts?** -A: Tranches are persisted in the SQLite database and automatically reloaded on startup. - ---- - -### Configuration Questions - -**Q: What's the best isolation threshold?** -A: Start with 5%. Adjust based on your risk tolerance and market volatility: -- Aggressive: 3% -- Balanced: 5-7% -- Conservative: 10%+ - -**Q: How many max tranches should I allow?** -A: Recommended: 3-5 for most strategies. More tranches = more complexity and monitoring. - -**Q: Should I allow tranches while isolated?** -A: Generally YES. This lets you keep trading while bad positions recover. Set to NO if you want stricter risk control. - -**Q: Can I change the closing strategy?** -A: The closing strategy is automatically set to LIFO (Last In, First Out), which is optimal for liquidation hunting. LIFO closes newest tranches first, allowing quick profit-taking while letting older positions recover. This is hardcoded for simplicity and best performance. - ---- - -### Technical Questions - -**Q: How does the bot track tranches vs exchange positions?** -A: The bot maintains a local "virtual" tracking layer while the exchange sees one combined position. The bot reconciles differences automatically. - -**Q: What if I manually close a position on the exchange?** -A: TrancheManager detects the close and reconciles local tranches accordingly. Check timeline for sync events. - -**Q: Can I manually close a specific tranche?** -A: Not directly. The bot's closing strategy determines which tranches close. You can close the entire exchange position manually if needed. - -**Q: What happens if quantities drift (bot vs exchange)?** -A: TrancheManager auto-syncs every 10 seconds and detects drift >1%. It creates recovery tranches or adjusts existing ones as needed. - ---- - -### Troubleshooting Questions - -**Q: My tranches aren't being created. Why?** -A: Check: -1. Is `enableTrancheManagement` enabled in config? -2. Have you reached max tranches limit? -3. Are too many tranches isolated (blocking new entries)? -4. Check bot console for TrancheManager errors - -**Q: Why is my P&L not updating?** -A: Check: -1. WebSocket connection status (timeline tab) -2. Refresh browser page -3. Verify bot is running and connected to exchange - -**Q: What does "sync drift" mean?** -A: Exchange position quantity doesn't match sum of local tranches (>1% difference). Usually auto-reconciles within 10 seconds. - -**Q: Can I delete old closed tranches?** -A: Yes, closed tranches are automatically cleaned up after a configurable retention period. You can also manually delete from database: -```sql -DELETE FROM tranches WHERE status = 'closed' AND exit_time < [timestamp]; -``` - ---- - -### Advanced Questions - -**Q: Can I implement custom closing strategies?** -A: Yes, modify `selectTranchesToClose()` in `src/lib/services/trancheManager.ts`. Requires TypeScript knowledge. - -**Q: How do I export tranche data for analysis?** -A: Query the database: -```sql -SELECT * FROM tranches WHERE symbol = 'BTCUSDT' ORDER BY entry_time DESC; -``` -Or use the `/api/tranches` API endpoint. - -**Q: Can I disable tranches for specific symbols only?** -A: Yes, set `enableTrancheManagement: false` for that symbol in config. Other symbols remain unaffected. - -**Q: Does the tranche system support hedging mode?** -A: Yes, tranches work with both ONE_WAY and HEDGE position modes. In HEDGE mode, LONG and SHORT sides have independent tranche tracking. - ---- - -## Support & Resources - -### Documentation -- **Implementation Plan:** `docs/TRANCHE_IMPLEMENTATION_PLAN.md` -- **Testing Guide:** `docs/TRANCHE_TESTING.md` -- **Technical Docs:** `CLAUDE.md` (Multi-Tranche section) - -### Community -- **Discord:** [Join Server](https://discord.gg/P8Ev3Up) -- **GitHub Issues:** [Report Problems](https://github.com/CryptoGnome/aster_lick_hunter_node/issues) - -### Code References -- **TrancheManager:** `src/lib/services/trancheManager.ts` -- **Database Layer:** `src/lib/db/trancheDb.ts` -- **UI Dashboard:** `src/app/tranches/page.tsx` -- **Types:** `src/lib/types.ts` (Tranche interfaces) - ---- - -## Conclusion - -The multi-tranche system is a powerful tool for managing complex trading scenarios. By isolating losing positions and continuing to trade fresh entries, you can: - -โœ… Generate consistent profits even when some positions are underwater -โœ… Maximize margin efficiency and capital utilization -โœ… Maintain trading velocity without adding to losers -โœ… Implement sophisticated strategies with granular control - -**Remember:** -- Start in paper mode -- Use conservative settings initially -- Monitor regularly via `/tranches` dashboard -- Adjust based on market conditions -- Test new strategies thoroughly before deployment - -Happy trading! ๐Ÿš€ diff --git a/package-lock.json b/package-lock.json index 14944f1..ef8e956 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,7 +32,6 @@ "clsx": "^2.1.1", "ethers": "^6.15.0", "gsap": "^3.13.0", - "html-to-image": "^1.11.13", "lucide-react": "^0.544.0", "next": "15.5.4", "next-auth": "^4.24.11", @@ -8105,12 +8104,6 @@ "dev": true, "peer": true }, - "node_modules/html-to-image": { - "version": "1.11.13", - "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz", - "integrity": "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==", - "license": "MIT" - }, "node_modules/http-cache-semantics": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", diff --git a/package.json b/package.json index 9ed1785..72c4bb8 100644 --- a/package.json +++ b/package.json @@ -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" }, @@ -55,7 +52,6 @@ "clsx": "^2.1.1", "ethers": "^6.15.0", "gsap": "^3.13.0", - "html-to-image": "^1.11.13", "lucide-react": "^0.544.0", "next": "15.5.4", "next-auth": "^4.24.11", diff --git a/src/app/api/auth/check/route.ts b/src/app/api/auth/check/route.ts index 23e15ed..d6b3f9a 100644 --- a/src/app/api/auth/check/route.ts +++ b/src/app/api/auth/check/route.ts @@ -7,9 +7,8 @@ export async function GET() { const config = await configLoader.loadConfig(); const dashboardPassword = config.global?.server?.dashboardPassword; - // Only require password if it's set and not the default "admin" return NextResponse.json({ - passwordRequired: !!dashboardPassword && dashboardPassword.length > 0 && dashboardPassword !== 'admin', + passwordRequired: !!dashboardPassword && dashboardPassword.length > 0, }); } catch (error) { console.error('Failed to check auth status:', error); diff --git a/src/app/api/bot/control/route.ts b/src/app/api/bot/control/route.ts index 282e3ea..629262f 100644 --- a/src/app/api/bot/control/route.ts +++ b/src/app/api/bot/control/route.ts @@ -37,9 +37,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 } ); } diff --git a/src/app/api/paper-mode/positions/route.ts b/src/app/api/paper-mode/positions/route.ts deleted file mode 100644 index 44dc7a9..0000000 --- a/src/app/api/paper-mode/positions/route.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { NextResponse } from 'next/server'; -import { paperModeSimulator } from '@/lib/services/paperModeSimulator'; -import { loadConfig } from '@/lib/bot/config'; - -/** - * GET /api/paper-mode/positions - * - * Returns all active paper mode positions - */ -export async function GET() { - try { - const config = await loadConfig(); - - // Only return positions if in paper mode - if (!config.global.paperMode) { - return NextResponse.json({ - positions: [], - paperMode: false, - message: 'Not in paper mode' - }); - } - - const positions = paperModeSimulator.getPositions(); - - return NextResponse.json({ - positions: positions.map(pos => ({ - symbol: pos.symbol, - side: pos.side, - quantity: pos.quantity, - entryPrice: pos.entryPrice, - markPrice: pos.lastMarkPrice, - slPrice: pos.slPrice, - tpPrice: pos.tpPrice, - leverage: pos.leverage, - pnlPercent: pos.lastPnL, - openTime: pos.openTime, - unrealizedPnl: (pos.lastPnL / 100) * pos.quantity * pos.entryPrice * pos.leverage, - })), - paperMode: true, - count: positions.length - }); - } catch (error: any) { - console.error('Error fetching paper mode positions:', error); - return NextResponse.json( - { - error: `Failed to fetch paper mode positions: ${error.message}`, - positions: [], - paperMode: true - }, - { status: 500 } - ); - } -} diff --git a/src/app/api/positions/[symbol]/[side]/close/route.ts b/src/app/api/positions/[symbol]/[side]/close/route.ts index 7f63c59..83cc9dc 100644 --- a/src/app/api/positions/[symbol]/[side]/close/route.ts +++ b/src/app/api/positions/[symbol]/[side]/close/route.ts @@ -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, @@ -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 }); } diff --git a/src/app/api/tranches/route.ts b/src/app/api/tranches/route.ts deleted file mode 100644 index 5d0d339..0000000 --- a/src/app/api/tranches/route.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { NextResponse } from 'next/server'; -import { getAllTranchesForSymbol, getActiveTranches, getIsolatedTranches } from '@/lib/db/trancheDb'; - -/** - * GET /api/tranches - Fetch tranche data - * Query params: - * - symbol: Filter by symbol (optional) - * - side: Filter by side (optional) - * - status: 'active', 'isolated', 'all' (default: 'all') - */ -export async function GET(request: Request) { - try { - const { searchParams } = new URL(request.url); - const symbol = searchParams.get('symbol'); - const side = searchParams.get('side'); - const status = searchParams.get('status') || 'all'; - - let tranches = []; - - if (symbol && side) { - // Fetch specific symbol and side - if (status === 'active') { - const activeTranches = await getActiveTranches(symbol, side); - tranches = activeTranches.filter(t => !t.isolated); - } else if (status === 'isolated') { - tranches = await getIsolatedTranches(symbol, side); - } else { - tranches = await getAllTranchesForSymbol(symbol); - tranches = tranches.filter(t => t.side === side); - } - } else if (symbol) { - // Fetch all sides for symbol - tranches = await getAllTranchesForSymbol(symbol); - - if (status === 'active') { - tranches = tranches.filter(t => t.status === 'active' && !t.isolated); - } else if (status === 'isolated') { - tranches = tranches.filter(t => t.isolated); - } - } else { - // Return error - need at least symbol - return NextResponse.json( - { error: 'Symbol parameter is required' }, - { status: 400 } - ); - } - - // Calculate aggregated metrics - const activeTranches = tranches.filter(t => t.status === 'active' && !t.isolated); - const isolatedTranches = tranches.filter(t => t.isolated); - const closedTranches = tranches.filter(t => t.status === 'closed'); - - const totalQuantity = activeTranches.reduce((sum, t) => sum + t.quantity, 0); - const totalMarginUsed = activeTranches.reduce((sum, t) => sum + t.marginUsed, 0); - const totalUnrealizedPnl = activeTranches.reduce((sum, t) => sum + t.unrealizedPnl, 0); - const totalRealizedPnl = closedTranches.reduce((sum, t) => sum + t.realizedPnl, 0); - - // Calculate weighted average entry - let weightedAvgEntry = 0; - if (totalQuantity > 0) { - const weightedSum = activeTranches.reduce( - (sum, t) => sum + t.entryPrice * t.quantity, - 0 - ); - weightedAvgEntry = weightedSum / totalQuantity; - } - - return NextResponse.json({ - tranches, - metrics: { - total: tranches.length, - active: activeTranches.length, - isolated: isolatedTranches.length, - closed: closedTranches.length, - totalQuantity, - totalMarginUsed, - totalUnrealizedPnl, - totalRealizedPnl, - weightedAvgEntry, - }, - }); - } catch (error: any) { - console.error('Error fetching tranches:', error); - return NextResponse.json( - { error: 'Failed to fetch tranches', details: error.message }, - { status: 500 } - ); - } -} diff --git a/src/app/config/page.tsx b/src/app/config/page.tsx index 7888cb4..276b500 100644 --- a/src/app/config/page.tsx +++ b/src/app/config/page.tsx @@ -3,20 +3,17 @@ import React, { useState } from 'react'; import { DashboardLayout } from '@/components/dashboard-layout'; import SymbolConfigForm from '@/components/SymbolConfigForm'; -import ShareConfigModal from '@/components/ShareConfigModal'; import { useConfig } from '@/components/ConfigProvider'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Skeleton } from '@/components/ui/skeleton'; import { Badge } from '@/components/ui/badge'; -import { Button } from '@/components/ui/button'; -import { AlertCircle, CheckCircle2, Settings, Share2 } from 'lucide-react'; +import { AlertCircle, CheckCircle2, Settings } from 'lucide-react'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { toast } from 'sonner'; export default function ConfigPage() { const { config, loading, updateConfig } = useConfig(); const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle'); - const [shareModalOpen, setShareModalOpen] = useState(false); const handleSave = async (newConfig: any) => { setSaveStatus('saving'); @@ -72,44 +69,33 @@ export default function ConfigPage() { return ( -
+
{/* Page Header */}
-
+
-

- +

+ Bot Configuration

-

+

Configure your API credentials and trading parameters for each symbol

-
- {saveStatus === 'saved' && ( - - - Saved - - )} - -
+ {saveStatus === 'saved' && ( + + + Saved + + )}
{/* Status Alert */} {config?.global?.paperMode && ( - - + + Paper Mode Active: The bot is currently in simulation mode. No real trades will be executed. Disable paper mode in the settings below to start live trading. @@ -126,14 +112,14 @@ export default function ConfigPage() { {/* Important Notes */} - - - + + + Important Notes -
    +
    • Keep your API credentials secure and never share them with anyone
    • Always start with Paper Mode enabled to test your configuration
    • Use conservative stop-loss percentages to limit risk (recommended: 1-2%)
    • @@ -144,15 +130,6 @@ export default function ConfigPage() {
- - {/* Share Config Modal */} - {config && ( - setShareModalOpen(false)} - config={config} - /> - )} ); } \ No newline at end of file diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 0664d6e..dce5bbf 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -20,10 +20,9 @@ function LoginForm() { const { data: _session, status } = useSession(); const { config } = useConfig(); - // Check if a custom password is configured (not the default "admin") + // Check if password is configured const isPasswordConfigured = config?.global?.server?.dashboardPassword && - config.global.server.dashboardPassword.trim().length > 0 && - config.global.server.dashboardPassword !== 'admin'; + config.global.server.dashboardPassword.trim().length > 0; // Redirect if already authenticated useEffect(() => { @@ -109,6 +108,7 @@ function LoginForm() { onChange={(e) => setPassword(e.target.value)} required autoFocus + minLength={4} /> {password.length > 0 && password.length < 4 && !(password === 'admin' && !isPasswordConfigured) && (

diff --git a/src/app/page.tsx b/src/app/page.tsx index 93891cd..b09b224 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -250,24 +250,26 @@ export default function DashboardPage() {

{/* Main Content */} -
- {/* Account Summary - Single Row */} -
+
+ {/* Account Summary - Minimal Design */} +
{/* Total Balance */} -
- +
+
- Balance -
+ Balance +
{isLoading ? ( - + ) : ( <> - {formatCurrency(liveAccountInfo.totalBalance)} + {formatCurrency(liveAccountInfo.totalBalance)} {balanceStatus.error ? ( - ! + ERROR ) : balanceStatus.source === 'websocket' ? ( - L + LIVE + ) : balanceStatus.source === 'rest-account' || balanceStatus.source === 'rest-balance' ? ( + REST ) : null} )} @@ -275,59 +277,59 @@ export default function DashboardPage() {
-
+
- {/* Available */} -
- + {/* Available Balance */} +
+
- Available + Available {isLoading ? ( - + ) : ( - {formatCurrency(liveAccountInfo.availableBalance)} + {formatCurrency(liveAccountInfo.availableBalance)} )}
-
+
- {/* In Position */} -
- + {/* Position Value */} +
+
- In Position + In Position {isLoading ? ( - + ) : ( - {formatCurrency(liveAccountInfo.totalPositionValue)} + {formatCurrency(liveAccountInfo.totalPositionValue)} )}
-
+
{/* Unrealized PnL */} -
+
{liveAccountInfo.totalPnL >= 0 ? ( - + ) : ( - + )}
- PnL + Unrealized PnL {isLoading ? ( - + ) : ( -
- + = 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400' }`}> {formatCurrency(liveAccountInfo.totalPnL)} = 0 ? "outline" : "destructive"} - className={`h-3 text-[9px] px-0.5 ${ + className={`h-4 text-[10px] px-1 ${ liveAccountInfo.totalPnL >= 0 ? 'border-green-600 text-green-600 dark:border-green-400 dark:text-green-400' : '' @@ -343,30 +345,42 @@ export default function DashboardPage() {
-
+
- {/* 24h Performance */} + {/* 24h Performance - Inline */} -
+
- {/* Session */} + {/* Live Session Performance */} -
+
- {/* Active Symbols */} -
- + {/* Active Trading Symbols */} +
+
- Symbols + Active Symbols
{config?.symbols && Object.keys(config.symbols).length > 0 ? ( <> - {Object.keys(config.symbols).length} + {Object.keys(config.symbols).length} +
+ {Object.keys(config.symbols).slice(0, 3).map((symbol, _index) => ( + + {symbol.replace('USDT', '')} + + ))} + {Object.keys(config.symbols).length > 3 && ( + + +{Object.keys(config.symbols).length - 3} + + )} +
) : ( - 0 + 0 )}
diff --git a/src/app/tranches/page.tsx b/src/app/tranches/page.tsx deleted file mode 100644 index eee5ce1..0000000 --- a/src/app/tranches/page.tsx +++ /dev/null @@ -1,178 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import { TrancheBreakdownCard } from '@/components/TrancheBreakdownCard'; -import { TrancheTimeline } from '@/components/TrancheTimeline'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import { Label } from '@/components/ui/label'; -import { Badge } from '@/components/ui/badge'; -import { Layers, TrendingUp, AlertTriangle, Info } from 'lucide-react'; - -export default function TranchesPage() { - const [selectedSymbol, setSelectedSymbol] = useState('BTCUSDT'); - const [selectedSide, setSelectedSide] = useState<'LONG' | 'SHORT'>('LONG'); - - // Common trading symbols - const symbols = ['BTCUSDT', 'ETHUSDT', 'BNBUSDT', 'SOLUSDT', 'XRPUSDT']; - - return ( -
- {/* Page Header */} -
-
- -
-

Multi-Tranche Management

-

- Track multiple position entries for better margin utilization -

-
-
- - {/* Info Card */} - - -
- -
-

What are Tranches?

-

- Tranches are virtual position entries that allow you to track multiple trades on the same symbol independently. - When a position goes underwater (>5% loss), it gets isolated - allowing you to open fresh - tranches without adding to the losing position. -

-
-
- - Active: Trading normally -
-
- - Isolated: Holding for recovery -
-
-
-
-
-
-
- - {/* Symbol/Side Selection */} - - - View Tranches - Select a symbol and position side to view tranches - - -
-
- - -
- -
- - -
-
-
-
- - {/* Main Content Tabs */} - - - Tranche Breakdown - Activity Timeline - - - - - - - - - - - - {/* How It Works */} - - - How Multi-Tranche Management Works - - -
-

1. Entry

-

- When you open a position, a tranche is created to track entry price, quantity, and P&L. -

-
- -
-

2. Isolation (Optional)

-

- If a tranche goes >5% underwater (configurable), it gets automatically isolated. This means new trades - won't add to this position - you can continue trading while waiting for recovery. -

-
- -
-

3. Continue Trading

-

- With isolated tranches, you can open fresh positions on the same symbol without adding to losers. - The bot tracks everything locally while the exchange sees one combined position. -

-
- -
-

4. Exit

-

- When SL/TP is hit, tranches are closed using your chosen strategy (FIFO, LIFO, etc.). P&L is tracked - per tranche and aggregated for total performance. -

-
- -
-

- Note: Tranche management is a local tracking system. The exchange still sees a single - position per symbol+side. Configure settings in the Configuration page. -

-
-
-
-
- ); -} diff --git a/src/bot/index.ts b/src/bot/index.ts index c71cb7d..2a1d5d9 100644 --- a/src/bot/index.ts +++ b/src/bot/index.ts @@ -39,7 +39,6 @@ class AsterBot { private positionManager: PositionManager | null = null; private config: Config | null = null; private isRunning = false; - private isPaused = false; private statusBroadcaster: StatusBroadcaster; private isHedgeMode: boolean = false; private tradeSizeWarnings: any[] = []; @@ -162,20 +161,6 @@ logErrorWithTimestamp('โŒ Config error:', error.message); this.statusBroadcaster.addError(`Config: ${error.message}`); }); - // Listen for bot control commands from web UI - this.statusBroadcaster.on('bot_control', async (action: string) => { - switch (action) { - case 'pause': - await this.pause(); - break; - case 'resume': - await this.resume(); - break; - default: - logWarnWithTimestamp(`Unknown bot control action: ${action}`); - } - }); - // Check API keys const hasValidApiKeys = this.config.api.apiKey && this.config.api.secretKey && this.config.api.apiKey.length > 0 && this.config.api.secretKey.length > 0; @@ -387,95 +372,6 @@ logErrorWithTimestamp('โš ๏ธ Position Manager failed to start:', error.message } } - // Initialize Tranche Manager (if enabled for any symbol) - const trancheEnabledSymbols = Object.entries(this.config.symbols).filter( - ([_symbol, config]) => config.enableTrancheManagement - ); - - if (trancheEnabledSymbols.length > 0) { - try { - const { initializeTrancheManager, getTrancheManager } = await import('../lib/services/trancheManager'); - const trancheManager = initializeTrancheManager(this.config); - await trancheManager.initialize(); - - // Connect tranche events to status broadcaster - trancheManager.on('trancheCreated', (tranche) => { - this.statusBroadcaster.broadcastTrancheCreated({ - trancheId: tranche.id, - symbol: tranche.symbol, - side: tranche.side, - entryPrice: tranche.entryPrice, - quantity: tranche.quantity, - marginUsed: tranche.marginUsed, - leverage: tranche.leverage, - tpPrice: tranche.tpPrice, - slPrice: tranche.slPrice, - }); - logWithTimestamp(`๐Ÿ“Š Tranche created: ${tranche.id.substring(0, 8)} for ${tranche.symbol} ${tranche.side}`); - }); - - trancheManager.on('trancheIsolated', (tranche) => { - const symbolConfig = this.config?.symbols[tranche.symbol]; - const currentPrice = tranche.isolationPrice || 0; - const pnlPercent = tranche.side === 'LONG' - ? ((currentPrice - tranche.entryPrice) / tranche.entryPrice) * 100 - : ((tranche.entryPrice - currentPrice) / tranche.entryPrice) * 100; - - this.statusBroadcaster.broadcastTrancheIsolated({ - trancheId: tranche.id, - symbol: tranche.symbol, - side: tranche.side, - entryPrice: tranche.entryPrice, - currentPrice, - unrealizedPnl: tranche.unrealizedPnl, - pnlPercent, - isolationThreshold: symbolConfig?.trancheIsolationThreshold || 5, - }); - logWithTimestamp(`โš ๏ธ Tranche isolated: ${tranche.id.substring(0, 8)} for ${tranche.symbol} (${pnlPercent.toFixed(2)}% loss)`); - }); - - trancheManager.on('trancheClosed', (tranche) => { - this.statusBroadcaster.broadcastTrancheClosed({ - trancheId: tranche.id, - symbol: tranche.symbol, - side: tranche.side, - entryPrice: tranche.entryPrice, - exitPrice: tranche.exitPrice || 0, - quantity: tranche.quantity, - realizedPnl: tranche.realizedPnl, - closedFully: tranche.status === 'closed', - orderId: tranche.exitOrderId, - }); - logWithTimestamp(`๐Ÿ’ฐ Tranche closed: ${tranche.id.substring(0, 8)} for ${tranche.symbol} (PnL: $${tranche.realizedPnl.toFixed(2)})`); - }); - - trancheManager.on('tranchePartialClose', (tranche) => { - this.statusBroadcaster.broadcastTrancheClosed({ - trancheId: tranche.id, - symbol: tranche.symbol, - side: tranche.side, - entryPrice: tranche.entryPrice, - exitPrice: 0, // Partial close - exit price varies - quantity: tranche.quantity, - realizedPnl: tranche.realizedPnl, - closedFully: false, - }); - logWithTimestamp(`๐Ÿ“‰ Tranche partially closed: ${tranche.id.substring(0, 8)} for ${tranche.symbol}`); - }); - - // Start periodic isolation monitoring - trancheManager.startIsolationMonitoring(10000); // Check every 10 seconds - - logWithTimestamp(`โœ… Tranche Manager initialized for ${trancheEnabledSymbols.length} symbol(s): ${trancheEnabledSymbols.map(([s]) => s).join(', ')}`); - } catch (error: any) { - logErrorWithTimestamp('โš ๏ธ Tranche Manager failed to start:', error.message); - this.statusBroadcaster.addError(`Tranche Manager: ${error.message}`); - // Continue without tranche management - } - } else { - logWithTimestamp('โ„น๏ธ Tranche Management disabled for all symbols'); - } - // Initialize Hunter this.hunter = new Hunter(this.config, this.isHedgeMode); @@ -606,98 +502,6 @@ logErrorWithTimestamp('โŒ Failed to start bot:', error); } } - async pause(): Promise { - if (!this.isRunning || this.isPaused) { -logWithTimestamp('โš ๏ธ Cannot pause: Bot is not running or already paused'); - return; - } - - try { -logWithTimestamp('โธ๏ธ Pausing bot...'); - this.isPaused = true; - this.statusBroadcaster.setBotState('paused'); - - // Stop the hunter from placing new trades - if (this.hunter) { - this.hunter.pause(); -logWithTimestamp('โœ… Hunter paused (no new trades will be placed)'); - } - -logWithTimestamp('โœ… Bot paused - existing positions will continue to be monitored'); - this.statusBroadcaster.logActivity('Bot paused'); - } catch (error) { -logErrorWithTimestamp('โŒ Error while pausing bot:', error); - this.statusBroadcaster.addError(`Failed to pause: ${error}`); - } - } - - async resume(): Promise { - if (!this.isRunning || !this.isPaused) { -logWithTimestamp('โš ๏ธ Cannot resume: Bot is not running or not paused'); - return; - } - - try { -logWithTimestamp('โ–ถ๏ธ Resuming bot...'); - this.isPaused = false; - this.statusBroadcaster.setBotState('running'); - - // Resume the hunter - if (this.hunter) { - this.hunter.resume(); -logWithTimestamp('โœ… Hunter resumed'); - } - -logWithTimestamp('โœ… Bot resumed - trading active'); - this.statusBroadcaster.logActivity('Bot resumed'); - } catch (error) { -logErrorWithTimestamp('โŒ Error while resuming bot:', error); - this.statusBroadcaster.addError(`Failed to resume: ${error}`); - } - } - - async stopAndCloseAll(): Promise { - if (!this.isRunning) { -logWithTimestamp('โš ๏ธ Cannot stop: Bot is not running'); - return; - } - - try { -logWithTimestamp('๐Ÿ›‘ Stopping bot and closing all positions...'); - this.isPaused = false; - this.statusBroadcaster.setBotState('stopped'); - - // Stop the hunter first - if (this.hunter) { - this.hunter.stop(); -logWithTimestamp('โœ… Hunter stopped'); - } - - // Close all positions - if (this.positionManager) { - const positions = this.positionManager.getPositions(); - if (positions.length > 0) { -logWithTimestamp(`๐Ÿ“Š Closing ${positions.length} open position(s)...`); - await this.positionManager.closeAllPositions(); -logWithTimestamp('โœ… All positions closed'); - } else { -logWithTimestamp('โ„น๏ธ No open positions to close'); - } - } - -logWithTimestamp('โœ… Bot stopped and all positions closed'); - this.statusBroadcaster.logActivity('Bot stopped and all positions closed'); - - // Don't actually exit the process - just set state to stopped - // This allows the bot to be restarted from the UI - this.isRunning = false; - this.statusBroadcaster.setRunning(false); - } catch (error) { -logErrorWithTimestamp('โŒ Error while stopping bot:', error); - this.statusBroadcaster.addError(`Failed to stop: ${error}`); - } - } - private async handleConfigUpdate(newConfig: Config): Promise { logWithTimestamp('๐Ÿ”„ Applying config update...'); diff --git a/src/bot/websocketServer.ts b/src/bot/websocketServer.ts index 08bf2b4..bcc8dcd 100644 --- a/src/bot/websocketServer.ts +++ b/src/bot/websocketServer.ts @@ -6,7 +6,6 @@ import { getRateLimitManager } from '../lib/api/rateLimitManager'; export interface BotStatus { isRunning: boolean; - botState?: 'running' | 'paused' | 'stopped'; paperMode: boolean; uptime: number; startTime: Date | null; @@ -29,7 +28,6 @@ export class StatusBroadcaster extends EventEmitter { private clients: Set = new Set(); private status: BotStatus = { isRunning: false, - botState: 'stopped', paperMode: true, uptime: 0, startTime: null, @@ -87,22 +85,6 @@ export class StatusBroadcaster extends EventEmitter { } break; - case 'bot_control': - // Handle bot control commands (pause, resume, stop) - const { action } = message; - console.log(`๐ŸŽฎ Bot control requested: ${action}`); - - // Emit event for AsterBot to handle - this.emit('bot_control', action); - - // Send acknowledgment - ws.send(JSON.stringify({ - type: 'bot_control_ack', - action, - timestamp: Date.now() - })); - break; - case 'ping': ws.send(JSON.stringify({ type: 'pong', timestamp: Date.now() })); break; @@ -183,7 +165,6 @@ export class StatusBroadcaster extends EventEmitter { setRunning(isRunning: boolean): void { this.status.isRunning = isRunning; - this.status.botState = isRunning ? 'running' : 'stopped'; if (isRunning) { this.status.startTime = new Date(); this.status.uptime = 0; @@ -194,11 +175,6 @@ export class StatusBroadcaster extends EventEmitter { this._broadcast('status', this.status); } - setBotState(state: 'running' | 'paused' | 'stopped'): void { - this.status.botState = state; - this._broadcast('status', this.status); - } - addError(error: string): void { this.status.errors.push(error); // Keep only last 10 errors @@ -317,7 +293,6 @@ export class StatusBroadcaster extends EventEmitter { price: number; type: 'opened' | 'closed' | 'updated'; pnl?: number; - paperMode?: boolean; }): void { this._broadcast('position_update', { ...data, @@ -414,7 +389,6 @@ export class StatusBroadcaster extends EventEmitter { quantity: number; pnl?: number; reason?: string; - paperMode?: boolean; }): void { this._broadcast('position_closed', { ...data, @@ -555,115 +529,4 @@ export class StatusBroadcaster extends EventEmitter { timestamp: new Date(), }); } - - // Tranche Management Broadcasting Methods - - // Broadcast when a new tranche is created - broadcastTrancheCreated(data: { - trancheId: string; - symbol: string; - side: 'LONG' | 'SHORT'; - entryPrice: number; - quantity: number; - marginUsed: number; - leverage: number; - tpPrice: number; - slPrice: number; - }): void { - this._broadcast('tranche_created', { - ...data, - timestamp: new Date(), - }); - } - - // Broadcast when a tranche is isolated (underwater >threshold%) - broadcastTrancheIsolated(data: { - trancheId: string; - symbol: string; - side: 'LONG' | 'SHORT'; - entryPrice: number; - currentPrice: number; - unrealizedPnl: number; - pnlPercent: number; - isolationThreshold: number; - }): void { - this._broadcast('tranche_isolated', { - ...data, - timestamp: new Date(), - }); - } - - // Broadcast when a tranche is closed (fully or partially) - broadcastTrancheClosed(data: { - trancheId: string; - symbol: string; - side: 'LONG' | 'SHORT'; - entryPrice: number; - exitPrice: number; - quantity: number; - realizedPnl: number; - closedFully: boolean; - orderId?: string; - }): void { - this._broadcast('tranche_closed', { - ...data, - timestamp: new Date(), - }); - } - - // Broadcast when tranches are synced with exchange position - broadcastTrancheSyncUpdate(data: { - symbol: string; - side: 'LONG' | 'SHORT'; - totalTranches: number; - activeTranches: number; - isolatedTranches: number; - totalQuantity: number; - exchangeQuantity: number; - syncStatus: 'synced' | 'drift' | 'conflict'; - quantityDrift?: number; - }): void { - this._broadcast('tranche_sync', { - ...data, - timestamp: new Date(), - }); - } - - // Broadcast real-time P&L updates for all tranches - broadcastTranchePnLUpdate(data: { - symbol: string; - side: 'LONG' | 'SHORT'; - activeTranches: Array<{ - trancheId: string; - entryPrice: number; - currentPrice: number; - quantity: number; - unrealizedPnl: number; - pnlPercent: number; - isolated: boolean; - }>; - totalUnrealizedPnl: number; - weightedAvgEntry: number; - }): void { - this._broadcast('tranche_pnl_update', { - ...data, - timestamp: new Date(), - }); - } - - // Broadcast when tranche limit is reached - broadcastTrancheLimitReached(data: { - symbol: string; - side: 'LONG' | 'SHORT'; - activeTranches: number; - maxTranches: number; - isolatedTranches: number; - maxIsolatedTranches: number; - reason: string; - }): void { - this._broadcast('tranche_limit_reached', { - ...data, - timestamp: new Date(), - }); - } } \ No newline at end of file diff --git a/src/components/BotControlButtons.tsx b/src/components/BotControlButtons.tsx index ea568ca..c3b8fe9 100644 --- a/src/components/BotControlButtons.tsx +++ b/src/components/BotControlButtons.tsx @@ -4,18 +4,29 @@ import { useState } from 'react'; import { useBotStatus } from '@/hooks/useBotStatus'; import { Button } from '@/components/ui/button'; import { toast } from 'sonner'; -import { Pause, Play, Loader2 } from 'lucide-react'; +import { Pause, Play, Square, Loader2 } from 'lucide-react'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog'; export default function BotControlButtons() { const { status, isConnected } = useBotStatus(); const [isLoading, setIsLoading] = useState(false); + const [showStopDialog, setShowStopDialog] = useState(false); const botState = (status as any)?.botState || 'stopped'; const isRunning = botState === 'running'; const isPaused = botState === 'paused'; const isStopped = botState === 'stopped'; - const sendControlCommand = async (action: 'pause' | 'resume') => { + const sendControlCommand = async (action: 'pause' | 'resume' | 'stop') => { setIsLoading(true); try { const response = await fetch('/api/bot/control', { @@ -30,9 +41,11 @@ export default function BotControlButtons() { throw new Error(data.error || `Failed to ${action} bot`); } - toast.success(`Bot ${action === 'pause' ? 'paused' : 'resumed'}`, { - description: action === 'pause' - ? 'No new trades will be placed' + toast.success(`Bot ${action === 'stop' ? 'stopped' : action === 'pause' ? 'paused' : 'resumed'} successfully`, { + description: action === 'stop' + ? 'All positions are being closed' + : action === 'pause' + ? 'No new trades will be placed, positions will continue to be monitored' : 'Trading has resumed' }); } catch (error: any) { @@ -47,46 +60,100 @@ export default function BotControlButtons() { const handlePause = () => sendControlCommand('pause'); const handleResume = () => sendControlCommand('resume'); + const handleStop = () => { + setShowStopDialog(true); + }; + + const confirmStop = () => { + setShowStopDialog(false); + sendControlCommand('stop'); + }; if (!isConnected || isStopped) { return null; } return ( -
- {isRunning && ( - - )} + <> +
+ {isRunning && ( + + )} + + {isPaused && ( + + )} - {isPaused && ( - )} -
+
+ + + + + Stop Bot & Close All Positions? + + This will: +
    +
  • Stop monitoring for new liquidations
  • +
  • Close all open positions at market price
  • +
  • Cancel all open orders
  • +
  • Stop the bot completely
  • +
+

+ This action cannot be undone. Are you sure? +

+
+
+ + Cancel + + Stop & Close All + + +
+
+ ); } diff --git a/src/components/LiquidationSidebar.tsx b/src/components/LiquidationSidebar.tsx index 47cf050..d4ff096 100644 --- a/src/components/LiquidationSidebar.tsx +++ b/src/components/LiquidationSidebar.tsx @@ -175,7 +175,7 @@ export default function LiquidationSidebar({ volumeThresholds = {}, maxEvents = }; return ( -
+
diff --git a/src/components/PerformanceCardInline.tsx b/src/components/PerformanceCardInline.tsx index 24aacaf..1d73e9f 100644 --- a/src/components/PerformanceCardInline.tsx +++ b/src/components/PerformanceCardInline.tsx @@ -115,42 +115,51 @@ export default function PerformanceCardInline() { if (isLoading || !pnlData) { return ( -
+
- 24h - + 24h Performance +
); } const totalPnL = pnlData.metrics.totalPnl; + const totalRealizedPnL = pnlData.metrics.totalRealizedPnl; + const totalFees = pnlData.metrics.totalCommission + pnlData.metrics.totalFundingFee; const totalTrades = pnlData.dailyPnL.reduce((sum, day) => sum + day.tradeCount, 0); const isProfit = totalPnL >= 0; const returnPercent = totalBalance > 0 ? (totalPnL / totalBalance) * 100 : 0; return ( -
+
-
- 24h +
+ 24h {totalTrades > 0 && ( - - {totalTrades} + + {totalTrades} trades )}
-
- - {formatCurrency(totalPnL)} - +
+
+ {isProfit ? ( + + ) : ( + + )} + + {formatCurrency(totalPnL)} + +
{formatPercentage(returnPercent)} +
+ Real: {formatCurrency(totalRealizedPnL)} + Fees: {formatCurrency(Math.abs(totalFees))} +
diff --git a/src/components/PnLChart.tsx b/src/components/PnLChart.tsx index 11162a0..eb92176 100644 --- a/src/components/PnLChart.tsx +++ b/src/components/PnLChart.tsx @@ -533,31 +533,31 @@ export default function PnLChart() { return ( - -
+ +
{!isCollapsed && ( -
+
setChartType(value as ChartType)}> - - Daily - Total - Breakdown - Per Symbol + + Daily + Total + Breakdown + Per Symbol
@@ -595,19 +595,19 @@ export default function PnLChart() { {/* Performance Summary - Minimal inline design */} {safeMetrics && ( -
-
+
+
{safeMetrics.totalPnl >= 0 ? ( - + ) : ( - + )} - = 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}`}> + = 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}`}> {formatTooltipValue(safeMetrics.totalPnl)} = 0 ? "outline" : "destructive"} - className={`h-3.5 md:h-4 text-[9px] md:text-[10px] px-0.5 md:px-1 ${safeMetrics.totalPnl >= 0 ? 'border-green-600 text-green-600 dark:border-green-400 dark:text-green-400' : ''}`} + className={`h-4 text-[10px] px-1 ${safeMetrics.totalPnl >= 0 ? 'border-green-600 text-green-600 dark:border-green-400 dark:text-green-400' : ''}`} > {pnlPercentage >= 0 ? '+' : ''}{pnlPercentage.toFixed(2)}% @@ -615,17 +615,17 @@ export default function PnLChart() {
-
- - Win - +
+ + Win + {safeMetrics.winRate.toFixed(1)}%
-
+
-
+
APR
-
+
Best: {safeMetrics.bestDay ? formatTooltipValue(safeMetrics.bestDay.netPnl) : '-'} Worst: {safeMetrics.worstDay ? formatTooltipValue(safeMetrics.worstDay.netPnl) : '-'} Avg: {formatTooltipValue(safeMetrics.avgDailyPnl)} @@ -656,19 +656,19 @@ export default function PnLChart() { ) : chartType === 'symbols' ? ( ) : ( - + {chartType === 'daily' ? ( - + - + } /> ) : ( - + - + } /> setIsCollapsed(!isCollapsed)} className="flex items-center gap-2 hover:opacity-80 transition-opacity" > - Positions - + Positions + {displayPositions.length} - + {!isCollapsed && ( - -
+ +
- Symbol - Side - Size - Entry/Mark - Liq. Price - PnL - Protection - Actions + Symbol + Side + Size + Entry/Mark + Liq. Price + PnL + Protection + Actions @@ -424,52 +424,52 @@ export default function PositionTable({ return ( - -
- + - + {position.side === 'LONG' ? ( - + ) : ( - + )} {position.side[0]} - -
+ +
{formatQuantity(position.symbol, position.quantity)}
-
+
${position.margin.toFixed(2)}
- -
+ +
${formatPriceWithCommas(position.symbol, position.entryPrice)}
-
+
${formatPriceWithCommas(position.symbol, position.markPrice)}
- + {position.liquidationPrice && position.liquidationPrice > 0 ? ( @@ -486,16 +486,16 @@ export default function PositionTable({ return ( <> {(isNearLiquidation || isCritical) && ( - + )} - + ${formatPriceWithCommas(position.symbol, position.liquidationPrice)} ); })()}
-
+
{(() => { const distancePercent = position.side === 'LONG' ? ((position.markPrice - position.liquidationPrice) / position.markPrice) * 100 @@ -521,20 +521,20 @@ export default function PositionTable({ โ€” )} - +
- = 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}`}> + = 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}`}> {position.pnl >= 0 ? '+' : ''}${Math.abs(position.pnl).toFixed(2)} = 0 ? "outline" : "destructive"} - className={`h-3 md:h-3.5 text-[8px] md:text-[9px] px-0.5 md:px-1 ${position.pnl >= 0 ? 'border-green-600 text-green-600 dark:border-green-400 dark:text-green-400' : ''}`} + className={`h-3.5 text-[9px] px-1 ${position.pnl >= 0 ? 'border-green-600 text-green-600 dark:border-green-400 dark:text-green-400' : ''}`} > {position.pnlPercent >= 0 ? '+' : ''}{position.pnlPercent.toFixed(1)}%
- +
@@ -612,7 +612,7 @@ export default function PositionTable({ )}
- + diff --git a/src/components/SessionPerformanceCard.tsx b/src/components/SessionPerformanceCard.tsx index 694eb6b..95bb10f 100644 --- a/src/components/SessionPerformanceCard.tsx +++ b/src/components/SessionPerformanceCard.tsx @@ -80,33 +80,70 @@ export default function SessionPerformanceCard() { if (isLoading || !sessionPnL) { return ( -
+
- Session - + Session +
); } + // Calculate win rate + const winRate = sessionPnL.tradeCount > 0 + ? (sessionPnL.winCount / sessionPnL.tradeCount) * 100 + : 0; + + // Calculate average profit per trade + const avgProfitPerTrade = sessionPnL.tradeCount > 0 + ? sessionPnL.realizedPnl / sessionPnL.tradeCount + : 0; + const isProfit = sessionPnL.realizedPnl >= 0; return ( -
+
-
- Session - +
+ Session + {formatDuration(sessionPnL.startTime)}
- - {formatCurrency(sessionPnL.realizedPnl)} - +
+
+ {isProfit ? ( + + ) : ( + + )} + + {formatCurrency(sessionPnL.realizedPnl)} + +
+ {sessionPnL.tradeCount > 0 && ( + <> + + {sessionPnL.tradeCount} trades + +
+ Win: {winRate.toFixed(0)}% + Avg: {formatCurrency(avgProfitPerTrade)} +
+ + )} +
); diff --git a/src/components/ShareConfigModal.tsx b/src/components/ShareConfigModal.tsx deleted file mode 100644 index 361d2bb..0000000 --- a/src/components/ShareConfigModal.tsx +++ /dev/null @@ -1,236 +0,0 @@ -'use client'; - -import React, { useRef } from 'react'; -import { Download, X } from 'lucide-react'; -import { toPng } from 'html-to-image'; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog'; -import { Button } from '@/components/ui/button'; -import { Badge } from '@/components/ui/badge'; -import { toast } from 'sonner'; -import type { Config } from '@/lib/config/types'; - -interface ShareConfigModalProps { - isOpen: boolean; - onClose: () => void; - config: Config; -} - -export default function ShareConfigModal({ isOpen, onClose, config }: ShareConfigModalProps) { - const contentRef = useRef(null); - - const handleExport = async () => { - if (!contentRef.current) return; - - try { - toast.info('Generating screenshot...'); - - const dataUrl = await toPng(contentRef.current, { - quality: 1.0, - pixelRatio: 2, - backgroundColor: '#ffffff', - }); - - const link = document.createElement('a'); - link.download = `aster-config-${new Date().toISOString().split('T')[0]}.png`; - link.href = dataUrl; - link.click(); - - toast.success('Configuration exported successfully!'); - } catch (error) { - console.error('Failed to export configuration:', error); - toast.error('Failed to export configuration'); - } - }; - - const symbols = Object.entries(config.symbols); - - return ( - - -
- - Share Configuration - -
- - -
-
- -
- {symbols.map(([symbol, symbolConfig]) => ( -
- {/* Symbol Header */} -
-

{symbol}

- - {symbolConfig.leverage}x - - - {symbolConfig.orderType || 'LIMIT'} - -
- - {/* Settings Grid */} -
-
- {/* Volume Thresholds */} -
- Long Vol: - - ${(symbolConfig.longVolumeThresholdUSDT || symbolConfig.volumeThresholdUSDT || 0).toLocaleString()} - -
-
- Short Vol: - - ${(symbolConfig.shortVolumeThresholdUSDT || symbolConfig.volumeThresholdUSDT || 0).toLocaleString()} - -
- - {/* Position Sizing */} -
- Base Size: - - {symbolConfig.tradeSize} - -
- {symbolConfig.longTradeSize !== undefined && ( -
- Long Size: - - ${symbolConfig.longTradeSize} - -
- )} - {symbolConfig.shortTradeSize !== undefined && ( -
- Short Size: - - ${symbolConfig.shortTradeSize} - -
- )} - {symbolConfig.maxPositionMarginUSDT !== undefined && ( -
- Max Margin: - - ${symbolConfig.maxPositionMarginUSDT} - -
- )} - - {/* Risk Parameters */} -
- Take Profit: - - {symbolConfig.tpPercent}% - -
-
- Stop Loss: - - {symbolConfig.slPercent}% - -
- - {/* Order Settings */} - {symbolConfig.priceOffsetBps !== undefined && ( -
- Price Offset: - - {symbolConfig.priceOffsetBps} bps - -
- )} - {symbolConfig.maxSlippageBps !== undefined && ( -
- Max Slippage: - - {symbolConfig.maxSlippageBps} bps - -
- )} - {symbolConfig.usePostOnly !== undefined && ( -
- Post-Only: - - {symbolConfig.usePostOnly ? 'Yes' : 'No'} - -
- )} - {symbolConfig.forceMarketEntry !== undefined && ( -
- Force Market: - - {symbolConfig.forceMarketEntry ? 'Yes' : 'No'} - -
- )} - - {/* VWAP Protection */} -
- VWAP: - - {symbolConfig.vwapProtection ? 'On' : 'Off'} - -
- {symbolConfig.vwapProtection && ( - <> -
- Timeframe: - - {symbolConfig.vwapTimeframe || '5m'} - -
-
- Lookback: - - {symbolConfig.vwapLookback || 200} - -
- - )} - - {/* Threshold System */} -
- Threshold: - - {symbolConfig.useThreshold ? 'On' : 'Off'} - -
- {symbolConfig.useThreshold && ( - <> -
- Window: - - {((symbolConfig.thresholdTimeWindow || 60000) / 1000).toFixed(0)}s - -
-
- Cooldown: - - {((symbolConfig.thresholdCooldown || 30000) / 1000).toFixed(0)}s - -
- - )} -
-
-
- ))} -
-
-
- ); -} diff --git a/src/components/SymbolConfigForm.tsx b/src/components/SymbolConfigForm.tsx index 9728d2a..8c1dab0 100644 --- a/src/components/SymbolConfigForm.tsx +++ b/src/components/SymbolConfigForm.tsx @@ -26,7 +26,6 @@ import { BarChart3, } from 'lucide-react'; import { toast } from 'sonner'; -import { TrancheSettingsSection } from './TrancheSettingsSection'; interface SymbolConfigFormProps { onSave: (config: Config) => void; @@ -439,12 +438,11 @@ export default function SymbolConfigForm({ onSave, currentConfig }: SymbolConfig { const value = parseFloat(e.target.value); handleGlobalChange('riskPercent', isNaN(value) ? 0 : value); }} - placeholder="90" className="w-24" min="0.1" max="100" @@ -455,7 +453,7 @@ export default function SymbolConfigForm({ onSave, currentConfig }: SymbolConfig

- Maximum percentage of your account to risk across all positions (default: 90%) + Maximum percentage of your account to risk across all positions

@@ -490,7 +488,7 @@ export default function SymbolConfigForm({ onSave, currentConfig }: SymbolConfig

- One-way: All positions use BOTH | Hedge: Separate LONG and SHORT positions (default: HEDGE) + One-way: All positions use BOTH | Hedge: Separate LONG and SHORT positions

@@ -508,19 +506,18 @@ export default function SymbolConfigForm({ onSave, currentConfig }: SymbolConfig { const value = parseInt(e.target.value); handleGlobalChange('maxOpenPositions', isNaN(value) ? 10 : value); }} - placeholder="5" className="w-24" min="1" max="50" step="1" /> - Maximum concurrent positions (default: 5, hedged pairs count as one) + Maximum concurrent positions (hedged pairs count as one)
@@ -598,7 +595,7 @@ export default function SymbolConfigForm({ onSave, currentConfig }: SymbolConfig { const value = parseInt(e.target.value); handleGlobalChange('server', { @@ -606,7 +603,6 @@ export default function SymbolConfigForm({ onSave, currentConfig }: SymbolConfig dashboardPort: isNaN(value) ? 3000 : value }); }} - placeholder="3000" className="w-24" min="1024" max="65535" @@ -623,7 +619,7 @@ export default function SymbolConfigForm({ onSave, currentConfig }: SymbolConfig { const value = parseInt(e.target.value); handleGlobalChange('server', { @@ -631,7 +627,6 @@ export default function SymbolConfigForm({ onSave, currentConfig }: SymbolConfig websocketPort: isNaN(value) ? 8080 : value }); }} - placeholder="8080" className="w-24" min="1024" max="65535" @@ -809,8 +804,7 @@ export default function SymbolConfigForm({ onSave, currentConfig }: SymbolConfig
{selectedSymbol && config.symbols[selectedSymbol] && ( - <> - +
{selectedSymbol} Settings @@ -830,16 +824,15 @@ export default function SymbolConfigForm({ onSave, currentConfig }: SymbolConfig { const value = parseFloat(e.target.value); handleSymbolChange(selectedSymbol, 'longVolumeThresholdUSDT', isNaN(value) ? 0 : value); }} - placeholder="10000" min="0" />

- Min liquidation volume for longs (default: 10000 USDT) + Min liquidation volume for longs (buy on sell liquidations)

@@ -847,16 +840,15 @@ export default function SymbolConfigForm({ onSave, currentConfig }: SymbolConfig { const value = parseFloat(e.target.value); handleSymbolChange(selectedSymbol, 'shortVolumeThresholdUSDT', isNaN(value) ? 0 : value); }} - placeholder="10000" min="0" />

- Min liquidation volume for shorts (default: 10000 USDT) + Min liquidation volume for shorts (sell on buy liquidations)

@@ -864,17 +856,16 @@ export default function SymbolConfigForm({ onSave, currentConfig }: SymbolConfig { const value = parseInt(e.target.value); handleSymbolChange(selectedSymbol, 'leverage', isNaN(value) ? 1 : value); }} - placeholder="10" min="1" max="125" />

- Trading leverage (default: 10x) + Trading leverage (1-125x)

@@ -932,18 +923,17 @@ export default function SymbolConfigForm({ onSave, currentConfig }: SymbolConfig { const value = parseFloat(e.target.value); handleSymbolChange(selectedSymbol, 'tradeSize', isNaN(value) ? 0 : value); }} - placeholder="100" min="0" step="0.01" />

- Position size in USDT (default: 100, used for both long and short) + Position size in USDT (used for both long and short)

{symbolDetails && !loadingDetails && getMinimumMargin() && (
@@ -1090,16 +1080,15 @@ export default function SymbolConfigForm({ onSave, currentConfig }: SymbolConfig { const value = parseFloat(e.target.value); handleSymbolChange(selectedSymbol, 'maxPositionMarginUSDT', isNaN(value) ? 0 : value); }} - placeholder="10000" min="0" />

- Max total margin exposure for this symbol (default: 10000 USDT) + Max total margin exposure for this symbol

@@ -1107,17 +1096,16 @@ export default function SymbolConfigForm({ onSave, currentConfig }: SymbolConfig { const value = parseFloat(e.target.value); handleSymbolChange(selectedSymbol, 'slPercent', isNaN(value) ? 0 : value); }} - placeholder="2" min="0.1" step="0.1" />

- Stop loss percentage (default: 2%) + Stop loss percentage

@@ -1125,17 +1113,16 @@ export default function SymbolConfigForm({ onSave, currentConfig }: SymbolConfig { const value = parseFloat(e.target.value); handleSymbolChange(selectedSymbol, 'tpPercent', isNaN(value) ? 0 : value); }} - placeholder="3" min="0.1" step="0.1" />

- Take profit percentage (default: 3%) + Take profit percentage

@@ -1146,13 +1133,13 @@ export default function SymbolConfigForm({ onSave, currentConfig }: SymbolConfig

- Default order type for opening positions (default: LIMIT) + Default order type for opening positions

@@ -1218,13 +1205,13 @@ export default function SymbolConfigForm({ onSave, currentConfig }: SymbolConfig

- Candle timeframe for VWAP calculation (default: 1m) + Candle timeframe for VWAP calculation

@@ -1243,29 +1230,20 @@ export default function SymbolConfigForm({ onSave, currentConfig }: SymbolConfig { const value = parseInt(e.target.value); - if (e.target.value === '' || isNaN(value)) { - // Remove the field if empty - will use default from config.default.json - const { vwapLookback, ...rest } = config.symbols[selectedSymbol]; - setConfig({ - ...config, - symbols: { - ...config.symbols, - [selectedSymbol]: rest, - }, - }); - } else { - handleSymbolChange(selectedSymbol, 'vwapLookback', value); - } + handleSymbolChange( + selectedSymbol, + 'vwapLookback', + isNaN(value) ? 100 : value + ); }} - placeholder="100" min="10" max="500" />

- Number of candles for VWAP (default: 100) + Number of candles for VWAP (10-500)

@@ -1313,24 +1291,12 @@ export default function SymbolConfigForm({ onSave, currentConfig }: SymbolConfig { const seconds = parseFloat(e.target.value); - if (e.target.value === '' || isNaN(seconds)) { - // Remove the field if empty - will use default from config.default.json - const { thresholdTimeWindow, ...rest } = config.symbols[selectedSymbol]; - setConfig({ - ...config, - symbols: { - ...config.symbols, - [selectedSymbol]: rest, - }, - }); - } else { - handleSymbolChange(selectedSymbol, 'thresholdTimeWindow', seconds * 1000); - } + const ms = isNaN(seconds) ? 60000 : seconds * 1000; + handleSymbolChange(selectedSymbol, 'thresholdTimeWindow', ms); }} - placeholder="60" min="10" max="300" step="10" @@ -1344,24 +1310,12 @@ export default function SymbolConfigForm({ onSave, currentConfig }: SymbolConfig { const seconds = parseFloat(e.target.value); - if (e.target.value === '' || isNaN(seconds)) { - // Remove the field if empty - will use default from config.default.json - const { thresholdCooldown, ...rest } = config.symbols[selectedSymbol]; - setConfig({ - ...config, - symbols: { - ...config.symbols, - [selectedSymbol]: rest, - }, - }); - } else { - handleSymbolChange(selectedSymbol, 'thresholdCooldown', seconds * 1000); - } + const ms = isNaN(seconds) ? 30000 : seconds * 1000; + handleSymbolChange(selectedSymbol, 'thresholdCooldown', ms); }} - placeholder="30" min="10" max="300" step="10" @@ -1386,25 +1340,17 @@ export default function SymbolConfigForm({ onSave, currentConfig }: SymbolConfig )} + )} + + )} - {/* Multi-Tranche Position Management */} - handleSymbolChange(selectedSymbol, field, value)} - /> - - )} - - )} - - {Object.keys(config.symbols).length === 0 && ( -
- -

No symbols configured yet

-

Add a symbol above to get started

-
- )} + {Object.keys(config.symbols).length === 0 && ( +
+ +

No symbols configured yet

+

Add a symbol above to get started

+
+ )} diff --git a/src/components/TrancheBreakdownCard.tsx b/src/components/TrancheBreakdownCard.tsx deleted file mode 100644 index e190e65..0000000 --- a/src/components/TrancheBreakdownCard.tsx +++ /dev/null @@ -1,336 +0,0 @@ -'use client'; - -import { useState, useEffect } from 'react'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { Badge } from '@/components/ui/badge'; -import { Separator } from '@/components/ui/separator'; -import { TrendingUp, TrendingDown, AlertTriangle, Clock, DollarSign } from 'lucide-react'; -import { Tranche } from '@/lib/types'; - -interface TrancheBreakdownCardProps { - symbol: string; - side: 'LONG' | 'SHORT'; -} - -interface TrancheMetrics { - total: number; - active: number; - isolated: number; - closed: number; - totalQuantity: number; - totalMarginUsed: number; - totalUnrealizedPnl: number; - totalRealizedPnl: number; - weightedAvgEntry: number; -} - -export function TrancheBreakdownCard({ symbol, side }: TrancheBreakdownCardProps) { - const [tranches, setTranches] = useState([]); - const [metrics, setMetrics] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - fetchTranches(); - // Refresh every 5 seconds - const interval = setInterval(fetchTranches, 5000); - return () => clearInterval(interval); - }, [symbol, side]); - - const fetchTranches = async () => { - try { - const response = await fetch(`/api/tranches?symbol=${symbol}&side=${side}&status=all`); - if (!response.ok) { - throw new Error('Failed to fetch tranches'); - } - const data = await response.json(); - setTranches(data.tranches || []); - setMetrics(data.metrics || null); - setError(null); - } catch (err: any) { - setError(err.message); - } finally { - setLoading(false); - } - }; - - const formatPrice = (price: number) => { - return price.toLocaleString('en-US', { - minimumFractionDigits: 2, - maximumFractionDigits: 2, - }); - }; - - const formatPnL = (pnl: number) => { - const formatted = Math.abs(pnl).toLocaleString('en-US', { - minimumFractionDigits: 2, - maximumFractionDigits: 2, - }); - return pnl >= 0 ? `+$${formatted}` : `-$${formatted}`; - }; - - const formatTime = (timestamp: number) => { - return new Date(timestamp).toLocaleString('en-US', { - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - }); - }; - - const getPnLColor = (pnl: number) => { - if (pnl > 0) return 'text-green-500'; - if (pnl < 0) return 'text-red-500'; - return 'text-gray-500'; - }; - - const activeTranches = tranches.filter(t => t.status === 'active' && !t.isolated); - const isolatedTranches = tranches.filter(t => t.isolated && t.status === 'active'); - const closedTranches = tranches.filter(t => t.status === 'closed'); - - if (loading) { - return ( - - - Tranche Breakdown - {symbol} {side} - - -
-
-
-
-
- ); - } - - if (error) { - return ( - - - Tranche Breakdown - {symbol} {side} - - -
Error: {error}
-
-
- ); - } - - return ( - - - - Tranche Breakdown - {symbol} {side} - - {side} - - - - Track multiple position entries (tranches) for better margin utilization - - - - {/* Summary Metrics */} - {metrics && ( -
-
-

Active Tranches

-

{metrics.active}

-
-
-

Isolated

-

{metrics.isolated}

-
-
-

Total Quantity

-

{metrics.totalQuantity.toFixed(4)}

-
-
-

Unrealized P&L

-

- {formatPnL(metrics.totalUnrealizedPnl)} -

-
-
- )} - - - - {/* Active Tranches */} - {activeTranches.length > 0 && ( -
-

- - Active Tranches ({activeTranches.length}) -

-
- {activeTranches.map((tranche) => ( -
-
-
- - {tranche.id.substring(0, 8)} - - - {formatTime(tranche.entryTime)} - -
- Active -
- -
-
-

Entry

-

${formatPrice(tranche.entryPrice)}

-
-
-

Quantity

-

{tranche.quantity.toFixed(4)}

-
-
-

Margin

-

${formatPrice(tranche.marginUsed)}

-
-
-

Unrealized P&L

-

- {formatPnL(tranche.unrealizedPnl)} -

-
-
- -
- TP: ${formatPrice(tranche.tpPrice)} - SL: ${formatPrice(tranche.slPrice)} - Leverage: {tranche.leverage}x -
-
- ))} -
-
- )} - - {/* Isolated Tranches */} - {isolatedTranches.length > 0 && ( -
-

- - Isolated Tranches ({isolatedTranches.length}) -

-
- {isolatedTranches.map((tranche) => ( -
-
-
- - {tranche.id.substring(0, 8)} - - - {formatTime(tranche.entryTime)} - -
- Isolated -
- -
-
-

Entry

-

${formatPrice(tranche.entryPrice)}

-
-
-

Quantity

-

{tranche.quantity.toFixed(4)}

-
-
-

Margin

-

${formatPrice(tranche.marginUsed)}

-
-
-

Unrealized P&L

-

- {formatPnL(tranche.unrealizedPnl)} -

-
-
- - {tranche.isolationTime && ( -
- - Isolated at {formatTime(tranche.isolationTime)} - {tranche.isolationPrice && ( - @ ${formatPrice(tranche.isolationPrice)} - )} -
- )} -
- ))} -
-
- )} - - {/* Recent Closed Tranches */} - {closedTranches.length > 0 && ( -
-

- - Recent Closed ({closedTranches.slice(0, 5).length}) -

-
- {closedTranches.slice(0, 5).map((tranche) => ( -
-
-
- - {tranche.id.substring(0, 8)} - - - {tranche.exitTime && formatTime(tranche.exitTime)} - -
- Closed -
- -
-
-

Entry โ†’ Exit

-

- ${formatPrice(tranche.entryPrice)} โ†’ ${formatPrice(tranche.exitPrice || 0)} -

-
-
-

Quantity

-

{tranche.quantity.toFixed(4)}

-
-
-

Realized P&L

-

- {formatPnL(tranche.realizedPnl)} -

-
-
-
- ))} -
-
- )} - - {/* Empty State */} - {tranches.length === 0 && ( -
-

No tranches found for {symbol} {side}

-

Tranches will appear here when positions are opened

-
- )} -
-
- ); -} diff --git a/src/components/TrancheSettingsSection.tsx b/src/components/TrancheSettingsSection.tsx deleted file mode 100644 index 8e6e094..0000000 --- a/src/components/TrancheSettingsSection.tsx +++ /dev/null @@ -1,251 +0,0 @@ -'use client'; - -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { Label } from '@/components/ui/label'; -import { Input } from '@/components/ui/input'; -import { Switch } from '@/components/ui/switch'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import { Separator } from '@/components/ui/separator'; -import { Info } from 'lucide-react'; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; - -interface TrancheSettingsSectionProps { - symbol: string; - config: any; - onChange: (field: string, value: any) => void; -} - -export function TrancheSettingsSection({ symbol, config, onChange }: TrancheSettingsSectionProps) { - const enabled = config.enableTrancheManagement ?? false; - - return ( - - - Multi-Tranche Position Management - - Track multiple position entries to isolate underwater positions and continue trading - - - - {/* Enable/Disable Toggle */} -
-
- -

- Track multiple virtual position entries for better margin utilization -

-
- onChange('enableTrancheManagement', checked)} - /> -
- - {enabled && ( - <> - - - {/* Isolation Threshold */} -
-
- - - - - - - -

- Tranches with unrealized loss exceeding this percentage will be isolated. - New trades won't add to isolated tranches. -

-
-
-
-
- onChange('trancheIsolationThreshold', parseFloat(e.target.value) || 5)} - placeholder="5" - /> -

- Default: 5% loss. Typical range: 3-10% -

-
- - {/* Max Tranches */} -
-
- - - - - - - -

- Maximum number of active (non-isolated) tranches allowed per symbol. - Prevents over-exposure to a single asset. -

-
-
-
-
- onChange('maxTranches', parseInt(e.target.value) || 3)} - placeholder="3" - /> -

- Default: 3. Typical range: 2-5 -

-
- - {/* Max Isolated Tranches */} -
-
- - - - - - - -

- Maximum number of isolated (underwater) tranches allowed before blocking new trades. -

-
-
-
-
- onChange('maxIsolatedTranches', parseInt(e.target.value) || 2)} - placeholder="2" - /> -

- Default: 2. Typical range: 1-3 -

-
- - - - {/* Strategy Info */} -
-

Tranche Strategies (Auto-configured)

-
-

- Closing Strategy: LIFO (Last In, First Out) -
- โ†’ Closes newest tranches first for quick profit-taking -

-

- SL/TP Strategy: Best Entry Price -
- โ†’ Protects your most favorable entry price -

-
-
- - - - {/* Advanced Options */} -
-

Advanced Options

- -
-
- -

- Continue opening new tranches even when isolated tranches exist -

-
- onChange('allowTrancheWhileIsolated', checked)} - /> -
- -
-
- -

- Automatically close isolated tranches when they recover -

-
- onChange('trancheAutoCloseIsolated', checked)} - /> -
- - {config.trancheAutoCloseIsolated && ( -
-
- - - - - - - -

- Isolated tranches will auto-close when unrealized profit exceeds this percentage. - Example: 0.5% means close at +0.5% profit (just above breakeven). -

-
-
-
-
- onChange('trancheRecoveryThreshold', parseFloat(e.target.value) || 0.5)} - placeholder="0.5" - /> -

- Default: 0.5% profit. Typical range: 0-2% -

-
- )} -
- - )} -
-
- ); -} diff --git a/src/components/TrancheTimeline.tsx b/src/components/TrancheTimeline.tsx deleted file mode 100644 index 9b102ac..0000000 --- a/src/components/TrancheTimeline.tsx +++ /dev/null @@ -1,218 +0,0 @@ -'use client'; - -import { useState, useEffect } from 'react'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { Badge } from '@/components/ui/badge'; -import { TrendingUp, TrendingDown, AlertTriangle, X, DollarSign } from 'lucide-react'; - -interface TrancheEvent { - id: string; - type: 'tranche_created' | 'tranche_isolated' | 'tranche_closed' | 'tranche_sync'; - timestamp: Date; - data: any; -} - -export function TrancheTimeline() { - const [events, setEvents] = useState([]); - const [wsConnected, setWsConnected] = useState(false); - - useEffect(() => { - // Connect to WebSocket for real-time events - const wsHost = process.env.NEXT_PUBLIC_WS_HOST || 'localhost'; - const wsPort = process.env.NEXT_PUBLIC_WS_PORT || '8080'; - const ws = new WebSocket(`ws://${wsHost}:${wsPort}`); - - ws.onopen = () => { - console.log('TrancheTimeline: WebSocket connected'); - setWsConnected(true); - }; - - ws.onmessage = (event) => { - try { - const message = JSON.parse(event.data); - const { type, data } = message; - - // Only handle tranche events - if ( - type === 'tranche_created' || - type === 'tranche_isolated' || - type === 'tranche_closed' || - type === 'tranche_sync' - ) { - const newEvent: TrancheEvent = { - id: `${type}-${Date.now()}-${Math.random()}`, - type, - timestamp: data.timestamp ? new Date(data.timestamp) : new Date(), - data, - }; - - setEvents((prev) => [newEvent, ...prev].slice(0, 50)); // Keep last 50 events - } - } catch (error) { - console.error('Error parsing WebSocket message:', error); - } - }; - - ws.onclose = () => { - console.log('TrancheTimeline: WebSocket disconnected'); - setWsConnected(false); - }; - - ws.onerror = (error) => { - console.error('TrancheTimeline: WebSocket error:', error); - }; - - return () => { - ws.close(); - }; - }, []); - - const getEventIcon = (type: string) => { - switch (type) { - case 'tranche_created': - return ; - case 'tranche_isolated': - return ; - case 'tranche_closed': - return ; - case 'tranche_sync': - return ; - default: - return ; - } - }; - - const getEventTitle = (event: TrancheEvent) => { - const { type, data } = event; - const trancheId = data.trancheId?.substring(0, 8) || 'Unknown'; - - switch (type) { - case 'tranche_created': - return `Tranche Created: ${data.symbol} ${data.side}`; - case 'tranche_isolated': - return `Tranche Isolated: ${data.symbol} (${data.pnlPercent?.toFixed(2)}% loss)`; - case 'tranche_closed': - return `Tranche Closed: ${data.symbol} (${data.closedFully ? 'Full' : 'Partial'})`; - case 'tranche_sync': - return `Exchange Sync: ${data.symbol} ${data.side} (${data.syncStatus})`; - default: - return 'Unknown Event'; - } - }; - - const getEventDetails = (event: TrancheEvent) => { - const { type, data } = event; - - switch (type) { - case 'tranche_created': - return ( -
-

Entry: ${data.entryPrice?.toLocaleString()}

-

Quantity: {data.quantity} | Margin: ${data.marginUsed}

-

TP: ${data.tpPrice?.toLocaleString()} | SL: ${data.slPrice?.toLocaleString()}

-
- ); - case 'tranche_isolated': - return ( -
-

Entry: ${data.entryPrice?.toLocaleString()} โ†’ Current: ${data.currentPrice?.toLocaleString()}

-

Unrealized P&L: ${data.unrealizedPnl?.toFixed(2)}

-

Threshold: {data.isolationThreshold}%

-
- ); - case 'tranche_closed': - return ( -
-

Entry: ${data.entryPrice?.toLocaleString()} โ†’ Exit: ${data.exitPrice?.toLocaleString()}

-

Quantity: {data.quantity}

-

= 0 ? 'text-green-500' : 'text-red-500'}> - Realized P&L: ${data.realizedPnl?.toFixed(2)} -

-
- ); - case 'tranche_sync': - return ( -
-

Local: {data.totalQuantity} | Exchange: {data.exchangeQuantity}

-

Active: {data.activeTranches} | Isolated: {data.isolatedTranches}

- {data.quantityDrift &&

Drift: {data.quantityDrift.toFixed(4)}

} -
- ); - default: - return null; - } - }; - - const formatTime = (date: Date) => { - return date.toLocaleTimeString('en-US', { - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - }); - }; - - return ( - - - - Tranche Activity Timeline - - {wsConnected ? 'Live' : 'Disconnected'} - - - - Real-time tranche events and lifecycle updates - - - -
- {events.length === 0 ? ( -
-

No tranche events yet

-

Events will appear here in real-time

-
- ) : ( -
- {events.map((event, index) => ( -
- {/* Timeline line */} - {index < events.length - 1 && ( -
- )} - - {/* Event card */} -
-
-
- {getEventIcon(event.type)} -
-
- -
-
-

{getEventTitle(event)}

- - {formatTime(event.timestamp)} - -
- - {getEventDetails(event)} - - {event.data.trancheId && ( -
- - {event.data.trancheId.substring(0, 8)} - -
- )} -
-
-
- ))} -
- )} -
- - - ); -} diff --git a/src/components/dashboard-layout.tsx b/src/components/dashboard-layout.tsx index f325ea4..65db837 100644 --- a/src/components/dashboard-layout.tsx +++ b/src/components/dashboard-layout.tsx @@ -16,7 +16,6 @@ import { LogOut } from "lucide-react" import { useConfig } from "@/components/ConfigProvider" import { signOut } from "next-auth/react" import { RateLimitBarCompact } from "@/components/RateLimitBar" -import BotControlButtons from "@/components/BotControlButtons" interface DashboardLayoutProps { children: React.ReactNode @@ -42,36 +41,25 @@ export function DashboardLayout({ children }: DashboardLayoutProps) { -
+
{/* Open Beta Warning */} -
+
โš ๏ธ OPEN BETA - Only use what you can afford to lose
- {/* Mobile Beta Badge */} -
- โš ๏ธ - BETA -
- {/* Rate Limit Compact Bar */} - -
- -
+ +
-
- {/* Bot Control Buttons */} - - +
{/* External Links */} -
+
{/* GitHub */} AsterDex
- -
- -
+ +
diff --git a/src/lib/bot/hunter.ts b/src/lib/bot/hunter.ts index b01438d..3bc7f76 100644 --- a/src/lib/bot/hunter.ts +++ b/src/lib/bot/hunter.ts @@ -10,7 +10,6 @@ import { liquidationStorage } from '../services/liquidationStorage'; import { vwapService } from '../services/vwapService'; import { vwapStreamer } from '../services/vwapStreamer'; import { thresholdMonitor } from '../services/thresholdMonitor'; -import { getTrancheManager } from '../services/trancheManager'; import { symbolPrecision } from '../utils/symbolPrecision'; import { parseExchangeError, @@ -29,7 +28,6 @@ export class Hunter extends EventEmitter { private ws: WebSocket | null = null; private config: Config; private isRunning = false; - private isPaused = false; private statusBroadcaster: any; // Will be injected private isHedgeMode: boolean; private positionTracker: PositionTracker | null = null; @@ -308,9 +306,9 @@ logErrorWithTimestamp('Hunter: Failed to initialize symbol precision manager:', // Continue anyway, will use default precision values } - // In paper mode, simulate liquidation events (regardless of API keys) - if (this.config.global.paperMode) { -logWithTimestamp('Hunter: Running in PAPER MODE - simulating liquidations with real market prices'); + // In paper mode with no API keys, simulate liquidation events + if (this.config.global.paperMode && (!this.config.api.apiKey || !this.config.api.secretKey)) { +logWithTimestamp('Hunter: Running in paper mode without API keys - simulating liquidations'); this.simulateLiquidations(); } else { this.connectWebSocket(); @@ -319,7 +317,6 @@ logWithTimestamp('Hunter: Running in PAPER MODE - simulating liquidations with r stop(): void { this.isRunning = false; - this.isPaused = false; // Stop periodic cleanup this.stopPeriodicCleanup(); @@ -337,24 +334,6 @@ logWithTimestamp('Hunter: Stopped periodic position mode sync'); } } - pause(): void { - if (!this.isRunning || this.isPaused) { -logWithTimestamp('Hunter: Cannot pause - not running or already paused'); - return; - } - this.isPaused = true; -logWithTimestamp('Hunter: Paused - no new trades will be placed'); - } - - resume(): void { - if (!this.isRunning || !this.isPaused) { -logWithTimestamp('Hunter: Cannot resume - not running or not paused'); - return; - } - this.isPaused = false; -logWithTimestamp('Hunter: Resumed - trading active'); - } - private connectWebSocket(): void { this.ws = new WebSocket('wss://fstream.asterdex.com/ws/!forceOrder@arr'); @@ -592,12 +571,6 @@ logWithTimestamp(`Hunter: โœ“ Cooldown passed - Triggering ${tradeSide} trade fo } private async analyzeAndTrade(liquidation: LiquidationEvent, symbolConfig: SymbolConfig, _forcedSide?: 'BUY' | 'SELL'): Promise { - // Check if bot is paused - if (this.isPaused) { -logWithTimestamp(`Hunter: Skipping trade - bot is paused (${liquidation.symbol} ${liquidation.side})`); - return; - } - try { // Get mark price and recent 1m kline const [markPriceData] = Array.isArray(await getMarkPrice(liquidation.symbol)) ? @@ -755,46 +728,6 @@ logErrorWithTimestamp('Hunter: Analysis error:', error); let order: any; // Declare order variable for error handling try { - // Check tranche management limits (if enabled) - if (symbolConfig.enableTrancheManagement) { - try { - const trancheManager = getTrancheManager(); - const trancheSide = side === 'BUY' ? 'LONG' : 'SHORT'; - - // Update P&L and check isolation conditions - const markPriceData = await getMarkPrice(symbol); - const price = parseFloat(Array.isArray(markPriceData) ? markPriceData[0].markPrice : markPriceData.markPrice); - await trancheManager.updateUnrealizedPnl(symbol, price); - - // Check if we can open a new tranche - const canOpen = trancheManager.canOpenNewTranche(symbol, trancheSide); - if (!canOpen.allowed) { - logWithTimestamp(`Hunter: ${canOpen.reason}`); - - // Broadcast to UI - if (this.statusBroadcaster) { - this.statusBroadcaster.broadcastTradingError( - `Tranche Limit Reached - ${symbol}`, - canOpen.reason || 'Cannot open new tranche', - { - component: 'Hunter', - symbol, - details: { - activeTranches: trancheManager.getTranches(symbol, trancheSide).length, - maxTranches: symbolConfig.maxTranches || 3, - } - } - ); - } - - return; // Block the trade - } - } catch (trancheError) { - // If TrancheManager is not initialized, log warning but continue - logWarnWithTimestamp('Hunter: TrancheManager check failed (not initialized?), continuing with trade:', trancheError); - } - } - // Check position limits before placing trade if (this.positionTracker && !this.config.global.paperMode) { // Check if we already have a pending order for this symbol @@ -1168,30 +1101,6 @@ logWarnWithTimestamp('Hunter: Cannot determine correct mode. Since we cannot ver // Only broadcast and emit if order was successfully placed if (order && order.orderId) { - // Create tranche if tranche management is enabled - if (symbolConfig.enableTrancheManagement) { - try { - const trancheManager = getTrancheManager(); - const trancheSide = side === 'BUY' ? 'LONG' : 'SHORT'; - - const tranche = await trancheManager.createTranche({ - symbol, - side, - positionSide: getPositionSide(this.isHedgeMode, side) as any, - entryPrice: orderType === 'LIMIT' ? orderPrice : entryPrice, - quantity: quantity!, - marginUsed: tradeSizeUSDT, - leverage: symbolConfig.leverage, - orderId: order.orderId.toString(), - }); - - logWithTimestamp(`Hunter: Created tranche ${tranche.id.substring(0, 8)} for ${symbol} ${side}`); - } catch (trancheError) { - logErrorWithTimestamp('Hunter: Failed to create tranche:', trancheError); - // Don't fail the trade, just log the error - } - } - // Broadcast order placed event if (this.statusBroadcaster) { this.statusBroadcaster.broadcastOrderPlaced({ @@ -1630,73 +1539,34 @@ logWithTimestamp('Hunter: No symbols configured for simulation'); return; } - // Generate realistic liquidation events using actual market prices - const generateEvent = async () => { + // Generate random liquidation events every 5-10 seconds + const generateEvent = () => { if (!this.isRunning) return; - try { - // Pick a random symbol from config - const symbol = symbols[Math.floor(Math.random() * symbols.length)]; - const symbolConfig = this.config.symbols[symbol]; - - // Fetch real market price - const markPriceData = await getMarkPrice(symbol); - const currentPrice = parseFloat( - Array.isArray(markPriceData) ? markPriceData[0].markPrice : markPriceData.markPrice - ); - - // Random side with slight bias - const side = Math.random() > 0.5 ? 'SELL' : 'BUY'; - - // Simulate price with small variance (ยฑ0.5%) - const priceVariance = 0.005; - const simulatedPrice = currentPrice * (1 + (Math.random() - 0.5) * priceVariance); - - // Calculate realistic quantity based on configured thresholds - const thresholdUSDT = side === 'SELL' - ? (symbolConfig.longVolumeThresholdUSDT || 1000) - : (symbolConfig.shortVolumeThresholdUSDT || 1000); - - // Generate quantity that's 1-3x the threshold - const volumeMultiplier = 1 + Math.random() * 2; - const volumeUSDT = thresholdUSDT * volumeMultiplier; - const qty = volumeUSDT / simulatedPrice; - - const mockEvent = { - e: 'forceOrder', - o: { - s: symbol, - S: side, - o: 'LIMIT', - p: simulatedPrice.toString(), - q: qty.toString(), - ap: simulatedPrice.toString(), - X: 'FILLED', - l: qty.toString(), - z: qty.toString(), - T: Date.now() - }, - E: Date.now() - }; - -logWithTimestamp( - `Hunter: [PAPER MODE] Simulated liquidation - ${symbol} ${side} ` + - `${volumeUSDT.toFixed(0)} USDT @ $${simulatedPrice.toFixed(4)}` - ); + const symbol = symbols[Math.floor(Math.random() * symbols.length)]; + const side = Math.random() > 0.5 ? 'SELL' : 'BUY'; + const price = symbol === 'BTCUSDT' ? 40000 + Math.random() * 5000 : 2000 + Math.random() * 500; + const qty = Math.random() * 10; + + const mockEvent = { + o: { + s: symbol, + S: side, + p: price.toString(), + q: qty.toString(), + T: Date.now() + } + }; - // Handle the simulated liquidation event - await this.handleLiquidationEvent(mockEvent); - } catch (error) { -logErrorWithTimestamp('Hunter: Error generating simulated liquidation:', error); - } +logWithTimestamp(`Hunter: Simulated liquidation - ${symbol} ${side} ${qty.toFixed(4)} @ $${price.toFixed(2)}`); + this.handleLiquidationEvent(mockEvent); - // Schedule next event (random interval 10-30 seconds for more realistic behavior) - const delay = 10000 + Math.random() * 20000; + // Schedule next event + const delay = 5000 + Math.random() * 5000; // 5-10 seconds setTimeout(generateEvent, delay); }; - // Start generating events after 3 seconds - setTimeout(generateEvent, 3000); -logWithTimestamp('Hunter: Simulation started - will generate liquidations every 10-30 seconds using real market prices'); + // Start generating events after 2 seconds + setTimeout(generateEvent, 2000); } } diff --git a/src/lib/bot/positionManager.ts b/src/lib/bot/positionManager.ts index 32cf56a..1a8eede 100644 --- a/src/lib/bot/positionManager.ts +++ b/src/lib/bot/positionManager.ts @@ -12,8 +12,6 @@ import { errorLogger } from '../services/errorLogger'; import { getPriceService } from '../services/priceService'; import { invalidateIncomeCache } from '../api/income'; import { logWithTimestamp, logErrorWithTimestamp, logWarnWithTimestamp } from '../utils/timestamp'; -import { paperModeSimulator } from '../services/paperModeSimulator'; -import { getTrancheManager } from '../services/trancheManager'; // Minimal local state - only track order IDs linked to positions interface PositionOrders { @@ -187,49 +185,10 @@ logErrorWithTimestamp('PositionManager: Failed to fetch exchange info:', error.m // Continue anyway - will use raw values } - // In paper mode, initialize the paper mode simulator instead of real streams - if (this.config.global.paperMode) { -logWithTimestamp('PositionManager: Running in PAPER MODE - initializing simulator'); - - // Initialize paper mode simulator - paperModeSimulator.initialize(this.config); - paperModeSimulator.start(); - - // Listen for paper mode events and broadcast to UI - paperModeSimulator.on('positionOpened', (data) => { - if (this.statusBroadcaster) { - this.statusBroadcaster.broadcastPositionUpdate({ - symbol: data.symbol, - side: data.side, - quantity: data.quantity, - price: data.entryPrice, - type: 'opened', - paperMode: true - }); - } - }); - - paperModeSimulator.on('positionClosed', (data) => { - if (this.statusBroadcaster) { - this.statusBroadcaster.broadcastPositionClosed({ - symbol: data.symbol, - side: data.side, - quantity: 0, // Not tracked in paper mode - pnl: data.pnlUSDT, - reason: data.reason, - paperMode: true - }); - } - }); - - paperModeSimulator.on('pnlUpdate', (data) => { - if (this.statusBroadcaster) { - this.statusBroadcaster.broadcast('paper_mode_pnl', data); - } - }); - -logWithTimestamp('โœ… Paper mode simulator active - positions will be tracked and simulated'); - return; // Don't start real WebSocket streams + // Skip user data stream in paper mode with no API keys + if (this.config.global.paperMode && (!this.config.api.apiKey || !this.config.api.secretKey)) { +logWithTimestamp('PositionManager: Running in paper mode without API keys - simulating streams'); + return; } try { @@ -247,12 +206,6 @@ logErrorWithTimestamp('PositionManager: Failed to start:', error); this.isRunning = false; logWithTimestamp('PositionManager: Stopping...'); - // Stop paper mode simulator if in paper mode - if (this.config.global.paperMode) { - paperModeSimulator.stop(); -logWithTimestamp('PositionManager: Paper mode simulator stopped'); - } - if (this.keepaliveInterval) clearInterval(this.keepaliveInterval); if (this.riskCheckInterval) clearInterval(this.riskCheckInterval); if (this.orderCheckInterval) clearInterval(this.orderCheckInterval); @@ -911,43 +864,6 @@ logErrorWithTimestamp(`PositionManager: Failed to ensure protection for ${symbol if (sizeChanged) { this.refreshBalance(); } - - // Sync tranches with exchange position if tranche management is enabled - const symbolConfig = this.config.symbols[symbol]; - if (symbolConfig?.enableTrancheManagement) { - try { - const trancheManager = getTrancheManager(); - const trancheSide = positionAmt > 0 ? 'LONG' : 'SHORT'; - - // Create exchange position object for sync - const exchangePosition: ExchangePosition = { - symbol: pos.s, - positionAmt: pos.pa, - entryPrice: pos.ep, - markPrice: pos.mp || '0', - unRealizedProfit: pos.up, - liquidationPrice: pos.lp || '0', - leverage: this.symbolLeverage.get(symbol)?.toString() || '0', - marginType: pos.mt, - isolatedMargin: pos.iw || '0', - isAutoAddMargin: pos.iam || 'false', - positionSide: positionSide, - updateTime: event.E, - }; - - // Sync with exchange (3 separate arguments) - await trancheManager.syncWithExchange( - symbol, - trancheSide, - exchangePosition - ); - -logWithTimestamp(`PositionManager: Synced tranches for ${symbol} ${trancheSide} with exchange`); - } catch (trancheError) { -logWarnWithTimestamp('PositionManager: Failed to sync tranches with exchange:', trancheError); - // Don't fail the position update, just log the warning - } - } } }); @@ -1215,46 +1131,6 @@ logWarnWithTimestamp(`PositionManager: Could not find position key for order ${o logWithTimestamp(`PositionManager: Using exchange-provided PnL for ${symbol} ${orderType}: $${realizedPnl.toFixed(2)}`); } - // Close tranche if tranche management is enabled - const symbolConfig = this.config.symbols[symbol]; - if (symbolConfig?.enableTrancheManagement) { - // Use async IIFE to handle await properly - (async () => { - try { - const trancheManager = getTrancheManager(); - - // Find position side from the position that was closed - let positionSideForTranche: 'LONG' | 'SHORT' | 'BOTH' = 'BOTH'; - for (const [key] of this.positionOrders.entries()) { - if (key.includes(symbol)) { - const position = this.currentPositions.get(key); - if (position) { - positionSideForTranche = position.positionSide as any; - break; - } - } - } - - // Process the order fill and close appropriate tranches - await trancheManager.processOrderFill({ - symbol, - side, // The order side (BUY or SELL) - positionSide: positionSideForTranche, - quantityFilled: executedQty, - fillPrice: avgPrice, - realizedPnl, - orderId: orderId.toString(), - }); - - const trancheSide = side === 'BUY' ? 'SHORT' : 'LONG'; -logWithTimestamp(`PositionManager: Processed tranche close for ${symbol} ${trancheSide}, PnL: $${realizedPnl.toFixed(2)}`); - } catch (trancheError) { -logErrorWithTimestamp('PositionManager: Failed to process tranche close:', trancheError); - // Don't fail the position close, just log the error - } - })(); - } - // Broadcast order filled event (SL/TP) if (this.statusBroadcaster) { this.statusBroadcaster.broadcastOrderFilled({ @@ -1338,30 +1214,36 @@ logWithTimestamp(`PositionManager: Position ${key} is closed, removing order tra } // Listen for new positions from Hunter - public async onNewPosition(data: { symbol: string; side: string; quantity: number; orderId?: number; paperMode?: boolean }): Promise { + public onNewPosition(data: { symbol: string; side: string; quantity: number; orderId?: number }): void { // In the new architecture, we wait for ACCOUNT_UPDATE to confirm the position // The WebSocket will tell us when the position is actually open logWithTimestamp(`PositionManager: Notified of potential new position: ${data.symbol} ${data.side}`); - // For paper mode, use the paper mode simulator - if (this.config.global.paperMode || data.paperMode) { - const symbolConfig = this.config.symbols[data.symbol]; - if (!symbolConfig) { -logErrorWithTimestamp(`PositionManager: Cannot open paper mode position - ${data.symbol} not in config`); - return; - } - -logWithTimestamp(`PositionManager: Opening paper mode position for ${data.symbol} ${data.side}`); + // For paper mode, simulate the position + if (this.config.global.paperMode) { + // Use the proper position side based on hedge mode + const positionSide = this.isHedgeMode ? + (data.side === 'BUY' ? 'LONG' : 'SHORT') : 'BOTH'; + const key = `${data.symbol}_${positionSide}`; - // Open simulated position with proper SL/TP - await paperModeSimulator.openPosition({ + // Simulate the position in our map + this.currentPositions.set(key, { symbol: data.symbol, - side: data.side as 'BUY' | 'SELL', - quantity: data.quantity, - leverage: symbolConfig.leverage || 10, - slPercent: symbolConfig.slPercent || 2, - tpPercent: symbolConfig.tpPercent || 5, + positionAmt: data.side === 'BUY' ? data.quantity.toString() : (-data.quantity).toString(), + entryPrice: '0', // Will be updated by market price + markPrice: '0', + unRealizedProfit: '0', + liquidationPrice: '0', + leverage: this.config.symbols[data.symbol]?.leverage?.toString() || '10', + marginType: 'isolated', + isolatedMargin: '0', + isAutoAddMargin: 'false', + positionSide: positionSide, + updateTime: Date.now() }); + + // Place SL/TP for paper mode + this.ensurePositionProtected(data.symbol, positionSide, data.side === 'BUY' ? data.quantity : -data.quantity); } } @@ -2852,65 +2734,4 @@ logErrorWithTimestamp('PositionManager: Failed to refresh balance:', error); public getPositionsMap(): Map { return this.currentPositions; } - - // Close all open positions (used by bot stop command) - public async closeAllPositions(): Promise { - const positions = this.getPositions().filter(p => Math.abs(parseFloat(p.positionAmt)) > 0); - - if (positions.length === 0) { -logWithTimestamp('PositionManager: No positions to close'); - return; - } - -logWithTimestamp(`PositionManager: Closing ${positions.length} position(s)...`); - - for (const position of positions) { - try { - const symbol = position.symbol; - const positionAmt = parseFloat(position.positionAmt); - const side = positionAmt > 0 ? 'SELL' : 'BUY'; - const quantity = Math.abs(positionAmt); - - // Cancel any open orders for this position - try { - const openOrders = await this.getOpenOrdersFromExchange(); - const ordersForSymbol = openOrders.filter(o => o.symbol === symbol); - - for (const order of ordersForSymbol) { - await this.cancelOrderById(symbol, order.orderId); -logWithTimestamp(`PositionManager: Cancelled order ${order.orderId} for ${symbol}`); - } - } catch (error) { -logErrorWithTimestamp(`PositionManager: Failed to cancel orders for ${symbol}:`, error); - } - - // Close position with market order - const positionSide = position.positionSide === 'LONG' ? 'LONG' : position.positionSide === 'SHORT' ? 'SHORT' : 'BOTH'; - - await placeOrder({ - symbol, - side, - type: 'MARKET', - quantity, - positionSide, - reduceOnly: true - }, this.config.api); - -logWithTimestamp(`PositionManager: Closed position ${symbol} ${positionSide} - ${quantity} @ MARKET`); - - if (this.statusBroadcaster) { - this.statusBroadcaster.broadcastPositionClosed({ - symbol, - side: positionSide, - quantity, - reason: 'Bot stopped - position closed by user' - }); - } - } catch (error) { -logErrorWithTimestamp(`PositionManager: Failed to close position ${position.symbol}:`, error); - } - } - -logWithTimestamp('PositionManager: Finished closing all positions'); - } } diff --git a/src/lib/config/types.ts b/src/lib/config/types.ts index 265395c..c05e451 100644 --- a/src/lib/config/types.ts +++ b/src/lib/config/types.ts @@ -30,11 +30,6 @@ export const symbolConfigSchema = z.object({ // Threshold system settings useThreshold: z.boolean().optional(), - thresholdTimeWindow: z.number().min(10000).optional(), // Minimum 10 seconds - thresholdCooldown: z.number().min(10000).optional(), // Minimum 10 seconds - - // Order execution settings - forceMarketEntry: z.boolean().optional(), }).refine(data => { // Ensure we have either legacy or new volume thresholds return data.volumeThresholdUSDT !== undefined || diff --git a/src/lib/db/initDb.ts b/src/lib/db/initDb.ts index f1feb35..7b7ffe6 100644 --- a/src/lib/db/initDb.ts +++ b/src/lib/db/initDb.ts @@ -1,5 +1,4 @@ import { db } from './database'; -import { initTrancheTables } from './trancheDb'; let initialized = false; @@ -7,8 +6,6 @@ export async function ensureDbInitialized(): Promise { if (!initialized) { try { await db.initialize(); - // Initialize tranche tables - await initTrancheTables(); initialized = true; } catch (error) { console.error('Failed to initialize database:', error); diff --git a/src/lib/db/trancheDb.ts b/src/lib/db/trancheDb.ts deleted file mode 100644 index f9623ae..0000000 --- a/src/lib/db/trancheDb.ts +++ /dev/null @@ -1,457 +0,0 @@ -import { db } from './database'; -import { Tranche, TrancheEvent } from '../types'; - -// Initialize tranche tables -export async function initTrancheTables(): Promise { - // Tranches table - await db.run(` - CREATE TABLE IF NOT EXISTS tranches ( - -- Identity - id TEXT PRIMARY KEY, - symbol TEXT NOT NULL, - side TEXT NOT NULL, - position_side TEXT NOT NULL, - - -- Entry details - entry_price REAL NOT NULL, - quantity REAL NOT NULL, - margin_used REAL NOT NULL, - leverage INTEGER NOT NULL, - entry_time INTEGER NOT NULL, - entry_order_id TEXT, - - -- Exit details - exit_price REAL, - exit_time INTEGER, - exit_order_id TEXT, - - -- P&L tracking - unrealized_pnl REAL DEFAULT 0, - realized_pnl REAL DEFAULT 0, - - -- Risk management - tp_percent REAL NOT NULL, - sl_percent REAL NOT NULL, - tp_price REAL NOT NULL, - sl_price REAL NOT NULL, - - -- Status - status TEXT DEFAULT 'active', - isolated INTEGER DEFAULT 0, - isolation_time INTEGER, - isolation_price REAL, - - -- Metadata - notes TEXT, - created_at INTEGER DEFAULT (strftime('%s', 'now')), - updated_at INTEGER DEFAULT (strftime('%s', 'now')) - ) - `); - - // Indexes for performance - await db.run(` - CREATE INDEX IF NOT EXISTS idx_tranches_symbol_side_status - ON tranches(symbol, side, status) - `); - - await db.run(` - CREATE INDEX IF NOT EXISTS idx_tranches_status - ON tranches(status) - `); - - await db.run(` - CREATE INDEX IF NOT EXISTS idx_tranches_entry_time - ON tranches(entry_time DESC) - `); - - await db.run(` - CREATE INDEX IF NOT EXISTS idx_tranches_isolated - ON tranches(isolated, status) - `); - - // Tranche events table (audit trail) - await db.run(` - CREATE TABLE IF NOT EXISTS tranche_events ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - tranche_id TEXT NOT NULL, - event_type TEXT NOT NULL, - event_time INTEGER NOT NULL, - - -- Event details - price REAL, - quantity REAL, - pnl REAL, - - -- Context - trigger TEXT, - metadata TEXT, - - FOREIGN KEY (tranche_id) REFERENCES tranches(id) - ) - `); - - await db.run(` - CREATE INDEX IF NOT EXISTS idx_tranche_events_tranche_id - ON tranche_events(tranche_id) - `); - - await db.run(` - CREATE INDEX IF NOT EXISTS idx_tranche_events_time - ON tranche_events(event_time DESC) - `); -} - -// Helper to convert DB row to Tranche object -function rowToTranche(row: any): Tranche { - return { - id: row.id, - symbol: row.symbol, - side: row.side as 'LONG' | 'SHORT', - positionSide: row.position_side as 'LONG' | 'SHORT' | 'BOTH', - entryPrice: row.entry_price, - quantity: row.quantity, - marginUsed: row.margin_used, - leverage: row.leverage, - entryTime: row.entry_time, - entryOrderId: row.entry_order_id || undefined, - exitPrice: row.exit_price || undefined, - exitTime: row.exit_time || undefined, - exitOrderId: row.exit_order_id || undefined, - unrealizedPnl: row.unrealized_pnl, - realizedPnl: row.realized_pnl, - tpPercent: row.tp_percent, - slPercent: row.sl_percent, - tpPrice: row.tp_price, - slPrice: row.sl_price, - status: row.status as 'active' | 'closed' | 'liquidated', - isolated: Boolean(row.isolated), - isolationTime: row.isolation_time || undefined, - isolationPrice: row.isolation_price || undefined, - notes: row.notes || undefined, - }; -} - -// Create a new tranche -export async function createTranche(tranche: Tranche): Promise { - await db.run( - ` - INSERT INTO tranches ( - id, symbol, side, position_side, - entry_price, quantity, margin_used, leverage, entry_time, entry_order_id, - exit_price, exit_time, exit_order_id, - unrealized_pnl, realized_pnl, - tp_percent, sl_percent, tp_price, sl_price, - status, isolated, isolation_time, isolation_price, - notes - ) VALUES ( - ?, ?, ?, ?, - ?, ?, ?, ?, ?, ?, - ?, ?, ?, - ?, ?, - ?, ?, ?, ?, - ?, ?, ?, ?, - ? - ) - `, - [ - tranche.id, - tranche.symbol, - tranche.side, - tranche.positionSide, - tranche.entryPrice, - tranche.quantity, - tranche.marginUsed, - tranche.leverage, - tranche.entryTime, - tranche.entryOrderId || null, - tranche.exitPrice || null, - tranche.exitTime || null, - tranche.exitOrderId || null, - tranche.unrealizedPnl, - tranche.realizedPnl, - tranche.tpPercent, - tranche.slPercent, - tranche.tpPrice, - tranche.slPrice, - tranche.status, - tranche.isolated ? 1 : 0, - tranche.isolationTime || null, - tranche.isolationPrice || null, - tranche.notes || null, - ] - ); -} - -// Get a single tranche by ID -export async function getTranche(id: string): Promise { - const row = await db.get('SELECT * FROM tranches WHERE id = ?', [id]); - return row ? rowToTranche(row) : null; -} - -// Get all active tranches for a symbol and side -export async function getActiveTranches(symbol: string, side: string): Promise { - const rows = await db.all( - ` - SELECT * FROM tranches - WHERE symbol = ? AND side = ? AND status = 'active' - ORDER BY entry_time ASC - `, - [symbol, side] - ); - - return rows.map(rowToTranche); -} - -// Get all isolated tranches for a symbol and side -export async function getIsolatedTranches(symbol: string, side: string): Promise { - const rows = await db.all( - ` - SELECT * FROM tranches - WHERE symbol = ? AND side = ? AND status = 'active' AND isolated = 1 - ORDER BY isolation_time ASC - `, - [symbol, side] - ); - - return rows.map(rowToTranche); -} - -// Get all tranches (active and closed) for a symbol -export async function getAllTranchesForSymbol(symbol: string): Promise { - const rows = await db.all( - ` - SELECT * FROM tranches - WHERE symbol = ? - ORDER BY entry_time DESC - `, - [symbol] - ); - - return rows.map(rowToTranche); -} - -// Update a tranche -export async function updateTranche(id: string, updates: Partial): Promise { - const fields: string[] = []; - const values: any[] = []; - - // Build dynamic UPDATE statement - if (updates.quantity !== undefined) { - fields.push('quantity = ?'); - values.push(updates.quantity); - } - if (updates.marginUsed !== undefined) { - fields.push('margin_used = ?'); - values.push(updates.marginUsed); - } - if (updates.unrealizedPnl !== undefined) { - fields.push('unrealized_pnl = ?'); - values.push(updates.unrealizedPnl); - } - if (updates.realizedPnl !== undefined) { - fields.push('realized_pnl = ?'); - values.push(updates.realizedPnl); - } - if (updates.exitPrice !== undefined) { - fields.push('exit_price = ?'); - values.push(updates.exitPrice); - } - if (updates.exitTime !== undefined) { - fields.push('exit_time = ?'); - values.push(updates.exitTime); - } - if (updates.exitOrderId !== undefined) { - fields.push('exit_order_id = ?'); - values.push(updates.exitOrderId); - } - if (updates.status !== undefined) { - fields.push('status = ?'); - values.push(updates.status); - } - if (updates.isolated !== undefined) { - fields.push('isolated = ?'); - values.push(updates.isolated ? 1 : 0); - } - if (updates.isolationTime !== undefined) { - fields.push('isolation_time = ?'); - values.push(updates.isolationTime); - } - if (updates.isolationPrice !== undefined) { - fields.push('isolation_price = ?'); - values.push(updates.isolationPrice); - } - if (updates.notes !== undefined) { - fields.push('notes = ?'); - values.push(updates.notes); - } - - if (fields.length === 0) return; // No updates - - // Always update timestamp - fields.push('updated_at = strftime("%s", "now")'); - - values.push(id); // Add ID for WHERE clause - - const sql = `UPDATE tranches SET ${fields.join(', ')} WHERE id = ?`; - await db.run(sql, values); -} - -// Update unrealized P&L for a tranche (fast path for frequent updates) -export async function updateTrancheUnrealizedPnl(id: string, pnl: number): Promise { - await db.run( - ` - UPDATE tranches - SET unrealized_pnl = ?, updated_at = strftime('%s', 'now') - WHERE id = ? - `, - [pnl, id] - ); -} - -// Isolate a tranche -export async function isolateTranche(id: string, price: number): Promise { - await db.run( - ` - UPDATE tranches - SET isolated = 1, isolation_time = ?, isolation_price = ?, updated_at = strftime('%s', 'now') - WHERE id = ? - `, - [Date.now(), price, id] - ); -} - -// Close a tranche -export async function closeTranche( - id: string, - exitPrice: number, - realizedPnl: number, - orderId?: string -): Promise { - await db.run( - ` - UPDATE tranches - SET status = 'closed', exit_price = ?, exit_time = ?, exit_order_id = ?, - realized_pnl = ?, updated_at = strftime('%s', 'now') - WHERE id = ? - `, - [exitPrice, Date.now(), orderId || null, realizedPnl, id] - ); -} - -// Liquidate a tranche -export async function liquidateTranche(id: string, liquidationPrice: number): Promise { - await db.run( - ` - UPDATE tranches - SET status = 'liquidated', exit_price = ?, exit_time = ?, updated_at = strftime('%s', 'now') - WHERE id = ? - `, - [liquidationPrice, Date.now(), id] - ); -} - -// Log a tranche event -export async function logTrancheEvent( - trancheId: string, - eventType: 'created' | 'isolated' | 'closed' | 'liquidated' | 'updated', - data: { - price?: number; - quantity?: number; - pnl?: number; - trigger?: string; - metadata?: any; - } -): Promise { - await db.run( - ` - INSERT INTO tranche_events ( - tranche_id, event_type, event_time, price, quantity, pnl, trigger, metadata - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) - `, - [ - trancheId, - eventType, - Date.now(), - data.price || null, - data.quantity || null, - data.pnl || null, - data.trigger || null, - data.metadata ? JSON.stringify(data.metadata) : null, - ] - ); -} - -// Get event history for a tranche -export async function getTrancheHistory(trancheId: string): Promise { - const rows = await db.all( - ` - SELECT * FROM tranche_events - WHERE tranche_id = ? - ORDER BY event_time DESC - `, - [trancheId] - ); - - return rows.map((row) => ({ - id: row.id, - trancheId: row.tranche_id, - eventType: row.event_type, - eventTime: row.event_time, - price: row.price || undefined, - quantity: row.quantity || undefined, - pnl: row.pnl || undefined, - trigger: row.trigger || undefined, - metadata: row.metadata || undefined, - })); -} - -// Clean up old closed tranches -export async function cleanupOldTranches(daysToKeep: number = 30): Promise { - const cutoffTime = Date.now() - daysToKeep * 24 * 60 * 60 * 1000; - - await db.run( - ` - DELETE FROM tranches - WHERE status IN ('closed', 'liquidated') AND exit_time < ? - `, - [cutoffTime] - ); - - // Return approximate count (sqlite3 doesn't support RETURNING) - const result = await db.get<{ count: number }>( - ` - SELECT COUNT(*) as count FROM tranches - WHERE status IN ('closed', 'liquidated') AND exit_time < ? - `, - [cutoffTime] - ); - - return result?.count || 0; -} - -// Get statistics -export async function getTrancheStats(): Promise<{ - totalActive: number; - totalIsolated: number; - totalClosed: number; - totalLiquidated: number; - totalPnl: number; -}> { - const row = await db.get(` - SELECT - SUM(CASE WHEN status = 'active' AND isolated = 0 THEN 1 ELSE 0 END) as active, - SUM(CASE WHEN status = 'active' AND isolated = 1 THEN 1 ELSE 0 END) as isolated, - SUM(CASE WHEN status = 'closed' THEN 1 ELSE 0 END) as closed, - SUM(CASE WHEN status = 'liquidated' THEN 1 ELSE 0 END) as liquidated, - SUM(CASE WHEN status IN ('closed', 'liquidated') THEN realized_pnl ELSE 0 END) as total_pnl - FROM tranches - `); - - return { - totalActive: row?.active || 0, - totalIsolated: row?.isolated || 0, - totalClosed: row?.closed || 0, - totalLiquidated: row?.liquidated || 0, - totalPnl: row?.total_pnl || 0, - }; -} diff --git a/src/lib/services/paperModeSimulator.ts b/src/lib/services/paperModeSimulator.ts deleted file mode 100644 index 2e9f116..0000000 --- a/src/lib/services/paperModeSimulator.ts +++ /dev/null @@ -1,335 +0,0 @@ -import { EventEmitter } from 'events'; -import { Config } from '../types'; -import { getMarkPrice } from '../api/market'; -import { logWithTimestamp, logErrorWithTimestamp } from '../utils/timestamp'; - -/** - * Paper Mode Position Simulator - * - * Simulates the full position lifecycle in paper mode: - * - Tracks simulated positions with real market prices - * - Monitors SL/TP triggers based on actual market data - * - Calculates realistic P&L - * - Broadcasts events to UI for real-time updates - * - * This service runs ONLY in paper mode and does not affect live trading. - */ - -interface SimulatedPosition { - symbol: string; - side: 'LONG' | 'SHORT'; - quantity: number; - entryPrice: number; - leverage: number; - slPrice: number; - tpPrice: number; - openTime: number; - lastPnL: number; - lastMarkPrice: number; -} - -export class PaperModeSimulator extends EventEmitter { - private positions: Map = new Map(); - private config: Config | null = null; - private monitorInterval: NodeJS.Timeout | null = null; - private isRunning = false; - - constructor() { - super(); - } - - /** - * Initialize the paper mode simulator with config - */ - public initialize(config: Config): void { - this.config = config; - logWithTimestamp('PaperModeSimulator: Initialized'); - } - - /** - * Update configuration - */ - public updateConfig(config: Config): void { - this.config = config; - logWithTimestamp('PaperModeSimulator: Configuration updated'); - } - - /** - * Start monitoring simulated positions - */ - public start(): void { - if (this.isRunning) return; - if (!this.config) { - logErrorWithTimestamp('PaperModeSimulator: Cannot start - no config loaded'); - return; - } - - this.isRunning = true; - logWithTimestamp('PaperModeSimulator: Starting position monitoring...'); - - // Monitor positions every 5 seconds - this.monitorInterval = setInterval(() => { - this.monitorPositions(); - }, 5000); - - logWithTimestamp('PaperModeSimulator: Monitoring active (checking every 5s)'); - } - - /** - * Stop monitoring - */ - public stop(): void { - if (!this.isRunning) return; - - this.isRunning = false; - - if (this.monitorInterval) { - clearInterval(this.monitorInterval); - this.monitorInterval = null; - } - - logWithTimestamp('PaperModeSimulator: Stopped'); - } - - /** - * Open a new simulated position - */ - public async openPosition(data: { - symbol: string; - side: 'BUY' | 'SELL'; - quantity: number; - leverage: number; - slPercent: number; - tpPercent: number; - }): Promise { - try { - // Fetch current market price for accurate entry - const markPriceData = await getMarkPrice(data.symbol); - const entryPrice = parseFloat( - Array.isArray(markPriceData) ? markPriceData[0].markPrice : markPriceData.markPrice - ); - - const isLong = data.side === 'BUY'; - const positionSide = isLong ? 'LONG' : 'SHORT'; - - // Calculate SL and TP prices - const slPrice = isLong - ? entryPrice * (1 - data.slPercent / 100) - : entryPrice * (1 + data.slPercent / 100); - - const tpPrice = isLong - ? entryPrice * (1 + data.tpPercent / 100) - : entryPrice * (1 - data.tpPercent / 100); - - const position: SimulatedPosition = { - symbol: data.symbol, - side: positionSide, - quantity: data.quantity, - entryPrice, - leverage: data.leverage, - slPrice, - tpPrice, - openTime: Date.now(), - lastPnL: 0, - lastMarkPrice: entryPrice, - }; - - const key = `${data.symbol}_${positionSide}`; - this.positions.set(key, position); - - logWithTimestamp( - `PaperModeSimulator: Opened ${positionSide} position for ${data.symbol} ` + - `at $${entryPrice.toFixed(2)} (SL: $${slPrice.toFixed(2)}, TP: $${tpPrice.toFixed(2)})` - ); - - // Emit position opened event - this.emit('positionOpened', { - symbol: data.symbol, - side: positionSide, - quantity: data.quantity, - entryPrice, - slPrice, - tpPrice, - leverage: data.leverage, - }); - } catch (error) { - logErrorWithTimestamp(`PaperModeSimulator: Failed to open position for ${data.symbol}:`, error); - } - } - - /** - * Close a simulated position - */ - public async closePosition(symbol: string, side: 'LONG' | 'SHORT', reason: string = 'Manual close'): Promise { - const key = `${symbol}_${side}`; - const position = this.positions.get(key); - - if (!position) { - logErrorWithTimestamp(`PaperModeSimulator: No position found for ${symbol} ${side}`); - return false; - } - - try { - // Fetch current market price for accurate exit - const markPriceData = await getMarkPrice(symbol); - const exitPrice = parseFloat( - Array.isArray(markPriceData) ? markPriceData[0].markPrice : markPriceData.markPrice - ); - - // Calculate final P&L - const isLong = side === 'LONG'; - const pnlPercent = isLong - ? ((exitPrice - position.entryPrice) / position.entryPrice) * 100 - : ((position.entryPrice - exitPrice) / position.entryPrice) * 100; - - const pnlUSDT = (pnlPercent / 100) * position.quantity * position.entryPrice * position.leverage; - const holdTime = Date.now() - position.openTime; - - logWithTimestamp( - `PaperModeSimulator: Closed ${side} position for ${symbol} ` + - `at $${exitPrice.toFixed(2)} (Entry: $${position.entryPrice.toFixed(2)}) ` + - `P&L: ${pnlPercent.toFixed(2)}% ($${pnlUSDT.toFixed(2)} USDT) ` + - `Hold: ${(holdTime / 1000).toFixed(0)}s - ${reason}` - ); - - // Emit position closed event - this.emit('positionClosed', { - symbol, - side, - entryPrice: position.entryPrice, - exitPrice, - pnlPercent, - pnlUSDT, - holdTime, - reason, - }); - - // Remove position - this.positions.delete(key); - return true; - } catch (error) { - logErrorWithTimestamp(`PaperModeSimulator: Failed to close position ${symbol} ${side}:`, error); - return false; - } - } - - /** - * Monitor all open positions and check SL/TP triggers - */ - private async monitorPositions(): Promise { - if (this.positions.size === 0) return; - - for (const [key, position] of this.positions.entries()) { - try { - // Fetch current market price - const markPriceData = await getMarkPrice(position.symbol); - const markPrice = parseFloat( - Array.isArray(markPriceData) ? markPriceData[0].markPrice : markPriceData.markPrice - ); - - position.lastMarkPrice = markPrice; - - // Calculate current P&L - const isLong = position.side === 'LONG'; - const pnlPercent = isLong - ? ((markPrice - position.entryPrice) / position.entryPrice) * 100 - : ((position.entryPrice - markPrice) / position.entryPrice) * 100; - - const pnlUSDT = (pnlPercent / 100) * position.quantity * position.entryPrice * position.leverage; - - // Only log if P&L changed significantly (> 0.1%) - if (Math.abs(pnlPercent - position.lastPnL) > 0.1) { - logWithTimestamp( - `PaperModeSimulator: ${position.symbol} ${position.side} @ $${markPrice.toFixed(2)} ` + - `P&L: ${pnlPercent.toFixed(2)}% ($${pnlUSDT.toFixed(2)} USDT)` - ); - position.lastPnL = pnlPercent; - } - - // Emit P&L update for UI - this.emit('pnlUpdate', { - symbol: position.symbol, - side: position.side, - markPrice, - pnlPercent, - pnlUSDT, - }); - - // Check SL trigger - const slTriggered = isLong - ? markPrice <= position.slPrice - : markPrice >= position.slPrice; - - if (slTriggered) { - logWithTimestamp( - `PaperModeSimulator: ๐Ÿ›‘ STOP LOSS triggered for ${position.symbol} ${position.side} ` + - `at $${markPrice.toFixed(2)} (SL: $${position.slPrice.toFixed(2)})` - ); - await this.closePosition(position.symbol, position.side, 'Stop Loss triggered'); - continue; - } - - // Check TP trigger - const tpTriggered = isLong - ? markPrice >= position.tpPrice - : markPrice <= position.tpPrice; - - if (tpTriggered) { - logWithTimestamp( - `PaperModeSimulator: ๐ŸŽฏ TAKE PROFIT triggered for ${position.symbol} ${position.side} ` + - `at $${markPrice.toFixed(2)} (TP: $${position.tpPrice.toFixed(2)})` - ); - await this.closePosition(position.symbol, position.side, 'Take Profit triggered'); - continue; - } - } catch (error) { - logErrorWithTimestamp(`PaperModeSimulator: Error monitoring ${key}:`, error); - } - } - } - - /** - * Get all open positions - */ - public getPositions(): SimulatedPosition[] { - return Array.from(this.positions.values()); - } - - /** - * Get specific position - */ - public getPosition(symbol: string, side: 'LONG' | 'SHORT'): SimulatedPosition | undefined { - return this.positions.get(`${symbol}_${side}`); - } - - /** - * Check if position exists - */ - public hasPosition(symbol: string, side: 'LONG' | 'SHORT'): boolean { - return this.positions.has(`${symbol}_${side}`); - } - - /** - * Get position count - */ - public getPositionCount(): number { - return this.positions.size; - } - - /** - * Close all positions - */ - public async closeAllPositions(): Promise { - logWithTimestamp(`PaperModeSimulator: Closing all ${this.positions.size} position(s)...`); - - const positions = Array.from(this.positions.values()); - for (const position of positions) { - await this.closePosition(position.symbol, position.side, 'Close all requested'); - } - - logWithTimestamp('PaperModeSimulator: All positions closed'); - } -} - -// Export singleton instance -export const paperModeSimulator = new PaperModeSimulator(); diff --git a/src/lib/services/pnlService.ts b/src/lib/services/pnlService.ts index af1a953..1da51cf 100644 --- a/src/lib/services/pnlService.ts +++ b/src/lib/services/pnlService.ts @@ -121,22 +121,23 @@ class PnLService extends EventEmitter { }); } - // Initialize starting accumulated PnL on first update (even if zero) - if (this.lastUpdateTime === 0) { + // Initialize starting accumulated PnL on first update + if (this.sessionPnL.startingAccumulatedPnl === 0 && totalAccumulatedPnl !== 0) { this.sessionPnL.startingAccumulatedPnl = totalAccumulatedPnl; } - // Update session PnL tracking + // Update session PnL const _previousAccumulated = this.sessionPnL.currentAccumulatedPnl; this.sessionPnL.currentAccumulatedPnl = totalAccumulatedPnl; this.sessionPnL.unrealizedPnl = totalUnrealizedPnl; - // Note: Session realized PnL is now accumulated from individual trades in updateFromOrderEvent - // using the 'rp' field which gives accurate per-trade realized profit - // We keep the accumulated PnL fields for reference but don't use them for session tracking - + // Session realized PnL is the difference from starting point + this.sessionPnL.realizedPnl = totalAccumulatedPnl - this.sessionPnL.startingAccumulatedPnl; this.sessionPnL.totalPnl = this.sessionPnL.realizedPnl + totalUnrealizedPnl; + // Trade counting is now handled in updateFromOrderEvent via the rp field + // We only track accumulated PnL changes here for verification + // Update drawdown const currentValue = this.sessionPnL.currentBalance + this.sessionPnL.unrealizedPnl; if (currentValue > this.sessionPnL.peak) { @@ -192,9 +193,6 @@ class PnLService extends EventEmitter { // Count closing trades (reduce-only or trades with realized PnL) const isReduceOnly = order.R === true || order.R === 'true'; if (isReduceOnly || realizedProfit !== 0) { - // Accumulate realized PnL for the session - this.sessionPnL.realizedPnl += realizedProfit; - this.sessionPnL.tradeCount++; // Track win/loss based on realized profit diff --git a/src/lib/services/trancheManager.ts b/src/lib/services/trancheManager.ts deleted file mode 100644 index 4912aad..0000000 --- a/src/lib/services/trancheManager.ts +++ /dev/null @@ -1,846 +0,0 @@ -import { EventEmitter } from 'events'; -import { v4 as uuidv4 } from 'uuid'; -import { Config, Tranche, TrancheGroup, TrancheStrategy } from '../types'; -import { getMarkPrice } from '../api/market'; -import { logWithTimestamp, logWarnWithTimestamp, logErrorWithTimestamp } from '../utils/timestamp'; -import { - createTranche as dbCreateTranche, - getTranche, - getActiveTranches, - getIsolatedTranches, - updateTranche, - updateTrancheUnrealizedPnl, - isolateTranche as dbIsolateTranche, - closeTranche as dbCloseTranche, - liquidateTranche as dbLiquidateTranche, - logTrancheEvent, -} from '../db/trancheDb'; - -// Exchange position interface (from positionManager) -interface ExchangePosition { - symbol: string; - positionAmt: string; - entryPrice: string; - markPrice: string; - unRealizedProfit: string; - liquidationPrice: string; - leverage: string; - marginType: string; - isolatedMargin: string; - isAutoAddMargin: string; - positionSide: string; - updateTime: number; -} - -export class TrancheManagerService extends EventEmitter { - private trancheGroups: Map = new Map(); // key: "BTCUSDT_LONG" - private config: Config; - private priceService: any; // For real-time price updates - private isolationCheckInterval?: NodeJS.Timeout; - private isInitialized = false; - - constructor(config: Config) { - super(); - this.config = config; - } - - // Initialize from database on startup - public async initialize(): Promise { - if (this.isInitialized) return; - - logWithTimestamp('TrancheManager: Initializing...'); - - try { - // Load all active tranches from database - const symbols = Object.keys(this.config.symbols); - - for (const symbol of symbols) { - const symbolConfig = this.config.symbols[symbol]; - if (!symbolConfig.enableTrancheManagement) continue; - - // Load LONG tranches - const longTranches = await getActiveTranches(symbol, 'LONG'); - if (longTranches.length > 0) { - const groupKey = this.getGroupKey(symbol, 'LONG'); - const group = this.createTrancheGroup(symbol, 'LONG', longTranches[0].positionSide); - group.tranches = longTranches; - group.activeTranches = longTranches.filter(t => !t.isolated); - group.isolatedTranches = longTranches.filter(t => t.isolated); - this.recalculateGroupMetrics(group); - this.trancheGroups.set(groupKey, group); - logWithTimestamp(`TrancheManager: Loaded ${longTranches.length} LONG tranches for ${symbol}`); - } - - // Load SHORT tranches - const shortTranches = await getActiveTranches(symbol, 'SHORT'); - if (shortTranches.length > 0) { - const groupKey = this.getGroupKey(symbol, 'SHORT'); - const group = this.createTrancheGroup(symbol, 'SHORT', shortTranches[0].positionSide); - group.tranches = shortTranches; - group.activeTranches = shortTranches.filter(t => !t.isolated); - group.isolatedTranches = shortTranches.filter(t => t.isolated); - this.recalculateGroupMetrics(group); - this.trancheGroups.set(groupKey, group); - logWithTimestamp(`TrancheManager: Loaded ${shortTranches.length} SHORT tranches for ${symbol}`); - } - } - - this.isInitialized = true; - logWithTimestamp(`TrancheManager: Initialized with ${this.trancheGroups.size} tranche groups`); - } catch (error) { - logErrorWithTimestamp('TrancheManager: Initialization failed:', error); - throw error; - } - } - - // Check if tranche management is enabled for a symbol - public isEnabled(symbol: string): boolean { - return this.config.symbols[symbol]?.enableTrancheManagement === true; - } - - // Update configuration - public updateConfig(newConfig: Config): void { - this.config = newConfig; - } - - // Create a new tranche when opening a position - public async createTranche(params: { - symbol: string; - side: 'BUY' | 'SELL'; // Order side - positionSide: 'LONG' | 'SHORT' | 'BOTH'; - entryPrice: number; - quantity: number; - marginUsed: number; - leverage: number; - orderId?: string; - }): Promise { - const symbolConfig = this.config.symbols[params.symbol]; - if (!symbolConfig) { - throw new Error(`Symbol ${params.symbol} not found in config`); - } - - const trancheSide = params.side === 'BUY' ? 'LONG' : 'SHORT'; - - // Calculate TP/SL prices - const tpPrice = this.calculateTpPrice(params.entryPrice, symbolConfig.tpPercent, trancheSide); - const slPrice = this.calculateSlPrice(params.entryPrice, symbolConfig.slPercent, trancheSide); - - const tranche: Tranche = { - id: uuidv4(), - symbol: params.symbol, - side: trancheSide, - positionSide: params.positionSide, - entryPrice: params.entryPrice, - quantity: params.quantity, - marginUsed: params.marginUsed, - leverage: params.leverage, - entryTime: Date.now(), - entryOrderId: params.orderId, - unrealizedPnl: 0, - realizedPnl: 0, - tpPercent: symbolConfig.tpPercent, - slPercent: symbolConfig.slPercent, - tpPrice, - slPrice, - status: 'active', - isolated: false, - }; - - // Save to database - await dbCreateTranche(tranche); - - // Add to in-memory tracking - const groupKey = this.getGroupKey(params.symbol, trancheSide); - let group = this.trancheGroups.get(groupKey); - if (!group) { - group = this.createTrancheGroup(params.symbol, trancheSide, params.positionSide); - this.trancheGroups.set(groupKey, group); - } - - group.tranches.push(tranche); - group.activeTranches.push(tranche); - this.recalculateGroupMetrics(group); - - // Log event - await logTrancheEvent(tranche.id, 'created', { - price: params.entryPrice, - quantity: params.quantity, - trigger: params.orderId, - }); - - // Emit event - this.emit('trancheCreated', tranche); - - logWithTimestamp(`TrancheManager: Created tranche ${tranche.id.substring(0, 8)} for ${params.symbol} ${trancheSide} @ ${params.entryPrice}`); - - return tranche; - } - - // Check if a tranche should be isolated (P&L < threshold) - public shouldIsolateTranche(tranche: Tranche, currentPrice: number): boolean { - if (tranche.isolated || tranche.status !== 'active') { - return false; - } - - const symbolConfig = this.config.symbols[tranche.symbol]; - if (!symbolConfig) return false; - - const threshold = symbolConfig.trancheIsolationThreshold || 5; - - // Calculate unrealized P&L % - const pnlPercent = this.calculatePnlPercent( - tranche.entryPrice, - currentPrice, - tranche.side - ); - - return pnlPercent <= -threshold; // Negative = loss - } - - // Isolate a tranche (mark as underwater) - public async isolateTranche(trancheId: string, currentPrice?: number): Promise { - const tranche = await getTranche(trancheId); - if (!tranche || tranche.isolated) return; - - const price = currentPrice || (await this.getCurrentPrice(tranche.symbol)); - - await dbIsolateTranche(trancheId, price); - - // Update in-memory - tranche.isolated = true; - tranche.isolationTime = Date.now(); - tranche.isolationPrice = price; - - const groupKey = this.getGroupKey(tranche.symbol, tranche.side); - const group = this.trancheGroups.get(groupKey); - if (group) { - // Move from active to isolated - group.activeTranches = group.activeTranches.filter((t) => t.id !== trancheId); - group.isolatedTranches.push(tranche); - this.recalculateGroupMetrics(group); - } - - // Log event - await logTrancheEvent(trancheId, 'isolated', { - price, - pnl: tranche.unrealizedPnl, - trigger: 'isolation_threshold', - }); - - // Emit event - this.emit('trancheIsolated', tranche); - - const pnlPercent = this.calculatePnlPercent(tranche.entryPrice, price, tranche.side); - logWithTimestamp( - `TrancheManager: Isolated tranche ${trancheId.substring(0, 8)} for ${tranche.symbol} at ${price} (${pnlPercent.toFixed(2)}% P&L)` - ); - } - - // Monitor all active tranches and isolate if needed - public async checkIsolationConditions(): Promise { - for (const [_key, group] of this.trancheGroups) { - if (group.activeTranches.length === 0) continue; - - try { - const currentPrice = await this.getCurrentPrice(group.symbol); - - for (const tranche of group.activeTranches) { - if (this.shouldIsolateTranche(tranche, currentPrice)) { - await this.isolateTranche(tranche.id, currentPrice); - } - } - } catch (error) { - logErrorWithTimestamp(`TrancheManager: Failed to check isolation for ${group.symbol}:`, error); - } - } - } - - // Check if an isolated tranche has recovered (P&L > recovery threshold) - public shouldRecoverTranche(tranche: Tranche, currentPrice: number): boolean { - if (!tranche.isolated || tranche.status !== 'active') { - return false; - } - - const symbolConfig = this.config.symbols[tranche.symbol]; - if (!symbolConfig || !symbolConfig.trancheAutoCloseIsolated) { - return false; - } - - const recoveryThreshold = symbolConfig.trancheRecoveryThreshold ?? 0.5; - - // Calculate unrealized P&L % - const pnlPercent = this.calculatePnlPercent( - tranche.entryPrice, - currentPrice, - tranche.side - ); - - // Recovered if P&L is positive and exceeds recovery threshold - return pnlPercent >= recoveryThreshold; - } - - // Monitor all isolated tranches and auto-close if recovered - public async checkRecoveryConditions(): Promise { - for (const [_key, group] of this.trancheGroups) { - if (group.isolatedTranches.length === 0) continue; - - try { - const currentPrice = await this.getCurrentPrice(group.symbol); - const symbolConfig = this.config.symbols[group.symbol]; - - // Skip if auto-close is not enabled - if (!symbolConfig?.trancheAutoCloseIsolated) { - continue; - } - - for (const tranche of group.isolatedTranches) { - if (this.shouldRecoverTranche(tranche, currentPrice)) { - await this.autoCloseRecoveredTranche(tranche.id, currentPrice); - } - } - } catch (error) { - logErrorWithTimestamp(`TrancheManager: Failed to check recovery for ${group.symbol}:`, error); - } - } - } - - // Auto-close a recovered isolated tranche - private async autoCloseRecoveredTranche(trancheId: string, currentPrice: number): Promise { - const tranche = await getTranche(trancheId); - if (!tranche || !tranche.isolated) return; - - const pnlPercent = this.calculatePnlPercent(tranche.entryPrice, currentPrice, tranche.side); - const realizedPnl = this.calculateUnrealizedPnl( - tranche.entryPrice, - currentPrice, - tranche.quantity, - tranche.side - ); - - logWithTimestamp( - `TrancheManager: Auto-closing recovered isolated tranche ${trancheId.substring(0, 8)} for ${tranche.symbol} at ${currentPrice} (${pnlPercent.toFixed(2)}% P&L, +${realizedPnl.toFixed(2)} USDT)` - ); - - // Close the tranche - await this.closeTranche({ - trancheId, - exitPrice: currentPrice, - realizedPnl, - orderId: `auto_recovery_${Date.now()}`, - }); - - // Log event - await logTrancheEvent(trancheId, 'closed', { - price: currentPrice, - quantity: tranche.quantity, - pnl: realizedPnl, - trigger: 'auto_close_recovery', - }); - - // Emit event - this.emit('trancheAutoClosedRecovery', { - tranche, - exitPrice: currentPrice, - pnlPercent, - realizedPnl, - }); - - logWithTimestamp( - `TrancheManager: Successfully auto-closed recovered tranche ${trancheId.substring(0, 8)} - freed ${tranche.marginUsed.toFixed(2)} USDT margin` - ); - } - - // Select which tranche(s) to close based on LIFO strategy (newest first) - public selectTranchesToClose( - symbol: string, - side: 'LONG' | 'SHORT', - quantityToClose: number - ): Tranche[] { - const groupKey = this.getGroupKey(symbol, side); - const group = this.trancheGroups.get(groupKey); - if (!group) return []; - - const tranchesToClose: Tranche[] = []; - let remainingQty = quantityToClose; - - // LIFO: Sort tranches by entry time (newest first) - const sortedTranches = [...group.activeTranches].sort((a, b) => b.entryTime - a.entryTime); - - // Select tranches until we have enough quantity - for (const tranche of sortedTranches) { - if (remainingQty <= 0) break; - - tranchesToClose.push(tranche); - remainingQty -= tranche.quantity; - } - - return tranchesToClose; - } - - // Close a tranche (fully or partially) - public async closeTranche(params: { - trancheId: string; - exitPrice: number; - quantityClosed?: number; // If partial close - realizedPnl: number; - orderId?: string; - }): Promise { - const tranche = await getTranche(params.trancheId); - if (!tranche) return; - - const isFullClose = !params.quantityClosed || params.quantityClosed >= tranche.quantity; - - if (isFullClose) { - // Full close - await dbCloseTranche(params.trancheId, params.exitPrice, params.realizedPnl, params.orderId); - - // Update in-memory - tranche.status = 'closed'; - tranche.exitPrice = params.exitPrice; - tranche.exitTime = Date.now(); - tranche.exitOrderId = params.orderId; - tranche.realizedPnl = params.realizedPnl; - - const groupKey = this.getGroupKey(tranche.symbol, tranche.side); - const group = this.trancheGroups.get(groupKey); - if (group) { - group.activeTranches = group.activeTranches.filter((t) => t.id !== params.trancheId); - group.isolatedTranches = group.isolatedTranches.filter((t) => t.id !== params.trancheId); - this.recalculateGroupMetrics(group); - } - - await logTrancheEvent(params.trancheId, 'closed', { - price: params.exitPrice, - quantity: tranche.quantity, - pnl: params.realizedPnl, - trigger: params.orderId, - }); - - this.emit('trancheClosed', tranche); - - logWithTimestamp( - `TrancheManager: Closed tranche ${params.trancheId.substring(0, 8)} for ${tranche.symbol} at ${params.exitPrice} (P&L: ${params.realizedPnl.toFixed(2)} USDT)` - ); - } else { - // Partial close - reduce quantity - const qtyToClose = params.quantityClosed!; // TypeScript: we know it's defined here - const newQuantity = tranche.quantity - qtyToClose; - const proportionalPnl = params.realizedPnl * (qtyToClose / tranche.quantity); - - await updateTranche(params.trancheId, { - quantity: newQuantity, - realizedPnl: tranche.realizedPnl + proportionalPnl, - }); - - // Update in-memory - tranche.quantity = newQuantity; - tranche.realizedPnl += proportionalPnl; - - const groupKey = this.getGroupKey(tranche.symbol, tranche.side); - const group = this.trancheGroups.get(groupKey); - if (group) { - this.recalculateGroupMetrics(group); - } - - await logTrancheEvent(params.trancheId, 'updated', { - price: params.exitPrice, - quantity: qtyToClose, - pnl: proportionalPnl, - trigger: 'partial_close', - }); - - this.emit('tranchePartialClose', tranche); - - logWithTimestamp( - `TrancheManager: Partially closed tranche ${params.trancheId.substring(0, 8)} - ${qtyToClose} of ${tranche.quantity + qtyToClose} (P&L: ${proportionalPnl.toFixed(2)} USDT)` - ); - } - } - - // Process order fill and close appropriate tranches - public async processOrderFill(params: { - symbol: string; - side: 'BUY' | 'SELL'; - positionSide: 'LONG' | 'SHORT' | 'BOTH'; - quantityFilled: number; - fillPrice: number; - realizedPnl: number; - orderId: string; - }): Promise { - const trancheSide = params.side === 'BUY' ? 'SHORT' : 'LONG'; // Closing side is opposite - - const tranchesToClose = this.selectTranchesToClose(params.symbol, trancheSide, params.quantityFilled); - - let remainingQty = params.quantityFilled; - let remainingPnl = params.realizedPnl; - - for (const tranche of tranchesToClose) { - const qtyToClose = Math.min(remainingQty, tranche.quantity); - const proportionalPnl = remainingPnl * (qtyToClose / params.quantityFilled); - - await this.closeTranche({ - trancheId: tranche.id, - exitPrice: params.fillPrice, - quantityClosed: qtyToClose, - realizedPnl: proportionalPnl, - orderId: params.orderId, - }); - - remainingQty -= qtyToClose; - remainingPnl -= proportionalPnl; - - if (remainingQty <= 0) break; - } - } - - // Sync local tranches with exchange position - public async syncWithExchange( - symbol: string, - side: 'LONG' | 'SHORT', - exchangePosition: ExchangePosition - ): Promise { - const groupKey = this.getGroupKey(symbol, side); - const group = this.trancheGroups.get(groupKey); - - const exchangeQty = Math.abs(parseFloat(exchangePosition.positionAmt)); - - if (!group) { - if (exchangeQty > 0) { - // Exchange has position but we have no tranches - create "unknown" tranche - logWarnWithTimestamp( - `TrancheManager: Found untracked position ${symbol} ${side}, creating recovery tranche` - ); - await this.createTranche({ - symbol, - side: side === 'LONG' ? 'BUY' : 'SELL', - positionSide: exchangePosition.positionSide as any, - entryPrice: parseFloat(exchangePosition.entryPrice), - quantity: exchangeQty, - marginUsed: - (exchangeQty * parseFloat(exchangePosition.entryPrice)) / - parseFloat(exchangePosition.leverage), - leverage: parseFloat(exchangePosition.leverage), - }); - } - return; - } - - // Compare quantities - const localQty = group.totalQuantity; - const drift = Math.abs(localQty - exchangeQty); - const driftPercent = (drift / Math.max(exchangeQty, 0.00001)) * 100; - - if (driftPercent > 1) { - // More than 1% drift - logWarnWithTimestamp( - `TrancheManager: Quantity drift detected for ${symbol} ${side} - Local: ${localQty.toFixed(6)}, Exchange: ${exchangeQty.toFixed(6)} (${driftPercent.toFixed(2)}% drift)` - ); - group.syncStatus = 'drift'; - - if (exchangeQty === 0 && localQty > 0) { - // Exchange position closed but we still have tranches - close all - logWarnWithTimestamp(`TrancheManager: Exchange position closed, closing all local tranches`); - for (const tranche of group.activeTranches) { - await this.closeTranche({ - trancheId: tranche.id, - exitPrice: parseFloat(exchangePosition.markPrice), - realizedPnl: 0, // Unknown - already realized on exchange - }); - } - } else if (exchangeQty > 0 && localQty === 0) { - // Exchange has position but we have no tranches - logWarnWithTimestamp(`TrancheManager: Creating recovery tranche for untracked position`); - await this.createTranche({ - symbol, - side: side === 'LONG' ? 'BUY' : 'SELL', - positionSide: exchangePosition.positionSide as any, - entryPrice: parseFloat(exchangePosition.entryPrice), - quantity: exchangeQty, - marginUsed: - (exchangeQty * parseFloat(exchangePosition.entryPrice)) / - parseFloat(exchangePosition.leverage), - leverage: parseFloat(exchangePosition.leverage), - }); - } else if (exchangeQty < localQty) { - // Partial close on exchange - close oldest tranches to match - const qtyToClose = localQty - exchangeQty; - const tranchesToClose = this.selectTranchesToClose(symbol, side, qtyToClose); - - for (const tranche of tranchesToClose) { - await this.closeTranche({ - trancheId: tranche.id, - exitPrice: parseFloat(exchangePosition.markPrice), - quantityClosed: Math.min(tranche.quantity, qtyToClose), - realizedPnl: 0, // Unknown - }); - } - } - } else { - group.syncStatus = 'synced'; - } - - group.lastExchangeQuantity = exchangeQty; - group.lastExchangeSync = Date.now(); - } - - // Check if we can open a new tranche - public canOpenNewTranche( - symbol: string, - side: 'LONG' | 'SHORT' - ): { - allowed: boolean; - reason?: string; - } { - const symbolConfig = this.config.symbols[symbol]; - if (!symbolConfig?.enableTrancheManagement) { - return { allowed: true }; // Not using tranche system - } - - const groupKey = this.getGroupKey(symbol, side); - const group = this.trancheGroups.get(groupKey); - - if (!group) { - return { allowed: true }; // First tranche - } - - // Check max active tranches - const maxTranches = symbolConfig.maxTranches || 3; - if (group.activeTranches.length >= maxTranches) { - return { - allowed: false, - reason: `Max active tranches (${maxTranches}) reached for ${symbol}`, - }; - } - - // Check max isolated tranches - const maxIsolated = symbolConfig.maxIsolatedTranches || 2; - if (group.isolatedTranches.length >= maxIsolated) { - if (!symbolConfig.allowTrancheWhileIsolated) { - return { - allowed: false, - reason: `Max isolated tranches (${maxIsolated}) reached for ${symbol}`, - }; - } - } - - return { allowed: true }; - } - - // Update unrealized P&L for all active tranches - public async updateUnrealizedPnl(symbol: string, currentPrice: number): Promise { - const groups = [ - this.trancheGroups.get(this.getGroupKey(symbol, 'LONG')), - this.trancheGroups.get(this.getGroupKey(symbol, 'SHORT')), - ]; - - for (const group of groups) { - if (!group) continue; - - for (const tranche of group.activeTranches) { - const pnl = this.calculateUnrealizedPnl( - tranche.entryPrice, - currentPrice, - tranche.quantity, - tranche.side - ); - - tranche.unrealizedPnl = pnl; - - // Update in DB (batch update for performance) - await updateTrancheUnrealizedPnl(tranche.id, pnl); - } - - this.recalculateGroupMetrics(group); - } - - // Check isolation and recovery conditions after P&L update - await this.checkIsolationConditions(); - await this.checkRecoveryConditions(); - } - - // Calculate unrealized P&L for a tranche - private calculateUnrealizedPnl( - entryPrice: number, - currentPrice: number, - quantity: number, - side: 'LONG' | 'SHORT' - ): number { - if (side === 'LONG') { - return (currentPrice - entryPrice) * quantity; - } else { - return (entryPrice - currentPrice) * quantity; - } - } - - // Calculate P&L percentage - private calculatePnlPercent( - entryPrice: number, - currentPrice: number, - side: 'LONG' | 'SHORT' - ): number { - if (side === 'LONG') { - return ((currentPrice - entryPrice) / entryPrice) * 100; - } else { - return ((entryPrice - currentPrice) / entryPrice) * 100; - } - } - - // Start isolation and recovery monitoring - public startIsolationMonitoring(intervalMs: number = 10000): void { - this.stopIsolationMonitoring(); - - this.isolationCheckInterval = setInterval(async () => { - try { - await this.checkIsolationConditions(); - await this.checkRecoveryConditions(); - } catch (error) { - logErrorWithTimestamp('TrancheManager: Isolation/Recovery check failed:', error); - } - }, intervalMs); - - logWithTimestamp(`TrancheManager: Started isolation and recovery monitoring (every ${intervalMs / 1000}s)`); - } - - public stopIsolationMonitoring(): void { - if (this.isolationCheckInterval) { - clearInterval(this.isolationCheckInterval); - this.isolationCheckInterval = undefined; - logWithTimestamp('TrancheManager: Stopped isolation monitoring'); - } - } - - // Helper methods - private getGroupKey(symbol: string, side: 'LONG' | 'SHORT'): string { - return `${symbol}_${side}`; - } - - private createTrancheGroup( - symbol: string, - side: 'LONG' | 'SHORT', - positionSide: 'LONG' | 'SHORT' | 'BOTH' - ): TrancheGroup { - return { - symbol, - side, - positionSide, - tranches: [], - activeTranches: [], - isolatedTranches: [], - totalQuantity: 0, - totalMarginUsed: 0, - weightedAvgEntry: 0, - totalUnrealizedPnl: 0, - lastExchangeQuantity: 0, - lastExchangeSync: Date.now(), - syncStatus: 'synced', - }; - } - - private recalculateGroupMetrics(group: TrancheGroup): void { - // Sum quantities and margins - let totalQty = 0; - let totalMargin = 0; - let weightedEntry = 0; - let totalPnl = 0; - - for (const tranche of group.activeTranches) { - totalQty += tranche.quantity; - totalMargin += tranche.marginUsed; - weightedEntry += tranche.entryPrice * tranche.quantity; - totalPnl += tranche.unrealizedPnl; - } - - group.totalQuantity = totalQty; - group.totalMarginUsed = totalMargin; - group.weightedAvgEntry = totalQty > 0 ? weightedEntry / totalQty : 0; - group.totalUnrealizedPnl = totalPnl; - } - - private async getCurrentPrice(symbol: string): Promise { - if (this.priceService) { - const price = this.priceService.getPrice(symbol); - if (price) return price; - } - - // Fallback to API - const markPriceData = await getMarkPrice(symbol); - return parseFloat( - Array.isArray(markPriceData) ? markPriceData[0].markPrice : markPriceData.markPrice - ); - } - - private calculateTpPrice(entryPrice: number, tpPercent: number, side: 'LONG' | 'SHORT'): number { - if (side === 'LONG') { - return entryPrice * (1 + tpPercent / 100); - } else { - return entryPrice * (1 - tpPercent / 100); - } - } - - private calculateSlPrice(entryPrice: number, slPercent: number, side: 'LONG' | 'SHORT'): number { - if (side === 'LONG') { - return entryPrice * (1 - slPercent / 100); - } else { - return entryPrice * (1 + slPercent / 100); - } - } - - // Public getters - public getTranches(symbol: string, side: 'LONG' | 'SHORT'): Tranche[] { - const groupKey = this.getGroupKey(symbol, side); - return this.trancheGroups.get(groupKey)?.activeTranches || []; - } - - public getTrancheGroup(symbol: string, side: 'LONG' | 'SHORT'): TrancheGroup | undefined { - const groupKey = this.getGroupKey(symbol, side); - return this.trancheGroups.get(groupKey); - } - - public getAllTrancheGroups(): TrancheGroup[] { - return Array.from(this.trancheGroups.values()); - } - - // Get the tranche with the best entry price (BEST_ENTRY strategy) - // For LONG: lowest entry price, For SHORT: highest entry price - public getBestEntryTranche(symbol: string, side: 'LONG' | 'SHORT'): Tranche | null { - const groupKey = this.getGroupKey(symbol, side); - const group = this.trancheGroups.get(groupKey); - - if (!group || group.activeTranches.length === 0) { - return null; - } - - // Find tranche with best entry - let bestTranche = group.activeTranches[0]; - for (const tranche of group.activeTranches) { - if (side === 'LONG') { - // For LONG: lower entry price is better - if (tranche.entryPrice < bestTranche.entryPrice) { - bestTranche = tranche; - } - } else { - // For SHORT: higher entry price is better - if (tranche.entryPrice > bestTranche.entryPrice) { - bestTranche = tranche; - } - } - } - - return bestTranche; - } -} - -// Singleton instance -let trancheManager: TrancheManagerService | null = null; - -export function initializeTrancheManager(config: Config): TrancheManagerService { - trancheManager = new TrancheManagerService(config); - return trancheManager; -} - -export function getTrancheManager(): TrancheManagerService { - if (!trancheManager) { - throw new Error('TrancheManager not initialized. Call initializeTrancheManager() first.'); - } - return trancheManager; -} diff --git a/src/lib/types.ts b/src/lib/types.ts index 53b874b..2ef39ca 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -31,20 +31,6 @@ export interface SymbolConfig { useThreshold?: boolean; // Enable threshold-based triggering for this symbol (default: false) thresholdTimeWindow?: number; // Time window in ms for volume accumulation (default: 60000) thresholdCooldown?: number; // Cooldown period in ms between triggers (default: 30000) - - // Tranche management settings - enableTrancheManagement?: boolean; // Enable multi-tranche system (default: false) - trancheIsolationThreshold?: number; // % loss to isolate tranche (default: 5) - maxTranches?: number; // Max active tranches (default: 3) - maxIsolatedTranches?: number; // Max isolated tranches before blocking (default: 2) - trancheAllocation?: 'equal' | 'dynamic'; // How to size new tranches (default: 'equal') - trancheStrategy?: TrancheStrategy; // Tranche behavior settings - - // Advanced tranche settings - allowTrancheWhileIsolated?: boolean; // Allow new tranches when some are isolated (default: true) - isolatedTrancheMinMargin?: number; // Min margin to keep in isolated tranches (USDT) - trancheAutoCloseIsolated?: boolean; // Auto-close isolated tranches when recovered (default: false) - trancheRecoveryThreshold?: number; // % profit to auto-close isolated tranche (default: 0.5%) } export interface ApiCredentials { @@ -144,101 +130,4 @@ export interface MarkPrice { symbol: string; markPrice: string; indexPrice: string; -} - -// Tranche Management Types - -export interface TrancheStrategy { - // Note: Closing strategy is hardcoded to LIFO (Last In, First Out) - // This closes newest tranches first for quick profit-taking - - // Note: SL/TP strategy is hardcoded to BEST_ENTRY - // This protects the most favorable entry price - - // Isolation behavior (future feature - currently only HOLD is implemented) - isolationAction: 'HOLD' | 'REDUCE_LEVERAGE' | 'PARTIAL_CLOSE'; -} - -export interface Tranche { - // Identity - id: string; // UUID v4 - symbol: string; // e.g., "BTCUSDT" - side: 'LONG' | 'SHORT'; // Position direction - positionSide: 'LONG' | 'SHORT' | 'BOTH'; // Exchange position side - - // Entry details - entryPrice: number; // Average entry price for this tranche - quantity: number; // Position size in base asset (BTC, ETH, etc.) - marginUsed: number; // USDT margin allocated - leverage: number; // Leverage used (1-125) - entryTime: number; // Unix timestamp - entryOrderId?: string; // Exchange order ID that created this tranche - - // Exit details - exitPrice?: number; // Average exit price (when closed) - exitTime?: number; // Unix timestamp - exitOrderId?: string; // Exchange order ID that closed this tranche - - // P&L tracking - unrealizedPnl: number; // Current unrealized P&L (updated real-time) - realizedPnl: number; // Final realized P&L (on close) - - // Risk management (inherited from SymbolConfig at entry time) - tpPercent: number; // Take profit % - slPercent: number; // Stop loss % - tpPrice: number; // Calculated TP price - slPrice: number; // Calculated SL price - - // Status tracking - status: 'active' | 'closed' | 'liquidated'; - isolated: boolean; // True if underwater > isolation threshold - isolationTime?: number; // When it became isolated - isolationPrice?: number; // Price when isolated - - // Metadata - notes?: string; // Optional notes (e.g., "manual entry", "recovered from restart") -} - -export interface TrancheGroup { - symbol: string; - side: 'LONG' | 'SHORT'; - positionSide: 'LONG' | 'SHORT' | 'BOTH'; - - // Tranche tracking - tranches: Tranche[]; // All tranches (active + closed) - activeTranches: Tranche[]; // Currently open tranches - isolatedTranches: Tranche[]; // Underwater tranches - - // Aggregated metrics (sum of active tranches) - totalQuantity: number; // Total position size - totalMarginUsed: number; // Total margin allocated - weightedAvgEntry: number; // Weighted average entry price - totalUnrealizedPnl: number; // Sum of all unrealized P&L - - // Exchange sync - lastExchangeQuantity: number; // Last known exchange position size - lastExchangeSync: number; // Last sync timestamp - syncStatus: 'synced' | 'drift' | 'conflict'; // Sync health - - // Order management - activeSlOrderId?: number; // Current exchange SL order - activeTpOrderId?: number; // Current exchange TP order - targetSlPrice?: number; // Target SL price - targetTpPrice?: number; // Target TP price -} - -export interface TrancheEvent { - id: number; // Auto-increment ID - trancheId: string; // Foreign key to tranche - eventType: 'created' | 'isolated' | 'closed' | 'liquidated' | 'updated'; - eventTime: number; // Unix timestamp - - // Event details - price?: number; // Price at event time - quantity?: number; // Quantity affected - pnl?: number; // P&L at event (if applicable) - - // Context - trigger?: string; // What triggered the event - metadata?: string; // JSON with additional details -} +}; diff --git a/tests/tranche-integration-test.ts b/tests/tranche-integration-test.ts deleted file mode 100644 index 2294ef8..0000000 --- a/tests/tranche-integration-test.ts +++ /dev/null @@ -1,766 +0,0 @@ -/** - * Multi-Tranche Position Management - Integration Tests - * - * Comprehensive automated tests for all integration points: - * - Hunter integration (entry logic) - * - PositionManager integration (exit logic) - * - Exchange synchronization - * - WebSocket broadcasting - * - Full lifecycle scenarios - */ - -import { EventEmitter } from 'events'; -import { initTrancheTables, createTranche, getTranche, getActiveTranches, getAllTranchesForSymbol, closeTranche as dbCloseTranche } from '../src/lib/db/trancheDb'; -import { initializeTrancheManager, getTrancheManager } from '../src/lib/services/trancheManager'; -import { Config } from '../src/lib/types'; -import { db } from '../src/lib/db/database'; - -const TEST_SYMBOL = 'BTCUSDT'; -const TEST_ENTRY_PRICE = 50000; -const TEST_QUANTITY = 0.001; -const TEST_MARGIN = 5; -const TEST_LEVERAGE = 10; - -// Test configuration -const testConfig: Config = { - api: { - apiKey: 'test-key', - secretKey: 'test-secret', - }, - symbols: { - [TEST_SYMBOL]: { - longVolumeThresholdUSDT: 10000, - shortVolumeThresholdUSDT: 10000, - tradeSize: 0.001, - maxPositionMarginUSDT: 200, - leverage: TEST_LEVERAGE, - tpPercent: 5, - slPercent: 2, - priceOffsetBps: 2, - maxSlippageBps: 50, - orderType: 'LIMIT', - postOnly: false, - forceMarketOrders: false, - vwapProtection: false, - vwapTimeframe: '5m', - vwapLookback: 200, - useThreshold: false, - thresholdTimeWindow: 60000, - thresholdCooldown: 30000, - enableTrancheManagement: true, - trancheIsolationThreshold: 5, - maxTranches: 3, - maxIsolatedTranches: 2, - trancheStrategy: { - closingStrategy: 'FIFO', - slTpStrategy: 'NEWEST', - isolationAction: 'HOLD', - }, - allowTrancheWhileIsolated: true, - trancheAutoCloseIsolated: false, - }, - }, - global: { - paperMode: true, - riskPercent: 90, - positionMode: 'HEDGE', - maxOpenPositions: 5, - useThresholdSystem: false, - server: { - dashboardPassword: 'test', - dashboardPort: 3000, - websocketPort: 8080, - useRemoteWebSocket: false, - websocketHost: null, - }, - rateLimit: { - maxRequestWeight: 2400, - maxOrderCount: 1200, - reservePercent: 30, - enableBatching: true, - queueTimeout: 30000, - enableDeduplication: true, - deduplicationWindowMs: 1000, - parallelProcessing: true, - maxConcurrentRequests: 3, - }, - }, - version: '1.1.0', -}; - -// Mock StatusBroadcaster for testing -class MockStatusBroadcaster extends EventEmitter { - public broadcastedEvents: any[] = []; - - broadcastTrancheCreated(data: any) { - this.broadcastedEvents.push({ type: 'tranche_created', data }); - this.emit('tranche_created', data); - } - - broadcastTrancheIsolated(data: any) { - this.broadcastedEvents.push({ type: 'tranche_isolated', data }); - this.emit('tranche_isolated', data); - } - - broadcastTrancheClosed(data: any) { - this.broadcastedEvents.push({ type: 'tranche_closed', data }); - this.emit('tranche_closed', data); - } - - broadcastTrancheSyncUpdate(data: any) { - this.broadcastedEvents.push({ type: 'tranche_sync', data }); - this.emit('tranche_sync', data); - } - - broadcastTradingError(title: string, message: string, details?: any) { - this.broadcastedEvents.push({ type: 'trading_error', title, message, details }); - this.emit('trading_error', { title, message, details }); - } - - clearEvents() { - this.broadcastedEvents = []; - } - - getEventsByType(type: string) { - return this.broadcastedEvents.filter(e => e.type === type); - } -} - -// Helper to clean up test data -async function cleanupTestData() { - // Delete events first (foreign key constraint) - await db.run(` - DELETE FROM tranche_events - WHERE tranche_id IN (SELECT id FROM tranches WHERE symbol = ?) - `, [TEST_SYMBOL]); - - // Then delete tranches - await db.run('DELETE FROM tranches WHERE symbol = ?', [TEST_SYMBOL]); -} - -async function runIntegrationTests() { - console.log('๐Ÿงช Multi-Tranche Integration Tests\n'); - console.log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•\n'); - - let testsPassed = 0; - let testsFailed = 0; - - // Initialize database - await db.initialize(); - await initTrancheTables(); - - // Test Suite 1: Hunter Integration Tests - console.log('๐Ÿ“‹ Test Suite 1: Hunter Integration\n'); - - // Test 1.1: Pre-trade tranche limit check - console.log('Test 1.1: Pre-trade Tranche Limit Check'); - try { - await cleanupTestData(); - const trancheManager = initializeTrancheManager(testConfig); - await trancheManager.initialize(); - - // Create max tranches (3) - for (let i = 0; i < 3; i++) { - await trancheManager.createTranche({ - symbol: TEST_SYMBOL, - side: 'BUY', - positionSide: 'LONG', - entryPrice: TEST_ENTRY_PRICE + i * 100, - quantity: TEST_QUANTITY, - marginUsed: TEST_MARGIN, - leverage: TEST_LEVERAGE, - orderId: `test-hunter-${i}`, - }); - } - - // Verify we have 3 active tranches - const activeTranches = await getActiveTranches(TEST_SYMBOL, 'LONG'); - const activeCount = activeTranches.filter(t => !t.isolated).length; - - // Verify limit is reached - const canOpen = trancheManager.canOpenNewTranche(TEST_SYMBOL, 'LONG'); - - if (activeCount === 3 && !canOpen.allowed && (canOpen.reason?.includes('maxTranches') || canOpen.reason?.includes('Max active tranches'))) { - console.log('โœ… Pre-trade limit check blocks new trades correctly'); - console.log(` Active tranches: ${activeCount}/3`); - console.log(` Can open new: ${canOpen.allowed} โœ“\n`); - testsPassed++; - } else { - throw new Error(`Limit check failed: activeCount=${activeCount}, canOpen=${canOpen.allowed}, reason=${canOpen.reason}`); - } - } catch (error) { - console.error('โŒ Test failed:', error); - testsFailed++; - } - - // Test 1.2: Post-order tranche creation - console.log('Test 1.2: Post-order Tranche Creation'); - try { - await cleanupTestData(); - const trancheManager = initializeTrancheManager(testConfig); - await trancheManager.initialize(); - - const tranchesBefore = await getActiveTranches(TEST_SYMBOL, 'LONG'); - const countBefore = tranchesBefore.length; - - // Simulate Hunter creating tranche after order filled - await trancheManager.createTranche({ - symbol: TEST_SYMBOL, - side: 'BUY', - positionSide: 'LONG', - entryPrice: TEST_ENTRY_PRICE, - quantity: TEST_QUANTITY, - marginUsed: TEST_MARGIN, - leverage: TEST_LEVERAGE, - orderId: 'hunter-order-123', - }); - - const tranchesAfter = await getActiveTranches(TEST_SYMBOL, 'LONG'); - const countAfter = tranchesAfter.length; - - if (countAfter === countBefore + 1 && tranchesAfter[0].entryOrderId === 'hunter-order-123') { - console.log('โœ… Tranche created correctly after order fill\n'); - testsPassed++; - } else { - throw new Error('Tranche not created or order ID mismatch'); - } - } catch (error) { - console.error('โŒ Test failed:', error); - testsFailed++; - } - - // Test Suite 2: PositionManager Integration Tests - console.log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•'); - console.log('๐Ÿ“‹ Test Suite 2: PositionManager Integration\n'); - - // Test 2.1: Tranche closing on SL/TP fill (FIFO strategy) - console.log('Test 2.1: Tranche Closing with FIFO Strategy'); - try { - await cleanupTestData(); - const trancheManager = initializeTrancheManager(testConfig); - await trancheManager.initialize(); - - // Create 3 tranches at different entry prices - const tranche1 = await trancheManager.createTranche({ - symbol: TEST_SYMBOL, - side: 'BUY', - positionSide: 'LONG', - entryPrice: 50000, - quantity: 0.001, - marginUsed: 5, - leverage: 10, - orderId: 'order-1', - }); - - await new Promise(resolve => setTimeout(resolve, 10)); - - const tranche2 = await trancheManager.createTranche({ - symbol: TEST_SYMBOL, - side: 'BUY', - positionSide: 'LONG', - entryPrice: 50100, - quantity: 0.001, - marginUsed: 5, - leverage: 10, - orderId: 'order-2', - }); - - await new Promise(resolve => setTimeout(resolve, 10)); - - const tranche3 = await trancheManager.createTranche({ - symbol: TEST_SYMBOL, - side: 'BUY', - positionSide: 'LONG', - entryPrice: 50200, - quantity: 0.001, - marginUsed: 5, - leverage: 10, - orderId: 'order-3', - }); - - // Simulate position manager closing order (SELL = closing LONG) - await trancheManager.processOrderFill({ - symbol: TEST_SYMBOL, - side: 'SELL', - positionSide: 'LONG', - quantityFilled: 0.001, - fillPrice: 52000, - realizedPnl: 2.0, - orderId: 'close-order-1', - }); - - // Verify FIFO: First tranche should be closed - const tranche1After = await getTranche(tranche1.id); - const tranche2After = await getTranche(tranche2.id); - - if (tranche1After?.status === 'closed' && tranche2After?.status === 'active') { - console.log('โœ… FIFO closing strategy works correctly'); - console.log(` Tranche 1 (oldest): closed โœ“`); - console.log(` Tranche 2 (middle): active โœ“\n`); - testsPassed++; - } else { - throw new Error('FIFO strategy not working correctly'); - } - } catch (error) { - console.error('โŒ Test failed:', error); - testsFailed++; - } - - // Test 2.2: Partial position close - console.log('Test 2.2: Partial Position Close'); - try { - await cleanupTestData(); - const trancheManager = initializeTrancheManager(testConfig); - await trancheManager.initialize(); - - // Create tranche with 0.003 BTC - await trancheManager.createTranche({ - symbol: TEST_SYMBOL, - side: 'BUY', - positionSide: 'LONG', - entryPrice: 50000, - quantity: 0.003, - marginUsed: 15, - leverage: 10, - orderId: 'large-order', - }); - - // Close only 0.001 BTC (partial) - await trancheManager.processOrderFill({ - symbol: TEST_SYMBOL, - side: 'SELL', - positionSide: 'LONG', - quantityFilled: 0.001, - fillPrice: 52000, - realizedPnl: 2.0, - orderId: 'partial-close-1', - }); - - const tranches = await getActiveTranches(TEST_SYMBOL, 'LONG'); - const remainingQty = tranches.reduce((sum, t) => sum + t.quantity, 0); - - if (Math.abs(remainingQty - 0.002) < 0.0001) { - console.log('โœ… Partial close handled correctly'); - console.log(` Original: 0.003 BTC, Closed: 0.001 BTC`); - console.log(` Remaining: ${remainingQty.toFixed(4)} BTC โœ“\n`); - testsPassed++; - } else { - throw new Error(`Partial close quantity mismatch: ${remainingQty}`); - } - } catch (error) { - console.error('โŒ Test failed:', error); - testsFailed++; - } - - // Test Suite 3: Exchange Synchronization - console.log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•'); - console.log('๐Ÿ“‹ Test Suite 3: Exchange Synchronization\n'); - - // Test 3.1: Sync with matching quantities - console.log('Test 3.1: Exchange Sync - Matching Quantities'); - try { - await cleanupTestData(); - const trancheManager = initializeTrancheManager(testConfig); - await trancheManager.initialize(); - - // Create 2 tranches (total 0.002 BTC) - await trancheManager.createTranche({ - symbol: TEST_SYMBOL, - side: 'BUY', - positionSide: 'LONG', - entryPrice: 50000, - quantity: 0.001, - marginUsed: 5, - leverage: 10, - orderId: 'sync-1', - }); - - await trancheManager.createTranche({ - symbol: TEST_SYMBOL, - side: 'BUY', - positionSide: 'LONG', - entryPrice: 50100, - quantity: 0.001, - marginUsed: 5, - leverage: 10, - orderId: 'sync-2', - }); - - // Simulate exchange position with matching quantity - const mockExchangePosition = { - symbol: TEST_SYMBOL, - positionAmt: '0.002', - entryPrice: '50050', - markPrice: '50500', - unRealizedProfit: '0.9', - liquidationPrice: '45000', - leverage: '10', - marginType: 'cross', - isolatedMargin: '0', - isAutoAddMargin: 'false', - positionSide: 'LONG', - updateTime: Date.now(), - }; - - await trancheManager.syncWithExchange(TEST_SYMBOL, 'LONG', mockExchangePosition); - - const group = trancheManager.getTrancheGroup(TEST_SYMBOL, 'LONG'); - - if (group && group.syncStatus === 'synced') { - console.log('โœ… Exchange sync successful with matching quantities'); - console.log(` Local: ${group.totalQuantity.toFixed(4)} BTC`); - console.log(` Exchange: 0.002 BTC`); - console.log(` Status: ${group.syncStatus} โœ“\n`); - testsPassed++; - } else { - throw new Error(`Sync status incorrect: ${group?.syncStatus}`); - } - } catch (error) { - console.error('โŒ Test failed:', error); - testsFailed++; - } - - // Test 3.2: Sync with quantity drift - console.log('Test 3.2: Exchange Sync - Quantity Drift Detection'); - try { - await cleanupTestData(); - const trancheManager = initializeTrancheManager(testConfig); - await trancheManager.initialize(); - - // Create tranches totaling 0.003 BTC - for (let i = 0; i < 3; i++) { - await trancheManager.createTranche({ - symbol: TEST_SYMBOL, - side: 'BUY', - positionSide: 'LONG', - entryPrice: 50000 + i * 50, - quantity: 0.001, - marginUsed: 5, - leverage: 10, - orderId: `drift-${i}`, - }); - } - - // Simulate exchange position with less quantity (drift) - const mockExchangePosition = { - symbol: TEST_SYMBOL, - positionAmt: '0.002', // 0.001 less than local - entryPrice: '50050', - markPrice: '50500', - unRealizedProfit: '0.9', - liquidationPrice: '45000', - leverage: '10', - marginType: 'cross', - isolatedMargin: '0', - isAutoAddMargin: 'false', - positionSide: 'LONG', - updateTime: Date.now(), - }; - - await trancheManager.syncWithExchange(TEST_SYMBOL, 'LONG', mockExchangePosition); - - const group = trancheManager.getTrancheGroup(TEST_SYMBOL, 'LONG'); - - if (group && group.syncStatus === 'drift') { - console.log('โœ… Quantity drift detected correctly'); - console.log(` Local: ${group.totalQuantity.toFixed(4)} BTC`); - console.log(` Exchange: 0.002 BTC`); - console.log(` Status: ${group.syncStatus} โœ“`); - console.log(` Drift: ${((group.totalQuantity - 0.002) * 100).toFixed(1)}%\n`); - testsPassed++; - } else { - throw new Error(`Drift not detected: ${group?.syncStatus}`); - } - } catch (error) { - console.error('โŒ Test failed:', error); - testsFailed++; - } - - // Test Suite 4: Isolation Logic - console.log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•'); - console.log('๐Ÿ“‹ Test Suite 4: Isolation Logic\n'); - - // Test 4.1: Isolation threshold detection - console.log('Test 4.1: Isolation Threshold Detection'); - try { - await cleanupTestData(); - const trancheManager = initializeTrancheManager(testConfig); - await trancheManager.initialize(); - - const tranche = await trancheManager.createTranche({ - symbol: TEST_SYMBOL, - side: 'BUY', - positionSide: 'LONG', - entryPrice: 50000, - quantity: 0.001, - marginUsed: 5, - leverage: 10, - orderId: 'iso-test', - }); - - // Update P&L at 47500 (5% loss - at threshold) - await trancheManager.updateUnrealizedPnl(TEST_SYMBOL, 47500); - - // Test shouldIsolateTranche logic - const shouldIsolate5 = trancheManager.shouldIsolateTranche(tranche, 47500); // 5% loss - const shouldIsolate4 = trancheManager.shouldIsolateTranche(tranche, 48000); // 4% loss - - if (shouldIsolate5 && !shouldIsolate4) { - console.log('โœ… Isolation threshold detection correct'); - console.log(` Entry: $50000`); - console.log(` At $47500 (5% loss): Should isolate = ${shouldIsolate5} โœ“`); - console.log(` At $48000 (4% loss): Should isolate = ${shouldIsolate4} โœ“\n`); - testsPassed++; - } else { - throw new Error(`Threshold detection failed: shouldIsolate5=${shouldIsolate5}, shouldIsolate4=${shouldIsolate4}`); - } - } catch (error) { - console.error('โŒ Test failed:', error); - testsFailed++; - } - - // Test 4.2: Manual tranche isolation - console.log('Test 4.2: Manual Tranche Isolation'); - try { - await cleanupTestData(); - const trancheManager = initializeTrancheManager(testConfig); - await trancheManager.initialize(); - - // Create tranche - const tranche1 = await trancheManager.createTranche({ - symbol: TEST_SYMBOL, - side: 'BUY', - positionSide: 'LONG', - entryPrice: 50000, - quantity: 0.001, - marginUsed: 5, - leverage: 10, - orderId: 'iso-manual', - }); - - // Manually isolate tranche - await trancheManager.isolateTranche(tranche1.id, 47500); - - // Verify isolation - const tranche1After = await getTranche(tranche1.id); - - // Create new tranche (should be allowed if allowTrancheWhileIsolated) - const canOpen = trancheManager.canOpenNewTranche(TEST_SYMBOL, 'LONG'); - - if (canOpen.allowed && tranche1After?.isolated) { - await trancheManager.createTranche({ - symbol: TEST_SYMBOL, - side: 'BUY', - positionSide: 'LONG', - entryPrice: 48000, - quantity: 0.001, - marginUsed: 5, - leverage: 10, - orderId: 'new-after-iso', - }); - - const group = trancheManager.getTrancheGroup(TEST_SYMBOL, 'LONG'); - - if (group && group.activeTranches.length === 1 && group.isolatedTranches.length === 1) { - console.log('โœ… New tranche created successfully with isolated tranche'); - console.log(` Active tranches: ${group.activeTranches.length}`); - console.log(` Isolated tranches: ${group.isolatedTranches.length} โœ“\n`); - testsPassed++; - } else { - throw new Error(`Tranche counts incorrect: active=${group?.activeTranches.length}, isolated=${group?.isolatedTranches.length}`); - } - } else { - throw new Error(`Cannot open new tranche: canOpen=${canOpen.allowed}, isolated=${tranche1After?.isolated}`); - } - } catch (error) { - console.error('โŒ Test failed:', error); - testsFailed++; - } - - // Test Suite 5: Event Broadcasting - console.log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•'); - console.log('๐Ÿ“‹ Test Suite 5: Event Broadcasting\n'); - - // Test 5.1: Tranche lifecycle events - console.log('Test 5.1: Tranche Lifecycle Events'); - try { - await cleanupTestData(); - const trancheManager = initializeTrancheManager(testConfig); - await trancheManager.initialize(); - - let createdEvent = false; - let isolatedEvent = false; - let closedEvent = false; - - trancheManager.on('trancheCreated', () => { createdEvent = true; }); - trancheManager.on('trancheIsolated', () => { isolatedEvent = true; }); - trancheManager.on('trancheClosed', () => { closedEvent = true; }); - - // Create tranche - const tranche = await trancheManager.createTranche({ - symbol: TEST_SYMBOL, - side: 'BUY', - positionSide: 'LONG', - entryPrice: 50000, - quantity: 0.001, - marginUsed: 5, - leverage: 10, - orderId: 'event-test', - }); - - // Isolate - await trancheManager.updateUnrealizedPnl(TEST_SYMBOL, 47500); - await trancheManager.isolateTranche(tranche.id, 47500); - - // Close - await trancheManager.closeTranche({ - trancheId: tranche.id, - exitPrice: 48000, - realizedPnl: -2.0, - orderId: 'close-event', - }); - - if (createdEvent && isolatedEvent && closedEvent) { - console.log('โœ… All lifecycle events emitted correctly'); - console.log(` Created: ${createdEvent} โœ“`); - console.log(` Isolated: ${isolatedEvent} โœ“`); - console.log(` Closed: ${closedEvent} โœ“\n`); - testsPassed++; - } else { - throw new Error(`Events missing: created=${createdEvent}, isolated=${isolatedEvent}, closed=${closedEvent}`); - } - } catch (error) { - console.error('โŒ Test failed:', error); - testsFailed++; - } - - // Test Suite 6: Full Lifecycle Scenarios - console.log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•'); - console.log('๐Ÿ“‹ Test Suite 6: Full Lifecycle Scenarios\n'); - - // Test 6.1: Profitable trade full lifecycle - console.log('Test 6.1: Profitable Trade - Entry to Exit'); - try { - await cleanupTestData(); - const trancheManager = initializeTrancheManager(testConfig); - await trancheManager.initialize(); - - // Entry - const tranche = await trancheManager.createTranche({ - symbol: TEST_SYMBOL, - side: 'BUY', - positionSide: 'LONG', - entryPrice: 50000, - quantity: 0.001, - marginUsed: 5, - leverage: 10, - orderId: 'profit-trade', - }); - - // Price moves up 5% (TP hit) - const tpPrice = 52500; - await trancheManager.processOrderFill({ - symbol: TEST_SYMBOL, - side: 'SELL', - positionSide: 'LONG', - quantityFilled: 0.001, - fillPrice: tpPrice, - realizedPnl: 2.5, - orderId: 'tp-fill', - }); - - const closedTranche = await getTranche(tranche.id); - - if (closedTranche?.status === 'closed' && closedTranche.realizedPnl > 0) { - console.log('โœ… Profitable trade lifecycle complete'); - console.log(` Entry: $${closedTranche.entryPrice}`); - console.log(` Exit: $${closedTranche.exitPrice}`); - console.log(` P&L: $${closedTranche.realizedPnl.toFixed(2)} โœ“\n`); - testsPassed++; - } else { - throw new Error('Trade lifecycle incomplete or not profitable'); - } - } catch (error) { - console.error('โŒ Test failed:', error); - testsFailed++; - } - - // Test 6.2: Multi-tranche P&L tracking - console.log('Test 6.2: Multi-Tranche P&L Tracking'); - try { - await cleanupTestData(); - const trancheManager = initializeTrancheManager(testConfig); - await trancheManager.initialize(); - - // Create 3 tranches at different prices - const entries = [50000, 49500, 49000]; - const trancheIds = []; - for (const entry of entries) { - const t = await trancheManager.createTranche({ - symbol: TEST_SYMBOL, - side: 'BUY', - positionSide: 'LONG', - entryPrice: entry, - quantity: 0.001, - marginUsed: 5, - leverage: 10, - orderId: `multi-${entry}`, - }); - trancheIds.push(t.id); - } - - // Update P&L at profitable price (51000) - await trancheManager.updateUnrealizedPnl(TEST_SYMBOL, 51000); - - const group = trancheManager.getTrancheGroup(TEST_SYMBOL, 'LONG'); - const allProfitable = group?.tranches.every(t => t.unrealizedPnl > 0); - const totalPnL = group?.totalUnrealizedPnl || 0; - - // All tranches should be profitable at 51000 - if (allProfitable && totalPnL > 0 && group.tranches.length === 3) { - console.log('โœ… Multi-tranche P&L tracking successful'); - console.log(` Total tranches: ${group.tranches.length}`); - console.log(` All profitable: ${allProfitable} โœ“`); - console.log(` Total unrealized P&L: $${totalPnL.toFixed(2)}\n`); - testsPassed++; - } else { - throw new Error(`P&L tracking failed: allProfitable=${allProfitable}, totalPnL=${totalPnL}, count=${group?.tranches.length}`); - } - } catch (error) { - console.error('โŒ Test failed:', error); - testsFailed++; - } - - // Summary - console.log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•'); - console.log('๐Ÿ“Š Integration Test Summary'); - console.log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•'); - console.log(`โœ… Tests Passed: ${testsPassed}`); - console.log(`โŒ Tests Failed: ${testsFailed}`); - console.log(`๐Ÿ“ˆ Success Rate: ${((testsPassed / (testsPassed + testsFailed)) * 100).toFixed(1)}%`); - console.log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•\n'); - - if (testsFailed === 0) { - console.log('๐ŸŽ‰ All integration tests passed!'); - console.log('โœ… Hunter integration working'); - console.log('โœ… PositionManager integration working'); - console.log('โœ… Exchange synchronization working'); - console.log('โœ… Isolation logic working'); - console.log('โœ… Event broadcasting working'); - console.log('โœ… Full lifecycle scenarios working\n'); - } else { - console.log('โš ๏ธ Some integration tests failed. Please review the errors above.\n'); - } - - // Cleanup - await cleanupTestData(); - await db.close(); - - process.exit(testsFailed > 0 ? 1 : 0); -} - -// Run tests -runIntegrationTests().catch(error => { - console.error('๐Ÿ’ฅ Integration test suite crashed:', error); - process.exit(1); -}); diff --git a/tests/tranche-system-test.ts b/tests/tranche-system-test.ts deleted file mode 100644 index 17e345d..0000000 --- a/tests/tranche-system-test.ts +++ /dev/null @@ -1,355 +0,0 @@ -/** - * Multi-Tranche Position Management - System Test - * - * This test verifies the core functionality of the tranche management system: - * - Database initialization - * - Tranche creation and retrieval - * - Isolation logic - * - P&L calculations - * - Exchange synchronization - */ - -import { initTrancheTables, createTranche, getTranche, getActiveTranches, updateTrancheUnrealizedPnl, isolateTranche, closeTranche } from '../src/lib/db/trancheDb'; -import { initializeTrancheManager } from '../src/lib/services/trancheManager'; -import { Config } from '../src/lib/types'; -import { db } from '../src/lib/db/database'; - -const TEST_SYMBOL = 'BTCUSDT'; -const TEST_ENTRY_PRICE = 50000; -const TEST_QUANTITY = 0.001; -const TEST_MARGIN = 5; -const TEST_LEVERAGE = 10; - -// Test configuration -const testConfig: Config = { - api: { - apiKey: 'test-key', - secretKey: 'test-secret', - }, - symbols: { - [TEST_SYMBOL]: { - longVolumeThresholdUSDT: 10000, - shortVolumeThresholdUSDT: 10000, - tradeSize: 0.001, - maxPositionMarginUSDT: 200, - leverage: TEST_LEVERAGE, - tpPercent: 5, - slPercent: 2, - priceOffsetBps: 2, - maxSlippageBps: 50, - orderType: 'LIMIT', - postOnly: false, - forceMarketOrders: false, - vwapProtection: false, - vwapTimeframe: '5m', - vwapLookback: 200, - useThreshold: false, - thresholdTimeWindow: 60000, - thresholdCooldown: 30000, - // Tranche management settings - enableTrancheManagement: true, - trancheIsolationThreshold: 5, - maxTranches: 3, - maxIsolatedTranches: 2, - trancheStrategy: { - closingStrategy: 'FIFO', - slTpStrategy: 'NEWEST', - isolationAction: 'HOLD', - }, - allowTrancheWhileIsolated: true, - trancheAutoCloseIsolated: false, - }, - }, - global: { - paperMode: true, - riskPercent: 90, - positionMode: 'HEDGE', - maxOpenPositions: 5, - useThresholdSystem: false, - server: { - dashboardPassword: 'test', - dashboardPort: 3000, - websocketPort: 8080, - useRemoteWebSocket: false, - websocketHost: null, - }, - rateLimit: { - maxRequestWeight: 2400, - maxOrderCount: 1200, - reservePercent: 30, - enableBatching: true, - queueTimeout: 30000, - enableDeduplication: true, - deduplicationWindowMs: 1000, - parallelProcessing: true, - maxConcurrentRequests: 3, - }, - }, - version: '1.1.0', -}; - -async function runTests() { - console.log('๐Ÿงช Starting Multi-Tranche System Tests\n'); - - let testsPassed = 0; - let testsFailed = 0; - - // Test 1: Database Initialization - console.log('Test 1: Database Initialization'); - try { - await db.initialize(); - await initTrancheTables(); - console.log('โœ… Database and tranche tables initialized\n'); - testsPassed++; - } catch (error) { - console.error('โŒ Database initialization failed:', error); - testsFailed++; - return; // Can't continue without database - } - - // Test 2: Tranche Creation (Database Layer) - console.log('Test 2: Tranche Creation (Database Layer)'); - const testTrancheId = `test-${Date.now()}`; - try { - await createTranche({ - id: testTrancheId, - symbol: TEST_SYMBOL, - side: 'LONG', - positionSide: 'LONG', - entryPrice: TEST_ENTRY_PRICE, - quantity: TEST_QUANTITY, - marginUsed: TEST_MARGIN, - leverage: TEST_LEVERAGE, - entryTime: Date.now(), - entryOrderId: 'test-order-001', - unrealizedPnl: 0, - realizedPnl: 0, - tpPercent: 5, - slPercent: 2, - tpPrice: TEST_ENTRY_PRICE * 1.05, - slPrice: TEST_ENTRY_PRICE * 0.98, - status: 'active', - isolated: false, - }); - - const retrieved = await getTranche(testTrancheId); - if (retrieved && retrieved.entryPrice === TEST_ENTRY_PRICE) { - console.log('โœ… Tranche created and retrieved successfully'); - console.log(` ID: ${testTrancheId.substring(0, 8)}...`); - console.log(` Entry: $${retrieved.entryPrice}, TP: $${retrieved.tpPrice}, SL: $${retrieved.slPrice}\n`); - testsPassed++; - } else { - throw new Error('Retrieved tranche does not match'); - } - } catch (error) { - console.error('โŒ Tranche creation failed:', error); - testsFailed++; - } - - // Test 3: TrancheManager Service Initialization - console.log('Test 3: TrancheManager Service Initialization'); - try { - const trancheManager = initializeTrancheManager(testConfig); - await trancheManager.initialize(); - console.log('โœ… TrancheManager initialized successfully\n'); - testsPassed++; - } catch (error) { - console.error('โŒ TrancheManager initialization failed:', error); - testsFailed++; - } - - // Test 4: Tranche Creation via Manager - console.log('Test 4: Tranche Creation via TrancheManager'); - try { - const trancheManager = initializeTrancheManager(testConfig); - await trancheManager.initialize(); - - const tranche = await trancheManager.createTranche({ - symbol: TEST_SYMBOL, - side: 'BUY', - positionSide: 'LONG', - entryPrice: TEST_ENTRY_PRICE, - quantity: TEST_QUANTITY, - marginUsed: TEST_MARGIN, - leverage: TEST_LEVERAGE, - orderId: 'test-order-002', - }); - - if (tranche.tpPrice > TEST_ENTRY_PRICE && tranche.slPrice < TEST_ENTRY_PRICE) { - console.log('โœ… Tranche created via manager with correct TP/SL'); - console.log(` Entry: $${tranche.entryPrice}`); - console.log(` TP: $${tranche.tpPrice} (+5%)`); - console.log(` SL: $${tranche.slPrice} (-2%)\n`); - testsPassed++; - } else { - throw new Error('TP/SL calculation incorrect'); - } - } catch (error) { - console.error('โŒ Tranche creation via manager failed:', error); - testsFailed++; - } - - // Test 5: Isolation Threshold Logic - console.log('Test 5: Isolation Threshold Logic'); - try { - const trancheManager = initializeTrancheManager(testConfig); - await trancheManager.initialize(); - - // Create test tranche - const tranche = await trancheManager.createTranche({ - symbol: TEST_SYMBOL, - side: 'BUY', - positionSide: 'LONG', - entryPrice: TEST_ENTRY_PRICE, - quantity: TEST_QUANTITY, - marginUsed: TEST_MARGIN, - leverage: TEST_LEVERAGE, - orderId: 'test-order-003', - }); - - // Test at 5% loss (should isolate) - const priceAt5PercentLoss = TEST_ENTRY_PRICE * 0.95; - const shouldIsolate = trancheManager.shouldIsolateTranche(tranche, priceAt5PercentLoss); - - // Test at 4% loss (should NOT isolate) - const priceAt4PercentLoss = TEST_ENTRY_PRICE * 0.96; - const shouldNotIsolate = trancheManager.shouldIsolateTranche(tranche, priceAt4PercentLoss); - - if (shouldIsolate && !shouldNotIsolate) { - console.log('โœ… Isolation threshold logic correct'); - console.log(` Entry: $${TEST_ENTRY_PRICE}`); - console.log(` At $${priceAt5PercentLoss} (5% loss): Should isolate = ${shouldIsolate} โœ“`); - console.log(` At $${priceAt4PercentLoss} (4% loss): Should isolate = ${shouldNotIsolate} โœ“\n`); - testsPassed++; - } else { - throw new Error(`Isolation logic failed: shouldIsolate=${shouldIsolate}, shouldNotIsolate=${shouldNotIsolate}`); - } - } catch (error) { - console.error('โŒ Isolation threshold test failed:', error); - testsFailed++; - } - - // Test 6: P&L Calculation - console.log('Test 6: Unrealized P&L Calculation'); - try { - const trancheManager = initializeTrancheManager(testConfig); - await trancheManager.initialize(); - - // Create LONG tranche at 50000 - const tranche = await trancheManager.createTranche({ - symbol: TEST_SYMBOL, - side: 'BUY', - positionSide: 'LONG', - entryPrice: TEST_ENTRY_PRICE, - quantity: TEST_QUANTITY, - marginUsed: TEST_MARGIN, - leverage: TEST_LEVERAGE, - orderId: 'test-order-004', - }); - - // Update P&L at 52000 (4% profit) - await trancheManager.updateUnrealizedPnl(TEST_SYMBOL, 52000); - - const updated = await getTranche(tranche.id); - const expectedPnl = (52000 - TEST_ENTRY_PRICE) * TEST_QUANTITY; // Should be ~$2 - - if (updated && Math.abs(updated.unrealizedPnl - expectedPnl) < 0.01) { - console.log('โœ… P&L calculation correct'); - console.log(` Entry: $${TEST_ENTRY_PRICE}, Current: $52000`); - console.log(` Expected P&L: $${expectedPnl.toFixed(2)}`); - console.log(` Actual P&L: $${updated.unrealizedPnl.toFixed(2)}\n`); - testsPassed++; - } else { - throw new Error(`P&L mismatch: expected ${expectedPnl}, got ${updated?.unrealizedPnl}`); - } - } catch (error) { - console.error('โŒ P&L calculation test failed:', error); - testsFailed++; - } - - // Test 7: Position Limits - console.log('Test 7: Position Limit Checks'); - try { - const trancheManager = initializeTrancheManager(testConfig); - await trancheManager.initialize(); - - // Get current active tranches - const existingTranches = trancheManager.getTranches(TEST_SYMBOL, 'LONG'); - const activeCount = existingTranches.filter(t => !t.isolated).length; - console.log(` Existing active tranches: ${activeCount}`); - - // Create tranches up to the limit - const maxTranches = testConfig.symbols[TEST_SYMBOL].maxTranches || 3; - const tranchesToCreate = Math.max(0, maxTranches - activeCount); - - for (let i = 0; i < tranchesToCreate; i++) { - await trancheManager.createTranche({ - symbol: TEST_SYMBOL, - side: 'BUY', - positionSide: 'LONG', - entryPrice: TEST_ENTRY_PRICE, - quantity: TEST_QUANTITY, - marginUsed: TEST_MARGIN, - leverage: TEST_LEVERAGE, - orderId: `test-order-limit-${i}`, - }); - } - - // Try to create one more (should be blocked) - const canOpen = trancheManager.canOpenNewTranche(TEST_SYMBOL, 'LONG'); - - if (!canOpen.allowed && canOpen.reason?.includes('maxTranches')) { - console.log('โœ… Position limit enforcement correct'); - console.log(` Max tranches: ${maxTranches}`); - console.log(` Current active: ${maxTranches}`); - console.log(` Can open new: ${canOpen.allowed} โœ“`); - console.log(` Reason: ${canOpen.reason}\n`); - testsPassed++; - } else { - throw new Error(`Position limit not enforced: allowed=${canOpen.allowed}, reason=${canOpen.reason}`); - } - } catch (error) { - console.error('โŒ Position limit test failed:', error); - testsFailed++; - } - - // Test 8: Tranche Retrieval - console.log('Test 8: Tranche Retrieval'); - try { - const activeTranches = await getActiveTranches(TEST_SYMBOL, 'LONG'); - if (activeTranches.length > 0) { - console.log(`โœ… Retrieved ${activeTranches.length} active tranches for ${TEST_SYMBOL} LONG`); - console.log(` Sample: ${activeTranches[0].id.substring(0, 8)}... at $${activeTranches[0].entryPrice}\n`); - testsPassed++; - } else { - throw new Error('No active tranches found'); - } - } catch (error) { - console.error('โŒ Tranche retrieval test failed:', error); - testsFailed++; - } - - // Summary - console.log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•'); - console.log('๐Ÿ“Š Test Summary'); - console.log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•'); - console.log(`โœ… Tests Passed: ${testsPassed}`); - console.log(`โŒ Tests Failed: ${testsFailed}`); - console.log(`๐Ÿ“ˆ Success Rate: ${((testsPassed / (testsPassed + testsFailed)) * 100).toFixed(1)}%`); - console.log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•\n'); - - if (testsFailed === 0) { - console.log('๐ŸŽ‰ All tests passed! The multi-tranche system is ready for integration testing.'); - } else { - console.log('โš ๏ธ Some tests failed. Please review the errors above.'); - } - - // Cleanup - await db.close(); -} - -// Run tests -runTests().catch(error => { - console.error('๐Ÿ’ฅ Test suite crashed:', error); - process.exit(1); -}); From e1013f45bc86df6b72cc34c5f37fa17ef659c064 Mon Sep 17 00:00:00 2001 From: Crypto Gnome <33667144+CryptoGnome@users.noreply.github.com> Date: Sun, 12 Oct 2025 17:04:02 -0400 Subject: [PATCH 02/30] feat(auth): skip password requirement for default "admin" password - Treat default "admin" password as unauthenticated access - Update password check logic in both auth API and login page to exclude "admin" from requiring authentication - Remove minLength constraint from password input field - Add clarifying comments about default password handling This change improves user experience by allowing immediate access when using the default password, while still requiring authentication for custom passwords. --- src/app/api/auth/check/route.ts | 3 ++- src/app/login/page.tsx | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/app/api/auth/check/route.ts b/src/app/api/auth/check/route.ts index d6b3f9a..23e15ed 100644 --- a/src/app/api/auth/check/route.ts +++ b/src/app/api/auth/check/route.ts @@ -7,8 +7,9 @@ export async function GET() { const config = await configLoader.loadConfig(); const dashboardPassword = config.global?.server?.dashboardPassword; + // Only require password if it's set and not the default "admin" return NextResponse.json({ - passwordRequired: !!dashboardPassword && dashboardPassword.length > 0, + passwordRequired: !!dashboardPassword && dashboardPassword.length > 0 && dashboardPassword !== 'admin', }); } catch (error) { console.error('Failed to check auth status:', error); diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index dce5bbf..0664d6e 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -20,9 +20,10 @@ function LoginForm() { const { data: _session, status } = useSession(); const { config } = useConfig(); - // Check if password is configured + // Check if a custom password is configured (not the default "admin") const isPasswordConfigured = config?.global?.server?.dashboardPassword && - config.global.server.dashboardPassword.trim().length > 0; + config.global.server.dashboardPassword.trim().length > 0 && + config.global.server.dashboardPassword !== 'admin'; // Redirect if already authenticated useEffect(() => { @@ -108,7 +109,6 @@ function LoginForm() { onChange={(e) => setPassword(e.target.value)} required autoFocus - minLength={4} /> {password.length > 0 && password.length < 4 && !(password === 'admin' && !isPasswordConfigured) && (

From 4ed3f50096e9e116bdd5565140134f9fc5e71ae1 Mon Sep 17 00:00:00 2001 From: jameslappin Date: Tue, 18 Nov 2025 23:45:51 +1000 Subject: [PATCH 03/30] feat: add TradingView charts with liquidation database management Features: - Interactive TradingView charts with 7-day kline caching - Manual and auto-refresh (60s intervals) with optimized updates - Liquidation markers grouped by configurable time intervals - Position lines and VWAP indicator - Recent orders overlay on chart - Compact, modern chart controls UI Database Management: - Configurable liquidation data retention (default: 90 days) - Automated cleanup scheduler with configurable intervals - UI controls for database retention settings - Statistics tracking for stored liquidations Performance: - Smart caching system prevents redundant API calls - Only updates chart when data actually changes - Efficient incremental updates for new candles - Optimized state management to prevent unnecessary re-renders --- config.default.json | 4 + package-lock.json | 4281 +++++++++++++-------- package.json | 1 + src/app/api/klines/route.ts | 76 + src/app/api/liquidations/symbols/route.ts | 23 + src/app/api/orders/all/route.ts | 10 +- src/app/api/vwap/route.ts | 49 + src/app/page.tsx | 54 + src/bot/index.ts | 22 +- src/components/RecentOrdersTable.tsx | 4 +- src/components/SymbolConfigForm.tsx | 80 + src/components/TradingViewChart.tsx | 1094 ++++++ src/hooks/useBotStatus.ts | 4 + src/lib/klineCache.ts | 128 + src/lib/services/cleanupScheduler.ts | 18 +- src/lib/services/liquidationStorage.ts | 32 +- src/lib/types.ts | 1 + src/middleware.ts | 2 +- 18 files changed, 4322 insertions(+), 1561 deletions(-) create mode 100644 src/app/api/klines/route.ts create mode 100644 src/app/api/liquidations/symbols/route.ts create mode 100644 src/app/api/vwap/route.ts create mode 100644 src/components/TradingViewChart.tsx create mode 100644 src/lib/klineCache.ts diff --git a/config.default.json b/config.default.json index 56a4960..cfd2de5 100644 --- a/config.default.json +++ b/config.default.json @@ -46,6 +46,10 @@ "deduplicationWindowMs": 1000, "parallelProcessing": true, "maxConcurrentRequests": 3 + }, + "liquidationDatabase": { + "retentionDays": 90, + "cleanupIntervalHours": 24 } }, "version": "1.1.0" diff --git a/package-lock.json b/package-lock.json index ef8e956..3a2fc56 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "clsx": "^2.1.1", "ethers": "^6.15.0", "gsap": "^3.13.0", + "lightweight-charts": "^4.1.3", "lucide-react": "^0.544.0", "next": "15.5.4", "next-auth": "^4.24.11", @@ -72,18 +73,21 @@ "version": "4.4.4", "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@adraffy/ens-normalize": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", - "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==" + "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", + "license": "MIT" }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -96,6 +100,7 @@ "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", "dev": true, + "license": "MIT", "dependencies": { "@csstools/css-calc": "^2.1.3", "@csstools/css-color-parser": "^3.0.9", @@ -108,13 +113,15 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", @@ -125,31 +132,33 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", - "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", - "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", + "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.4", + "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.4", - "@babel/types": "^7.28.4", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -165,38 +174,27 @@ "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/core/node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "peer": true, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/@babel/core/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", "peer": true, "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/generator": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", - "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { - "@babel/parser": "^7.28.3", - "@babel/types": "^7.28.2", + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -210,6 +208,7 @@ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@babel/compat-data": "^7.27.2", @@ -222,38 +221,23 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "peer": true, - "dependencies": { - "yallist": "^3.0.2" - } - }, "node_modules/@babel/helper-compilation-targets/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", "peer": true, "bin": { "semver": "bin/semver.js" } }, - "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "peer": true - }, "node_modules/@babel/helper-globals": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=6.9.0" @@ -264,6 +248,7 @@ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@babel/traverse": "^7.27.1", @@ -278,6 +263,7 @@ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@babel/helper-module-imports": "^7.27.1", @@ -296,6 +282,7 @@ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=6.9.0" @@ -306,16 +293,18 @@ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -325,6 +314,7 @@ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=6.9.0" @@ -335,6 +325,7 @@ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@babel/template": "^7.27.2", @@ -345,13 +336,14 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", - "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { - "@babel/types": "^7.28.4" + "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" @@ -365,6 +357,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -378,6 +371,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -391,6 +385,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" @@ -404,6 +399,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" @@ -420,6 +416,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -436,6 +433,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" @@ -449,6 +447,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -462,6 +461,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -478,6 +478,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" @@ -491,6 +492,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -504,6 +506,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" @@ -517,6 +520,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -530,6 +534,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -543,6 +548,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -556,6 +562,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" @@ -572,6 +579,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" @@ -588,6 +596,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -603,6 +612,7 @@ "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -612,6 +622,7 @@ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", @@ -623,18 +634,19 @@ } }, "node_modules/@babel/traverse": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", - "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", + "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.4", + "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4", + "@babel/types": "^7.28.5", "debug": "^4.3.1" }, "engines": { @@ -642,14 +654,15 @@ } }, "node_modules/@babel/types": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", - "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -660,6 +673,7 @@ "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true, + "license": "MIT", "peer": true }, "node_modules/@csstools/color-helpers": { @@ -677,6 +691,7 @@ "url": "https://opencollective.com/csstools" } ], + "license": "MIT-0", "engines": { "node": ">=18" } @@ -696,6 +711,7 @@ "url": "https://opencollective.com/csstools" } ], + "license": "MIT", "engines": { "node": ">=18" }, @@ -719,6 +735,7 @@ "url": "https://opencollective.com/csstools" } ], + "license": "MIT", "dependencies": { "@csstools/color-helpers": "^5.1.0", "@csstools/css-calc": "^2.1.4" @@ -746,6 +763,7 @@ "url": "https://opencollective.com/csstools" } ], + "license": "MIT", "engines": { "node": ">=18" }, @@ -768,15 +786,17 @@ "url": "https://opencollective.com/csstools" } ], + "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/@emnapi/core": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz", - "integrity": "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", + "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==", "dev": true, + "license": "MIT", "optional": true, "dependencies": { "@emnapi/wasi-threads": "1.1.0", @@ -784,9 +804,10 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", - "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", + "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", + "license": "MIT", "optional": true, "dependencies": { "tslib": "^2.4.0" @@ -797,19 +818,21 @@ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", "dev": true, + "license": "MIT", "optional": true, "dependencies": { "tslib": "^2.4.0" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", - "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "aix" @@ -819,13 +842,14 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", - "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -835,13 +859,14 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", - "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -851,13 +876,14 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", - "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -867,13 +893,14 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", - "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -883,13 +910,14 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", - "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -899,13 +927,14 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", - "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -915,13 +944,14 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", - "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -931,13 +961,14 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", - "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -947,13 +978,14 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", - "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -963,13 +995,14 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", - "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -979,13 +1012,14 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", - "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", "cpu": [ "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -995,13 +1029,14 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", - "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", "cpu": [ "mips64el" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1011,13 +1046,14 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", - "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1027,13 +1063,14 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", - "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1043,13 +1080,14 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", - "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1059,13 +1097,14 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", - "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1075,13 +1114,14 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", - "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" @@ -1091,13 +1131,14 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", - "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" @@ -1107,13 +1148,14 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", - "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" @@ -1123,13 +1165,14 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", - "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" @@ -1139,13 +1182,14 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", - "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openharmony" @@ -1155,13 +1199,14 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", - "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "sunos" @@ -1171,13 +1216,14 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", - "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -1187,13 +1233,14 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", - "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -1203,13 +1250,14 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", - "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -1223,6 +1271,7 @@ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "dev": true, + "license": "MIT", "dependencies": { "eslint-visitor-keys": "^3.4.3" }, @@ -1241,6 +1290,7 @@ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -1249,21 +1299,23 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, + "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, "node_modules/@eslint/config-array": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", - "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.6", + "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -1272,19 +1324,24 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", - "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", - "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" }, @@ -1297,6 +1354,7 @@ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", "dev": true, + "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -1316,10 +1374,11 @@ } }, "node_modules/@eslint/js": { - "version": "9.36.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.36.0.tgz", - "integrity": "sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw==", + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -1328,21 +1387,23 @@ } }, "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", - "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.15.2", + "@eslint/core": "^0.17.0", "levn": "^0.4.1" }, "engines": { @@ -1353,6 +1414,7 @@ "version": "1.7.3", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", "dependencies": { "@floating-ui/utils": "^0.2.10" } @@ -1361,6 +1423,7 @@ "version": "1.7.4", "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" @@ -1370,6 +1433,7 @@ "version": "2.1.6", "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "license": "MIT", "dependencies": { "@floating-ui/dom": "^1.7.4" }, @@ -1381,12 +1445,14 @@ "node_modules/@floating-ui/utils": { "version": "0.2.10", "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", - "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==" + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "license": "MIT", "optional": true }, "node_modules/@humanfs/core": { @@ -1394,6 +1460,7 @@ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=18.18.0" } @@ -1403,6 +1470,7 @@ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" @@ -1416,6 +1484,7 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=12.22" }, @@ -1429,6 +1498,7 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=18.18" }, @@ -1441,18 +1511,20 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "license": "MIT", "optional": true, "engines": { "node": ">=18" } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.4.tgz", - "integrity": "sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", "cpu": [ "arm64" ], + "license": "Apache-2.0", "optional": true, "os": [ "darwin" @@ -1464,16 +1536,17 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.3" + "@img/sharp-libvips-darwin-arm64": "1.2.4" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.4.tgz", - "integrity": "sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", "cpu": [ "x64" ], + "license": "Apache-2.0", "optional": true, "os": [ "darwin" @@ -1485,16 +1558,17 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.3" + "@img/sharp-libvips-darwin-x64": "1.2.4" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.3.tgz", - "integrity": "sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", "cpu": [ "arm64" ], + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "darwin" @@ -1504,12 +1578,13 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.3.tgz", - "integrity": "sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", "cpu": [ "x64" ], + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "darwin" @@ -1519,12 +1594,13 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.3.tgz", - "integrity": "sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", "cpu": [ "arm" ], + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" @@ -1534,12 +1610,13 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.3.tgz", - "integrity": "sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", "cpu": [ "arm64" ], + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" @@ -1549,12 +1626,29 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.3.tgz", - "integrity": "sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", "cpu": [ "ppc64" ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" @@ -1564,12 +1658,13 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.3.tgz", - "integrity": "sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", "cpu": [ "s390x" ], + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" @@ -1579,12 +1674,13 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.3.tgz", - "integrity": "sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", "cpu": [ "x64" ], + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" @@ -1594,12 +1690,13 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.3.tgz", - "integrity": "sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", "cpu": [ "arm64" ], + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" @@ -1609,12 +1706,13 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.3.tgz", - "integrity": "sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", "cpu": [ "x64" ], + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" @@ -1624,12 +1722,13 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.4.tgz", - "integrity": "sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", "cpu": [ "arm" ], + "license": "Apache-2.0", "optional": true, "os": [ "linux" @@ -1641,16 +1740,17 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.3" + "@img/sharp-libvips-linux-arm": "1.2.4" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.4.tgz", - "integrity": "sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", "cpu": [ "arm64" ], + "license": "Apache-2.0", "optional": true, "os": [ "linux" @@ -1662,16 +1762,39 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.3" + "@img/sharp-libvips-linux-arm64": "1.2.4" } }, "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.4.tgz", - "integrity": "sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", "cpu": [ "ppc64" ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", "optional": true, "os": [ "linux" @@ -1683,16 +1806,17 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.3" + "@img/sharp-libvips-linux-riscv64": "1.2.4" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.4.tgz", - "integrity": "sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", "cpu": [ "s390x" ], + "license": "Apache-2.0", "optional": true, "os": [ "linux" @@ -1704,16 +1828,17 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.3" + "@img/sharp-libvips-linux-s390x": "1.2.4" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.4.tgz", - "integrity": "sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", "cpu": [ "x64" ], + "license": "Apache-2.0", "optional": true, "os": [ "linux" @@ -1725,16 +1850,17 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.3" + "@img/sharp-libvips-linux-x64": "1.2.4" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.4.tgz", - "integrity": "sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", "cpu": [ "arm64" ], + "license": "Apache-2.0", "optional": true, "os": [ "linux" @@ -1746,16 +1872,17 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.3" + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.4.tgz", - "integrity": "sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", "cpu": [ "x64" ], + "license": "Apache-2.0", "optional": true, "os": [ "linux" @@ -1767,19 +1894,20 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.3" + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.4.tgz", - "integrity": "sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", "cpu": [ "wasm32" ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.5.0" + "@emnapi/runtime": "^1.7.0" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -1789,12 +1917,13 @@ } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.4.tgz", - "integrity": "sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", "cpu": [ "arm64" ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ "win32" @@ -1807,12 +1936,13 @@ } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.4.tgz", - "integrity": "sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", "cpu": [ "ia32" ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ "win32" @@ -1825,12 +1955,13 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.4.tgz", - "integrity": "sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", "cpu": [ "x64" ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ "win32" @@ -1847,6 +1978,7 @@ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "dev": true, + "license": "ISC", "peer": true, "dependencies": { "string-width": "^5.1.2", @@ -1860,101 +1992,12 @@ "node": ">=12" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "peer": true, - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "dev": true, - "peer": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "peer": true, - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "dev": true, - "dependencies": { - "minipass": "^7.0.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", "dev": true, + "license": "ISC", "peer": true, "dependencies": { "camelcase": "^5.3.1", @@ -1972,6 +2015,7 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "sprintf-js": "~1.0.2" @@ -1982,6 +2026,7 @@ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "locate-path": "^5.0.0", @@ -1992,10 +2037,11 @@ } }, "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "argparse": "^1.0.7", @@ -2010,6 +2056,7 @@ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "p-locate": "^4.1.0" @@ -2023,6 +2070,7 @@ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "p-try": "^2.0.0" @@ -2039,6 +2087,7 @@ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "p-limit": "^2.2.0" @@ -2052,6 +2101,7 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=8" @@ -2062,6 +2112,7 @@ "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=8" @@ -2072,6 +2123,7 @@ "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.2.0.tgz", "integrity": "sha512-+O1ifRjkvYIkBqASKWgLxrpEhQAAE7hY77ALLUufSk5717KfOShg6IbqLmdsLMPdUiFvA2kTs0R7YZy+l0IzZQ==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@jest/types": "30.2.0", @@ -2090,6 +2142,7 @@ "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.2.0.tgz", "integrity": "sha512-03W6IhuhjqTlpzh/ojut/pDB2LPRygyWX8ExpgHtQA8H/3K7+1vKmcINx5UzeOX1se6YEsBsOHQ1CRzf3fOwTQ==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@jest/console": "30.2.0", @@ -2138,6 +2191,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=10" @@ -2146,110 +2200,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@jest/core/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "peer": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@jest/core/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "peer": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@jest/core/node_modules/jest-config": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.2.0.tgz", - "integrity": "sha512-g4WkyzFQVWHtu6uqGmQR4CQxz/CH3yDSlhzXMWzNjDx843gYjReZnMRanjRCq5XZFuQrGDxgUaiYWE8BRfVckA==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/core": "^7.27.4", - "@jest/get-type": "30.1.0", - "@jest/pattern": "30.0.1", - "@jest/test-sequencer": "30.2.0", - "@jest/types": "30.2.0", - "babel-jest": "30.2.0", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "deepmerge": "^4.3.1", - "glob": "^10.3.10", - "graceful-fs": "^4.2.11", - "jest-circus": "30.2.0", - "jest-docblock": "30.2.0", - "jest-environment-node": "30.2.0", - "jest-regex-util": "30.0.1", - "jest-resolve": "30.2.0", - "jest-runner": "30.2.0", - "jest-util": "30.2.0", - "jest-validate": "30.2.0", - "micromatch": "^4.0.8", - "parse-json": "^5.2.0", - "pretty-format": "30.2.0", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "@types/node": "*", - "esbuild-register": ">=3.4.0", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "esbuild-register": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/@jest/core/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "peer": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@jest/core/node_modules/pretty-format": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@jest/schemas": "30.0.5", @@ -2265,6 +2221,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true, + "license": "MIT", "peer": true }, "node_modules/@jest/diff-sequences": { @@ -2272,6 +2229,7 @@ "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", "dev": true, + "license": "MIT", "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } @@ -2281,6 +2239,7 @@ "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz", "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==", "dev": true, + "license": "MIT", "dependencies": { "@jest/fake-timers": "30.2.0", "@jest/types": "30.2.0", @@ -2296,6 +2255,7 @@ "resolved": "https://registry.npmjs.org/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.2.0.tgz", "integrity": "sha512-kazxw2L9IPuZpQ0mEt9lu9Z98SqR74xcagANmMBU16X0lS23yPc0+S6hGLUz8kVRlomZEs/5S/Zlpqwf5yu6OQ==", "dev": true, + "license": "MIT", "dependencies": { "@jest/environment": "30.2.0", "@jest/fake-timers": "30.2.0", @@ -2323,6 +2283,7 @@ "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.2.0.tgz", "integrity": "sha512-V9yxQK5erfzx99Sf+7LbhBwNWEZ9eZay8qQ9+JSC0TrMR1pMDHLMY+BnVPacWU6Jamrh252/IKo4F1Xn/zfiqA==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "expect": "30.2.0", @@ -2337,6 +2298,7 @@ "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.2.0.tgz", "integrity": "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==", "dev": true, + "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0" }, @@ -2349,6 +2311,7 @@ "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz", "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==", "dev": true, + "license": "MIT", "dependencies": { "@jest/types": "30.2.0", "@sinonjs/fake-timers": "^13.0.0", @@ -2366,6 +2329,7 @@ "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", "dev": true, + "license": "MIT", "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } @@ -2375,6 +2339,7 @@ "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.2.0.tgz", "integrity": "sha512-b63wmnKPaK+6ZZfpYhz9K61oybvbI1aMcIs80++JI1O1rR1vaxHUCNqo3ITu6NU0d4V34yZFoHMn/uoKr/Rwfw==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@jest/environment": "30.2.0", @@ -2391,6 +2356,7 @@ "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*", "jest-regex-util": "30.0.1" @@ -2404,6 +2370,7 @@ "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.2.0.tgz", "integrity": "sha512-DRyW6baWPqKMa9CzeiBjHwjd8XeAyco2Vt8XbcLFjiwCOEKOvy82GJ8QQnJE9ofsxCMPjH4MfH8fCWIHHDKpAQ==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@bcoe/v8-coverage": "^0.2.3", @@ -2442,58 +2409,12 @@ } } }, - "node_modules/@jest/reporters/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "peer": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@jest/reporters/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "peer": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@jest/reporters/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "peer": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@jest/schemas": { "version": "30.0.5", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", "dev": true, + "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.34.0" }, @@ -2506,6 +2427,7 @@ "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.2.0.tgz", "integrity": "sha512-0aVxM3RH6DaiLcjj/b0KrIBZhSX1373Xci4l3cW5xiUWPctZ59zQ7jj4rqcJQ/Z8JuN/4wX3FpJSa3RssVvCug==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@jest/types": "30.2.0", @@ -2522,6 +2444,7 @@ "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", @@ -2537,6 +2460,7 @@ "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.2.0.tgz", "integrity": "sha512-RF+Z+0CCHkARz5HT9mcQCBulb1wgCP3FBvl9VFokMX27acKphwyQsNuWH3c+ojd1LeWBLoTYoxF0zm6S/66mjg==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@jest/console": "30.2.0", @@ -2553,6 +2477,7 @@ "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.2.0.tgz", "integrity": "sha512-wXKgU/lk8fKXMu/l5Hog1R61bL4q5GCdT6OJvdAFz1P+QrpoFuLU68eoKuVc4RbrTtNnTL5FByhWdLgOPSph+Q==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@jest/test-result": "30.2.0", @@ -2569,6 +2494,7 @@ "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.2.0.tgz", "integrity": "sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@babel/core": "^7.27.4", @@ -2596,6 +2522,7 @@ "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", "dev": true, + "license": "MIT", "dependencies": { "@jest/pattern": "30.0.1", "@jest/schemas": "30.0.5", @@ -2614,6 +2541,7 @@ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" @@ -2624,6 +2552,7 @@ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" @@ -2634,6 +2563,7 @@ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.0.0" } @@ -2642,13 +2572,15 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -2659,6 +2591,7 @@ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", "dev": true, + "license": "MIT", "optional": true, "dependencies": { "@emnapi/core": "^1.4.3", @@ -2669,13 +2602,15 @@ "node_modules/@next/env": { "version": "15.5.4", "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.4.tgz", - "integrity": "sha512-27SQhYp5QryzIT5uO8hq99C69eLQ7qkzkDPsk3N+GuS2XgOgoYEeOav7Pf8Tn4drECOVDsDg8oj+/DVy8qQL2A==" + "integrity": "sha512-27SQhYp5QryzIT5uO8hq99C69eLQ7qkzkDPsk3N+GuS2XgOgoYEeOav7Pf8Tn4drECOVDsDg8oj+/DVy8qQL2A==", + "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { "version": "15.5.4", "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.4.tgz", "integrity": "sha512-SR1vhXNNg16T4zffhJ4TS7Xn7eq4NfKfcOsRwea7RIAHrjRpI9ALYbamqIJqkAhowLlERffiwk0FMvTLNdnVtw==", "dev": true, + "license": "MIT", "dependencies": { "fast-glob": "3.3.1" } @@ -2687,6 +2622,7 @@ "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "darwin" @@ -2702,6 +2638,7 @@ "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "darwin" @@ -2717,6 +2654,7 @@ "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -2732,6 +2670,7 @@ "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -2747,6 +2686,7 @@ "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -2762,6 +2702,7 @@ "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -2777,6 +2718,7 @@ "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -2792,6 +2734,7 @@ "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -2804,6 +2747,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "license": "MIT", "dependencies": { "@noble/hashes": "1.3.2" }, @@ -2815,6 +2759,7 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "license": "MIT", "engines": { "node": ">= 16" }, @@ -2827,6 +2772,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -2840,6 +2786,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } @@ -2849,6 +2796,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -2862,6 +2810,7 @@ "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", "dev": true, + "license": "MIT", "engines": { "node": ">=12.4.0" } @@ -2870,6 +2819,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "license": "ISC", "optional": true, "dependencies": { "@gar/promisify": "^1.0.1", @@ -2881,6 +2831,7 @@ "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", "deprecated": "This functionality has been moved to @npmcli/fs", + "license": "MIT", "optional": true, "dependencies": { "mkdirp": "^1.0.4", @@ -2899,11 +2850,24 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@pkgr/core": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": "^12.20.0 || ^14.18.0 || >=16.0.0" @@ -2915,12 +2879,14 @@ "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", - "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==" + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" }, "node_modules/@radix-ui/primitive": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", - "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==" + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" }, "node_modules/@radix-ui/react-alert-dialog": { "version": "1.1.15", @@ -2950,10 +2916,29 @@ } } }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-arrow": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, @@ -3036,6 +3021,7 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", @@ -3057,10 +3043,29 @@ } } }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -3075,6 +3080,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -3089,6 +3095,7 @@ "version": "1.1.15", "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", @@ -3120,10 +3127,14 @@ } } }, - "node_modules/@radix-ui/react-direction": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -3134,10 +3145,26 @@ } } }, - "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", @@ -3164,6 +3191,7 @@ "version": "2.1.16", "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", @@ -3192,6 +3220,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -3206,6 +3235,7 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", @@ -3230,6 +3260,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, @@ -3244,11 +3275,35 @@ } }, "node_modules/@radix-ui/react-label": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", - "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz", + "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", + "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.3" + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", @@ -3269,6 +3324,7 @@ "version": "2.1.16", "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", @@ -3304,10 +3360,29 @@ } } }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popover": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", @@ -3340,10 +3415,29 @@ } } }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popper": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", @@ -3375,6 +3469,7 @@ "version": "1.1.9", "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" @@ -3398,6 +3493,7 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" @@ -3421,6 +3517,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", "dependencies": { "@radix-ui/react-slot": "1.2.3" }, @@ -3439,13 +3536,70 @@ } } }, + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-progress": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", - "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.8.tgz", + "integrity": "sha512-+gISHcSPUJ7ktBy9RnTqbdKW78bcGke3t6taawyZ71pio1JewwGSJizycs7rLhGTvMJYCQB1DBK4KQsxs7U8dA==", + "license": "MIT", "dependencies": { - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3" + "@radix-ui/react-context": "1.1.3", + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-context": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.3.tgz", + "integrity": "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", @@ -3466,6 +3620,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", @@ -3496,6 +3651,7 @@ "version": "2.2.6", "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", @@ -3534,12 +3690,54 @@ } } }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-separator": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", - "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz", + "integrity": "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==", + "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.3" + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", @@ -3560,6 +3758,7 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", + "license": "MIT", "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", @@ -3589,9 +3788,9 @@ } }, "node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" @@ -3610,6 +3809,7 @@ "version": "1.2.6", "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", + "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", @@ -3638,6 +3838,7 @@ "version": "1.1.13", "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", @@ -3667,6 +3868,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", @@ -3696,10 +3898,29 @@ } } }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -3714,6 +3935,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" @@ -3732,6 +3954,7 @@ "version": "0.0.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, @@ -3749,6 +3972,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, @@ -3766,6 +3990,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -3780,6 +4005,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -3794,6 +4020,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", "dependencies": { "@radix-ui/rect": "1.1.1" }, @@ -3811,6 +4038,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, @@ -3828,6 +4056,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, @@ -3849,31 +4078,36 @@ "node_modules/@radix-ui/rect": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", - "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==" + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@rushstack/eslint-patch": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.12.0.tgz", - "integrity": "sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==", - "dev": true + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.15.0.tgz", + "integrity": "sha512-ojSshQPKwVvSMR8yT2L/QtUkV5SXi/IfDiJ4/8d6UbTPjiHVmxZzUAzGD8Tzks1b9+qQkZa0isUOvYObedITaw==", + "dev": true, + "license": "MIT" }, "node_modules/@sinclair/typebox": { "version": "0.34.41", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@sinonjs/commons": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "type-detect": "4.0.8" } @@ -3883,6 +4117,7 @@ "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^3.0.1" } @@ -3891,61 +4126,60 @@ "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", "dependencies": { "tslib": "^2.8.0" } }, "node_modules/@tailwindcss/node": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.13.tgz", - "integrity": "sha512-eq3ouolC1oEFOAvOMOBAmfCIqZBJuvWvvYWh5h5iOYfe1HFC6+GZ6EIL0JdM3/niGRJmnrOc+8gl9/HGUaaptw==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.17.tgz", + "integrity": "sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", - "jiti": "^2.5.1", - "lightningcss": "1.30.1", - "magic-string": "^0.30.18", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.21", "source-map-js": "^1.2.1", - "tailwindcss": "4.1.13" + "tailwindcss": "4.1.17" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.13.tgz", - "integrity": "sha512-CPgsM1IpGRa880sMbYmG1s4xhAy3xEt1QULgTJGQmZUeNgXFR7s1YxYygmJyBGtou4SyEosGAGEeYqY7R53bIA==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.17.tgz", + "integrity": "sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==", "dev": true, - "hasInstallScript": true, - "dependencies": { - "detect-libc": "^2.0.4", - "tar": "^7.4.3" - }, + "license": "MIT", "engines": { "node": ">= 10" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.13", - "@tailwindcss/oxide-darwin-arm64": "4.1.13", - "@tailwindcss/oxide-darwin-x64": "4.1.13", - "@tailwindcss/oxide-freebsd-x64": "4.1.13", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.13", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.13", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.13", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.13", - "@tailwindcss/oxide-linux-x64-musl": "4.1.13", - "@tailwindcss/oxide-wasm32-wasi": "4.1.13", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.13", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.13" + "@tailwindcss/oxide-android-arm64": "4.1.17", + "@tailwindcss/oxide-darwin-arm64": "4.1.17", + "@tailwindcss/oxide-darwin-x64": "4.1.17", + "@tailwindcss/oxide-freebsd-x64": "4.1.17", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.17", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.17", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.17", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.17", + "@tailwindcss/oxide-linux-x64-musl": "4.1.17", + "@tailwindcss/oxide-wasm32-wasi": "4.1.17", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.17", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.17" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.13.tgz", - "integrity": "sha512-BrpTrVYyejbgGo57yc8ieE+D6VT9GOgnNdmh5Sac6+t0m+v+sKQevpFVpwX3pBrM2qKrQwJ0c5eDbtjouY/+ew==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.17.tgz", + "integrity": "sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -3955,13 +4189,14 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.13.tgz", - "integrity": "sha512-YP+Jksc4U0KHcu76UhRDHq9bx4qtBftp9ShK/7UGfq0wpaP96YVnnjFnj3ZFrUAjc5iECzODl/Ts0AN7ZPOANQ==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.17.tgz", + "integrity": "sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -3971,13 +4206,14 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.13.tgz", - "integrity": "sha512-aAJ3bbwrn/PQHDxCto9sxwQfT30PzyYJFG0u/BWZGeVXi5Hx6uuUOQEI2Fa43qvmUjTRQNZnGqe9t0Zntexeuw==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.17.tgz", + "integrity": "sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -3987,13 +4223,14 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.13.tgz", - "integrity": "sha512-Wt8KvASHwSXhKE/dJLCCWcTSVmBj3xhVhp/aF3RpAhGeZ3sVo7+NTfgiN8Vey/Fi8prRClDs6/f0KXPDTZE6nQ==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.17.tgz", + "integrity": "sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -4003,13 +4240,14 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.13.tgz", - "integrity": "sha512-mbVbcAsW3Gkm2MGwA93eLtWrwajz91aXZCNSkGTx/R5eb6KpKD5q8Ueckkh9YNboU8RH7jiv+ol/I7ZyQ9H7Bw==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.17.tgz", + "integrity": "sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -4019,13 +4257,14 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.13.tgz", - "integrity": "sha512-wdtfkmpXiwej/yoAkrCP2DNzRXCALq9NVLgLELgLim1QpSfhQM5+ZxQQF8fkOiEpuNoKLp4nKZ6RC4kmeFH0HQ==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.17.tgz", + "integrity": "sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -4035,13 +4274,14 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.13.tgz", - "integrity": "sha512-hZQrmtLdhyqzXHB7mkXfq0IYbxegaqTmfa1p9MBj72WPoDD3oNOh1Lnxf6xZLY9C3OV6qiCYkO1i/LrzEdW2mg==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.17.tgz", + "integrity": "sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -4051,13 +4291,14 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.13.tgz", - "integrity": "sha512-uaZTYWxSXyMWDJZNY1Ul7XkJTCBRFZ5Fo6wtjrgBKzZLoJNrG+WderJwAjPzuNZOnmdrVg260DKwXCFtJ/hWRQ==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.17.tgz", + "integrity": "sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -4067,13 +4308,14 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.13.tgz", - "integrity": "sha512-oXiPj5mi4Hdn50v5RdnuuIms0PVPI/EG4fxAfFiIKQh5TgQgX7oSuDWntHW7WNIi/yVLAiS+CRGW4RkoGSSgVQ==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.17.tgz", + "integrity": "sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -4083,9 +4325,9 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.13.tgz", - "integrity": "sha512-+LC2nNtPovtrDwBc/nqnIKYh/W2+R69FA0hgoeOn64BdCX522u19ryLh3Vf3F8W49XBcMIxSe665kwy21FkhvA==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.17.tgz", + "integrity": "sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -4098,27 +4340,29 @@ "wasm32" ], "dev": true, + "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.4.5", - "@emnapi/runtime": "^1.4.5", - "@emnapi/wasi-threads": "^1.0.4", - "@napi-rs/wasm-runtime": "^0.2.12", - "@tybys/wasm-util": "^0.10.0", - "tslib": "^2.8.0" + "@emnapi/core": "^1.6.0", + "@emnapi/runtime": "^1.6.0", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.0.7", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.13.tgz", - "integrity": "sha512-dziTNeQXtoQ2KBXmrjCxsuPk3F3CQ/yb7ZNZNA+UkNTeiTGgfeh+gH5Pi7mRncVgcPD2xgHvkFCh/MhZWSgyQg==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.17.tgz", + "integrity": "sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -4128,13 +4372,14 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.13.tgz", - "integrity": "sha512-3+LKesjXydTkHk5zXX01b5KMzLV1xl2mcktBJkje7rhFUpUlYJy7IMOLqjIRQncLTa1WZZiFY/foAeB5nmaiTw==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.17.tgz", + "integrity": "sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -4144,16 +4389,17 @@ } }, "node_modules/@tailwindcss/postcss": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.13.tgz", - "integrity": "sha512-HLgx6YSFKJT7rJqh9oJs/TkBFhxuMOfUKSBEPYwV+t78POOBsdQ7crhZLzwcH3T0UyUuOzU/GK5pk5eKr3wCiQ==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.17.tgz", + "integrity": "sha512-+nKl9N9mN5uJ+M7dBOOCzINw94MPstNR/GtIhz1fpZysxL/4a+No64jCBD6CPN+bIHWFx3KWuu8XJRrj/572Dw==", "dev": true, + "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.1.13", - "@tailwindcss/oxide": "4.1.13", + "@tailwindcss/node": "4.1.17", + "@tailwindcss/oxide": "4.1.17", "postcss": "^8.4.41", - "tailwindcss": "4.1.13" + "tailwindcss": "4.1.17" } }, "node_modules/@testing-library/dom": { @@ -4161,6 +4407,7 @@ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", @@ -4176,21 +4423,12 @@ "node": ">=18" } }, - "node_modules/@testing-library/dom/node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "dev": true, - "peer": true, - "dependencies": { - "dequal": "^2.0.3" - } - }, "node_modules/@testing-library/jest-dom": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.8.0.tgz", - "integrity": "sha512-WgXcWzVM6idy5JaftTVC8Vs83NKRmGJz4Hqs4oyOuO2J4r/y79vvKZsb+CaGyCSEbUPI6OsewfPd0G1A0/TUZQ==", + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", "dev": true, + "license": "MIT", "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", @@ -4209,13 +4447,15 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@testing-library/react": { "version": "16.3.0", "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", "dev": true, + "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.5" }, @@ -4242,6 +4482,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "license": "MIT", "optional": true, "engines": { "node": ">= 6" @@ -4252,6 +4493,7 @@ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", "dev": true, + "license": "MIT", "optional": true, "dependencies": { "tslib": "^2.4.0" @@ -4262,6 +4504,7 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, + "license": "MIT", "peer": true }, "node_modules/@types/babel__core": { @@ -4269,6 +4512,7 @@ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@babel/parser": "^7.20.7", @@ -4283,6 +4527,7 @@ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@babel/types": "^7.0.0" @@ -4293,6 +4538,7 @@ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@babel/parser": "^7.1.0", @@ -4304,6 +4550,7 @@ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@babel/types": "^7.28.2" @@ -4312,22 +4559,26 @@ "node_modules/@types/d3-array": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", - "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==" + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" }, "node_modules/@types/d3-color": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", - "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==" + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" }, "node_modules/@types/d3-ease": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", - "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==" + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" }, "node_modules/@types/d3-interpolate": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", "dependencies": { "@types/d3-color": "*" } @@ -4335,12 +4586,14 @@ "node_modules/@types/d3-path": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", - "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==" + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" }, "node_modules/@types/d3-scale": { "version": "4.0.9", "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", "dependencies": { "@types/d3-time": "*" } @@ -4349,6 +4602,7 @@ "version": "3.1.7", "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", "dependencies": { "@types/d3-path": "*" } @@ -4356,30 +4610,35 @@ "node_modules/@types/d3-time": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", - "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==" + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" }, "node_modules/@types/d3-timer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", - "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==" + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/istanbul-lib-report": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", "dev": true, + "license": "MIT", "dependencies": { "@types/istanbul-lib-coverage": "*" } @@ -4389,6 +4648,7 @@ "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", "dev": true, + "license": "MIT", "dependencies": { "@types/istanbul-lib-report": "*" } @@ -4398,6 +4658,7 @@ "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", "dev": true, + "license": "MIT", "dependencies": { "expect": "^30.0.0", "pretty-format": "^30.0.0" @@ -4408,6 +4669,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -4420,6 +4682,7 @@ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", "dev": true, + "license": "MIT", "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", @@ -4433,13 +4696,15 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/jsdom": { "version": "21.1.7", "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz", "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*", "@types/tough-cookie": "*", @@ -4450,39 +4715,44 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/node": { - "version": "20.19.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.17.tgz", - "integrity": "sha512-gfehUI8N1z92kygssiuWvLiwcbOB3IRktR6hTDgJlXMYh5OvkPSRmgfoBUmfZt+vhwJtX7v1Yw4KvvAf7c5QKQ==", + "version": "20.19.25", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz", + "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==", "dev": true, + "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, "node_modules/@types/react": { - "version": "19.1.13", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.13.tgz", - "integrity": "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==", + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.5.tgz", + "integrity": "sha512-keKxkZMqnDicuvFoJbzrhbtdLSPhj/rZThDlKWCDbgXmUg0rEUFtRssDXKYmtXluZlIqiC5VqkCgRwzuyLHKHw==", "devOptional": true, + "license": "MIT", "dependencies": { "csstype": "^3.0.2" } }, "node_modules/@types/react-dom": { - "version": "19.1.9", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.9.tgz", - "integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==", + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, + "license": "MIT", "peerDependencies": { - "@types/react": "^19.0.0" + "@types/react": "^19.2.0" } }, "node_modules/@types/sqlite3": { @@ -4490,6 +4760,7 @@ "resolved": "https://registry.npmjs.org/@types/sqlite3/-/sqlite3-5.1.0.tgz", "integrity": "sha512-w25Gd6OzcN0Sb6g/BO7cyee0ugkiLgonhgGYfG+H0W9Ub6PUsC2/4R+KXy2tc80faPIWO3Qytbvr8gP1fU4siA==", "deprecated": "This is a stub types definition. sqlite3 provides its own type definitions, so you do not need this installed.", + "license": "MIT", "dependencies": { "sqlite3": "*" } @@ -4498,19 +4769,22 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/tough-cookie": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/uuid": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-11.0.0.tgz", "integrity": "sha512-HVyk8nj2m+jcFRNazzqyVKiZezyhDKrGUA3jlEcg/nZ6Ms+qHwocba1Y/AaVaznJTAM9xpdFSh+ptbNrhOGvZA==", "deprecated": "This is a stub types definition. uuid provides its own type definitions, so you do not need this installed.", + "license": "MIT", "dependencies": { "uuid": "*" } @@ -4520,15 +4794,17 @@ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/yargs": { - "version": "17.0.33", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", - "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", "dev": true, + "license": "MIT", "dependencies": { "@types/yargs-parser": "*" } @@ -4537,19 +4813,21 @@ "version": "21.0.3", "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.44.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.44.1.tgz", - "integrity": "sha512-molgphGqOBT7t4YKCSkbasmu1tb1MgrZ2szGzHbclF7PNmOkSTQVHy+2jXOSnxvR3+Xe1yySHFZoqMpz3TfQsw==", + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.47.0.tgz", + "integrity": "sha512-fe0rz9WJQ5t2iaLfdbDc9T80GJy0AeO453q8C3YCilnGozvOyCG5t+EZtg7j7D88+c3FipfP/x+wzGnh1xp8ZA==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.44.1", - "@typescript-eslint/type-utils": "8.44.1", - "@typescript-eslint/utils": "8.44.1", - "@typescript-eslint/visitor-keys": "8.44.1", + "@typescript-eslint/scope-manager": "8.47.0", + "@typescript-eslint/type-utils": "8.47.0", + "@typescript-eslint/utils": "8.47.0", + "@typescript-eslint/visitor-keys": "8.47.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -4563,7 +4841,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.44.1", + "@typescript-eslint/parser": "^8.47.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -4573,20 +4851,22 @@ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.44.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.44.1.tgz", - "integrity": "sha512-EHrrEsyhOhxYt8MTg4zTF+DJMuNBzWwgvvOYNj/zm1vnaD/IC5zCXFehZv94Piqa2cRFfXrTFxIvO95L7Qc/cw==", + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.47.0.tgz", + "integrity": "sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.44.1", - "@typescript-eslint/types": "8.44.1", - "@typescript-eslint/typescript-estree": "8.44.1", - "@typescript-eslint/visitor-keys": "8.44.1", + "@typescript-eslint/scope-manager": "8.47.0", + "@typescript-eslint/types": "8.47.0", + "@typescript-eslint/typescript-estree": "8.47.0", + "@typescript-eslint/visitor-keys": "8.47.0", "debug": "^4.3.4" }, "engines": { @@ -4602,13 +4882,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.44.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.44.1.tgz", - "integrity": "sha512-ycSa60eGg8GWAkVsKV4E6Nz33h+HjTXbsDT4FILyL8Obk5/mx4tbvCNsLf9zret3ipSumAOG89UcCs/KRaKYrA==", + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.47.0.tgz", + "integrity": "sha512-2X4BX8hUeB5JcA1TQJ7GjcgulXQ+5UkNb0DL8gHsHUHdFoiCTJoYLTpib3LtSDPZsRET5ygN4qqIWrHyYIKERA==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.44.1", - "@typescript-eslint/types": "^8.44.1", + "@typescript-eslint/tsconfig-utils": "^8.47.0", + "@typescript-eslint/types": "^8.47.0", "debug": "^4.3.4" }, "engines": { @@ -4623,13 +4904,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.44.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.44.1.tgz", - "integrity": "sha512-NdhWHgmynpSvyhchGLXh+w12OMT308Gm25JoRIyTZqEbApiBiQHD/8xgb6LqCWCFcxFtWwaVdFsLPQI3jvhywg==", + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.47.0.tgz", + "integrity": "sha512-a0TTJk4HXMkfpFkL9/WaGTNuv7JWfFTQFJd6zS9dVAjKsojmv9HT55xzbEpnZoY+VUb+YXLMp+ihMLz/UlZfDg==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.44.1", - "@typescript-eslint/visitor-keys": "8.44.1" + "@typescript-eslint/types": "8.47.0", + "@typescript-eslint/visitor-keys": "8.47.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4640,10 +4922,11 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.44.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.44.1.tgz", - "integrity": "sha512-B5OyACouEjuIvof3o86lRMvyDsFwZm+4fBOqFHccIctYgBjqR3qT39FBYGN87khcgf0ExpdCBeGKpKRhSFTjKQ==", + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.47.0.tgz", + "integrity": "sha512-ybUAvjy4ZCL11uryalkKxuT3w3sXJAuWhOoGS3T/Wu+iUu1tGJmk5ytSY8gbdACNARmcYEB0COksD2j6hfGK2g==", "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -4656,14 +4939,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.44.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.44.1.tgz", - "integrity": "sha512-KdEerZqHWXsRNKjF9NYswNISnFzXfXNDfPxoTh7tqohU/PRIbwTmsjGK6V9/RTYWau7NZvfo52lgVk+sJh0K3g==", + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.47.0.tgz", + "integrity": "sha512-QC9RiCmZ2HmIdCEvhd1aJELBlD93ErziOXXlHEZyuBo3tBiAZieya0HLIxp+DoDWlsQqDawyKuNEhORyku+P8A==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.44.1", - "@typescript-eslint/typescript-estree": "8.44.1", - "@typescript-eslint/utils": "8.44.1", + "@typescript-eslint/types": "8.47.0", + "@typescript-eslint/typescript-estree": "8.47.0", + "@typescript-eslint/utils": "8.47.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -4680,10 +4964,11 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.44.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.44.1.tgz", - "integrity": "sha512-Lk7uj7y9uQUOEguiDIDLYLJOrYHQa7oBiURYVFqIpGxclAFQ78f6VUOM8lI2XEuNOKNB7XuvM2+2cMXAoq4ALQ==", + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.47.0.tgz", + "integrity": "sha512-nHAE6bMKsizhA2uuYZbEbmp5z2UpffNrPEqiKIeN7VsV6UY/roxanWfoRrf6x/k9+Obf+GQdkm0nPU+vnMXo9A==", "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -4693,15 +4978,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.44.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.44.1.tgz", - "integrity": "sha512-qnQJ+mVa7szevdEyvfItbO5Vo+GfZ4/GZWWDRRLjrxYPkhM+6zYB2vRYwCsoJLzqFCdZT4mEqyJoyzkunsZ96A==", + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.47.0.tgz", + "integrity": "sha512-k6ti9UepJf5NpzCjH31hQNLHQWupTRPhZ+KFF8WtTuTpy7uHPfeg2NM7cP27aCGajoEplxJDFVCEm9TGPYyiVg==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.44.1", - "@typescript-eslint/tsconfig-utils": "8.44.1", - "@typescript-eslint/types": "8.44.1", - "@typescript-eslint/visitor-keys": "8.44.1", + "@typescript-eslint/project-service": "8.47.0", + "@typescript-eslint/tsconfig-utils": "8.47.0", + "@typescript-eslint/types": "8.47.0", + "@typescript-eslint/visitor-keys": "8.47.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -4725,6 +5011,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -4734,6 +5021,7 @@ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -4750,6 +5038,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -4762,6 +5051,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -4773,15 +5063,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.44.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.44.1.tgz", - "integrity": "sha512-DpX5Fp6edTlocMCwA+mHY8Mra+pPjRZ0TfHkXI8QFelIKcbADQz1LUPNtzOFUriBB2UYqw4Pi9+xV4w9ZczHFg==", + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.47.0.tgz", + "integrity": "sha512-g7XrNf25iL4TJOiPqatNuaChyqt49a/onq5YsJ9+hXeugK+41LVg7AxikMfM02PC6jbNtZLCJj6AUcQXJS/jGQ==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.44.1", - "@typescript-eslint/types": "8.44.1", - "@typescript-eslint/typescript-estree": "8.44.1" + "@typescript-eslint/scope-manager": "8.47.0", + "@typescript-eslint/types": "8.47.0", + "@typescript-eslint/typescript-estree": "8.47.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4796,12 +5087,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.44.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.44.1.tgz", - "integrity": "sha512-576+u0QD+Jp3tZzvfRfxon0EA2lzcDt3lhUbsC6Lgzy9x2VR4E+JUiNyGHi5T8vk0TV+fpJ5GLG1JsJuWCaKhw==", + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.47.0.tgz", + "integrity": "sha512-SIV3/6eftCy1bNzCQoPmbWsRLujS8t5iDIZ4spZOBHqrM+yfX2ogg8Tt3PDTAVKw3sSCiUgg30uOAvK2r9zGjQ==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.44.1", + "@typescript-eslint/types": "8.47.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -4817,6 +5109,7 @@ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "dev": true, + "license": "ISC", "peer": true }, "node_modules/@unrs/resolver-binding-android-arm-eabi": { @@ -4827,6 +5120,7 @@ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -4840,6 +5134,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -4853,6 +5148,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -4866,6 +5162,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -4879,6 +5176,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -4892,6 +5190,7 @@ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -4905,6 +5204,7 @@ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -4918,6 +5218,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -4931,6 +5232,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -4944,6 +5246,7 @@ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -4957,6 +5260,7 @@ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -4970,6 +5274,7 @@ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -4983,6 +5288,7 @@ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -4996,6 +5302,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -5009,6 +5316,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -5022,6 +5330,7 @@ "wasm32" ], "dev": true, + "license": "MIT", "optional": true, "dependencies": { "@napi-rs/wasm-runtime": "^0.2.11" @@ -5038,6 +5347,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -5051,6 +5361,7 @@ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -5064,6 +5375,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -5073,6 +5385,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC", "optional": true }, "node_modules/acorn": { @@ -5080,6 +5393,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -5092,6 +5406,7 @@ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, + "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -5099,24 +5414,24 @@ "node_modules/aes-js": { "version": "4.0.0-beta.5", "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", - "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==" + "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", + "license": "MIT" }, "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "optional": true, - "dependencies": { - "debug": "4" - }, + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", "engines": { - "node": ">= 6.0.0" + "node": ">= 14" } }, "node_modules/agentkeepalive": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", "optional": true, "dependencies": { "humanize-ms": "^1.2.1" @@ -5129,6 +5444,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "license": "MIT", "optional": true, "dependencies": { "clean-stack": "^2.0.0", @@ -5143,6 +5459,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -5159,6 +5476,7 @@ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "type-fest": "^0.21.3" @@ -5175,6 +5493,7 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "devOptional": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -5184,6 +5503,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -5199,6 +5519,7 @@ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, + "license": "ISC", "peer": true, "dependencies": { "normalize-path": "^3.0.0", @@ -5212,6 +5533,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "license": "ISC", "optional": true }, "node_modules/are-we-there-yet": { @@ -5219,6 +5541,7 @@ "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", "deprecated": "This package is no longer supported.", + "license": "ISC", "optional": true, "dependencies": { "delegates": "^1.0.0", @@ -5232,12 +5555,14 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "dev": true, + "license": "Python-2.0" }, "node_modules/aria-hidden": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", "dependencies": { "tslib": "^2.0.0" }, @@ -5246,12 +5571,13 @@ } }, "node_modules/aria-query": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, - "engines": { - "node": ">= 0.4" + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" } }, "node_modules/array-buffer-byte-length": { @@ -5259,6 +5585,7 @@ "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" @@ -5275,6 +5602,7 @@ "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", @@ -5297,6 +5625,7 @@ "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -5317,6 +5646,7 @@ "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", @@ -5338,6 +5668,7 @@ "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", @@ -5356,6 +5687,7 @@ "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", @@ -5374,6 +5706,7 @@ "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -5390,6 +5723,7 @@ "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", "dev": true, + "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", @@ -5410,13 +5744,15 @@ "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -5424,13 +5760,15 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, + "license": "MIT", "dependencies": { "possible-typed-array-names": "^1.0.0" }, @@ -5442,18 +5780,20 @@ } }, "node_modules/axe-core": { - "version": "4.10.3", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz", - "integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.0.tgz", + "integrity": "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==", "dev": true, + "license": "MPL-2.0", "engines": { "node": ">=4" } }, "node_modules/axios": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", - "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", @@ -5465,6 +5805,7 @@ "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">= 0.4" } @@ -5474,6 +5815,7 @@ "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz", "integrity": "sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@jest/transform": "30.2.0", @@ -5496,7 +5838,11 @@ "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", "dev": true, + "license": "BSD-3-Clause", "peer": true, + "workspaces": [ + "test/babel-8" + ], "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@istanbuljs/load-nyc-config": "^1.0.0", @@ -5513,6 +5859,7 @@ "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.2.0.tgz", "integrity": "sha512-ftzhzSGMUnOzcCXd6WHdBGMyuwy15Wnn0iyyWGKgBDLxf9/s5ABuraCSpBX2uG0jUg4rqJnxsLc5+oYBqoxVaA==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@types/babel__core": "^7.20.5" @@ -5526,6 +5873,7 @@ "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", @@ -5553,6 +5901,7 @@ "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.2.0.tgz", "integrity": "sha512-US4Z3NOieAQumwFnYdUWKvUKh8+YSnS/gB3t6YBiz0bskpu7Pine8pPCheNxlPEW4wnUkma2a94YuW2q3guvCQ==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "babel-plugin-jest-hoist": "30.2.0", @@ -5569,7 +5918,8 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "devOptional": true + "devOptional": true, + "license": "MIT" }, "node_modules/base64-js": { "version": "1.5.1", @@ -5588,13 +5938,15 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.8.tgz", - "integrity": "sha512-be0PUaPsQX/gPWWgFsdD+GFzaoig5PXaUC1xLkQiYdDnANU8sMnHoQd8JhbJQuvTWrWLyeFN9Imb5Qtfvr4RrQ==", + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.29.tgz", + "integrity": "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA==", "dev": true, + "license": "Apache-2.0", "peer": true, "bin": { "baseline-browser-mapping": "dist/cli.js" @@ -5618,6 +5970,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", "dependencies": { "file-uri-to-path": "1.0.0" } @@ -5626,6 +5979,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", @@ -5637,6 +5991,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "devOptional": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5647,6 +6002,7 @@ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, + "license": "MIT", "dependencies": { "fill-range": "^7.1.1" }, @@ -5655,9 +6011,9 @@ } }, "node_modules/browserslist": { - "version": "4.26.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz", - "integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", + "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", "dev": true, "funding": [ { @@ -5673,13 +6029,14 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "peer": true, "dependencies": { - "baseline-browser-mapping": "^2.8.3", - "caniuse-lite": "^1.0.30001741", - "electron-to-chromium": "^1.5.218", - "node-releases": "^2.0.21", - "update-browserslist-db": "^1.1.3" + "baseline-browser-mapping": "^2.8.25", + "caniuse-lite": "^1.0.30001754", + "electron-to-chromium": "^1.5.249", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.1.4" }, "bin": { "browserslist": "cli.js" @@ -5693,6 +6050,7 @@ "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", "dev": true, + "license": "MIT", "dependencies": { "fast-json-stable-stringify": "2.x" }, @@ -5705,6 +6063,7 @@ "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", "dev": true, + "license": "Apache-2.0", "peer": true, "dependencies": { "node-int64": "^0.4.0" @@ -5728,6 +6087,7 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" @@ -5738,12 +6098,14 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true, + "license": "MIT", "peer": true }, "node_modules/cacache": { "version": "15.3.0", "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "license": "ISC", "optional": true, "dependencies": { "@npmcli/fs": "^1.0.0", @@ -5769,62 +6131,50 @@ "node": ">= 10" } }, - "node_modules/cacache/node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "optional": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/cacache/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "node_modules/cacache/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", "optional": true, "dependencies": { - "yallist": "^4.0.0" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, "engines": { - "node": ">=8" + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/cacache/node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "node_modules/cacache/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", "optional": true, "dependencies": { - "minipass": "^3.0.0", "yallist": "^4.0.0" }, "engines": { - "node": ">= 8" + "node": ">=10" } }, - "node_modules/cacache/node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "node_modules/cacache/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", "optional": true, "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", "yallist": "^4.0.0" }, - "engines": { - "node": ">=10" - } - }, - "node_modules/cacache/node_modules/tar/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "optional": true, "engines": { "node": ">=8" } @@ -5833,6 +6183,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", "optional": true }, "node_modules/call-bind": { @@ -5840,6 +6191,7 @@ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "dev": true, + "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", @@ -5857,6 +6209,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" @@ -5870,6 +6223,7 @@ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "dev": true, + "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" @@ -5886,6 +6240,7 @@ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -5895,15 +6250,16 @@ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=6" } }, "node_modules/caniuse-lite": { - "version": "1.0.30001745", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001745.tgz", - "integrity": "sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ==", + "version": "1.0.30001755", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001755.tgz", + "integrity": "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA==", "funding": [ { "type": "opencollective", @@ -5917,13 +6273,15 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -5935,29 +6293,43 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/char-regex": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=10" } }, "node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "dev": true, + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", "engines": { - "node": ">=18" + "node": ">=10" } }, "node_modules/ci-info": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", - "integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", "dev": true, "funding": [ { @@ -5965,21 +6337,24 @@ "url": "https://github.com/sponsors/sibiraj-s" } ], + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/cjs-module-lexer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.1.0.tgz", - "integrity": "sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.1.1.tgz", + "integrity": "sha512-+CmxIZ/L2vNcEfvNtLdU0ZQ6mbq3FZnwAP2PPTiKP+1QOoKwlKlPgb8UKV0Dds7QVaMnHm+FwSft2VB0s/SLjQ==", "dev": true, + "license": "MIT", "peer": true }, "node_modules/class-variance-authority": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", "dependencies": { "clsx": "^2.1.1" }, @@ -5991,6 +6366,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "license": "MIT", "optional": true, "engines": { "node": ">=6" @@ -5999,13 +6375,15 @@ "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", - "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, + "license": "ISC", "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -6015,30 +6393,86 @@ "node": ">=12" } }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "engines": { - "node": ">=6" - } + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" }, - "node_modules/co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, - "peer": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "peer": true, "engines": { "iojs": ">= 1.0.0", "node": ">= 0.12.0" } }, "node_modules/collect-v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", - "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", "dev": true, + "license": "MIT", "peer": true }, "node_modules/color-convert": { @@ -6046,6 +6480,7 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -6057,12 +6492,14 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/color-support": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", "optional": true, "bin": { "color-support": "bin.js" @@ -6072,6 +6509,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" }, @@ -6083,13 +6521,15 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "devOptional": true + "devOptional": true, + "license": "MIT" }, "node_modules/concurrently": { "version": "9.2.1", "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", "dev": true, + "license": "MIT", "dependencies": { "chalk": "4.1.2", "rxjs": "7.8.2", @@ -6109,25 +6549,11 @@ "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" } }, - "node_modules/concurrently/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, "node_modules/console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC", "optional": true }, "node_modules/convert-source-map": { @@ -6135,6 +6561,7 @@ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true, + "license": "MIT", "peer": true }, "node_modules/cookie": { @@ -6151,6 +6578,7 @@ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -6164,13 +6592,15 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/cssstyle": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", "dev": true, + "license": "MIT", "dependencies": { "@asamuzakjp/css-color": "^3.2.0", "rrweb-cssom": "^0.8.0" @@ -6180,14 +6610,16 @@ } }, "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" }, "node_modules/d3-array": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", "dependencies": { "internmap": "1 - 2" }, @@ -6199,6 +6631,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", "engines": { "node": ">=12" } @@ -6207,6 +6640,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", "engines": { "node": ">=12" } @@ -6215,6 +6649,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", "engines": { "node": ">=12" } @@ -6223,6 +6658,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", "dependencies": { "d3-color": "1 - 3" }, @@ -6234,6 +6670,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", "engines": { "node": ">=12" } @@ -6242,6 +6679,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", @@ -6257,6 +6695,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", "dependencies": { "d3-path": "^3.1.0" }, @@ -6268,6 +6707,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", "dependencies": { "d3-array": "2 - 3" }, @@ -6279,6 +6719,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", "dependencies": { "d3-time": "1 - 3" }, @@ -6290,6 +6731,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", "engines": { "node": ">=12" } @@ -6298,13 +6740,15 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", - "dev": true + "dev": true, + "license": "BSD-2-Clause" }, "node_modules/data-urls": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", "dev": true, + "license": "MIT", "dependencies": { "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.0.0" @@ -6318,6 +6762,7 @@ "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", @@ -6335,6 +6780,7 @@ "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", @@ -6352,6 +6798,7 @@ "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -6369,6 +6816,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "devOptional": true, + "license": "MIT", "dependencies": { "ms": "^2.1.3" }, @@ -6385,17 +6833,20 @@ "version": "10.6.0", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/decimal.js-light": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", - "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==" + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", "dependencies": { "mimic-response": "^3.1.0" }, @@ -6411,6 +6862,7 @@ "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==", "dev": true, + "license": "MIT", "peer": true, "peerDependencies": { "babel-plugin-macros": "^3.1.0" @@ -6425,6 +6877,7 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", "engines": { "node": ">=4.0.0" } @@ -6433,13 +6886,15 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=0.10.0" @@ -6450,6 +6905,7 @@ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dev": true, + "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -6467,6 +6923,7 @@ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "dev": true, + "license": "MIT", "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", @@ -6483,6 +6940,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", "engines": { "node": ">=0.4.0" } @@ -6491,6 +6949,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT", "optional": true }, "node_modules/dequal": { @@ -6498,15 +6957,16 @@ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", "dev": true, - "peer": true, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/detect-libc": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.1.tgz", - "integrity": "sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", "engines": { "node": ">=8" } @@ -6516,6 +6976,7 @@ "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=8" @@ -6524,13 +6985,15 @@ "node_modules/detect-node-es": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dev": true, + "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" }, @@ -6543,12 +7006,14 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, + "license": "MIT", "peer": true }, "node_modules/dom-helpers": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" @@ -6558,6 +7023,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", @@ -6572,13 +7038,15 @@ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true, + "license": "MIT", "peer": true }, "node_modules/electron-to-chromium": { - "version": "1.5.227", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.227.tgz", - "integrity": "sha512-ITxuoPfJu3lsNWUi2lBM2PaBPYgH3uqmxut5vmBxgYvyI4AlJ6P3Cai1O76mOrkJCBzq0IxWg/NtqOrpu/0gKA==", + "version": "1.5.255", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.255.tgz", + "integrity": "sha512-Z9oIp4HrFF/cZkDPMpz2XSuVpc1THDpT4dlmATFlJUIBVCy9Vap5/rIXsASP1CscBacBqhabwh8vLctqBwEerQ==", "dev": true, + "license": "ISC", "peer": true }, "node_modules/emittery": { @@ -6586,6 +7054,7 @@ "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=12" @@ -6598,12 +7067,14 @@ "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/encoding": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "license": "MIT", "optional": true, "dependencies": { "iconv-lite": "^0.6.2" @@ -6613,6 +7084,7 @@ "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", "dependencies": { "once": "^1.4.0" } @@ -6622,6 +7094,7 @@ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", "dev": true, + "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" @@ -6635,6 +7108,7 @@ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=0.12" }, @@ -6646,6 +7120,7 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", "optional": true, "engines": { "node": ">=6" @@ -6655,6 +7130,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "license": "MIT", "optional": true }, "node_modules/error-ex": { @@ -6662,6 +7138,7 @@ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "is-arrayish": "^0.2.1" @@ -6672,6 +7149,7 @@ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", "dev": true, + "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", @@ -6739,6 +7217,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -6747,6 +7226,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -6756,6 +7236,7 @@ "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -6782,6 +7263,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0" }, @@ -6793,6 +7275,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", @@ -6808,6 +7291,7 @@ "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", "dev": true, + "license": "MIT", "dependencies": { "hasown": "^2.0.2" }, @@ -6820,6 +7304,7 @@ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", "dev": true, + "license": "MIT", "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", @@ -6833,11 +7318,12 @@ } }, "node_modules/esbuild": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", - "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "dev": true, "hasInstallScript": true, + "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, @@ -6845,32 +7331,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.10", - "@esbuild/android-arm": "0.25.10", - "@esbuild/android-arm64": "0.25.10", - "@esbuild/android-x64": "0.25.10", - "@esbuild/darwin-arm64": "0.25.10", - "@esbuild/darwin-x64": "0.25.10", - "@esbuild/freebsd-arm64": "0.25.10", - "@esbuild/freebsd-x64": "0.25.10", - "@esbuild/linux-arm": "0.25.10", - "@esbuild/linux-arm64": "0.25.10", - "@esbuild/linux-ia32": "0.25.10", - "@esbuild/linux-loong64": "0.25.10", - "@esbuild/linux-mips64el": "0.25.10", - "@esbuild/linux-ppc64": "0.25.10", - "@esbuild/linux-riscv64": "0.25.10", - "@esbuild/linux-s390x": "0.25.10", - "@esbuild/linux-x64": "0.25.10", - "@esbuild/netbsd-arm64": "0.25.10", - "@esbuild/netbsd-x64": "0.25.10", - "@esbuild/openbsd-arm64": "0.25.10", - "@esbuild/openbsd-x64": "0.25.10", - "@esbuild/openharmony-arm64": "0.25.10", - "@esbuild/sunos-x64": "0.25.10", - "@esbuild/win32-arm64": "0.25.10", - "@esbuild/win32-ia32": "0.25.10", - "@esbuild/win32-x64": "0.25.10" + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" } }, "node_modules/escalade": { @@ -6878,6 +7364,7 @@ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -6887,6 +7374,7 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -6895,24 +7383,24 @@ } }, "node_modules/eslint": { - "version": "9.36.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.36.0.tgz", - "integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==", + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.1", - "@eslint/core": "^0.15.2", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.36.0", - "@eslint/plugin-kit": "^0.3.5", + "@eslint/js": "9.39.1", + "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", @@ -6959,6 +7447,7 @@ "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.5.4.tgz", "integrity": "sha512-BzgVVuT3kfJes8i2GHenC1SRJ+W3BTML11lAOYFOOPzrk2xp66jBOAGEFRw+3LkYCln5UzvFsLhojrshb5Zfaw==", "dev": true, + "license": "MIT", "dependencies": { "@next/eslint-plugin-next": "15.5.4", "@rushstack/eslint-patch": "^1.10.3", @@ -6986,6 +7475,7 @@ "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", "dev": true, + "license": "MIT", "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.13.0", @@ -6997,6 +7487,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, + "license": "MIT", "dependencies": { "ms": "^2.1.1" } @@ -7006,6 +7497,7 @@ "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", "dev": true, + "license": "ISC", "dependencies": { "@nolyfill/is-core-module": "1.0.39", "debug": "^4.4.0", @@ -7040,6 +7532,7 @@ "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", "dev": true, + "license": "MIT", "dependencies": { "debug": "^3.2.7" }, @@ -7057,6 +7550,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, + "license": "MIT", "dependencies": { "ms": "^2.1.1" } @@ -7066,6 +7560,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, + "license": "MIT", "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -7099,6 +7594,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, + "license": "MIT", "dependencies": { "ms": "^2.1.1" } @@ -7108,6 +7604,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" } @@ -7117,6 +7614,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", "dev": true, + "license": "MIT", "dependencies": { "aria-query": "^5.3.2", "array-includes": "^3.1.8", @@ -7141,11 +7639,22 @@ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eslint-plugin-react": { "version": "7.37.5", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", "dev": true, + "license": "MIT", "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", @@ -7178,6 +7687,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -7190,6 +7700,7 @@ "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", "dev": true, + "license": "MIT", "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -7207,6 +7718,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" } @@ -7216,6 +7728,7 @@ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -7232,6 +7745,7 @@ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -7244,6 +7758,7 @@ "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", @@ -7261,6 +7776,7 @@ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true, + "license": "BSD-2-Clause", "peer": true, "bin": { "esparse": "bin/esparse.js", @@ -7275,6 +7791,7 @@ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" }, @@ -7287,6 +7804,7 @@ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, @@ -7299,6 +7817,7 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } @@ -7308,6 +7827,7 @@ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" } @@ -7326,6 +7846,7 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "license": "MIT", "dependencies": { "@adraffy/ens-normalize": "1.10.1", "@noble/curves": "1.2.0", @@ -7343,6 +7864,7 @@ "version": "22.7.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "license": "MIT", "dependencies": { "undici-types": "~6.19.2" } @@ -7350,17 +7872,20 @@ "node_modules/ethers/node_modules/tslib": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", - "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "license": "0BSD" }, "node_modules/ethers/node_modules/undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "license": "MIT" }, "node_modules/ethers/node_modules/ws": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -7380,13 +7905,15 @@ "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "cross-spawn": "^7.0.3", @@ -7406,11 +7933,20 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC", + "peer": true + }, "node_modules/exit-x": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">= 0.8.0" @@ -7420,6 +7956,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", "engines": { "node": ">=6" } @@ -7429,6 +7966,7 @@ "resolved": "https://registry.npmjs.org/expect/-/expect-30.2.0.tgz", "integrity": "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==", "dev": true, + "license": "MIT", "dependencies": { "@jest/expect-utils": "30.2.0", "@jest/get-type": "30.1.0", @@ -7441,16 +7979,24 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/fancy-canvas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fancy-canvas/-/fancy-canvas-2.1.0.tgz", + "integrity": "sha512-nifxXJ95JNLFR2NgRV4/MxVP45G9909wJTEKz5fg/TZS20JJZA6hfgRVh/bC9bwl2zBtBNcYPjiBE4njQHVBwQ==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fast-equals": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.3.0.tgz", - "integrity": "sha512-xwP+dG/in/nJelMOUEQBiIYeOoHKihWPB2sNZ8ZeDbZFoGb1OwTGMggGRgg6CRitNx7kmHgtIz2dOHDQ8Ap7Bw==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.3.3.tgz", + "integrity": "sha512-/boTcHZeIAQ2r/tL11voclBHDeP9WPxLt+tyAbVSyyXuUFyh0Tne7gJZTqGbxnvj79TjLdCXLOY7UIPhyG5MTw==", + "license": "MIT", "engines": { "node": ">=6.0.0" } @@ -7460,6 +8006,7 @@ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -7476,6 +8023,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -7487,19 +8035,22 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", "dev": true, + "license": "ISC", "dependencies": { "reusify": "^1.0.4" } @@ -7509,6 +8060,7 @@ "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", "dev": true, + "license": "Apache-2.0", "peer": true, "dependencies": { "bser": "2.1.1" @@ -7519,6 +8071,7 @@ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, + "license": "MIT", "dependencies": { "flat-cache": "^4.0.0" }, @@ -7529,13 +8082,15 @@ "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -7548,6 +8103,7 @@ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, + "license": "MIT", "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -7564,6 +8120,7 @@ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, + "license": "MIT", "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" @@ -7576,7 +8133,8 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/follow-redirects": { "version": "1.15.11", @@ -7588,6 +8146,7 @@ "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, @@ -7602,6 +8161,7 @@ "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "dev": true, + "license": "MIT", "dependencies": { "is-callable": "^1.2.7" }, @@ -7617,6 +8177,7 @@ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "dev": true, + "license": "ISC", "peer": true, "dependencies": { "cross-spawn": "^7.0.6", @@ -7629,23 +8190,11 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "peer": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -7660,12 +8209,14 @@ "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" }, "node_modules/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", "dependencies": { "minipass": "^3.0.0" }, @@ -7677,6 +8228,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", "dependencies": { "yallist": "^4.0.0" }, @@ -7687,13 +8239,15 @@ "node_modules/fs-minipass/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "devOptional": true + "devOptional": true, + "license": "ISC" }, "node_modules/fsevents": { "version": "2.3.3", @@ -7701,6 +8255,7 @@ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -7713,6 +8268,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -7722,6 +8278,7 @@ "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -7742,6 +8299,7 @@ "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -7751,6 +8309,7 @@ "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", "deprecated": "This package is no longer supported.", + "license": "ISC", "optional": true, "dependencies": { "aproba": "^1.0.3 || ^2.0.0", @@ -7766,11 +8325,64 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/gauge/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", + "optional": true + }, + "node_modules/gauge/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC", + "optional": true + }, + "node_modules/gauge/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/gauge/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=6.9.0" @@ -7781,6 +8393,7 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, + "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -7789,6 +8402,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", @@ -7812,6 +8426,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", "engines": { "node": ">=6" } @@ -7821,6 +8436,7 @@ "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=8.0.0" @@ -7830,6 +8446,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" @@ -7843,6 +8460,7 @@ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=10" @@ -7856,6 +8474,7 @@ "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", @@ -7869,10 +8488,11 @@ } }, "node_modules/get-tsconfig": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", - "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", "dev": true, + "license": "MIT", "dependencies": { "resolve-pkg-maps": "^1.0.0" }, @@ -7883,24 +8503,26 @@ "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" }, "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "devOptional": true, + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "peer": true, "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, - "engines": { - "node": "*" + "bin": { + "glob": "dist/esm/bin.mjs" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -7911,6 +8533,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.3" }, @@ -7918,11 +8541,40 @@ "node": ">=10.13.0" } }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -7935,6 +8587,7 @@ "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "dev": true, + "license": "MIT", "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" @@ -7950,6 +8603,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -7961,24 +8615,28 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "devOptional": true + "devOptional": true, + "license": "ISC" }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/gsap": { "version": "3.13.0", "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.13.0.tgz", - "integrity": "sha512-QL7MJ2WMjm1PHWsoFrAQH/J8wUeqZvMtHO58qdekHpCfhvhSL4gSiz6vJf5EeMP0LOn3ZCprL2ki/gjED8ghVw==" + "integrity": "sha512-QL7MJ2WMjm1PHWsoFrAQH/J8wUeqZvMtHO58qdekHpCfhvhSL4gSiz6vJf5EeMP0LOn3ZCprL2ki/gjED8ghVw==", + "license": "Standard 'no charge' license: https://gsap.com/standard-license." }, "node_modules/handlebars": { "version": "4.7.8", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", "dev": true, + "license": "MIT", "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", @@ -8000,6 +8658,7 @@ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -8012,6 +8671,7 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -8021,6 +8681,7 @@ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, + "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" }, @@ -8033,6 +8694,7 @@ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", "dev": true, + "license": "MIT", "dependencies": { "dunder-proto": "^1.0.0" }, @@ -8047,6 +8709,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -8058,6 +8721,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" }, @@ -8072,12 +8736,14 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC", "optional": true }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -8090,6 +8756,7 @@ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", "dev": true, + "license": "MIT", "dependencies": { "whatwg-encoding": "^3.1.1" }, @@ -8102,39 +8769,42 @@ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true, + "license": "MIT", "peer": true }, "node_modules/http-cache-semantics": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause", "optional": true }, "node_modules/http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", - "optional": true, + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", "dependencies": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4" + "agent-base": "^7.1.0", + "debug": "^4.3.4" }, "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "optional": true, + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", "dependencies": { - "agent-base": "6", + "agent-base": "^7.1.2", "debug": "4" }, "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/human-signals": { @@ -8142,6 +8812,7 @@ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true, + "license": "Apache-2.0", "peer": true, "engines": { "node": ">=10.17.0" @@ -8151,6 +8822,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", "optional": true, "dependencies": { "ms": "^2.0.0" @@ -8161,6 +8833,7 @@ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "devOptional": true, + "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -8185,13 +8858,15 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "BSD-3-Clause" }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } @@ -8201,6 +8876,7 @@ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, + "license": "MIT", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -8217,6 +8893,7 @@ "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "pkg-dir": "^4.2.0", @@ -8237,6 +8914,7 @@ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "devOptional": true, + "license": "MIT", "engines": { "node": ">=0.8.19" } @@ -8246,6 +8924,7 @@ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "devOptional": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -8254,6 +8933,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "license": "ISC", "optional": true }, "node_modules/inflight": { @@ -8262,6 +8942,7 @@ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "devOptional": true, + "license": "ISC", "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -8270,18 +8951,21 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" }, "node_modules/ini": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", "dev": true, + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", @@ -8295,14 +8979,16 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", "engines": { "node": ">=12" } }, "node_modules/ip-address": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", - "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", "optional": true, "engines": { "node": ">= 12" @@ -8313,6 +8999,7 @@ "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -8330,6 +9017,7 @@ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "dev": true, + "license": "MIT", "peer": true }, "node_modules/is-async-function": { @@ -8337,6 +9025,7 @@ "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", "dev": true, + "license": "MIT", "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", @@ -8356,6 +9045,7 @@ "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", "dev": true, + "license": "MIT", "dependencies": { "has-bigints": "^1.0.2" }, @@ -8371,6 +9061,7 @@ "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" @@ -8387,6 +9078,7 @@ "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", "dev": true, + "license": "MIT", "dependencies": { "semver": "^7.7.1" } @@ -8396,6 +9088,7 @@ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -8408,6 +9101,7 @@ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "dev": true, + "license": "MIT", "dependencies": { "hasown": "^2.0.2" }, @@ -8423,6 +9117,7 @@ "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", @@ -8440,6 +9135,7 @@ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" @@ -8456,6 +9152,7 @@ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -8465,6 +9162,7 @@ "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3" }, @@ -8480,6 +9178,7 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "devOptional": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -8489,19 +9188,22 @@ "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=6" } }, "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", "dev": true, + "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" }, @@ -8517,6 +9219,7 @@ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, + "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -8528,6 +9231,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "license": "MIT", "optional": true }, "node_modules/is-map": { @@ -8535,6 +9239,7 @@ "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -8547,6 +9252,7 @@ "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -8559,6 +9265,7 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -8568,6 +9275,7 @@ "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" @@ -8583,13 +9291,15 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", @@ -8608,6 +9318,7 @@ "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -8620,6 +9331,7 @@ "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3" }, @@ -8635,6 +9347,7 @@ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=8" @@ -8648,6 +9361,7 @@ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" @@ -8664,6 +9378,7 @@ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", @@ -8681,6 +9396,7 @@ "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, + "license": "MIT", "dependencies": { "which-typed-array": "^1.1.16" }, @@ -8696,6 +9412,7 @@ "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -8708,6 +9425,7 @@ "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3" }, @@ -8723,6 +9441,7 @@ "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" @@ -8738,19 +9457,22 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "devOptional": true + "devOptional": true, + "license": "ISC" }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, + "license": "BSD-3-Clause", "peer": true, "engines": { "node": ">=8" @@ -8761,6 +9483,7 @@ "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", "dev": true, + "license": "BSD-3-Clause", "peer": true, "dependencies": { "@babel/core": "^7.23.9", @@ -8778,6 +9501,7 @@ "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, + "license": "BSD-3-Clause", "peer": true, "dependencies": { "istanbul-lib-coverage": "^3.0.0", @@ -8788,11 +9512,26 @@ "node": ">=10" } }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/istanbul-lib-source-maps": { "version": "5.0.6", "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", "dev": true, + "license": "BSD-3-Clause", "peer": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.23", @@ -8808,6 +9547,7 @@ "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", "dev": true, + "license": "BSD-3-Clause", "peer": true, "dependencies": { "html-escaper": "^2.0.0", @@ -8822,6 +9562,7 @@ "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", "dev": true, + "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", @@ -8839,6 +9580,7 @@ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "dev": true, + "license": "BlueOak-1.0.0", "peer": true, "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -8855,6 +9597,7 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz", "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@jest/core": "30.2.0", @@ -8882,6 +9625,7 @@ "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.2.0.tgz", "integrity": "sha512-L8lR1ChrRnSdfeOvTrwZMlnWV8G/LLjQ0nG9MBclwWZidA2N5FviRki0Bvh20WRMOX31/JYvzdqTJrk5oBdydQ==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "execa": "^5.1.1", @@ -8897,6 +9641,7 @@ "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.2.0.tgz", "integrity": "sha512-Fh0096NC3ZkFx05EP2OXCxJAREVxj1BcW/i6EWqqymcgYKWjyyDpral3fMxVcHXg6oZM7iULer9wGRFvfpl+Tg==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@jest/environment": "30.2.0", @@ -8929,6 +9674,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=10" @@ -8942,6 +9688,7 @@ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@jest/schemas": "30.0.5", @@ -8957,6 +9704,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true, + "license": "MIT", "peer": true }, "node_modules/jest-cli": { @@ -8964,6 +9712,7 @@ "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.2.0.tgz", "integrity": "sha512-Os9ukIvADX/A9sLt6Zse3+nmHtHaE6hqOsjQtNiugFTbKRHYIYtZXNGNK9NChseXy7djFPjndX1tL0sCTlfpAA==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@jest/core": "30.2.0", @@ -8992,55 +9741,12 @@ } } }, - "node_modules/jest-cli/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-cli/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "peer": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/jest-cli/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "peer": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/jest-cli/node_modules/jest-config": { + "node_modules/jest-config": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.2.0.tgz", "integrity": "sha512-g4WkyzFQVWHtu6uqGmQR4CQxz/CH3yDSlhzXMWzNjDx843gYjReZnMRanjRCq5XZFuQrGDxgUaiYWE8BRfVckA==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@babel/core": "^7.27.4", @@ -9088,27 +9794,26 @@ } } }, - "node_modules/jest-cli/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "node_modules/jest-config/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, + "license": "MIT", "peer": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/jest-cli/node_modules/pretty-format": { + "node_modules/jest-config/node_modules/pretty-format": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@jest/schemas": "30.0.5", @@ -9119,11 +9824,12 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-cli/node_modules/react-is": { + "node_modules/jest-config/node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true, + "license": "MIT", "peer": true }, "node_modules/jest-diff": { @@ -9131,6 +9837,7 @@ "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==", "dev": true, + "license": "MIT", "dependencies": { "@jest/diff-sequences": "30.0.1", "@jest/get-type": "30.1.0", @@ -9146,6 +9853,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -9158,6 +9866,7 @@ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", "dev": true, + "license": "MIT", "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", @@ -9171,13 +9880,15 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/jest-docblock": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.2.0.tgz", "integrity": "sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "detect-newline": "^3.1.0" @@ -9191,6 +9902,7 @@ "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.2.0.tgz", "integrity": "sha512-lpWlJlM7bCUf1mfmuqTA8+j2lNURW9eNafOy99knBM01i5CQeY5UH1vZjgT9071nDJac1M4XsbyI44oNOdhlDQ==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@jest/get-type": "30.1.0", @@ -9208,6 +9920,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=10" @@ -9221,6 +9934,7 @@ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@jest/schemas": "30.0.5", @@ -9236,6 +9950,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true, + "license": "MIT", "peer": true }, "node_modules/jest-environment-jsdom": { @@ -9243,6 +9958,7 @@ "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.2.0.tgz", "integrity": "sha512-zbBTiqr2Vl78pKp/laGBREYzbZx9ZtqPjOK4++lL4BNDhxRnahg51HtoDrk9/VjIy9IthNEWdKVd7H5bqBhiWQ==", "dev": true, + "license": "MIT", "dependencies": { "@jest/environment": "30.2.0", "@jest/environment-jsdom-abstract": "30.2.0", @@ -9267,6 +9983,7 @@ "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.2.0.tgz", "integrity": "sha512-ElU8v92QJ9UrYsKrxDIKCxu6PfNj4Hdcktcn0JX12zqNdqWHB0N+hwOnnBBXvjLd2vApZtuLUGs1QSY+MsXoNA==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@jest/environment": "30.2.0", @@ -9286,6 +10003,7 @@ "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz", "integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@jest/types": "30.2.0", @@ -9311,6 +10029,7 @@ "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.2.0.tgz", "integrity": "sha512-M6jKAjyzjHG0SrQgwhgZGy9hFazcudwCNovY/9HPIicmNSBuockPSedAP9vlPK6ONFJ1zfyH/M2/YYJxOz5cdQ==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@jest/get-type": "30.1.0", @@ -9325,6 +10044,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=10" @@ -9338,6 +10058,7 @@ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@jest/schemas": "30.0.5", @@ -9353,6 +10074,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true, + "license": "MIT", "peer": true }, "node_modules/jest-matcher-utils": { @@ -9360,6 +10082,7 @@ "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz", "integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==", "dev": true, + "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0", "chalk": "^4.1.2", @@ -9375,6 +10098,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -9387,6 +10111,7 @@ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", "dev": true, + "license": "MIT", "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", @@ -9400,13 +10125,15 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/jest-message-util": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", "dev": true, + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", "@jest/types": "30.2.0", @@ -9427,6 +10154,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -9439,6 +10167,7 @@ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", "dev": true, + "license": "MIT", "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", @@ -9452,13 +10181,15 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/jest-mock": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", "dev": true, + "license": "MIT", "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", @@ -9473,6 +10204,7 @@ "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=6" @@ -9491,6 +10223,7 @@ "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", "dev": true, + "license": "MIT", "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } @@ -9500,6 +10233,7 @@ "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.2.0.tgz", "integrity": "sha512-TCrHSxPlx3tBY3hWNtRQKbtgLhsXa1WmbJEqBlTBrGafd5fiQFByy2GNCEoGR+Tns8d15GaL9cxEzKOO3GEb2A==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "chalk": "^4.1.2", @@ -9520,6 +10254,7 @@ "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.2.0.tgz", "integrity": "sha512-xTOIGug/0RmIe3mmCqCT95yO0vj6JURrn1TKWlNbhiAefJRWINNPgwVkrVgt/YaerPzY3iItufd80v3lOrFJ2w==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "jest-regex-util": "30.0.1", @@ -9534,6 +10269,7 @@ "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.2.0.tgz", "integrity": "sha512-PqvZ2B2XEyPEbclp+gV6KO/F1FIFSbIwewRgmROCMBo/aZ6J1w8Qypoj2pEOcg3G2HzLlaP6VUtvwCI8dM3oqQ==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@jest/console": "30.2.0", @@ -9568,6 +10304,7 @@ "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.2.0.tgz", "integrity": "sha512-p1+GVX/PJqTucvsmERPMgCPvQJpFt4hFbM+VN3n8TMo47decMUcJbt+rgzwrEme0MQUA/R+1de2axftTHkKckg==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@jest/environment": "30.2.0", @@ -9597,68 +10334,12 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-runtime/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "peer": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/jest-runtime/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "peer": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/jest-runtime/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "peer": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/jest-runtime/node_modules/strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true, - "peer": true, - "engines": { - "node": ">=8" - } - }, "node_modules/jest-snapshot": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.2.0.tgz", "integrity": "sha512-5WEtTy2jXPFypadKNpbNkZ72puZCa6UjSr/7djeecHWOu7iYhSXSnHScT8wBz3Rn8Ena5d5RYRcsyKIeqG1IyA==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@babel/core": "^7.27.4", @@ -9692,6 +10373,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=10" @@ -9705,6 +10387,7 @@ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@jest/schemas": "30.0.5", @@ -9720,6 +10403,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true, + "license": "MIT", "peer": true }, "node_modules/jest-util": { @@ -9727,6 +10411,7 @@ "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", "dev": true, + "license": "MIT", "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", @@ -9744,6 +10429,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -9756,6 +10442,7 @@ "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.2.0.tgz", "integrity": "sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@jest/get-type": "30.1.0", @@ -9774,6 +10461,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=10" @@ -9787,6 +10475,7 @@ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=10" @@ -9800,6 +10489,7 @@ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@jest/schemas": "30.0.5", @@ -9815,6 +10505,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true, + "license": "MIT", "peer": true }, "node_modules/jest-watcher": { @@ -9822,6 +10513,7 @@ "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.2.0.tgz", "integrity": "sha512-PYxa28dxJ9g777pGm/7PrbnMeA0Jr7osHP9bS7eJy9DuAjMgdGtxgf0uKMyoIsTWAkIbUW5hSDdJ3urmgXBqxg==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@jest/test-result": "30.2.0", @@ -9842,6 +10534,7 @@ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@types/node": "*", @@ -9854,27 +10547,12 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "peer": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, "node_modules/jiti": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.0.tgz", - "integrity": "sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", "dev": true, + "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -9891,13 +10569,15 @@ "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -9910,6 +10590,7 @@ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, + "license": "MIT", "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -9944,46 +10625,12 @@ } } }, - "node_modules/jsdom/node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, - "node_modules/jsdom/node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/jsdom/node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, + "license": "MIT", "peer": true, "bin": { "jsesc": "bin/jsesc" @@ -9996,37 +10643,42 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true, + "license": "MIT", "peer": true }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, - "dependencies": { - "minimist": "^1.2.0" - }, + "license": "MIT", "bin": { "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" } }, "node_modules/jsx-ast-utils": { @@ -10034,6 +10686,7 @@ "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", "dev": true, + "license": "MIT", "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", @@ -10049,6 +10702,7 @@ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, + "license": "MIT", "dependencies": { "json-buffer": "3.0.1" } @@ -10057,13 +10711,15 @@ "version": "0.3.23", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", - "dev": true + "dev": true, + "license": "CC0-1.0" }, "node_modules/language-tags": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", "dev": true, + "license": "MIT", "dependencies": { "language-subtag-registry": "^0.3.20" }, @@ -10076,6 +10732,7 @@ "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=6" @@ -10086,6 +10743,7 @@ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, + "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -10095,10 +10753,11 @@ } }, "node_modules/lightningcss": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", - "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", "dev": true, + "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" }, @@ -10110,26 +10769,49 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-darwin-arm64": "1.30.1", - "lightningcss-darwin-x64": "1.30.1", - "lightningcss-freebsd-x64": "1.30.1", - "lightningcss-linux-arm-gnueabihf": "1.30.1", - "lightningcss-linux-arm64-gnu": "1.30.1", - "lightningcss-linux-arm64-musl": "1.30.1", - "lightningcss-linux-x64-gnu": "1.30.1", - "lightningcss-linux-x64-musl": "1.30.1", - "lightningcss-win32-arm64-msvc": "1.30.1", - "lightningcss-win32-x64-msvc": "1.30.1" + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", - "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", "cpu": [ "arm64" ], "dev": true, + "license": "MPL-2.0", "optional": true, "os": [ "darwin" @@ -10143,13 +10825,14 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", - "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", "cpu": [ "x64" ], "dev": true, + "license": "MPL-2.0", "optional": true, "os": [ "darwin" @@ -10163,13 +10846,14 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", - "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", "cpu": [ "x64" ], "dev": true, + "license": "MPL-2.0", "optional": true, "os": [ "freebsd" @@ -10183,13 +10867,14 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", - "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", "cpu": [ "arm" ], "dev": true, + "license": "MPL-2.0", "optional": true, "os": [ "linux" @@ -10203,13 +10888,14 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", - "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", "cpu": [ "arm64" ], "dev": true, + "license": "MPL-2.0", "optional": true, "os": [ "linux" @@ -10223,13 +10909,14 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", - "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", "cpu": [ "arm64" ], "dev": true, + "license": "MPL-2.0", "optional": true, "os": [ "linux" @@ -10243,13 +10930,14 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", - "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", "cpu": [ "x64" ], "dev": true, + "license": "MPL-2.0", "optional": true, "os": [ "linux" @@ -10263,13 +10951,14 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", - "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", "cpu": [ "x64" ], "dev": true, + "license": "MPL-2.0", "optional": true, "os": [ "linux" @@ -10283,13 +10972,14 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", - "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MPL-2.0", "optional": true, "os": [ "win32" @@ -10303,13 +10993,14 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", - "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", "cpu": [ "x64" ], "dev": true, + "license": "MPL-2.0", "optional": true, "os": [ "win32" @@ -10322,11 +11013,21 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lightweight-charts": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/lightweight-charts/-/lightweight-charts-4.2.3.tgz", + "integrity": "sha512-5kS/2hY3wNYNzhnS8Gb+GAS07DX8GPF2YVDnd2NMC85gJVQ6RLU6YrXNgNJ6eg0AnWPwCnvaGtYmGky3HiLQEw==", + "license": "Apache-2.0", + "dependencies": { + "fancy-canvas": "2.1.0" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true, + "license": "MIT", "peer": true }, "node_modules/locate-path": { @@ -10334,6 +11035,7 @@ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, + "license": "MIT", "dependencies": { "p-locate": "^5.0.0" }, @@ -10347,24 +11049,28 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -10373,25 +11079,21 @@ } }, "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "peer": true, "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" + "yallist": "^3.0.2" } }, - "node_modules/lru-cache/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, "node_modules/lucide-react": { "version": "0.544.0", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.544.0.tgz", "integrity": "sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw==", + "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } @@ -10401,16 +11103,18 @@ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, + "license": "MIT", "peer": true, "bin": { "lz-string": "bin/bin.js" } }, "node_modules/magic-string": { - "version": "0.30.19", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", - "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } @@ -10420,6 +11124,7 @@ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "semver": "^7.5.3" @@ -10435,12 +11140,14 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/make-fetch-happen": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "license": "ISC", "optional": true, "dependencies": { "agentkeepalive": "^4.1.3", @@ -10464,10 +11171,66 @@ "node": ">= 10" } }, + "node_modules/make-fetch-happen/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/make-fetch-happen/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/make-fetch-happen/node_modules/minipass": { "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", "optional": true, "dependencies": { "yallist": "^4.0.0" @@ -10480,6 +11243,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", "optional": true }, "node_modules/makeerror": { @@ -10487,6 +11251,7 @@ "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", "dev": true, + "license": "BSD-3-Clause", "peer": true, "dependencies": { "tmpl": "1.0.5" @@ -10496,6 +11261,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -10505,6 +11271,7 @@ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true, + "license": "MIT", "peer": true }, "node_modules/merge2": { @@ -10512,6 +11279,7 @@ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } @@ -10521,6 +11289,7 @@ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -10533,6 +11302,7 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -10541,6 +11311,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", "dependencies": { "mime-db": "1.52.0" }, @@ -10553,6 +11324,7 @@ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=6" @@ -10562,6 +11334,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -10574,6 +11347,7 @@ "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -10583,6 +11357,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "devOptional": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -10594,6 +11369,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -10603,6 +11379,8 @@ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true, + "license": "ISC", + "peer": true, "engines": { "node": ">=16 || 14 >=14.17" } @@ -10611,6 +11389,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "license": "ISC", "optional": true, "dependencies": { "minipass": "^3.0.0" @@ -10623,6 +11402,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", "optional": true, "dependencies": { "yallist": "^4.0.0" @@ -10635,12 +11415,14 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", "optional": true }, "node_modules/minipass-fetch": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "license": "MIT", "optional": true, "dependencies": { "minipass": "^3.1.0", @@ -10658,6 +11440,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", "optional": true, "dependencies": { "yallist": "^4.0.0" @@ -10666,29 +11449,18 @@ "node": ">=8" } }, - "node_modules/minipass-fetch/node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "optional": true, - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/minipass-fetch/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", "optional": true }, "node_modules/minipass-flush": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "license": "ISC", "optional": true, "dependencies": { "minipass": "^3.0.0" @@ -10701,6 +11473,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", "optional": true, "dependencies": { "yallist": "^4.0.0" @@ -10713,12 +11486,14 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", "optional": true }, "node_modules/minipass-pipeline": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "license": "ISC", "optional": true, "dependencies": { "minipass": "^3.0.0" @@ -10731,6 +11506,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", "optional": true, "dependencies": { "yallist": "^4.0.0" @@ -10743,12 +11519,14 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", "optional": true }, "node_modules/minipass-sized": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "license": "ISC", "optional": true, "dependencies": { "minipass": "^3.0.0" @@ -10761,6 +11539,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", "optional": true, "dependencies": { "yallist": "^4.0.0" @@ -10773,24 +11552,45 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", "optional": true }, "node_modules/minizlib": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", - "dev": true, + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", "dependencies": { - "minipass": "^7.1.2" + "yallist": "^4.0.0" }, "engines": { - "node": ">= 18" + "node": ">=8" } }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", "bin": { "mkdirp": "bin/cmd.js" }, @@ -10801,13 +11601,15 @@ "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "devOptional": true + "devOptional": true, + "license": "MIT" }, "node_modules/nanoid": { "version": "3.3.11", @@ -10819,6 +11621,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -10829,13 +11632,15 @@ "node_modules/napi-build-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==" + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" }, "node_modules/napi-postinstall": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.3.tgz", - "integrity": "sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", "dev": true, + "license": "MIT", "bin": { "napi-postinstall": "lib/cli.js" }, @@ -10850,12 +11655,14 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/negotiator": { "version": "0.6.4", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", "optional": true, "engines": { "node": ">= 0.6" @@ -10865,12 +11672,14 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/next": { "version": "15.5.4", "resolved": "https://registry.npmjs.org/next/-/next-15.5.4.tgz", "integrity": "sha512-xH4Yjhb82sFYQfY3vbkJfgSDgXvBB6a8xPs9i35k6oZJRoQRihZH+4s9Yo2qsWpzBmZ3lPXaJ2KPXLfkvW4LnA==", + "license": "MIT", "dependencies": { "@next/env": "15.5.4", "@swc/helpers": "0.5.15", @@ -10919,9 +11728,10 @@ } }, "node_modules/next-auth": { - "version": "4.24.11", - "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.11.tgz", - "integrity": "sha512-pCFXzIDQX7xmHFs4KVH4luCjaCbuPRtZ9oBUjUhOk84mZ9WVPf94n87TxYI4rSRf9HmfHEF8Yep3JrYDVOo3Cw==", + "version": "4.24.13", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.13.tgz", + "integrity": "sha512-sgObCfcfL7BzIK76SS5TnQtc3yo2Oifp/yIpfv6fMfeBOiBJkDWF3A2y9+yqnmJ4JKc2C+nMjSjmgDeTwgN1rQ==", + "license": "ISC", "dependencies": { "@babel/runtime": "^7.20.13", "@panva/hkdf": "^1.0.2", @@ -10934,9 +11744,9 @@ "uuid": "^8.3.2" }, "peerDependencies": { - "@auth/core": "0.34.2", - "next": "^12.2.5 || ^13 || ^14 || ^15", - "nodemailer": "^6.6.5", + "@auth/core": "0.34.3", + "next": "^12.2.5 || ^13 || ^14 || ^15 || ^16", + "nodemailer": "^7.0.7", "react": "^17.0.2 || ^18 || ^19", "react-dom": "^17.0.2 || ^18 || ^19" }, @@ -10962,6 +11772,7 @@ "version": "0.4.6", "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" @@ -10985,6 +11796,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", @@ -10995,9 +11807,10 @@ } }, "node_modules/node-abi": { - "version": "3.77.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.77.0.tgz", - "integrity": "sha512-DSmt0OEcLoK4i3NuscSbGjOf3bqiDEutejqENSplMSFA/gmB8mkED9G4pKWnPl7MDU4rSHebKPHeitpDfyH0cQ==", + "version": "3.85.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.85.0.tgz", + "integrity": "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==", + "license": "MIT", "dependencies": { "semver": "^7.3.5" }, @@ -11008,12 +11821,14 @@ "node_modules/node-addon-api": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==" + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" }, "node_modules/node-gyp": { "version": "8.4.1", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "license": "MIT", "optional": true, "dependencies": { "env-paths": "^2.2.0", @@ -11034,90 +11849,49 @@ "node": ">= 10.12.0" } }, - "node_modules/node-gyp/node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "optional": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-gyp/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/node-gyp/node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "optional": true, - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/node-gyp/node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "node_modules/node-gyp/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", "optional": true, "dependencies": { - "yallist": "^4.0.0" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, "engines": { - "node": ">=8" - } - }, - "node_modules/node-gyp/node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "optional": true, - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" + "node": "*" }, - "engines": { - "node": ">=10" + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/node-gyp/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "optional": true - }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", "dev": true, + "license": "MIT", "peer": true }, "node_modules/node-releases": { - "version": "2.0.21", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz", - "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==", + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "dev": true, + "license": "MIT", "peer": true }, "node_modules/nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", "optional": true, "dependencies": { "abbrev": "1" @@ -11134,6 +11908,7 @@ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=0.10.0" @@ -11144,6 +11919,7 @@ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "path-key": "^3.0.0" @@ -11157,6 +11933,7 @@ "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", "deprecated": "This package is no longer supported.", + "license": "ISC", "optional": true, "dependencies": { "are-we-there-yet": "^3.0.0", @@ -11172,7 +11949,8 @@ "version": "2.2.22", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.22.tgz", "integrity": "sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/oauth": { "version": "0.9.15", @@ -11184,6 +11962,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -11202,6 +11981,7 @@ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -11214,6 +11994,7 @@ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -11223,6 +12004,7 @@ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -11243,6 +12025,7 @@ "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", @@ -11258,6 +12041,7 @@ "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -11276,6 +12060,7 @@ "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -11290,6 +12075,7 @@ "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -11304,9 +12090,9 @@ } }, "node_modules/oidc-token-hash": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.1.1.tgz", - "integrity": "sha512-D7EmwxJV6DsEB6vOFLrBM2OzsVgQzgPWyHlV2OOAVj772n+WTXpudC9e9u5BVKQnYwaD30Ivhi9b+4UeBcGu9g==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.2.0.tgz", + "integrity": "sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw==", "license": "MIT", "engines": { "node": "^10.13.0 || >=12.0.0" @@ -11316,6 +12102,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", "dependencies": { "wrappy": "1" } @@ -11325,6 +12112,7 @@ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "mimic-fn": "^2.1.0" @@ -11351,11 +12139,30 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/openid-client/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/openid-client/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, + "license": "MIT", "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", @@ -11373,6 +12180,7 @@ "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", "dev": true, + "license": "MIT", "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", @@ -11390,6 +12198,7 @@ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, + "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" }, @@ -11405,6 +12214,7 @@ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, + "license": "MIT", "dependencies": { "p-limit": "^3.0.2" }, @@ -11419,6 +12229,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "license": "MIT", "optional": true, "dependencies": { "aggregate-error": "^3.0.0" @@ -11435,6 +12246,7 @@ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=6" @@ -11445,6 +12257,7 @@ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "dev": true, + "license": "BlueOak-1.0.0", "peer": true }, "node_modules/parent-module": { @@ -11452,6 +12265,7 @@ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, + "license": "MIT", "dependencies": { "callsites": "^3.0.0" }, @@ -11464,6 +12278,7 @@ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@babel/code-frame": "^7.0.0", @@ -11483,6 +12298,7 @@ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", "dev": true, + "license": "MIT", "dependencies": { "entities": "^6.0.0" }, @@ -11495,6 +12311,7 @@ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -11504,6 +12321,7 @@ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "devOptional": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -11513,6 +12331,7 @@ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -11521,13 +12340,15 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, + "license": "BlueOak-1.0.0", "peer": true, "dependencies": { "lru-cache": "^10.2.0", @@ -11545,18 +12366,21 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, + "license": "ISC", "peer": true }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -11569,6 +12393,7 @@ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">= 6" @@ -11579,6 +12404,7 @@ "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "find-up": "^4.0.0" @@ -11592,6 +12418,7 @@ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "locate-path": "^5.0.0", @@ -11606,6 +12433,7 @@ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "p-locate": "^4.1.0" @@ -11619,6 +12447,7 @@ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "p-try": "^2.0.0" @@ -11635,6 +12464,7 @@ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "p-limit": "^2.2.0" @@ -11648,6 +12478,7 @@ "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -11671,6 +12502,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -11712,6 +12544,7 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", @@ -11738,6 +12571,7 @@ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8.0" } @@ -11747,6 +12581,7 @@ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "ansi-regex": "^5.0.1", @@ -11762,6 +12597,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=10" @@ -11770,23 +12606,18 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/pretty-format/node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true, - "peer": true - }, "node_modules/promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "license": "ISC", "optional": true }, "node_modules/promise-retry": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "license": "MIT", "optional": true, "dependencies": { "err-code": "^2.0.2", @@ -11800,21 +12631,30 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" }, "node_modules/pump": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -11825,6 +12665,7 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -11844,6 +12685,7 @@ "url": "https://opencollective.com/fast-check" } ], + "license": "MIT", "peer": true }, "node_modules/queue-microtask": { @@ -11864,12 +12706,14 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -11884,6 +12728,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -11892,6 +12737,7 @@ "version": "19.1.0", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -11900,6 +12746,7 @@ "version": "19.1.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "license": "MIT", "dependencies": { "scheduler": "^0.26.0" }, @@ -11908,14 +12755,18 @@ } }, "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true }, "node_modules/react-remove-scroll": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", + "license": "MIT", "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", @@ -11940,6 +12791,7 @@ "version": "2.3.8", "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" @@ -11961,6 +12813,7 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", "dependencies": { "fast-equals": "^5.0.1", "prop-types": "^15.8.1", @@ -11975,6 +12828,7 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" @@ -11996,6 +12850,7 @@ "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", @@ -12011,6 +12866,7 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -12024,6 +12880,7 @@ "version": "2.15.4", "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", + "license": "MIT", "dependencies": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", @@ -12046,6 +12903,7 @@ "version": "0.4.5", "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", "dependencies": { "decimal.js-light": "^2.4.1" } @@ -12053,13 +12911,15 @@ "node_modules/recharts/node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", "dev": true, + "license": "MIT", "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" @@ -12073,6 +12933,7 @@ "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", @@ -12095,6 +12956,7 @@ "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", @@ -12115,17 +12977,19 @@ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", "dev": true, + "license": "MIT", "dependencies": { - "is-core-module": "^2.16.0", + "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -12144,6 +13008,7 @@ "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "resolve-from": "^5.0.0" @@ -12157,6 +13022,7 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=8" @@ -12167,6 +13033,7 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -12176,6 +13043,7 @@ "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } @@ -12184,6 +13052,7 @@ "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", "optional": true, "engines": { "node": ">= 4" @@ -12194,6 +13063,7 @@ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, + "license": "MIT", "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -12204,6 +13074,7 @@ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", "optional": true, "dependencies": { "glob": "^7.1.3" @@ -12215,23 +13086,46 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/rrweb-cssom": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", - "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", - "dev": true - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { "type": "patreon", "url": "https://www.patreon.com/feross" }, @@ -12240,6 +13134,7 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "queue-microtask": "^1.2.2" } @@ -12249,6 +13144,7 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "dev": true, + "license": "Apache-2.0", "dependencies": { "tslib": "^2.1.0" } @@ -12258,6 +13154,7 @@ "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", @@ -12289,13 +13186,15 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", "dev": true, + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" @@ -12312,6 +13211,7 @@ "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -12328,13 +13228,15 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "devOptional": true + "devOptional": true, + "license": "MIT" }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", "dev": true, + "license": "ISC", "dependencies": { "xmlchars": "^2.2.0" }, @@ -12345,12 +13247,14 @@ "node_modules/scheduler": { "version": "0.26.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", - "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==" + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" }, "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -12368,6 +13272,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC", "optional": true }, "node_modules/set-function-length": { @@ -12375,6 +13280,7 @@ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dev": true, + "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -12392,6 +13298,7 @@ "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "dev": true, + "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -12407,6 +13314,7 @@ "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", "dev": true, + "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", @@ -12417,15 +13325,16 @@ } }, "node_modules/sharp": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.4.tgz", - "integrity": "sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", "hasInstallScript": true, + "license": "Apache-2.0", "optional": true, "dependencies": { "@img/colour": "^1.0.0", - "detect-libc": "^2.1.0", - "semver": "^7.7.2" + "detect-libc": "^2.1.2", + "semver": "^7.7.3" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -12434,28 +13343,30 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.4", - "@img/sharp-darwin-x64": "0.34.4", - "@img/sharp-libvips-darwin-arm64": "1.2.3", - "@img/sharp-libvips-darwin-x64": "1.2.3", - "@img/sharp-libvips-linux-arm": "1.2.3", - "@img/sharp-libvips-linux-arm64": "1.2.3", - "@img/sharp-libvips-linux-ppc64": "1.2.3", - "@img/sharp-libvips-linux-s390x": "1.2.3", - "@img/sharp-libvips-linux-x64": "1.2.3", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.3", - "@img/sharp-libvips-linuxmusl-x64": "1.2.3", - "@img/sharp-linux-arm": "0.34.4", - "@img/sharp-linux-arm64": "0.34.4", - "@img/sharp-linux-ppc64": "0.34.4", - "@img/sharp-linux-s390x": "0.34.4", - "@img/sharp-linux-x64": "0.34.4", - "@img/sharp-linuxmusl-arm64": "0.34.4", - "@img/sharp-linuxmusl-x64": "0.34.4", - "@img/sharp-wasm32": "0.34.4", - "@img/sharp-win32-arm64": "0.34.4", - "@img/sharp-win32-ia32": "0.34.4", - "@img/sharp-win32-x64": "0.34.4" + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" } }, "node_modules/shebang-command": { @@ -12463,6 +13374,7 @@ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, + "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, @@ -12475,6 +13387,7 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -12484,6 +13397,7 @@ "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -12496,6 +13410,7 @@ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dev": true, + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", @@ -12515,6 +13430,7 @@ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", "dev": true, + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" @@ -12531,6 +13447,7 @@ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -12549,6 +13466,7 @@ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -12564,10 +13482,18 @@ } }, "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "devOptional": true + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "peer": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, "node_modules/simple-concat": { "version": "1.0.1", @@ -12586,7 +13512,8 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/simple-get": { "version": "4.0.1", @@ -12606,6 +13533,7 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", @@ -12617,6 +13545,7 @@ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -12625,6 +13554,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", "optional": true, "engines": { "node": ">= 6.0.0", @@ -12635,6 +13565,7 @@ "version": "2.8.7", "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", "optional": true, "dependencies": { "ip-address": "^10.0.1", @@ -12649,6 +13580,7 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "license": "MIT", "optional": true, "dependencies": { "agent-base": "^6.0.2", @@ -12659,10 +13591,24 @@ "node": ">= 10" } }, + "node_modules/socks-proxy-agent/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/sonner": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "license": "MIT", "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" @@ -12673,6 +13619,7 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -12681,6 +13628,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -12690,6 +13638,7 @@ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "buffer-from": "^1.0.0", @@ -12701,6 +13650,7 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true, + "license": "BSD-3-Clause", "peer": true }, "node_modules/sqlite3": { @@ -12708,6 +13658,7 @@ "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz", "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", "hasInstallScript": true, + "license": "BSD-3-Clause", "dependencies": { "bindings": "^1.5.0", "node-addon-api": "^7.0.0", @@ -12726,70 +13677,11 @@ } } }, - "node_modules/sqlite3/node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "engines": { - "node": ">=10" - } - }, - "node_modules/sqlite3/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/sqlite3/node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/sqlite3/node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/sqlite3/node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/sqlite3/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, "node_modules/ssri": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "license": "ISC", "optional": true, "dependencies": { "minipass": "^3.1.1" @@ -12802,6 +13694,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", "optional": true, "dependencies": { "yallist": "^4.0.0" @@ -12814,19 +13707,22 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", "optional": true }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", "dev": true, + "license": "MIT", "dependencies": { "escape-string-regexp": "^2.0.0" }, @@ -12839,6 +13735,7 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -12848,6 +13745,7 @@ "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", "dev": true, + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" @@ -12860,6 +13758,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" } @@ -12869,6 +13768,7 @@ "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "char-regex": "^1.0.2", @@ -12878,26 +13778,46 @@ "node": ">=10" } }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "devOptional": true, + "node_modules/string-length/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "peer": true, "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "ansi-regex": "^5.0.1" }, "engines": { "node": ">=8" } }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/string-width-cjs": { "name": "string-width", "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "emoji-regex": "^8.0.0", @@ -12913,19 +13833,29 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, + "license": "MIT", "peer": true }, - "node_modules/string-width/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "devOptional": true + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -12940,6 +13870,7 @@ "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -12967,6 +13898,7 @@ "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", "dev": true, + "license": "MIT", "dependencies": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" @@ -12977,6 +13909,7 @@ "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", @@ -12998,6 +13931,7 @@ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", @@ -13016,6 +13950,7 @@ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -13029,15 +13964,20 @@ } }, "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "devOptional": true, + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "peer": true, "dependencies": { - "ansi-regex": "^5.0.1" + "ansi-regex": "^6.0.1" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/strip-ansi-cjs": { @@ -13046,6 +13986,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "ansi-regex": "^5.0.1" @@ -13054,13 +13995,29 @@ "node": ">=8" } }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", "dev": true, + "license": "MIT", + "peer": true, "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/strip-final-newline": { @@ -13068,6 +14025,7 @@ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=6" @@ -13078,6 +14036,7 @@ "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", "dev": true, + "license": "MIT", "dependencies": { "min-indent": "^1.0.0" }, @@ -13090,6 +14049,7 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -13101,6 +14061,7 @@ "version": "5.1.6", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", "dependencies": { "client-only": "0.0.1" }, @@ -13120,15 +14081,19 @@ } }, "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, "node_modules/supports-preserve-symlinks-flag": { @@ -13136,6 +14101,7 @@ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -13147,13 +14113,15 @@ "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/synckit": { "version": "0.11.11", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@pkgr/core": "^0.2.9" @@ -13166,25 +14134,28 @@ } }, "node_modules/tailwind-merge": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", - "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", + "integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/dcastil" } }, "node_modules/tailwindcss": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz", - "integrity": "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==", - "dev": true + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", + "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", + "dev": true, + "license": "MIT" }, "node_modules/tapable": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz", - "integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" }, @@ -13194,25 +14165,27 @@ } }, "node_modules/tar": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.1.tgz", - "integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==", - "dev": true, + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "license": "ISC", "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.1.0", - "yallist": "^5.0.0" + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" }, "engines": { - "node": ">=18" + "node": ">=10" } }, "node_modules/tar-fs": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", @@ -13223,12 +14196,14 @@ "node_modules/tar-fs/node_modules/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" }, "node_modules/tar-stream": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", @@ -13240,11 +14215,27 @@ "node": ">=6" } }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", "dev": true, + "license": "ISC", "peer": true, "dependencies": { "@istanbuljs/schema": "^0.1.2", @@ -13255,16 +14246,41 @@ "node": ">=8" } }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", - "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, + "license": "MIT", "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" @@ -13281,6 +14297,7 @@ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, + "license": "MIT", "engines": { "node": ">=12.0.0" }, @@ -13298,6 +14315,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -13310,6 +14328,7 @@ "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", "dev": true, + "license": "MIT", "dependencies": { "tldts-core": "^6.1.86" }, @@ -13321,13 +14340,15 @@ "version": "6.1.86", "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "dev": true, + "license": "BSD-3-Clause", "peer": true }, "node_modules/to-regex-range": { @@ -13335,6 +14356,7 @@ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -13347,6 +14369,7 @@ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "tldts": "^6.1.32" }, @@ -13359,6 +14382,7 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", "dev": true, + "license": "MIT", "dependencies": { "punycode": "^2.3.1" }, @@ -13371,6 +14395,7 @@ "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", "dev": true, + "license": "MIT", "bin": { "tree-kill": "cli.js" } @@ -13380,6 +14405,7 @@ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=18.12" }, @@ -13388,10 +14414,11 @@ } }, "node_modules/ts-jest": { - "version": "29.4.4", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.4.tgz", - "integrity": "sha512-ccVcRABct5ZELCT5U0+DZwkXMCcOCLi2doHRrKy1nK/s7J7bch6TzJMsrY09WxgUUIP/ITfmcDS8D2yl63rnXw==", + "version": "29.4.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.5.tgz", + "integrity": "sha512-HO3GyiWn2qvTQA4kTgjDcXiMwYQt68a1Y8+JuLRVpdIzm+UOLSHgl/XqR4c6nzJkq5rOkjc02O2I7P7l/Yof0Q==", "dev": true, + "license": "MIT", "dependencies": { "bs-logger": "^0.2.6", "fast-json-stable-stringify": "^2.1.0", @@ -13399,7 +14426,7 @@ "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", - "semver": "^7.7.2", + "semver": "^7.7.3", "type-fest": "^4.41.0", "yargs-parser": "^21.1.1" }, @@ -13439,23 +14466,12 @@ } } }, - "node_modules/ts-jest/node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/ts-jest/node_modules/type-fest": { "version": "4.41.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=16" }, @@ -13468,6 +14484,7 @@ "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", "dev": true, + "license": "MIT", "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", @@ -13475,16 +14492,41 @@ "strip-bom": "^3.0.0" } }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" }, "node_modules/tsx": { - "version": "4.20.5", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.5.tgz", - "integrity": "sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==", + "version": "4.20.6", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", + "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", "dev": true, + "license": "MIT", "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" @@ -13503,6 +14545,7 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", "dependencies": { "safe-buffer": "^5.0.1" }, @@ -13515,6 +14558,7 @@ "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/Wombosvideo" } @@ -13524,6 +14568,7 @@ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, + "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1" }, @@ -13536,6 +14581,7 @@ "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -13545,6 +14591,7 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "dev": true, + "license": "(MIT OR CC0-1.0)", "peer": true, "engines": { "node": ">=10" @@ -13558,6 +14605,7 @@ "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", @@ -13572,6 +14620,7 @@ "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", @@ -13591,6 +14640,7 @@ "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", "dev": true, + "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", @@ -13612,6 +14662,7 @@ "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", @@ -13628,10 +14679,11 @@ } }, "node_modules/typescript": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", - "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -13645,6 +14697,7 @@ "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", "dev": true, + "license": "BSD-2-Clause", "optional": true, "bin": { "uglifyjs": "bin/uglifyjs" @@ -13658,6 +14711,7 @@ "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", @@ -13675,12 +14729,14 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/unique-filename": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "license": "ISC", "optional": true, "dependencies": { "unique-slug": "^2.0.0" @@ -13690,6 +14746,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "license": "ISC", "optional": true, "dependencies": { "imurmurhash": "^0.1.4" @@ -13701,6 +14758,7 @@ "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", "dev": true, "hasInstallScript": true, + "license": "MIT", "dependencies": { "napi-postinstall": "^0.3.0" }, @@ -13730,9 +14788,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", "dev": true, "funding": [ { @@ -13748,6 +14806,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "peer": true, "dependencies": { "escalade": "^3.2.0", @@ -13765,6 +14824,7 @@ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } @@ -13773,6 +14833,7 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", "dependencies": { "tslib": "^2.0.0" }, @@ -13793,6 +14854,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" @@ -13813,7 +14875,8 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" }, "node_modules/uuid": { "version": "13.0.0", @@ -13823,6 +14886,7 @@ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], + "license": "MIT", "bin": { "uuid": "dist-node/bin/uuid" } @@ -13832,6 +14896,7 @@ "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", "dev": true, + "license": "ISC", "peer": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", @@ -13846,6 +14911,7 @@ "version": "36.9.2", "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", @@ -13868,6 +14934,7 @@ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", "dev": true, + "license": "MIT", "dependencies": { "xml-name-validator": "^5.0.0" }, @@ -13880,6 +14947,7 @@ "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", "dev": true, + "license": "Apache-2.0", "peer": true, "dependencies": { "makeerror": "1.0.12" @@ -13890,6 +14958,7 @@ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=12" } @@ -13899,6 +14968,7 @@ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", "dev": true, + "license": "MIT", "dependencies": { "iconv-lite": "0.6.3" }, @@ -13911,6 +14981,7 @@ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" } @@ -13920,6 +14991,7 @@ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", "dev": true, + "license": "MIT", "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" @@ -13933,6 +15005,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "devOptional": true, + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -13948,6 +15021,7 @@ "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", "dev": true, + "license": "MIT", "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", @@ -13967,6 +15041,7 @@ "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", @@ -13994,6 +15069,7 @@ "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", "dev": true, + "license": "MIT", "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", @@ -14012,6 +15088,7 @@ "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", "dev": true, + "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", @@ -14032,16 +15109,53 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", "optional": true, "dependencies": { "string-width": "^1.0.2 || 2 || 3 || 4" } }, + "node_modules/wide-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", + "optional": true + }, + "node_modules/wide-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wide-align/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -14050,20 +15164,23 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, + "license": "MIT", + "peer": true, "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" }, "engines": { - "node": ">=10" + "node": ">=12" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" @@ -14075,6 +15192,7 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "ansi-styles": "^4.0.0", @@ -14088,16 +15206,70 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" }, "node_modules/write-file-atomic": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", "dev": true, + "license": "ISC", "peer": true, "dependencies": { "imurmurhash": "^0.1.4", @@ -14107,23 +15279,11 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/write-file-atomic/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "peer": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/ws": { "version": "8.18.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -14145,6 +15305,7 @@ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=18" } @@ -14153,31 +15314,33 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } }, "node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, - "engines": { - "node": ">=18" - } + "license": "ISC", + "peer": true }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, + "license": "MIT", "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -14196,15 +15359,52 @@ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, + "license": "ISC", "engines": { "node": ">=12" } }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -14216,6 +15416,7 @@ "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 72c4bb8..2b1e9e6 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "clsx": "^2.1.1", "ethers": "^6.15.0", "gsap": "^3.13.0", + "lightweight-charts": "^4.1.3", "lucide-react": "^0.544.0", "next": "15.5.4", "next-auth": "^4.24.11", diff --git a/src/app/api/klines/route.ts b/src/app/api/klines/route.ts new file mode 100644 index 0000000..bfe3a26 --- /dev/null +++ b/src/app/api/klines/route.ts @@ -0,0 +1,76 @@ +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'); + + // 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); + + // 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 } + ); + } +} \ No newline at end of file diff --git a/src/app/api/liquidations/symbols/route.ts b/src/app/api/liquidations/symbols/route.ts new file mode 100644 index 0000000..072c3ee --- /dev/null +++ b/src/app/api/liquidations/symbols/route.ts @@ -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 } + ); + } +} diff --git a/src/app/api/orders/all/route.ts b/src/app/api/orders/all/route.ts index 807a92a..baeec0e 100644 --- a/src/app/api/orders/all/route.ts +++ b/src/app/api/orders/all/route.ts @@ -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 { @@ -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); diff --git a/src/app/api/vwap/route.ts b/src/app/api/vwap/route.ts new file mode 100644 index 0000000..6b376ab --- /dev/null +++ b/src/app/api/vwap/route.ts @@ -0,0 +1,49 @@ +import { NextResponse } from 'next/server'; +import { vwapService } from '@/lib/services/vwapService'; +import { loadConfig } from '@/lib/bot/config'; + +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const symbol = searchParams.get('symbol'); + const timeframe = searchParams.get('timeframe') || '1m'; + const lookback = parseInt(searchParams.get('lookback') || '100'); + + if (!symbol) { + return NextResponse.json( + { error: 'Symbol parameter is required' }, + { status: 400 } + ); + } + + // Read config to get VWAP settings for this symbol (optional fallback) + const config = await loadConfig(); + const symbolConfig = config.symbols[symbol]; + + // Use provided params or fall back to config + const finalTimeframe = timeframe || symbolConfig?.vwapTimeframe || '1m'; + const finalLookback = lookback || symbolConfig?.vwapLookback || 100; + + // Calculate VWAP + const vwap = await vwapService.getVWAP(symbol, finalTimeframe, finalLookback); + + return NextResponse.json({ + vwap, + symbol, + timeframe: finalTimeframe, + lookback: finalLookback, + timestamp: Date.now() + }); + + } catch (error: any) { + console.error('Failed to fetch VWAP data:', error); + + return NextResponse.json( + { + error: 'Failed to fetch VWAP data', + details: error.message + }, + { status: 500 } + ); + } +} diff --git a/src/app/page.tsx b/src/app/page.tsx index b09b224..bb6e9e6 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -4,6 +4,7 @@ import React, { useState, useEffect, useMemo } from 'react'; import { DashboardLayout } from '@/components/dashboard-layout'; import { Skeleton } from '@/components/ui/skeleton'; import { Badge } from '@/components/ui/badge'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { DollarSign, TrendingUp, @@ -15,6 +16,7 @@ import { import MinimalBotStatus from '@/components/MinimalBotStatus'; import LiquidationSidebar from '@/components/LiquidationSidebar'; import PositionTable from '@/components/PositionTable'; +import TradingViewChart from '@/components/TradingViewChart'; import PnLChart from '@/components/PnLChart'; import PerformanceCardInline from '@/components/PerformanceCardInline'; import SessionPerformanceCard from '@/components/SessionPerformanceCard'; @@ -48,6 +50,8 @@ export default function DashboardPage() { const [isLoading, setIsLoading] = useState(true); const [positions, setPositions] = useState([]); const [markPrices, setMarkPrices] = useState>({}); + const [selectedSymbol, setSelectedSymbol] = useState(''); + const [availableChartSymbols, setAvailableChartSymbols] = useState([]); // Initialize toast notifications useOrderNotifications(); @@ -71,6 +75,24 @@ export default function DashboardPage() { setAccountInfo(balanceData); setPositions(positionsData); setBalanceStatus({ source: 'api', timestamp: Date.now() }); + + // Fetch available symbols from liquidation database + try { + const liquidationSymbolsResp = await fetch('/api/liquidations/symbols'); + const liquidationSymbolsData = await liquidationSymbolsResp.json(); + if (liquidationSymbolsData.success && liquidationSymbolsData.symbols) { + // Combine configured symbols with symbols that have liquidation data + const configuredSymbols = config?.symbols ? Object.keys(config.symbols) : []; + const allSymbols = Array.from(new Set([...configuredSymbols, ...liquidationSymbolsData.symbols])); + setAvailableChartSymbols(allSymbols); + } + } catch (error) { + console.error('[Dashboard] Failed to fetch liquidation symbols:', error); + // Fallback to configured symbols only + if (config?.symbols) { + setAvailableChartSymbols(Object.keys(config.symbols)); + } + } } catch (error) { console.error('[Dashboard] Failed to load initial data:', error); setBalanceStatus({ error: error instanceof Error ? error.message : 'Unknown error' }); @@ -158,6 +180,28 @@ export default function DashboardPage() { }), {}); }, [config?.symbols]); + // Set default symbol when config loads + useEffect(() => { + if (config?.symbols && Object.keys(config.symbols).length > 0 && !selectedSymbol) { + // First try to find a symbol with open positions + const positionSymbols = positions.map(pos => pos.symbol); + const symbolsWithPositions = Object.keys(config.symbols).filter(symbol => + positionSymbols.includes(symbol) + ); + + const defaultSymbol = symbolsWithPositions.length > 0 + ? symbolsWithPositions[0] // Use symbol with position + : Object.keys(config.symbols)[0]; // Fallback to first configured symbol + + console.log(`[Dashboard] Setting default symbol: ${defaultSymbol}`, { + availableSymbols: Object.keys(config.symbols), + positionSymbols, + symbolsWithPositions + }); + setSelectedSymbol(defaultSymbol); + } + }, [config?.symbols, selectedSymbol, positions]); + // Calculate live account info with real-time mark prices // This supplements the official balance data with live price updates const liveAccountInfo = useMemo(() => { @@ -395,6 +439,16 @@ export default function DashboardPage() { onClosePosition={handleClosePosition} /> + {/* Trading Chart with Symbol Selector */} + {config?.symbols && Object.keys(config.symbols).length > 0 && selectedSymbol && ( + 0 ? availableChartSymbols : Object.keys(config.symbols)} + onSymbolChange={setSelectedSymbol} + /> + )} + {/* Recent Orders Table */}

diff --git a/src/bot/index.ts b/src/bot/index.ts index 2a1d5d9..735a546 100644 --- a/src/bot/index.ts +++ b/src/bot/index.ts @@ -9,7 +9,6 @@ import { initializePriceService, stopPriceService, getPriceService } from '../li import { vwapStreamer } from '../lib/services/vwapStreamer'; import { getPositionMode, setPositionMode } from '../lib/api/positionMode'; import { execSync } from 'child_process'; -import { cleanupScheduler } from '../lib/services/cleanupScheduler'; import { db } from '../lib/db/database'; import { configManager } from '../lib/services/configManager'; import pnlService from '../lib/services/pnlService'; @@ -42,6 +41,7 @@ class AsterBot { private statusBroadcaster: StatusBroadcaster; private isHedgeMode: boolean = false; private tradeSizeWarnings: any[] = []; + private cleanupScheduler: any = null; constructor() { // Will be initialized with config port @@ -454,8 +454,20 @@ logErrorWithTimestamp('โŒ Hunter error:', error); logWithTimestamp('โœ… Liquidation Hunter started'); // Start the cleanup scheduler for liquidation database - cleanupScheduler.start(); -logWithTimestamp('โœ… Database cleanup scheduler started (7-day retention)'); + const dbConfig = this.config.global.liquidationDatabase; + const retentionDays = dbConfig?.retentionDays ?? 90; + const cleanupHours = dbConfig?.cleanupIntervalHours ?? 24; + + // Create a new scheduler instance with config values + const { CleanupScheduler } = await import('../lib/services/cleanupScheduler'); + this.cleanupScheduler = new CleanupScheduler(cleanupHours, retentionDays); + this.cleanupScheduler.start(); + + if (retentionDays > 0) { + logWithTimestamp(`โœ… Database cleanup scheduler started (${retentionDays}-day retention, runs every ${cleanupHours}h)`); + } else { + logWithTimestamp('โœ… Database cleanup scheduler started (retention disabled)'); + } this.isRunning = true; this.statusBroadcaster.setRunning(true); @@ -609,7 +621,9 @@ logWithTimestamp('โœ… Balance service stopped'); stopPriceService(); logWithTimestamp('โœ… Price service stopped'); - cleanupScheduler.stop(); + if (this.cleanupScheduler) { + this.cleanupScheduler.stop(); + } logWithTimestamp('โœ… Cleanup scheduler stopped'); configManager.stop(); diff --git a/src/components/RecentOrdersTable.tsx b/src/components/RecentOrdersTable.tsx index d4f1e76..35a5a88 100644 --- a/src/components/RecentOrdersTable.tsx +++ b/src/components/RecentOrdersTable.tsx @@ -130,7 +130,9 @@ export default function RecentOrdersTable({ maxRows: _maxRows = 50 }: RecentOrde // Initial load useEffect(() => { - loadOrders(); + // Clear cache and force initial load + orderStore.clearCache(); + loadOrders(true); }, [loadOrders]); // Subscribe to order updates diff --git a/src/components/SymbolConfigForm.tsx b/src/components/SymbolConfigForm.tsx index 8c1dab0..b15cbdc 100644 --- a/src/components/SymbolConfigForm.tsx +++ b/src/components/SymbolConfigForm.tsx @@ -24,6 +24,7 @@ import { AlertCircle, Settings2, BarChart3, + Database, } from 'lucide-react'; import { toast } from 'sonner'; @@ -690,6 +691,85 @@ export default function SymbolConfigForm({ onSave, currentConfig }: SymbolConfig + + {/* Liquidation Database Settings Card */} + + + + + Liquidation Database + + + Configure how long to keep liquidation data for chart analysis + + + +
+ +
+ { + const value = parseInt(e.target.value); + handleGlobalChange('liquidationDatabase', { + ...config.global.liquidationDatabase, + retentionDays: isNaN(value) ? 90 : Math.max(0, value) + }); + }} + className="w-24" + min="0" + max="3650" + step="1" + /> + + Days to keep liquidation data (0 = never delete) + +
+

+ More data means better chart analysis but uses more disk space. + Set to 0 to keep all liquidation data permanently. +

+
+ +
+ +
+ { + const value = parseInt(e.target.value); + handleGlobalChange('liquidationDatabase', { + ...config.global.liquidationDatabase, + cleanupIntervalHours: isNaN(value) ? 24 : Math.max(1, value) + }); + }} + className="w-24" + min="1" + max="168" + step="1" + /> + + How often to run database cleanup (default: 24) + +
+
+ + + + + Current settings: { + (config.global.liquidationDatabase?.retentionDays ?? 90) === 0 + ? "All liquidation data will be kept permanently" + : `Liquidation data older than ${config.global.liquidationDatabase?.retentionDays ?? 90} days will be automatically deleted every ${config.global.liquidationDatabase?.cleanupIntervalHours ?? 24} hours` + } + + +
+
diff --git a/src/components/TradingViewChart.tsx b/src/components/TradingViewChart.tsx new file mode 100644 index 0000000..96d4473 --- /dev/null +++ b/src/components/TradingViewChart.tsx @@ -0,0 +1,1094 @@ +'use client'; + +import React, { useRef, useEffect, useState, useCallback, useMemo } from 'react'; +import orderStore from '@/lib/services/orderStore'; +import { createChart, IChartApi, ISeriesApi, CandlestickData, Time } from 'lightweight-charts'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { getCachedKlines, setCachedKlines, updateCachedKlines, getCandlesFor7Days } from '@/lib/klineCache'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Checkbox } from '@/components/ui/checkbox'; +import { Label } from '@/components/ui/label'; +import { Loader2, AlertCircle, RefreshCw } from 'lucide-react'; + +// Types +interface LiquidationData { + time: number; + event_time: number; + volume: number; + volume_usdt: number; + side: 'BUY' | 'SELL'; + price: number; + quantity: number; +} + +interface GroupedLiquidation { + timestamp: number; + side: number; // 1 = long liquidation (red), 0 = short liquidation (blue) + totalVolume: number; + count: number; + price: number; +} + +interface TradingViewChartProps { + symbol: string; + liquidations?: LiquidationData[]; + positions?: any[]; + className?: string; + availableSymbols?: string[]; + onSymbolChange?: (symbol: string) => void; +} + +const TIMEFRAMES = [ + { value: '1m', label: '1 Min' }, + { value: '5m', label: '5 Min' }, + { value: '15m', label: '15 Min' }, + { value: '30m', label: '30 Min' }, + { value: '1h', label: '1 Hour' }, + { value: '4h', label: '4 Hours' }, + { value: '1d', label: '1 Day' }, +]; + +const LIQUIDATION_GROUPINGS = [ + { value: '1m', label: '1 Min' }, + { value: '5m', label: '5 Min' }, + { value: '15m', label: '15 Min' }, + { value: '30m', label: '30 Min' }, + { value: '1h', label: '1 Hour' }, + { value: '2h', label: '2 Hours' }, + { value: '4h', label: '4 Hours' }, + { value: '6h', label: '6 Hours' }, + { value: '12h', label: '12 Hours' }, + { value: '1d', label: '1 Day' }, +]; + +// Debounce utility +function debounce any>( + func: T, + wait: number +): (...args: Parameters) => void { + let timeout: NodeJS.Timeout; + return (...args: Parameters) => { + clearTimeout(timeout); + timeout = setTimeout(() => func(...args), wait); + }; +} + +// Convert timeframe to seconds for liquidation grouping +function timeframeToSeconds(timeframe: string): number { + const timeframes: Record = { + '1m': 60, + '3m': 180, + '5m': 300, + '15m': 900, + '30m': 1800, + '1h': 3600, + '2h': 7200, + '4h': 14400, + '6h': 21600, + '8h': 28800, + '12h': 43200, + '1d': 86400, + '3d': 259200, + '1w': 604800, + '1M': 2592000 + }; + return timeframes[timeframe] || 300; // Default to 5 minutes +} + +export default function TradingViewChart({ + symbol, + liquidations = [], + positions = [], + className, + availableSymbols = [], + onSymbolChange +}: TradingViewChartProps) { + // Chart refs + const chartContainerRef = useRef(null); + // Responsive chart height (550px - slightly bigger for better visibility) + const [chartHeight, setChartHeight] = useState(550); + // Chart visibility toggle + const [isVisible, setIsVisible] = useState(true); + + useEffect(() => { + function handleResize() { + setChartHeight(550); // Fixed 550px height + } + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + + const chartRef = useRef(null); + const candlestickSeriesRef = useRef | null>(null); + const positionLinesRef = useRef([]); + const vwapLineRef = useRef(null); + const orderMarkersRef = useRef([]); + + // State + const [timeframe, setTimeframe] = useState('5m'); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [klineData, setKlineData] = useState([]); + const [dbLiquidations, setDbLiquidations] = useState([]); + const [showLiquidations, setShowLiquidations] = useState(true); + const [liquidationGrouping, setLiquidationGrouping] = useState('5m'); + const [openOrders, setOpenOrders] = useState([]); + const [showVWAP, setShowVWAP] = useState(false); + const [showRecentOrders, setShowRecentOrders] = useState(false); + const [autoRefresh, setAutoRefresh] = useState(false); + const [lastUpdate, setLastUpdate] = useState(null); + const [isRefreshing, setIsRefreshing] = useState(false); + + // Refs to store refresh functions for auto-refresh + const fetchKlineDataRef = useRef<(force?: boolean) => Promise>(); + const fetchLiquidationDataRef = useRef<() => Promise>(); + const fetchOpenOrdersRef = useRef<() => Promise>(); + + // Combine props liquidations with database liquidations + const allLiquidations = useMemo(() => + [...liquidations, ...dbLiquidations], + [liquidations, dbLiquidations] + ); + + // Group liquidations by time for marker display + const groupLiquidationsByTime = useCallback((liquidations: LiquidationData[], timeframeStr: string): GroupedLiquidation[] => { + const groups: Record = {}; + const periodSeconds = timeframeToSeconds(timeframeStr); + + // Sort liquidations by time first (don't modify original array) + const sortedLiquidations = [...liquidations].sort((a, b) => a.event_time - b.event_time); + + sortedLiquidations.forEach(liq => { + const timestamp = liq.event_time; // Already in milliseconds + const timestampSeconds = Math.floor(timestamp / 1000); // Convert to seconds + const periodStart = Math.floor(timestampSeconds / periodSeconds) * periodSeconds; + + // SHOW ON LAST CANDLE: Add period duration to show at END of period + const periodEnd = periodStart + periodSeconds; + + // Map database sides: 'SELL' = long liquidation (red), 'BUY' = short liquidation (blue) + const side = liq.side === 'SELL' ? 1 : 0; + const key = `${periodStart}_${side}`; + + if (!groups[key]) { + groups[key] = { + timestamp: periodEnd * 1000, // Use END of period (last candle) + side, + totalVolume: 0, + count: 0, + price: 0 + }; + } + + groups[key].totalVolume += liq.volume_usdt; + groups[key].count += 1; + groups[key].price = (groups[key].price * (groups[key].count - 1) + liq.price) / groups[key].count; + }); + + // Sort the grouped results by timestamp to ensure proper ordering + return Object.values(groups).sort((a, b) => a.timestamp - b.timestamp); + }, []); + + // Get color by volume and side + const getColorByVolume = useCallback((volume: number, side: number): string => { + if (side === 1) { // Long liquidations (red spectrum) + return volume > 1000000 ? '#ff1744' : // >$1M: Bright red + volume > 100000 ? '#ff5722' : // >$100K: Orange-red + '#ff9800'; // <$100K: Orange + } else { // Short liquidations (blue spectrum) + return volume > 1000000 ? '#1976d2' : // >$1M: Dark blue + volume > 100000 ? '#2196f3' : // >$100K: Medium blue + '#64b5f6'; // <$100K: Light blue + } + }, []); + + // Get size by volume + const getSizeByVolume = useCallback((volume: number): number => { + return volume > 1000000 ? 2 : // >$1M: Large + volume > 100000 ? 1 : // >$100K: Medium + 0; // <$100K: Small + }, []); + + // Update position indicators + const updatePositionIndicators = useCallback((positions: any[], orders: any[]) => { + if (!candlestickSeriesRef.current) { + return; + } + + // Clear existing position lines + positionLinesRef.current.forEach(line => { + try { + candlestickSeriesRef.current?.removePriceLine(line); + } catch (_e) { + // Ignore errors from already removed lines + } + }); + positionLinesRef.current = []; + + // Filter positions for current symbol + const symbolPositions = positions.filter(pos => pos.symbol === symbol); + + symbolPositions.forEach(position => { + try { + const entryPrice = parseFloat(position.entryPrice || position.markPrice || position.avgPrice || '0'); + const quantity = parseFloat(position.quantity || position.positionAmt || position.size || '0'); + const side = position.side; // "LONG" or "SHORT" + const positionAmt = side === 'SHORT' ? -quantity : quantity; // Convert to signed amount + const unrealizedPnl = parseFloat(position.unrealizedProfit || position.pnl || '0'); + const liquidationPrice = parseFloat(position.liquidationPrice || '0'); + + if (entryPrice > 0 && Math.abs(positionAmt) > 0) { + const isLong = positionAmt > 0; + + // Entry price line - using different approach + const entryLine = candlestickSeriesRef.current!.createPriceLine({ + price: entryPrice, + color: isLong ? '#26a69a' : '#ef5350', + lineWidth: 2, + lineStyle: 0, // Solid line + axisLabelVisible: true, + title: `${isLong ? 'LONG' : 'SHORT'} Entry: ${entryPrice}`, + }); + positionLinesRef.current.push(entryLine); + + // Liquidation price line (if available) + if (liquidationPrice > 0) { + const liqLine = candlestickSeriesRef.current!.createPriceLine({ + price: liquidationPrice, + color: '#ff1744', // Bright red for liquidation + lineWidth: 1, + lineStyle: 1, // Dashed line + axisLabelVisible: true, + title: `Liquidation: ${liquidationPrice}`, + }); + positionLinesRef.current.push(liqLine); + } + } + } catch (error) { + console.error('[TradingViewChart] Error adding position line:', error); + } + }); + + // Find and process open orders for current symbol + const symbolOrders = orders.filter(order => order.symbol === symbol); + + symbolOrders.forEach(order => { + try { + const orderPrice = parseFloat(order.stopPrice || order.price || '0'); + + if (orderPrice > 0) { + const isTP = order.type.includes('TAKE_PROFIT'); + const isSL = order.type.includes('STOP') && !isTP; + + let color = '#ffa726'; // Default orange + let title = `Order: ${orderPrice}`; + + if (isTP) { + color = '#4caf50'; // Green for TP + title = `TP: ${orderPrice}`; + } else if (isSL) { + color = '#f44336'; // Red for SL + title = `SL: ${orderPrice}`; + } + + const orderLine = candlestickSeriesRef.current!.createPriceLine({ + price: orderPrice, + color, + lineWidth: 1, + lineStyle: 2, // Dotted line + axisLabelVisible: true, + title, + }); + positionLinesRef.current.push(orderLine); + } + } catch (error) { + console.error('[TradingViewChart] Error adding order line:', error); + } + }); + }, [symbol]); + + // Debounced position updates + const debouncedUpdatePositions = useCallback( + // eslint-disable-next-line react-hooks/exhaustive-deps + debounce((positions: any[], orders: any[]) => { + updatePositionIndicators(positions, orders); + }, 250), + [updatePositionIndicators] + ); + + // Fetch liquidation data from database + const fetchLiquidationData = useCallback(async () => { + if (!symbol) return; + + try { + const response = await fetch(`/api/liquidations?symbol=${symbol}&limit=2000`); + const result = await response.json(); + + if (result.success && result.data) { + const transformedLiquidations: LiquidationData[] = result.data.map((liq: any) => ({ + time: liq.event_time, + event_time: liq.event_time, + volume: liq.volume_usdt, + volume_usdt: liq.volume_usdt, + side: liq.side, + price: liq.price, + quantity: liq.quantity + })); + + // Only update if data has changed (check length and latest timestamp) + setDbLiquidations(prev => { + if (prev.length === transformedLiquidations.length && + prev.length > 0 && transformedLiquidations.length > 0 && + prev[prev.length - 1]?.event_time === transformedLiquidations[transformedLiquidations.length - 1]?.event_time) { + return prev; // No change + } + return transformedLiquidations; + }); + } + } catch (error) { + console.error('Error fetching liquidation data:', error); + } + }, [symbol]); + + fetchLiquidationDataRef.current = fetchLiquidationData; + + // Fetch open orders for TP/SL display + const fetchOpenOrders = useCallback(async () => { + if (!symbol) return; + + try { + const response = await fetch('/api/orders'); + const result = await response.json(); + + if (Array.isArray(result)) { + // Filter orders for current symbol + const symbolOrders = result.filter((order: any) => order.symbol === symbol); + + // Only update if data has changed (check length and order IDs) + setOpenOrders(prev => { + if (prev.length === symbolOrders.length && prev.length > 0 && symbolOrders.length > 0) { + const prevIds = prev.map(o => o.orderId).sort().join(','); + const newIds = symbolOrders.map(o => o.orderId).sort().join(','); + if (prevIds === newIds) { + return prev; // No change + } + } + return symbolOrders; + }); + } + } catch (error) { + console.error('Error fetching open orders:', error); + } + }, [symbol]); + + fetchOpenOrdersRef.current = fetchOpenOrders; + + // Fetch kline data with caching + const fetchKlineData = useCallback(async (force = false) => { + if (!symbol || !timeframe) return; + + if (force) { + setIsRefreshing(true); + } else { + setLoading(true); + } + setError(null); + + try { + // When forcing refresh, always fetch latest data + if (force) { + // Get the latest candles from the API + const cached = getCachedKlines(symbol, timeframe); + const since = cached?.lastCandleTime || Date.now() - (7 * 24 * 60 * 60 * 1000); + + const response = await fetch(`/api/klines?symbol=${symbol}&interval=${timeframe}&since=${since}&limit=500`); + const result = await response.json(); + + if (result.success && result.data.length > 0) { + // Update cache with new data + const updated = cached + ? updateCachedKlines(symbol, timeframe, result.data) + : { data: result.data, lastUpdate: Date.now(), lastCandleTime: result.data[result.data.length - 1][0] }; + + if (updated) { + // Update chart with merged data + const transformedData: CandlestickData[] = updated.data.map((kline: any[]) => { + const timestamp = typeof kline[0] === 'number' ? kline[0] : parseInt(kline[0]); + return { + time: timestamp as Time, + open: parseFloat(kline[1]), + high: parseFloat(kline[2]), + low: parseFloat(kline[3]), + close: parseFloat(kline[4]) + }; + }); + + transformedData.sort((a, b) => (a.time as number) - (b.time as number)); + + // Only update if data has actually changed + setKlineData(prev => { + if (prev.length === transformedData.length && + prev[prev.length - 1]?.close === transformedData[transformedData.length - 1]?.close) { + return prev; // No change + } + return transformedData; + }); + + // Cache the merged data + if (!cached) { + setCachedKlines(symbol, timeframe, updated.data); + } + } + } + + setIsRefreshing(false); + setLastUpdate(new Date()); + return; + } + + // Check cache first for normal loads + const cached = getCachedKlines(symbol, timeframe); + + if (cached) { + // Use cached data immediately + const transformedData: CandlestickData[] = cached.data.map((kline: any[]) => { + const timestamp = typeof kline[0] === 'number' ? kline[0] : parseInt(kline[0]); + return { + time: timestamp as Time, + open: parseFloat(kline[1]), + high: parseFloat(kline[2]), + low: parseFloat(kline[3]), + close: parseFloat(kline[4]) + }; + }); + + // Sort data by time (TradingView requires chronological order) + transformedData.sort((a, b) => (a.time as number) - (b.time as number)); + setKlineData(transformedData); + + // Check if we need to fetch recent updates (cache older than 2 minutes) + const cacheAge = Date.now() - cached.lastUpdate; + const needsUpdate = cacheAge > 2 * 60 * 1000; // 2 minutes + + if (!needsUpdate) { + setLoading(false); + return; + } + + // Fetch only recent candles since last cache update + try { + const updateResponse = await fetch(`/api/klines?symbol=${symbol}&interval=${timeframe}&since=${cached.lastCandleTime}&limit=100`); + const updateResult = await updateResponse.json(); + + if (updateResult.success && updateResult.data.length > 0) { + // Update cache with new data + const updated = updateCachedKlines(symbol, timeframe, updateResult.data); + + if (updated) { + // Update chart with merged data + const updatedTransformed: CandlestickData[] = updated.data.map((kline: any[]) => { + const timestamp = typeof kline[0] === 'number' ? kline[0] : parseInt(kline[0]); + return { + time: timestamp as Time, + open: parseFloat(kline[1]), + high: parseFloat(kline[2]), + low: parseFloat(kline[3]), + close: parseFloat(kline[4]) + }; + }); + + updatedTransformed.sort((a, b) => (a.time as number) - (b.time as number)); + setKlineData(updatedTransformed); + } + } + } catch (updateError) { + console.warn('[TradingViewChart] Failed to fetch updates, using cached data:', updateError); + } + + setLoading(false); + return; + } + + // No cache available, fetch full 7-day history + const sevenDayLimit = getCandlesFor7Days(timeframe); + + const response = await fetch(`/api/klines?symbol=${symbol}&interval=${timeframe}&limit=${sevenDayLimit}`); + const result = await response.json(); + + if (!result.success) { + throw new Error(result.error || 'Failed to fetch kline data'); + } + + // Transform API response to lightweight-charts format + const transformedData: CandlestickData[] = result.data.map((kline: any[]) => { + const timestamp = typeof kline[0] === 'number' ? kline[0] : parseInt(kline[0]); + return { + time: timestamp as Time, + open: parseFloat(kline[1]), + high: parseFloat(kline[2]), + low: parseFloat(kline[3]), + close: parseFloat(kline[4]) + }; + }); + + // Sort data by time (TradingView requires chronological order) + transformedData.sort((a, b) => (a.time as number) - (b.time as number)); + + // Cache the data + setCachedKlines(symbol, timeframe, result.data); + + setKlineData(transformedData); + } catch (error) { + console.error('[TradingViewChart] Error fetching kline data:', error); + setError(error instanceof Error ? error.message : 'Failed to fetch chart data'); + } finally { + setLoading(false); + setIsRefreshing(false); + setLastUpdate(new Date()); + } + }, [symbol, timeframe]); + + // Store function refs for auto-refresh + fetchKlineDataRef.current = fetchKlineData; + + // Initialize chart + useEffect(() => { + // Don't initialize chart if still loading or there's an error or chart is hidden + if (loading || error || !isVisible) { + return; + } + + if (!chartContainerRef.current) { + return; + } + + const containerWidth = chartContainerRef.current.clientWidth; + + try { + const chart = createChart(chartContainerRef.current, { + autoSize: true, + layout: { + textColor: 'white', + background: { color: '#1a1a1a' }, + }, + grid: { + vertLines: { color: 'rgba(197, 203, 206, 0.1)' }, + horzLines: { color: 'rgba(197, 203, 206, 0.1)' }, + }, + crosshair: { + mode: 1, + }, + rightPriceScale: { + borderColor: 'rgba(197, 203, 206, 0.5)', + }, + timeScale: { + borderColor: 'rgba(197, 203, 206, 0.5)', + timeVisible: true, + secondsVisible: false, + }, + }); + + const candlestickSeries = chart.addCandlestickSeries({ + upColor: '#26a69a', + downColor: '#ef5350', + borderVisible: false, + wickUpColor: '#26a69a', + wickDownColor: '#ef5350', + }); + + chartRef.current = chart; + candlestickSeriesRef.current = candlestickSeries; + } catch (error) { + console.error(`[TradingViewChart] Error creating chart:`, error); + } + + return () => { + if (chartRef.current) { + chartRef.current.remove(); + chartRef.current = null; + candlestickSeriesRef.current = null; + } + }; + }, [loading, error, isVisible, chartHeight]); // Re-initialize when loading/error/visibility states change + + // Fetch data when symbol or timeframe changes + useEffect(() => { + if (symbol && timeframe && isVisible) { + fetchKlineData(); + fetchLiquidationData(); + fetchOpenOrders(); + } + }, [symbol, timeframe, isVisible, fetchKlineData, fetchLiquidationData, fetchOpenOrders]); + + // Auto-refresh effect - refreshes every 60 seconds when enabled + useEffect(() => { + if (!autoRefresh || !isVisible || !symbol || !timeframe) { + return; + } + + const interval = setInterval(() => { + console.log('[TradingViewChart] Auto-refresh triggered'); + // Use refs to avoid dependency issues + if (fetchKlineDataRef.current) fetchKlineDataRef.current(true); + if (fetchLiquidationDataRef.current) fetchLiquidationDataRef.current(); + if (fetchOpenOrdersRef.current) fetchOpenOrdersRef.current(); + }, 60000); // 60 seconds + + return () => clearInterval(interval); + }, [autoRefresh, isVisible, symbol, timeframe]); + + // Update chart data when klineData changes + useEffect(() => { + if (candlestickSeriesRef.current && klineData.length > 0) { + candlestickSeriesRef.current.setData(klineData); + + // Set visible logical range: most recent candle at 2/3 mark, 1/3 empty space on right + if (chartRef.current && klineData.length > 0) { + const totalBars = klineData.length; + + // Calculate how many bars to show (e.g., show 60 bars = 1 hour of 1m candles) + // Adjust this number based on your preference + const barsToShow = Math.min(60, totalBars); // Show up to 60 bars + + // The most recent bar is at index (totalBars - 1) + // We want it at 2/3 of the visible area, so we need to show more bars on the right + const lastBarIndex = totalBars - 1; + const firstBarIndex = Math.max(0, lastBarIndex - barsToShow); + + // Add empty space on the right (1/3 of visible area means adding half of barsToShow) + const rightPadding = Math.floor(barsToShow / 2); + + chartRef.current.timeScale().setVisibleLogicalRange({ + from: firstBarIndex, + to: lastBarIndex + rightPadding, + }); + } + } + }, [klineData]); + + // Update position indicators when positions change + useEffect(() => { + if (positions.length > 0) { + debouncedUpdatePositions(positions, openOrders); + } + }, [positions, openOrders, debouncedUpdatePositions]); + + // Manual refresh handler + const handleRefresh = useCallback(() => { + console.log('[TradingViewChart] Manual refresh triggered'); + if (fetchKlineDataRef.current) fetchKlineDataRef.current(true); + if (fetchLiquidationDataRef.current) fetchLiquidationDataRef.current(); + if (fetchOpenOrdersRef.current) fetchOpenOrdersRef.current(); + }, []); + + if (!symbol) { + return ( + + +
+ +

Select a symbol to view chart

+
+
+
+ ); + } + + // --- Recent orders overlay logic --- + // Use filled orders from orderStore (same as RecentOrdersTable) + const [filledOrders, setFilledOrders] = React.useState([]); + useEffect(() => { + const loadOrders = async () => { + // Only load if toggle is enabled + if (!showRecentOrders) { + setFilledOrders([]); + return; + } + + // Get ALL orders from store data, then filter locally for this symbol + const allOrders = orderStore.getOrders().data; + const symbolFilledOrders = allOrders.filter((order: any) => + order.status === 'FILLED' && order.symbol === symbol + ); + setFilledOrders(symbolFilledOrders); + }; + + loadOrders(); + + // Listen for updates + const handleUpdate = () => { + if (!showRecentOrders) return; // Don't update if toggle is off + // Get ALL orders from store data, then filter locally for this symbol + const allOrders = orderStore.getOrders().data; + const symbolFilledOrders = allOrders.filter((order: any) => + order.status === 'FILLED' && order.symbol === symbol + ); + setFilledOrders(symbolFilledOrders); + }; + orderStore.on('orders:updated', handleUpdate); + orderStore.on('orders:filtered', handleUpdate); + return () => { + orderStore.off('orders:updated', handleUpdate); + orderStore.off('orders:filtered', handleUpdate); + }; + }, [symbol, showRecentOrders]); + + // Combine all overlays into one marker array + React.useEffect(() => { + if (!candlestickSeriesRef.current) return; + let markers: any[] = []; + // Add liquidation markers if enabled + if (showLiquidations && allLiquidations.length > 0) { + const groupedLiquidations = groupLiquidationsByTime(allLiquidations, liquidationGrouping); + const liqMarkers = groupedLiquidations.map(group => ({ + time: Math.floor(group.timestamp / 1000) as Time, + position: 'belowBar', + color: getColorByVolume(group.totalVolume, group.side), + shape: 'circle', + size: getSizeByVolume(group.totalVolume), + text: `${group.count}${group.side === 1 ? 'L' : 'S'} $${group.totalVolume >= 1000 ? (group.totalVolume/1000).toFixed(0) + 'K' : group.totalVolume.toFixed(0)}`, + id: `liq_${group.timestamp}_${group.side}` + })); + markers = markers.concat(liqMarkers); + } + // Add recent order markers if enabled + if (showRecentOrders && filledOrders.length > 0) { + const seenOrderIds = new Set(); + const orderMarkers = filledOrders.map((order: any) => { + if (!order.orderId || seenOrderIds.has(order.orderId)) return null; + seenOrderIds.add(order.orderId); + const orderTime = Number(order.updateTime || order.time || order.transactTime); + let candle = klineData.find(k => typeof k.time === 'number' && Math.abs((k.time * 1000) - orderTime) < 60 * 1000); + if (!candle && klineData.length > 0) { + candle = klineData.reduce((closest, k) => { + return Math.abs((k.time as number * 1000) - orderTime) < Math.abs((closest.time as number * 1000) - orderTime) ? k : closest; + }, klineData[0]); + } + if (!candle) return null; + + // Determine order characteristics + const isBuy = order.side === 'BUY'; + const isReduceOnly = order.reduceOnly === true || order.reduceOnly === 'true'; + const realizedPnl = order.realizedProfit ? parseFloat(order.realizedProfit) : 0; + + // Determine position type based on side and reduce flag + let positionType = ''; + if (isReduceOnly) { + // Reduce order - exiting position + positionType = isBuy ? 'Close SHORT' : 'Close LONG'; + } else { + // Opening order + positionType = isBuy ? 'LONG' : 'SHORT'; + } + + // Determine color and shape + let color: string; + let shape: 'arrowUp' | 'arrowDown' | 'circle'; + let position: 'aboveBar' | 'belowBar'; + + if (isReduceOnly) { + // Exit orders - show profit/loss color + if (realizedPnl > 0) { + color = '#4caf50'; // Green for profit + shape = 'arrowDown'; + position = isBuy ? 'aboveBar' : 'belowBar'; + } else if (realizedPnl < 0) { + color = '#f44336'; // Red for loss + shape = 'arrowDown'; + position = isBuy ? 'aboveBar' : 'belowBar'; + } else { + color = '#9e9e9e'; // Gray for breakeven + shape = 'arrowDown'; + position = isBuy ? 'aboveBar' : 'belowBar'; + } + } else { + // Entry orders + if (isBuy) { + color = '#26a69a'; // Teal for LONG + shape = 'arrowUp'; + position = 'belowBar'; + } else { + color = '#ef5350'; // Red for SHORT + shape = 'arrowDown'; + position = 'aboveBar'; + } + } + + // Build text label with quantity + const qty = order.executedQty || order.origQty || '0'; + const price = order.avgPrice || order.price || order.stopPrice || ''; + + let text = ''; + if (isReduceOnly) { + // Exit order - show close info with P&L + if (realizedPnl !== 0) { + const pnlSign = realizedPnl > 0 ? '+' : ''; + text = `${positionType}\n${qty} @ ${price}\n${pnlSign}$${realizedPnl.toFixed(2)}`; + } else { + text = `${positionType}\n${qty} @ ${price}`; + } + } else { + // Entry order - show position type and size + text = `${positionType}\n${qty} @ ${price}`; + } + + return { + time: candle.time, + position, + color, + shape, + size: 2, + text, + id: `order_${order.orderId}`, + type: 'order' + }; + }).filter(Boolean); + markers = markers.concat(orderMarkers); + } + // Sort all markers by time in ascending order (required by lightweight-charts) + markers.sort((a, b) => (a.time as number) - (b.time as number)); + + // Always update markers when dependencies change (don't use complex comparison) + candlestickSeriesRef.current.setMarkers(markers); + }, [showLiquidations, allLiquidations, liquidationGrouping, showRecentOrders, filledOrders, klineData]); + + // --- VWAP overlay logic --- + React.useEffect(() => { + if (!showVWAP) { + if (candlestickSeriesRef.current && vwapLineRef.current) { + candlestickSeriesRef.current.removePriceLine(vwapLineRef.current); + vwapLineRef.current = null; + } + return; + } + if (!candlestickSeriesRef.current || !symbol) { + return; + } + // Fetch VWAP from streamer API (or fallback to service) + const fetchVWAP = async () => { + try { + const configResp = await fetch('/api/config'); + const configData = await configResp.json(); + const symbolConfig = configData.symbols?.[symbol] || {}; + const timeframe = symbolConfig.vwapTimeframe || '1m'; + const lookback = symbolConfig.vwapLookback || 100; + const vwapResp = await fetch(`/api/vwap?symbol=${symbol}&timeframe=${timeframe}&lookback=${lookback}`); + const vwapData = await vwapResp.json(); + + if (vwapData && vwapData.vwap) { + // Remove previous VWAP line if any + if (vwapLineRef.current) { + candlestickSeriesRef.current?.removePriceLine(vwapLineRef.current); + vwapLineRef.current = null; + } + // Add VWAP line + vwapLineRef.current = candlestickSeriesRef.current?.createPriceLine({ + price: vwapData.vwap, + color: '#ffd600', + lineWidth: 2, + lineStyle: 0, + axisLabelVisible: true, + title: `VWAP (${timeframe})` + }); + } else { + console.warn('[TradingViewChart] No VWAP data returned for', symbol, timeframe, vwapData); + } + } catch (err) { + console.warn('[TradingViewChart] VWAP fetch error', err); + } + }; + fetchVWAP(); + // Optionally, poll for updates every 10s + const interval = setInterval(fetchVWAP, 10000); + return () => { + clearInterval(interval); + if (candlestickSeriesRef.current && vwapLineRef.current) { + candlestickSeriesRef.current.removePriceLine(vwapLineRef.current); + vwapLineRef.current = null; + } + }; + }, [showVWAP, symbol]); + + return ( + + +
+ {availableSymbols.length > 0 && onSymbolChange ? ( + + ) : ( + + {symbol} + + )} + + + + {isVisible && ( + <> +
+ + {lastUpdate && ( + + {lastUpdate.toLocaleTimeString()} + + )} + + )} +
+ + {isVisible && ( +
+
+
+ setAutoRefresh(checked as boolean)} + className="h-4 w-4" + /> + +
+ +
+ setShowLiquidations(checked as boolean)} + className="h-4 w-4" + /> + +
+ +
+ setShowRecentOrders(checked as boolean)} + className="h-4 w-4" + /> + +
+ +
+ setShowVWAP(checked as boolean)} + className="h-4 w-4" + /> + +
+
+ +
+ + {showLiquidations && ( +
+ + +
+ )} + +
+ + +
+
+ )} + + {isVisible && ( + + {loading && ( +
+
+ +

Loading chart data...

+
+
+ )} + + {error && ( +
+
+ +

{error}

+ +
+
+ )} + + {!loading && !error && ( +
+ )} + + )} + + ); +} \ No newline at end of file diff --git a/src/hooks/useBotStatus.ts b/src/hooks/useBotStatus.ts index 3f26d12..cec6a88 100644 --- a/src/hooks/useBotStatus.ts +++ b/src/hooks/useBotStatus.ts @@ -51,6 +51,10 @@ export function useBotStatus(): UseBotStatusReturn { case 'trade_opportunity': case 'vwap_update': case 'vwap_bulk': + case 'rateLimit': + case 'sl_placed': + case 'tp_placed': + case 'threshold_update': // These messages are handled by other components, ignore silently break; default: diff --git a/src/lib/klineCache.ts b/src/lib/klineCache.ts new file mode 100644 index 0000000..ae419d5 --- /dev/null +++ b/src/lib/klineCache.ts @@ -0,0 +1,128 @@ +// Kline caching utility for 7-day historical data +export interface CachedKlineData { + symbol: string; + interval: string; + data: number[][]; // [timestamp, open, high, low, close, volume] + lastUpdate: number; + lastCandleTime: number; +} + +// Calculate candles needed for 7 days based on timeframe +export const getCandlesFor7Days = (interval: string): number => { + const minutesInInterval = { + '1m': 1, + '3m': 3, + '5m': 5, + '15m': 15, + '30m': 30, + '1h': 60, + '2h': 120, + '4h': 240, + '6h': 360, + '8h': 480, + '12h': 720, + '1d': 1440, + '3d': 4320, + '1w': 10080, + '1M': 43200 // Approximate 30 days + } as const; + + const minutes = minutesInInterval[interval as keyof typeof minutesInInterval]; + if (!minutes) return 500; // Default fallback + + const minutesIn7Days = 7 * 24 * 60; // 10,080 minutes + const candlesNeeded = Math.ceil(minutesIn7Days / minutes); + + // Cap at API limit but ensure we get at least 7 days + return Math.min(candlesNeeded, 1500); +}; + +// In-memory cache +const klineCache = new Map(); + +export const getCacheKey = (symbol: string, interval: string): string => { + return `${symbol}_${interval}`; +}; + +export const getCachedKlines = (symbol: string, interval: string): CachedKlineData | null => { + const key = getCacheKey(symbol, interval); + const cached = klineCache.get(key); + + if (!cached) return null; + + // Check if cache is still valid (within 5 minutes for most recent data) + const now = Date.now(); + const cacheAge = now - cached.lastUpdate; + const maxAge = 5 * 60 * 1000; // 5 minutes + + if (cacheAge > maxAge) { + // Cache is stale, but we can still use historical data + // We'll just need to fetch recent candles + return cached; + } + + return cached; +}; + +export const setCachedKlines = (symbol: string, interval: string, data: number[][]): void => { + const key = getCacheKey(symbol, interval); + const now = Date.now(); + + if (data.length === 0) return; + + // Sort data by timestamp to ensure correct order + const sortedData = [...data].sort((a, b) => a[0] - b[0]); + + const cached: CachedKlineData = { + symbol, + interval, + data: sortedData, + lastUpdate: now, + lastCandleTime: sortedData[sortedData.length - 1][0] * 1000 // Convert back to milliseconds + }; + + klineCache.set(key, cached); +}; + +export const updateCachedKlines = (symbol: string, interval: string, newData: number[][]): CachedKlineData | null => { + const key = getCacheKey(symbol, interval); + const cached = klineCache.get(key); + + if (!cached || newData.length === 0) return null; + + // Merge new data with existing cache + const existingData = cached.data; + const newTimestamps = new Set(newData.map(candle => candle[0])); + + // Remove any existing candles that are being updated + const filteredExisting = existingData.filter(candle => !newTimestamps.has(candle[0])); + + // Combine and sort + const combinedData = [...filteredExisting, ...newData].sort((a, b) => a[0] - b[0]); + + // Keep only the most recent candles (limit to prevent memory issues) + const maxCandles = getCandlesFor7Days(interval) + 100; // Extra buffer + const trimmedData = combinedData.slice(-maxCandles); + + const updated: CachedKlineData = { + symbol, + interval, + data: trimmedData, + lastUpdate: Date.now(), + lastCandleTime: trimmedData[trimmedData.length - 1][0] * 1000 + }; + + klineCache.set(key, updated); + return updated; +}; + +export const clearCache = (): void => { + klineCache.clear(); +}; + +export const getCacheStats = (): { size: number; keys: string[] } => { + return { + size: klineCache.size, + keys: Array.from(klineCache.keys()) + }; +}; \ No newline at end of file diff --git a/src/lib/services/cleanupScheduler.ts b/src/lib/services/cleanupScheduler.ts index 3ce8c54..7062cd8 100644 --- a/src/lib/services/cleanupScheduler.ts +++ b/src/lib/services/cleanupScheduler.ts @@ -1,11 +1,14 @@ import { liquidationStorage } from './liquidationStorage'; +import { loadConfig } from '../bot/config'; export class CleanupScheduler { private intervalId: NodeJS.Timeout | null = null; private readonly intervalMs: number; + private readonly retentionDays: number; - constructor(intervalHours: number = 24) { + constructor(intervalHours: number = 24, retentionDays: number = 90) { this.intervalMs = intervalHours * 60 * 60 * 1000; + this.retentionDays = retentionDays; } start(): void { @@ -14,7 +17,7 @@ export class CleanupScheduler { return; } - console.log(`Starting cleanup scheduler (runs every ${this.intervalMs / (1000 * 60 * 60)} hours)`); + console.log(`Starting cleanup scheduler (runs every ${this.intervalMs / (1000 * 60 * 60)} hours, keeps ${this.retentionDays} days of data)`); this.runCleanup(); @@ -36,7 +39,11 @@ export class CleanupScheduler { console.log('Running liquidation cleanup...'); const startTime = Date.now(); - const deletedCount = await liquidationStorage.cleanupOldLiquidations(); + // Load current config to get retention settings + const config = await loadConfig(); + const retentionDays = config.global.liquidationDatabase?.retentionDays ?? this.retentionDays; + + const deletedCount = await liquidationStorage.cleanupOldLiquidations(retentionDays); const duration = Date.now() - startTime; console.log(`Cleanup completed in ${duration}ms. Deleted ${deletedCount} records.`); @@ -57,4 +64,7 @@ export class CleanupScheduler { } } -export const cleanupScheduler = new CleanupScheduler(24); \ No newline at end of file +// Default: cleanup every 24 hours, keep 90 days of liquidation data +// To disable cleanup: set retentionDays to 0 +// To keep more data: increase retentionDays (e.g., 365 for 1 year) +export const cleanupScheduler = new CleanupScheduler(24, 90); \ No newline at end of file diff --git a/src/lib/services/liquidationStorage.ts b/src/lib/services/liquidationStorage.ts index 68688b1..d497b0a 100644 --- a/src/lib/services/liquidationStorage.ts +++ b/src/lib/services/liquidationStorage.ts @@ -125,17 +125,23 @@ export class LiquidationStorage { return { liquidations, total }; } - async cleanupOldLiquidations(): Promise { - const sevenDaysAgo = Math.floor(Date.now() / 1000) - (7 * 24 * 60 * 60); + async cleanupOldLiquidations(retentionDays: number = 90): Promise { + // If retentionDays is 0, disable cleanup entirely + if (retentionDays <= 0) { + console.log('Liquidation cleanup disabled (retentionDays = 0)'); + return 0; + } + + const cutoffTime = Math.floor(Date.now() / 1000) - (retentionDays * 24 * 60 * 60); const countSql = 'SELECT COUNT(*) as count FROM liquidations WHERE created_at < ?'; - const countResult = await db.get<{ count: number }>(countSql, [sevenDaysAgo]); + const countResult = await db.get<{ count: number }>(countSql, [cutoffTime]); const deletedCount = countResult?.count || 0; const sql = 'DELETE FROM liquidations WHERE created_at < ?'; - await db.run(sql, [sevenDaysAgo]); + await db.run(sql, [cutoffTime]); - console.log(`Cleaned up ${deletedCount} liquidations older than 7 days`); + console.log(`Cleaned up ${deletedCount} liquidations older than ${retentionDays} days`); return deletedCount; } @@ -207,6 +213,22 @@ export class LiquidationStorage { return await db.all(sql, [limit]); } + + async getUniqueSymbols(): Promise { + try { + const sql = ` + SELECT DISTINCT symbol + FROM liquidations + ORDER BY symbol ASC + `; + + const result = await db.all<{ symbol: string }>(sql, []); + return result.map(row => row.symbol); + } catch (error) { + console.error('Error getting unique symbols:', error); + return []; + } + } } export const liquidationStorage = new LiquidationStorage(); \ No newline at end of file diff --git a/src/lib/types.ts b/src/lib/types.ts index 2ef39ca..bb20043 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -66,6 +66,7 @@ export interface GlobalConfig { useThresholdSystem?: boolean; // Enable 60-second rolling volume threshold system (default: false) server?: ServerConfig; // Optional server configuration rateLimit?: RateLimitConfig; // Rate limit configuration + liquidationDatabase?: LiquidationDatabaseConfig; // Liquidation data retention settings } export interface Config { diff --git a/src/middleware.ts b/src/middleware.ts index a8f5090..0cb9570 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -12,7 +12,7 @@ export default withAuth( const pathname = req.nextUrl.pathname; // Allow public paths - const PUBLIC_PATHS = ['/login', '/api/auth', '/api/health']; + const PUBLIC_PATHS = ['/login', '/api/auth', '/api/health', '/api/klines']; if (PUBLIC_PATHS.some(path => pathname.startsWith(path))) { return true; } From bb0a7b60fb7c0a4bc7ccc19ef500ab2a522c40f9 Mon Sep 17 00:00:00 2001 From: jameslappin Date: Tue, 18 Nov 2025 23:58:29 +1000 Subject: [PATCH 04/30] feat: add infinite historical data loading to chart - Automatically loads older candles when scrolling back in time - Monitors visible time range and loads 500 candles at a time - Adds endTime parameter support to klines API - Tracks earliest loaded candle timestamp in cache - Shows 'Loading history...' indicator during fetch - Prepends historical data without disrupting current view - No more 7-day limit - can view unlimited historical data --- src/app/api/klines/route.ts | 3 +- src/components/TradingViewChart.tsx | 85 ++++++++++++++++++++++++++--- src/lib/api/market.ts | 10 +++- src/lib/klineCache.ts | 67 ++++++++++++++++++++++- 4 files changed, 152 insertions(+), 13 deletions(-) diff --git a/src/app/api/klines/route.ts b/src/app/api/klines/route.ts index bfe3a26..ac197d2 100644 --- a/src/app/api/klines/route.ts +++ b/src/app/api/klines/route.ts @@ -17,6 +17,7 @@ export async function GET(request: NextRequest) { 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']; @@ -34,7 +35,7 @@ export async function GET(request: NextRequest) { console.log(`[Klines API] Fetching ${limit} candles for ${symbol} ${interval} (7-day optimized: ${getCandlesFor7Days(interval)})`); - const klines = await getKlines(symbol, interval, limit); + 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 => [ diff --git a/src/components/TradingViewChart.tsx b/src/components/TradingViewChart.tsx index 96d4473..2c78619 100644 --- a/src/components/TradingViewChart.tsx +++ b/src/components/TradingViewChart.tsx @@ -5,7 +5,7 @@ import orderStore from '@/lib/services/orderStore'; import { createChart, IChartApi, ISeriesApi, CandlestickData, Time } from 'lightweight-charts'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { getCachedKlines, setCachedKlines, updateCachedKlines, getCandlesFor7Days } from '@/lib/klineCache'; +import { getCachedKlines, setCachedKlines, updateCachedKlines, getCandlesFor7Days, prependHistoricalKlines } from '@/lib/klineCache'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Checkbox } from '@/components/ui/checkbox'; import { Label } from '@/components/ui/label'; @@ -139,11 +139,13 @@ export default function TradingViewChart({ const [autoRefresh, setAutoRefresh] = useState(false); const [lastUpdate, setLastUpdate] = useState(null); const [isRefreshing, setIsRefreshing] = useState(false); + const [isLoadingHistorical, setIsLoadingHistorical] = useState(false); // Refs to store refresh functions for auto-refresh const fetchKlineDataRef = useRef<(force?: boolean) => Promise>(); const fetchLiquidationDataRef = useRef<() => Promise>(); const fetchOpenOrdersRef = useRef<() => Promise>(); + const isLoadingHistoricalRef = useRef(false); // Combine props liquidations with database liquidations const allLiquidations = useMemo(() => @@ -317,6 +319,53 @@ export default function TradingViewChart({ [updatePositionIndicators] ); + // Load historical data when scrolling back in time + const loadHistoricalData = useCallback(async () => { + if (!symbol || !timeframe || isLoadingHistoricalRef.current) return; + + const cached = getCachedKlines(symbol, timeframe); + if (!cached) return; + + isLoadingHistoricalRef.current = true; + setIsLoadingHistorical(true); + + try { + // Fetch candles before the earliest loaded candle + const endTime = cached.earliestCandleTime - 1; + const response = await fetch(`/api/klines?symbol=${symbol}&interval=${timeframe}&endTime=${endTime}&limit=500`); + const result = await response.json(); + + if (result.success && result.data.length > 0) { + // Prepend historical data to cache + const updated = prependHistoricalKlines(symbol, timeframe, result.data); + + if (updated) { + // Transform and update chart data + const transformedData: CandlestickData[] = updated.data.map((kline: any[]) => { + const timestamp = typeof kline[0] === 'number' ? kline[0] : parseInt(kline[0]); + return { + time: timestamp as Time, + open: parseFloat(kline[1]), + high: parseFloat(kline[2]), + low: parseFloat(kline[3]), + close: parseFloat(kline[4]) + }; + }); + + transformedData.sort((a, b) => (a.time as number) - (b.time as number)); + setKlineData(transformedData); + + console.log(`[TradingViewChart] Loaded ${result.data.length} historical candles`); + } + } + } catch (error) { + console.error('[TradingViewChart] Error loading historical data:', error); + } finally { + setIsLoadingHistorical(false); + isLoadingHistoricalRef.current = false; + } + }, [symbol, timeframe]); + // Fetch liquidation data from database const fetchLiquidationData = useCallback(async () => { if (!symbol) return; @@ -599,6 +648,20 @@ export default function TradingViewChart({ chartRef.current = chart; candlestickSeriesRef.current = candlestickSeries; + + // Monitor visible time range to load historical data when user scrolls back + const handleVisibleLogicalRangeChange = debounce((newRange: any) => { + if (!newRange || !klineData.length) return; + + // Check if we're approaching the beginning of loaded data + const firstVisibleBar = Math.floor(newRange.from); + if (firstVisibleBar < 20 && !loading) { + // User is getting close to the oldest loaded data + loadHistoricalData(); + } + }, 500); + + chart.timeScale().subscribeVisibleLogicalRangeChange(handleVisibleLogicalRangeChange); } catch (error) { console.error(`[TradingViewChart] Error creating chart:`, error); } @@ -610,7 +673,7 @@ export default function TradingViewChart({ candlestickSeriesRef.current = null; } }; - }, [loading, error, isVisible, chartHeight]); // Re-initialize when loading/error/visibility states change + }, [loading, error, isVisible, chartHeight, klineData.length]); // Re-initialize when loading/error/visibility states change // Fetch data when symbol or timeframe changes useEffect(() => { @@ -1081,11 +1144,19 @@ export default function TradingViewChart({ )} {!loading && !error && ( -
+
+ {isLoadingHistorical && ( +
+ + Loading history... +
+ )} +
+
)} )} diff --git a/src/lib/api/market.ts b/src/lib/api/market.ts index 327fdf8..8b13c6e 100644 --- a/src/lib/api/market.ts +++ b/src/lib/api/market.ts @@ -22,8 +22,14 @@ export async function getMarkPrice(symbol?: string): Promise { - const params = { symbol, interval, limit }; +export async function getKlines(symbol: string, interval: string = '1m', limit: number = 500, endTime?: number): Promise { + const params: any = { symbol, interval, limit }; + + // If endTime is provided, fetch candles BEFORE that time + if (endTime) { + params.endTime = endTime; + } + const query = paramsToQuery(params); const axios = getRateLimitedAxios(); const response: AxiosResponse = await axios.get(`${BASE_URL}/fapi/v1/klines?${query}`); diff --git a/src/lib/klineCache.ts b/src/lib/klineCache.ts index ae419d5..4e05b87 100644 --- a/src/lib/klineCache.ts +++ b/src/lib/klineCache.ts @@ -1,10 +1,11 @@ -// Kline caching utility for 7-day historical data +// Kline caching utility for historical data export interface CachedKlineData { symbol: string; interval: string; data: number[][]; // [timestamp, open, high, low, close, volume] lastUpdate: number; lastCandleTime: number; + earliestCandleTime: number; // Track oldest loaded candle } // Calculate candles needed for 7 days based on timeframe @@ -78,7 +79,8 @@ export const setCachedKlines = (symbol: string, interval: string, data: number[] interval, data: sortedData, lastUpdate: now, - lastCandleTime: sortedData[sortedData.length - 1][0] * 1000 // Convert back to milliseconds + lastCandleTime: sortedData[sortedData.length - 1][0] * 1000, // Convert back to milliseconds + earliestCandleTime: sortedData[0][0] * 1000 // Track oldest loaded candle }; klineCache.set(key, cached); @@ -109,7 +111,66 @@ export const updateCachedKlines = (symbol: string, interval: string, newData: nu interval, data: trimmedData, lastUpdate: Date.now(), - lastCandleTime: trimmedData[trimmedData.length - 1][0] * 1000 + lastCandleTime: trimmedData[trimmedData.length - 1][0] * 1000, + earliestCandleTime: trimmedData[0][0] * 1000 + }; + + klineCache.set(key, updated); + return updated; +}; + +// Prepend historical data to the beginning of the cache +export const prependHistoricalKlines = (symbol: string, interval: string, historicalData: number[][]): CachedKlineData | null => { + const key = getCacheKey(symbol, interval); + const cached = klineCache.get(key); + + if (!cached || historicalData.length === 0) return null; + + // Filter out any duplicates + const existingTimestamps = new Set(cached.data.map(candle => candle[0])); + const newHistorical = historicalData.filter(candle => !existingTimestamps.has(candle[0])); + + if (newHistorical.length === 0) return cached; // No new data + + // Prepend and sort + const combinedData = [...newHistorical, ...cached.data].sort((a, b) => a[0] - b[0]); + + const updated: CachedKlineData = { + symbol, + interval, + data: combinedData, + lastUpdate: Date.now(), + lastCandleTime: combinedData[combinedData.length - 1][0] * 1000, + earliestCandleTime: combinedData[0][0] * 1000 + }; + + klineCache.set(key, updated); + return updated; +}; + +// Prepend historical data to the beginning of the cache +export const prependHistoricalKlines = (symbol: string, interval: string, historicalData: number[][]): CachedKlineData | null => { + const key = getCacheKey(symbol, interval); + const cached = klineCache.get(key); + + if (!cached || historicalData.length === 0) return null; + + // Filter out any duplicates + const existingTimestamps = new Set(cached.data.map(candle => candle[0])); + const newHistorical = historicalData.filter(candle => !existingTimestamps.has(candle[0])); + + if (newHistorical.length === 0) return cached; // No new data + + // Prepend and sort + const combinedData = [...newHistorical, ...cached.data].sort((a, b) => a[0] - b[0]); + + const updated: CachedKlineData = { + symbol, + interval, + data: combinedData, + lastUpdate: Date.now(), + lastCandleTime: combinedData[combinedData.length - 1][0] * 1000, + earliestCandleTime: combinedData[0][0] * 1000 }; klineCache.set(key, updated); From 0046305a5ce324a4c059ba80f47961d4e61a8841 Mon Sep 17 00:00:00 2001 From: jameslappin Date: Wed, 19 Nov 2025 00:00:54 +1000 Subject: [PATCH 05/30] fix: remove duplicate prependHistoricalKlines function --- src/lib/klineCache.ts | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/src/lib/klineCache.ts b/src/lib/klineCache.ts index 4e05b87..e12c34a 100644 --- a/src/lib/klineCache.ts +++ b/src/lib/klineCache.ts @@ -148,35 +148,6 @@ export const prependHistoricalKlines = (symbol: string, interval: string, histor return updated; }; -// Prepend historical data to the beginning of the cache -export const prependHistoricalKlines = (symbol: string, interval: string, historicalData: number[][]): CachedKlineData | null => { - const key = getCacheKey(symbol, interval); - const cached = klineCache.get(key); - - if (!cached || historicalData.length === 0) return null; - - // Filter out any duplicates - const existingTimestamps = new Set(cached.data.map(candle => candle[0])); - const newHistorical = historicalData.filter(candle => !existingTimestamps.has(candle[0])); - - if (newHistorical.length === 0) return cached; // No new data - - // Prepend and sort - const combinedData = [...newHistorical, ...cached.data].sort((a, b) => a[0] - b[0]); - - const updated: CachedKlineData = { - symbol, - interval, - data: combinedData, - lastUpdate: Date.now(), - lastCandleTime: combinedData[combinedData.length - 1][0] * 1000, - earliestCandleTime: combinedData[0][0] * 1000 - }; - - klineCache.set(key, updated); - return updated; -}; - export const clearCache = (): void => { klineCache.clear(); }; From f0b82f380dbb5077ab31dd316cc0e08512c7134f Mon Sep 17 00:00:00 2001 From: jameslappin Date: Wed, 19 Nov 2025 00:04:50 +1000 Subject: [PATCH 06/30] fix: resolve chart initialization issues with historical data loading - Use ref for loadHistoricalData to avoid scope issues - Remove klineData.length from chart init dependencies to prevent re-initialization - Chart now initializes correctly and loads historical data on scroll --- src/components/TradingViewChart.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/components/TradingViewChart.tsx b/src/components/TradingViewChart.tsx index 2c78619..6f35e05 100644 --- a/src/components/TradingViewChart.tsx +++ b/src/components/TradingViewChart.tsx @@ -146,6 +146,7 @@ export default function TradingViewChart({ const fetchLiquidationDataRef = useRef<() => Promise>(); const fetchOpenOrdersRef = useRef<() => Promise>(); const isLoadingHistoricalRef = useRef(false); + const loadHistoricalDataRef = useRef<() => Promise>(); // Combine props liquidations with database liquidations const allLiquidations = useMemo(() => @@ -365,6 +366,9 @@ export default function TradingViewChart({ isLoadingHistoricalRef.current = false; } }, [symbol, timeframe]); + + // Store function ref + loadHistoricalDataRef.current = loadHistoricalData; // Fetch liquidation data from database const fetchLiquidationData = useCallback(async () => { @@ -651,13 +655,13 @@ export default function TradingViewChart({ // Monitor visible time range to load historical data when user scrolls back const handleVisibleLogicalRangeChange = debounce((newRange: any) => { - if (!newRange || !klineData.length) return; + if (!newRange) return; // Check if we're approaching the beginning of loaded data const firstVisibleBar = Math.floor(newRange.from); - if (firstVisibleBar < 20 && !loading) { + if (firstVisibleBar < 20 && !loading && loadHistoricalDataRef.current) { // User is getting close to the oldest loaded data - loadHistoricalData(); + loadHistoricalDataRef.current(); } }, 500); @@ -673,7 +677,7 @@ export default function TradingViewChart({ candlestickSeriesRef.current = null; } }; - }, [loading, error, isVisible, chartHeight, klineData.length]); // Re-initialize when loading/error/visibility states change + }, [loading, error, isVisible, chartHeight]); // Re-initialize when loading/error/visibility states change // Fetch data when symbol or timeframe changes useEffect(() => { From b6299c0bdc69dfc80efc58188b5fb3edea61f207 Mon Sep 17 00:00:00 2001 From: jameslappin Date: Wed, 19 Nov 2025 00:09:52 +1000 Subject: [PATCH 07/30] fix: preserve chart view position after user interaction - Track user interactions (scrolling/zooming) with chart - Only reset to 2/3 position on initial load - Maintain view position during auto-refresh and historical data loading - Reset interaction state when symbol or timeframe changes - Improves UX by not disrupting user's chosen view --- src/components/TradingViewChart.tsx | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/components/TradingViewChart.tsx b/src/components/TradingViewChart.tsx index 6f35e05..f6ddd07 100644 --- a/src/components/TradingViewChart.tsx +++ b/src/components/TradingViewChart.tsx @@ -140,6 +140,8 @@ export default function TradingViewChart({ const [lastUpdate, setLastUpdate] = useState(null); const [isRefreshing, setIsRefreshing] = useState(false); const [isLoadingHistorical, setIsLoadingHistorical] = useState(false); + const [hasUserInteracted, setHasUserInteracted] = useState(false); + const isInitialLoadRef = useRef(true); // Refs to store refresh functions for auto-refresh const fetchKlineDataRef = useRef<(force?: boolean) => Promise>(); @@ -653,10 +655,15 @@ export default function TradingViewChart({ chartRef.current = chart; candlestickSeriesRef.current = candlestickSeries; - // Monitor visible time range to load historical data when user scrolls back + // Track user interactions (scrolling, zooming) const handleVisibleLogicalRangeChange = debounce((newRange: any) => { if (!newRange) return; + // Mark that user has interacted if this wasn't triggered by initial load + if (!isInitialLoadRef.current) { + setHasUserInteracted(true); + } + // Check if we're approaching the beginning of loaded data const firstVisibleBar = Math.floor(newRange.from); if (firstVisibleBar < 20 && !loading && loadHistoricalDataRef.current) { @@ -682,6 +689,10 @@ export default function TradingViewChart({ // Fetch data when symbol or timeframe changes useEffect(() => { if (symbol && timeframe && isVisible) { + // Reset interaction state for new symbol/timeframe + setHasUserInteracted(false); + isInitialLoadRef.current = true; + fetchKlineData(); fetchLiquidationData(); fetchOpenOrders(); @@ -710,8 +721,8 @@ export default function TradingViewChart({ if (candlestickSeriesRef.current && klineData.length > 0) { candlestickSeriesRef.current.setData(klineData); - // Set visible logical range: most recent candle at 2/3 mark, 1/3 empty space on right - if (chartRef.current && klineData.length > 0) { + // Only set visible range on initial load or if user hasn't interacted + if (chartRef.current && klineData.length > 0 && !hasUserInteracted) { const totalBars = klineData.length; // Calculate how many bars to show (e.g., show 60 bars = 1 hour of 1m candles) @@ -730,9 +741,12 @@ export default function TradingViewChart({ from: firstBarIndex, to: lastBarIndex + rightPadding, }); + + // Mark that initial load is complete + isInitialLoadRef.current = false; } } - }, [klineData]); + }, [klineData, hasUserInteracted]); // Update position indicators when positions change useEffect(() => { From 5c6973e2cba174df52521080382a3d1a71180306 Mon Sep 17 00:00:00 2001 From: birdbath Date: Wed, 19 Nov 2025 09:46:55 +1000 Subject: [PATCH 08/30] fix: Add WebSocket keepalive and inactivity monitoring to Hunter - Add ping/pong keepalive every 30 seconds to detect silent disconnections - Add inactivity monitor: auto-reconnect if no liquidations received for 5 minutes - Add proper cleanup of keepalive and inactivity timers on disconnect/stop - Log warnings when stream becomes inactive - Broadcast inactivity warnings to UI for visibility This fixes the issue where the liquidation stream would stop receiving data without triggering any errors or reconnection attempts. --- src/lib/bot/hunter.ts | 100 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 95 insertions(+), 5 deletions(-) diff --git a/src/lib/bot/hunter.ts b/src/lib/bot/hunter.ts index 3bc7f76..d5ac0bd 100644 --- a/src/lib/bot/hunter.ts +++ b/src/lib/bot/hunter.ts @@ -36,6 +36,9 @@ export class Hunter extends EventEmitter { private cleanupInterval: NodeJS.Timeout | null = null; // Periodic cleanup timer private syncInterval: NodeJS.Timeout | null = null; // Position mode sync timer private lastModeSync: number = Date.now(); // Track last mode sync time + private wsKeepAliveInterval: NodeJS.Timeout | null = null; // WebSocket keepalive ping timer + private wsInactivityTimeout: NodeJS.Timeout | null = null; // WebSocket inactivity detector + private lastLiquidationTime: number = Date.now(); // Track last liquidation received constructor(config: Config, isHedgeMode: boolean = false) { super(); @@ -325,9 +328,12 @@ logWithTimestamp('Hunter: Running in paper mode without API keys - simulating li if (this.syncInterval) { clearInterval(this.syncInterval); this.syncInterval = null; -logWithTimestamp('Hunter: Stopped periodic position mode sync'); + logWithTimestamp('Hunter: Stopped periodic position mode sync'); } + // Clean up WebSocket keepalive and inactivity timers + this.cleanupWebSocketTimers(); + if (this.ws) { this.ws.close(); this.ws = null; @@ -335,18 +341,52 @@ logWithTimestamp('Hunter: Stopped periodic position mode sync'); } private connectWebSocket(): void { + // Clean up any existing keepalive/inactivity timers + if (this.wsKeepAliveInterval) { + clearInterval(this.wsKeepAliveInterval); + this.wsKeepAliveInterval = null; + } + if (this.wsInactivityTimeout) { + clearTimeout(this.wsInactivityTimeout); + this.wsInactivityTimeout = null; + } + this.ws = new WebSocket('wss://fstream.asterdex.com/ws/!forceOrder@arr'); this.ws.on('open', () => { -logWithTimestamp('Hunter WS connected'); + logWithTimestamp('Hunter WS connected'); + this.lastLiquidationTime = Date.now(); + + // Start ping/pong keepalive - send ping every 30 seconds + this.wsKeepAliveInterval = setInterval(() => { + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + this.ws.ping(); + } + }, 30000); + + // Start inactivity monitor - reconnect if no liquidations for 5 minutes + this.startInactivityMonitor(); + }); + + this.ws.on('ping', () => { + // Server sent ping, respond with pong (ws library handles this automatically) + }); + + this.ws.on('pong', () => { + // Received pong response from server - connection is alive }); this.ws.on('message', (data: Buffer) => { try { const event = JSON.parse(data.toString()); + + // Update last liquidation time for any valid message + this.lastLiquidationTime = Date.now(); + this.startInactivityMonitor(); // Reset inactivity timer + this.handleLiquidationEvent(event); } catch (error) { -logErrorWithTimestamp('Hunter: WS message parse error:', error); + logErrorWithTimestamp('Hunter: WS message parse error:', error); // Log to error database errorLogger.logError(error instanceof Error ? error : new Error(String(error)), { type: 'websocket', @@ -372,7 +412,7 @@ logErrorWithTimestamp('Hunter: WS message parse error:', error); }); this.ws.on('error', (error) => { -logErrorWithTimestamp('Hunter WS error:', error); + logErrorWithTimestamp('Hunter WS error:', error); // Log to error database errorLogger.logWebSocketError( 'wss://fstream.asterdex.com/ws/!forceOrder@arr', @@ -390,12 +430,17 @@ logErrorWithTimestamp('Hunter WS error:', error); } ); } + // Clean up timers before reconnecting + this.cleanupWebSocketTimers(); // Reconnect after delay setTimeout(() => this.connectWebSocket(), 5000); }); this.ws.on('close', () => { -logWithTimestamp('Hunter WS closed'); + logWithTimestamp('Hunter WS closed'); + // Clean up timers + this.cleanupWebSocketTimers(); + if (this.isRunning) { // Broadcast reconnection attempt to UI if (this.statusBroadcaster) { @@ -412,6 +457,51 @@ logWithTimestamp('Hunter WS closed'); }); } + private startInactivityMonitor(): void { + // Clear any existing inactivity timeout + if (this.wsInactivityTimeout) { + clearTimeout(this.wsInactivityTimeout); + } + + // Set up new inactivity timeout - 5 minutes without liquidations + this.wsInactivityTimeout = setTimeout(() => { + const timeSinceLastLiq = Date.now() - this.lastLiquidationTime; + const minutesInactive = Math.floor(timeSinceLastLiq / 60000); + + logWarnWithTimestamp(`โš ๏ธ Hunter: No liquidations received for ${minutesInactive} minutes. Reconnecting...`); + + // Broadcast warning to UI + if (this.statusBroadcaster) { + this.statusBroadcaster.broadcastWebSocketError( + 'Hunter Stream Inactive', + `No liquidations received for ${minutesInactive} minutes. Reconnecting to ensure stream is alive...`, + { + component: 'Hunter', + inactiveMinutes: minutesInactive, + } + ); + } + + // Force reconnection + if (this.ws) { + this.ws.close(); + this.ws = null; + } + this.connectWebSocket(); + }, 5 * 60 * 1000); // 5 minutes + } + + private cleanupWebSocketTimers(): void { + if (this.wsKeepAliveInterval) { + clearInterval(this.wsKeepAliveInterval); + this.wsKeepAliveInterval = null; + } + if (this.wsInactivityTimeout) { + clearTimeout(this.wsInactivityTimeout); + this.wsInactivityTimeout = null; + } + } + private async handleLiquidationEvent(event: any): Promise { if (event.e !== 'forceOrder') return; // Not a liquidation event From e6aa8c7aa2612bb3f0879ceb21122fc242f99774 Mon Sep 17 00:00:00 2001 From: birdbath Date: Wed, 19 Nov 2025 10:13:24 +1000 Subject: [PATCH 09/30] feat: Add TP/SL toggle and reorganize chart controls - Add 'TP/SL' toggle to show/hide position entry and TP/SL lines - Move 'Liquidations' toggle next to 'Group' setting (they're related) - Reorganize controls: [Auto-refresh, Orders, TP/SL, VWAP] | [Liquidations, Group] | [Timeframe] - Fix position lines disappearing when changing chart settings - Position lines now persist through timeframe/symbol/grouping changes - Lines are only cleared when toggle is disabled or positions change This improves chart control organization and fixes the issue where TP/SL lines would disappear when adjusting chart settings. --- src/components/TradingViewChart.tsx | 94 +++++++++++++++++++---------- 1 file changed, 63 insertions(+), 31 deletions(-) diff --git a/src/components/TradingViewChart.tsx b/src/components/TradingViewChart.tsx index f6ddd07..5285b29 100644 --- a/src/components/TradingViewChart.tsx +++ b/src/components/TradingViewChart.tsx @@ -136,6 +136,7 @@ export default function TradingViewChart({ const [openOrders, setOpenOrders] = useState([]); const [showVWAP, setShowVWAP] = useState(false); const [showRecentOrders, setShowRecentOrders] = useState(false); + const [showPositions, setShowPositions] = useState(true); // Show TP/SL lines const [autoRefresh, setAutoRefresh] = useState(false); const [lastUpdate, setLastUpdate] = useState(null); const [isRefreshing, setIsRefreshing] = useState(false); @@ -231,6 +232,11 @@ export default function TradingViewChart({ }); positionLinesRef.current = []; + // Don't show position lines if toggle is off + if (!showPositions) { + return; + } + // Filter positions for current symbol const symbolPositions = positions.filter(pos => pos.symbol === symbol); @@ -311,7 +317,7 @@ export default function TradingViewChart({ console.error('[TradingViewChart] Error adding order line:', error); } }); - }, [symbol]); + }, [symbol, showPositions]); // Debounced position updates const debouncedUpdatePositions = useCallback( @@ -748,12 +754,22 @@ export default function TradingViewChart({ } }, [klineData, hasUserInteracted]); - // Update position indicators when positions change + // Update position indicators when positions change or toggle changes useEffect(() => { - if (positions.length > 0) { + if (showPositions && positions.length > 0) { debouncedUpdatePositions(positions, openOrders); + } else if (!showPositions) { + // Clear lines when toggle is off + positionLinesRef.current.forEach(line => { + try { + candlestickSeriesRef.current?.removePriceLine(line); + } catch (_e) { + // Ignore errors + } + }); + positionLinesRef.current = []; } - }, [positions, openOrders, debouncedUpdatePositions]); + }, [positions, openOrders, showPositions, debouncedUpdatePositions]); // Manual refresh handler const handleRefresh = useCallback(() => { @@ -1058,18 +1074,6 @@ export default function TradingViewChart({ Auto-refresh
- -
- setShowLiquidations(checked as boolean)} - className="h-4 w-4" - /> - -
+
+ setShowPositions(checked as boolean)} + className="h-4 w-4" + /> + +
+
- {showLiquidations && ( +
- - + setShowLiquidations(checked as boolean)} + className="h-4 w-4" + /> +
- )} + + {showLiquidations && ( +
+ + +
+ )} +
+ +
From 8d9aad1808bd0a3de3f8b290e7f9d054b6b52c44 Mon Sep 17 00:00:00 2001 From: jameslappin Date: Wed, 19 Nov 2025 19:11:20 +1000 Subject: [PATCH 10/30] fix: prevent duplicate liquidations from accumulated event listeners - Reuse Hunter instance instead of creating new one on bot restart - Remove all event listeners before re-attaching to prevent duplicates - Clean up thresholdMonitor listeners by event name - Add detailed logging for liquidation sidebar API loading - Fix: Hunter now calls removeAllListeners() on stop to clear handlers This fixes the issue where liquidations would appear multiple times in the UI due to event listeners accumulating across bot restarts and HMR cycles. --- src/bot/index.ts | 16 +++++-- src/bot/websocketServer.ts | 7 +++ src/components/LiquidationFeed.tsx | 31 ++++++++----- src/components/LiquidationSidebar.tsx | 52 +++++++++++++++------ src/components/PerformanceCardInline.tsx | 2 +- src/components/SessionPerformanceCard.tsx | 2 +- src/lib/bot/hunter.ts | 56 +++++++++++++---------- src/lib/services/websocketService.ts | 4 ++ 8 files changed, 116 insertions(+), 54 deletions(-) diff --git a/src/bot/index.ts b/src/bot/index.ts index b5803d7..200e2fc 100644 --- a/src/bot/index.ts +++ b/src/bot/index.ts @@ -461,8 +461,14 @@ logErrorWithTimestamp('โš ๏ธ Position Manager failed to start:', error.message logWithTimestamp('โ„น๏ธ Tranche Management disabled for all symbols'); } - // Initialize Hunter - this.hunter = new Hunter(this.config, this.isHedgeMode); + // Initialize Hunter (or reuse existing instance to prevent duplicate listeners) + if (!this.hunter) { + this.hunter = new Hunter(this.config, this.isHedgeMode); + } else { + // Remove all old listeners before re-attaching to prevent duplicates + this.hunter.removeAllListeners(); + console.log('[Bot] Removed all old hunter event listeners to prevent duplicates'); + } // Inject status broadcaster for order events this.hunter.setStatusBroadcaster(this.statusBroadcaster); @@ -474,7 +480,8 @@ logErrorWithTimestamp('โš ๏ธ Position Manager failed to start:', error.message // Connect hunter events to position manager and status broadcaster this.hunter.on('liquidationDetected', (liquidationEvent: any) => { - logWithTimestamp(`๐Ÿ’ฅ Liquidation: ${liquidationEvent.symbol} ${liquidationEvent.side} ${liquidationEvent.quantity}`); + console.log(`[Bot] liquidationDetected event received for ${liquidationEvent.symbol}`); + // Broadcast to UI and log activity (don't log to console - already logged in hunter.ts) this.statusBroadcaster.broadcastLiquidation(liquidationEvent); this.statusBroadcaster.logActivity(`Liquidation: ${liquidationEvent.symbol} ${liquidationEvent.side} ${liquidationEvent.quantity}`); }); @@ -491,6 +498,9 @@ logErrorWithTimestamp('โš ๏ธ Position Manager failed to start:', error.message this.statusBroadcaster.logActivity(`Blocked: ${data.symbol} ${data.side} - ${data.blockType}`); }); + // Remove old threshold monitor listeners to prevent duplicates + thresholdMonitor.removeAllListeners('thresholdUpdate'); + // Listen for threshold updates and broadcast to UI thresholdMonitor.on('thresholdUpdate', (thresholdUpdate: any) => { this.statusBroadcaster.broadcastThresholdUpdate(thresholdUpdate); diff --git a/src/bot/websocketServer.ts b/src/bot/websocketServer.ts index bcc8dcd..471d8ea 100644 --- a/src/bot/websocketServer.ts +++ b/src/bot/websocketServer.ts @@ -211,12 +211,18 @@ export class StatusBroadcaster extends EventEmitter { private _broadcast(type: string, data: any): void { const message = JSON.stringify({ type, data }); + let sentCount = 0; this.clients.forEach(client => { if (client.readyState === WebSocket.OPEN) { client.send(message); + sentCount++; } }); + + if (type === 'liquidation') { + console.log(`[WebSocketServer] Sent ${type} to ${sentCount} open clients (${this.clients.size} total)`); + } } logActivity(activity: string): void { @@ -229,6 +235,7 @@ export class StatusBroadcaster extends EventEmitter { // Broadcast liquidation events to connected clients broadcastLiquidation(liquidationEvent: LiquidationEvent): void { + console.log(`[WebSocketServer] Broadcasting liquidation ${liquidationEvent.symbol} to ${this.clients.size} clients`); this._broadcast('liquidation', { symbol: liquidationEvent.symbol, side: liquidationEvent.side, diff --git a/src/components/LiquidationFeed.tsx b/src/components/LiquidationFeed.tsx index 29810f7..16dff37 100644 --- a/src/components/LiquidationFeed.tsx +++ b/src/components/LiquidationFeed.tsx @@ -41,21 +41,27 @@ export default function LiquidationFeed({ volumeThresholds = {}, maxEvents = 50 }); const { formatQuantity, formatPriceWithCommas } = useSymbolPrecision(); - // Capture initial values to avoid re-running effect when props change - const initialMaxEvents = useRef(maxEvents); - const initialVolumeThresholds = useRef(volumeThresholds); + // Use refs to track current prop values without causing re-subscriptions + const volumeThresholdsRef = useRef(volumeThresholds); + const maxEventsRef = useRef(maxEvents); + + // Update refs when props change (doesn't trigger WebSocket re-subscription) + useEffect(() => { + volumeThresholdsRef.current = volumeThresholds; + maxEventsRef.current = maxEvents; + }, [volumeThresholds, maxEvents]); // Load historical liquidations on mount useEffect(() => { const loadHistoricalLiquidations = async () => { try { - const response = await fetch(`/api/liquidations?limit=${initialMaxEvents.current}`); + const response = await fetch(`/api/liquidations?limit=${maxEventsRef.current}`); if (response.ok) { const result = await response.json(); if (result.success && result.data) { const historicalEvents = result.data.map((liq: any) => { - const volume = liq.volume_usdt || (liq.quantity * liq.price); - const threshold = initialVolumeThresholds.current[liq.symbol] || 10000; + const volume = liq.quantity * liq.price; + const threshold = volumeThresholdsRef.current[liq.symbol] || 10000; return { symbol: liq.symbol, side: liq.side, @@ -98,15 +104,17 @@ export default function LiquidationFeed({ volumeThresholds = {}, maxEvents = 50 loadHistoricalLiquidations(); }, []); // Only run once on mount - using refs for current values - // Handle WebSocket messages + // Handle WebSocket messages - Subscribe ONCE on mount useEffect(() => { + console.log('[LiquidationFeed] Subscribing to WebSocket'); + const handleMessage = (message: any) => { if (message.type === 'liquidation') { const liquidationData = message.data; - // Calculate volume and determine if high volume + // Calculate volume and determine if high volume (use ref for latest threshold) const volume = liquidationData.quantity * liquidationData.price; - const threshold = volumeThresholds[liquidationData.symbol] || 10000; // Default $10k + const threshold = volumeThresholdsRef.current[liquidationData.symbol] || 10000; const isHighVolume = volume >= threshold; const liquidationEvent: LiquidationEvent = { @@ -116,7 +124,7 @@ export default function LiquidationFeed({ volumeThresholds = {}, maxEvents = 50 }; setEvents(prev => { - const newEvents = [liquidationEvent, ...prev].slice(0, maxEvents); + const newEvents = [liquidationEvent, ...prev].slice(0, maxEventsRef.current); // Update stats const now = Date.now(); @@ -148,10 +156,11 @@ export default function LiquidationFeed({ volumeThresholds = {}, maxEvents = 50 const cleanupConnectionListener = websocketService.addConnectionListener(handleConnectionChange); return () => { + console.log('[LiquidationFeed] Cleaning up WebSocket subscription'); cleanupMessageHandler(); cleanupConnectionListener(); }; - }, [volumeThresholds, maxEvents]); + }, []); // Empty deps - subscribe to WebSocket only once on mount const formatTime = (timestamp: Date | number): string => { const date = timestamp instanceof Date ? timestamp : new Date(timestamp); diff --git a/src/components/LiquidationSidebar.tsx b/src/components/LiquidationSidebar.tsx index d4ff096..3b53b00 100644 --- a/src/components/LiquidationSidebar.tsx +++ b/src/components/LiquidationSidebar.tsx @@ -35,22 +35,38 @@ export default function LiquidationSidebar({ volumeThresholds = {}, maxEvents = const [newEventIds, setNewEventIds] = useState>(new Set()); const _containerRef = useRef(null); const prevEventsRef = useRef([]); + + // Generate unique instance ID for debugging + const instanceId = useRef(Math.random().toString(36).substring(7)); - // Capture initial values to avoid re-running effect when props change - const initialMaxEvents = useRef(maxEvents); - const initialVolumeThresholds = useRef(volumeThresholds); + // Use refs to track current prop values without causing re-subscriptions + const volumeThresholdsRef = useRef(volumeThresholds); + const maxEventsRef = useRef(maxEvents); + + // Update refs when props change (doesn't trigger WebSocket re-subscription) + useEffect(() => { + volumeThresholdsRef.current = volumeThresholds; + maxEventsRef.current = maxEvents; + }, [volumeThresholds, maxEvents]); // Load historical liquidations on mount useEffect(() => { + console.log(`[LiquidationSidebar:${instanceId.current}] Historical liquidation useEffect triggered`); + const loadHistoricalLiquidations = async () => { try { - const response = await fetch(`/api/liquidations?limit=${initialMaxEvents.current}`); + console.log(`[LiquidationSidebar:${instanceId.current}] Fetching historical liquidations from API...`); + const response = await fetch(`/api/liquidations?limit=${maxEventsRef.current}`); + console.log(`[LiquidationSidebar:${instanceId.current}] API response status:`, response.status); + if (response.ok) { const result = await response.json(); + console.log(`[LiquidationSidebar:${instanceId.current}] API response:`, result); + if (result.success && result.data) { const historicalEvents = result.data.map((liq: any) => { const volume = liq.volume_usdt || (liq.quantity * liq.price); - const threshold = initialVolumeThresholds.current[liq.symbol] || 10000; + const threshold = volumeThresholdsRef.current[liq.symbol] || 10000; return { symbol: liq.symbol, side: liq.side, @@ -65,12 +81,16 @@ export default function LiquidationSidebar({ volumeThresholds = {}, maxEvents = isHighVolume: volume >= threshold, }; }); - console.log(`Loaded ${historicalEvents.length} historical liquidations`); + console.log(`[LiquidationSidebar:${instanceId.current}] Loaded ${historicalEvents.length} historical liquidations`); setEvents(historicalEvents); + } else { + console.log(`[LiquidationSidebar:${instanceId.current}] No data in API response or unsuccessful`); } + } else { + console.error(`[LiquidationSidebar:${instanceId.current}] API request failed with status:`, response.status); } } catch (error) { - console.error('Failed to load historical liquidations:', error); + console.error(`[LiquidationSidebar:${instanceId.current}] Failed to load historical liquidations:`, error); } finally { setIsLoading(false); } @@ -79,15 +99,18 @@ export default function LiquidationSidebar({ volumeThresholds = {}, maxEvents = loadHistoricalLiquidations(); }, []); // Only run once on mount - using refs for current values - // Handle WebSocket messages for real-time updates + // Handle WebSocket messages for real-time updates - Subscribe ONCE on mount useEffect(() => { + console.log(`[LiquidationSidebar:${instanceId.current}] Subscribing to WebSocket`); + const handleMessage = (message: any) => { if (message.type === 'liquidation') { + console.log(`[LiquidationSidebar:${instanceId.current}] Received liquidation message:`, message.data?.symbol); const liquidationData = message.data; - // Calculate volume and determine if high volume + // Calculate volume and determine if high volume (use ref for latest threshold) const volume = liquidationData.quantity * liquidationData.price; - const threshold = volumeThresholds[liquidationData.symbol] || 10000; // Default $10k + const threshold = volumeThresholdsRef.current[liquidationData.symbol] || 10000; const isHighVolume = volume >= threshold; const liquidationEvent: LiquidationEvent = { @@ -98,10 +121,12 @@ export default function LiquidationSidebar({ volumeThresholds = {}, maxEvents = // Mark this event as new for animation const eventId = `${liquidationData.symbol}-${liquidationData.eventTime}`; - setNewEventIds(prev => new Set([...prev, eventId])); setEvents(prev => { - const newEvents = [liquidationEvent, ...prev].slice(0, maxEvents); + // Mark as new for animation + setNewEventIds(prevIds => new Set([...prevIds, eventId])); + + const newEvents = [liquidationEvent, ...prev].slice(0, maxEventsRef.current); prevEventsRef.current = newEvents; return newEvents; }); @@ -127,10 +152,11 @@ export default function LiquidationSidebar({ volumeThresholds = {}, maxEvents = const cleanupConnectionListener = websocketService.addConnectionListener(handleConnectionChange); return () => { + console.log(`[LiquidationSidebar:${instanceId.current}] Cleaning up WebSocket subscription`); cleanupMessageHandler(); cleanupConnectionListener(); }; - }, [volumeThresholds, maxEvents]); + }, []); // Empty deps - subscribe to WebSocket only once on mount const formatTime = (timestamp: Date | number): string => { const date = timestamp instanceof Date ? timestamp : new Date(timestamp); diff --git a/src/components/PerformanceCardInline.tsx b/src/components/PerformanceCardInline.tsx index 1c066d0..1d73e9f 100644 --- a/src/components/PerformanceCardInline.tsx +++ b/src/components/PerformanceCardInline.tsx @@ -3,7 +3,7 @@ import React, { useEffect, useState } from 'react'; import { Badge } from '@/components/ui/badge'; import { Skeleton } from '@/components/ui/skeleton'; -import { Clock } from 'lucide-react'; +import { Clock, TrendingUp, TrendingDown } from 'lucide-react'; import websocketService from '@/lib/services/websocketService'; import dataStore from '@/lib/services/dataStore'; diff --git a/src/components/SessionPerformanceCard.tsx b/src/components/SessionPerformanceCard.tsx index e54eb7b..601f4f7 100644 --- a/src/components/SessionPerformanceCard.tsx +++ b/src/components/SessionPerformanceCard.tsx @@ -3,7 +3,7 @@ import React, { useEffect, useState } from 'react'; import { Badge } from '@/components/ui/badge'; import { Skeleton } from '@/components/ui/skeleton'; -import { Activity } from 'lucide-react'; +import { Activity, TrendingUp, TrendingDown } from 'lucide-react'; import websocketService from '@/lib/services/websocketService'; interface SessionPnL { diff --git a/src/lib/bot/hunter.ts b/src/lib/bot/hunter.ts index 0e1c4bb..af36736 100644 --- a/src/lib/bot/hunter.ts +++ b/src/lib/bot/hunter.ts @@ -39,6 +39,7 @@ export class Hunter extends EventEmitter { private wsKeepAliveInterval: NodeJS.Timeout | null = null; // WebSocket keepalive ping timer private wsInactivityTimeout: NodeJS.Timeout | null = null; // WebSocket inactivity detector private lastLiquidationTime: number = Date.now(); // Track last liquidation received + private statusLogInterval: NodeJS.Timeout | null = null; // Periodic status logging constructor(config: Config, isHedgeMode: boolean = false) { super(); @@ -338,6 +339,9 @@ logWithTimestamp('Hunter: Running in paper mode without API keys - simulating li this.ws.close(); this.ws = null; } + + // Remove all event listeners to prevent memory leaks and duplicate event handlers + this.removeAllListeners(); } private connectWebSocket(): void { @@ -366,6 +370,19 @@ logWithTimestamp('Hunter: Running in paper mode without API keys - simulating li // Start inactivity monitor - reconnect if no liquidations for 5 minutes this.startInactivityMonitor(); + + // Start periodic status logging - every 2 minutes + this.statusLogInterval = setInterval(() => { + const timeSinceLastLiq = Date.now() - this.lastLiquidationTime; + const minutesInactive = Math.floor(timeSinceLastLiq / 60000); + const secondsInactive = Math.floor((timeSinceLastLiq % 60000) / 1000); + + if (minutesInactive >= 1) { + logWithTimestamp(`๐Ÿ“Š Hunter: Monitoring | Last liquidation: ${minutesInactive}m ${secondsInactive}s ago`); + } else { + logWithTimestamp(`๐Ÿ“Š Hunter: Monitoring | Last liquidation: ${secondsInactive}s ago`); + } + }, 120000); // Every 2 minutes }); this.ws.on('ping', () => { @@ -442,16 +459,7 @@ logWithTimestamp('Hunter: Running in paper mode without API keys - simulating li this.cleanupWebSocketTimers(); if (this.isRunning) { - // Broadcast reconnection attempt to UI - if (this.statusBroadcaster) { - this.statusBroadcaster.broadcastWebSocketError( - 'Hunter WebSocket Closed', - 'Liquidation stream disconnected. Reconnecting in 5 seconds...', - { - component: 'Hunter', - } - ); - } + // Reconnect silently - close events are often normal (like during inactivity reconnect) setTimeout(() => this.connectWebSocket(), 5000); } }); @@ -468,19 +476,7 @@ logWithTimestamp('Hunter: Running in paper mode without API keys - simulating li const timeSinceLastLiq = Date.now() - this.lastLiquidationTime; const minutesInactive = Math.floor(timeSinceLastLiq / 60000); - logWarnWithTimestamp(`โš ๏ธ Hunter: No liquidations received for ${minutesInactive} minutes. Reconnecting...`); - - // Broadcast warning to UI - if (this.statusBroadcaster) { - this.statusBroadcaster.broadcastWebSocketError( - 'Hunter Stream Inactive', - `No liquidations received for ${minutesInactive} minutes. Reconnecting to ensure stream is alive...`, - { - component: 'Hunter', - inactiveMinutes: minutesInactive, - } - ); - } + logWarnWithTimestamp(`โš ๏ธ Hunter: No liquidations for ${minutesInactive} minutes. Reconnecting stream...`); // Force reconnection if (this.ws) { @@ -500,10 +496,16 @@ logWithTimestamp('Hunter: Running in paper mode without API keys - simulating li clearTimeout(this.wsInactivityTimeout); this.wsInactivityTimeout = null; } + if (this.statusLogInterval) { + clearInterval(this.statusLogInterval); + this.statusLogInterval = null; + } } private async handleLiquidationEvent(event: any): Promise { if (event.e !== 'forceOrder') return; // Not a liquidation event + + console.log(`[Hunter] handleLiquidationEvent START: ${event.o.s} @ ${Date.now()}`); const liquidation: LiquidationEvent = { symbol: event.o.s, @@ -521,6 +523,10 @@ logWithTimestamp('Hunter: Running in paper mode without API keys - simulating li time: event.E, // Keep for backward compatibility }; + // Log liquidation received with basic info + const volumeUSDT = liquidation.qty * liquidation.price; + logWithTimestamp(`๐Ÿ’ฅ Liquidation: ${liquidation.symbol} ${liquidation.side} ${liquidation.qty.toFixed(4)} @ $${liquidation.price.toLocaleString()} ($${volumeUSDT.toFixed(2)})`); + // Check if threshold system is enabled globally and for this symbol const useThresholdSystem = this.config.global.useThresholdSystem === true && this.config.symbols[liquidation.symbol]?.useThreshold === true; @@ -529,16 +535,16 @@ logWithTimestamp('Hunter: Running in paper mode without API keys - simulating li const thresholdStatus = useThresholdSystem ? thresholdMonitor.processLiquidation(liquidation) : null; // Emit liquidation event to WebSocket clients (all liquidations) with threshold info + console.log(`[Hunter] About to emit liquidationDetected for ${liquidation.symbol}`); this.emit('liquidationDetected', { ...liquidation, thresholdStatus }); + console.log(`[Hunter] Finished emitting liquidationDetected for ${liquidation.symbol}`); const symbolConfig = this.config.symbols[liquidation.symbol]; if (!symbolConfig) return; // Symbol not in config - const volumeUSDT = liquidation.qty * liquidation.price; - // Store liquidation in database (non-blocking) liquidationStorage.saveLiquidation(liquidation, volumeUSDT).catch(error => { logErrorWithTimestamp('Hunter: Failed to store liquidation:', error); diff --git a/src/lib/services/websocketService.ts b/src/lib/services/websocketService.ts index c1f40df..f1c35a0 100644 --- a/src/lib/services/websocketService.ts +++ b/src/lib/services/websocketService.ts @@ -172,6 +172,8 @@ class WebSocketService { this.isIntentionalDisconnect = true; } + // Broadcast to all handlers + console.log(`[WebSocketService] Broadcasting ${message.type} to ${this.handlers.size} handlers`); this.handlers.forEach(handler => { try { handler(message); @@ -242,6 +244,7 @@ class WebSocketService { addMessageHandler(handler: MessageHandler): () => void { this.handlers.add(handler); + console.log(`[WebSocketService] Handler added. Total handlers: ${this.handlers.size}`); // Check if we should auto-connect (skip on excluded pages) if (typeof window !== 'undefined') { @@ -275,6 +278,7 @@ class WebSocketService { // Return cleanup function return () => { this.handlers.delete(handler); + console.log(`[WebSocketService] Handler removed. Total handlers: ${this.handlers.size}`); // If no more handlers, disconnect if (this.handlers.size === 0) { From 57ebca21bae4608d586c6000114be0d700be06c9 Mon Sep 17 00:00:00 2001 From: jameslappin Date: Wed, 19 Nov 2025 19:18:39 +1000 Subject: [PATCH 11/30] feat: add configurable auto-refresh interval for chart (5s to 5min options) --- src/components/TradingViewChart.tsx | 51 +++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/src/components/TradingViewChart.tsx b/src/components/TradingViewChart.tsx index 5285b29..c059c66 100644 --- a/src/components/TradingViewChart.tsx +++ b/src/components/TradingViewChart.tsx @@ -138,6 +138,7 @@ export default function TradingViewChart({ const [showRecentOrders, setShowRecentOrders] = useState(false); const [showPositions, setShowPositions] = useState(true); // Show TP/SL lines const [autoRefresh, setAutoRefresh] = useState(false); + const [refreshInterval, setRefreshInterval] = useState(30); // Default 30 seconds const [lastUpdate, setLastUpdate] = useState(null); const [isRefreshing, setIsRefreshing] = useState(false); const [isLoadingHistorical, setIsLoadingHistorical] = useState(false); @@ -705,22 +706,23 @@ export default function TradingViewChart({ } }, [symbol, timeframe, isVisible, fetchKlineData, fetchLiquidationData, fetchOpenOrders]); - // Auto-refresh effect - refreshes every 60 seconds when enabled + // Auto-refresh effect - refreshes at configured interval when enabled useEffect(() => { if (!autoRefresh || !isVisible || !symbol || !timeframe) { return; } + const intervalMs = refreshInterval * 1000; const interval = setInterval(() => { - console.log('[TradingViewChart] Auto-refresh triggered'); + console.log(`[TradingViewChart] Auto-refresh triggered (${refreshInterval}s interval)`); // Use refs to avoid dependency issues if (fetchKlineDataRef.current) fetchKlineDataRef.current(true); if (fetchLiquidationDataRef.current) fetchLiquidationDataRef.current(); if (fetchOpenOrdersRef.current) fetchOpenOrdersRef.current(); - }, 60000); // 60 seconds + }, intervalMs); return () => clearInterval(interval); - }, [autoRefresh, isVisible, symbol, timeframe]); + }, [autoRefresh, isVisible, symbol, timeframe, refreshInterval]); // Update chart data when klineData changes useEffect(() => { @@ -1063,16 +1065,37 @@ export default function TradingViewChart({ {isVisible && (
-
- setAutoRefresh(checked as boolean)} - className="h-4 w-4" - /> - +
+
+ setAutoRefresh(checked as boolean)} + className="h-4 w-4" + /> + +
+ {autoRefresh && ( + + )}
From c09577facb84f13ba49a9ee38c5bcd1631550d58 Mon Sep 17 00:00:00 2001 From: jameslappin Date: Wed, 19 Nov 2025 19:22:04 +1000 Subject: [PATCH 12/30] perf: optimize auto-refresh to fetch only latest 2 candles instead of 500 --- src/components/TradingViewChart.tsx | 75 ++++++++++++++++++----------- 1 file changed, 48 insertions(+), 27 deletions(-) diff --git a/src/components/TradingViewChart.tsx b/src/components/TradingViewChart.tsx index c059c66..a5550a1 100644 --- a/src/components/TradingViewChart.tsx +++ b/src/components/TradingViewChart.tsx @@ -458,24 +458,55 @@ export default function TradingViewChart({ setError(null); try { - // When forcing refresh, always fetch latest data + // When forcing refresh, only fetch the latest candles (much more efficient) if (force) { - // Get the latest candles from the API const cached = getCachedKlines(symbol, timeframe); - const since = cached?.lastCandleTime || Date.now() - (7 * 24 * 60 * 60 * 1000); - const response = await fetch(`/api/klines?symbol=${symbol}&interval=${timeframe}&since=${since}&limit=500`); - const result = await response.json(); - - if (result.success && result.data.length > 0) { - // Update cache with new data - const updated = cached - ? updateCachedKlines(symbol, timeframe, result.data) - : { data: result.data, lastUpdate: Date.now(), lastCandleTime: result.data[result.data.length - 1][0] }; + if (cached) { + // We have cached data - only fetch latest 2 candles to update + const lastCachedTime = cached.lastCandleTime || cached.data[cached.data.length - 1][0]; - if (updated) { - // Update chart with merged data - const transformedData: CandlestickData[] = updated.data.map((kline: any[]) => { + // Fetch just the latest 2 candles (current incomplete + most recent complete) + const response = await fetch(`/api/klines?symbol=${symbol}&interval=${timeframe}&since=${lastCachedTime}&limit=2`); + const result = await response.json(); + + if (result.success && result.data.length > 0) { + // Update cache with just the new candles + const updated = updateCachedKlines(symbol, timeframe, result.data); + + if (updated) { + // Update chart with merged data + const transformedData: CandlestickData[] = updated.data.map((kline: any[]) => { + const timestamp = typeof kline[0] === 'number' ? kline[0] : parseInt(kline[0]); + return { + time: timestamp as Time, + open: parseFloat(kline[1]), + high: parseFloat(kline[2]), + low: parseFloat(kline[3]), + close: parseFloat(kline[4]) + }; + }); + + transformedData.sort((a, b) => (a.time as number) - (b.time as number)); + + // Only update if data has actually changed + setKlineData(prev => { + if (prev.length === transformedData.length && + prev[prev.length - 1]?.close === transformedData[transformedData.length - 1]?.close) { + return prev; // No change + } + return transformedData; + }); + } + } + } else { + // No cache - do a full initial fetch + const since = Date.now() - (7 * 24 * 60 * 60 * 1000); + const response = await fetch(`/api/klines?symbol=${symbol}&interval=${timeframe}&since=${since}&limit=500`); + const result = await response.json(); + + if (result.success && result.data.length > 0) { + const transformedData: CandlestickData[] = result.data.map((kline: any[]) => { const timestamp = typeof kline[0] === 'number' ? kline[0] : parseInt(kline[0]); return { time: timestamp as Time, @@ -487,20 +518,10 @@ export default function TradingViewChart({ }); transformedData.sort((a, b) => (a.time as number) - (b.time as number)); + setKlineData(transformedData); - // Only update if data has actually changed - setKlineData(prev => { - if (prev.length === transformedData.length && - prev[prev.length - 1]?.close === transformedData[transformedData.length - 1]?.close) { - return prev; // No change - } - return transformedData; - }); - - // Cache the merged data - if (!cached) { - setCachedKlines(symbol, timeframe, updated.data); - } + // Cache the data + setCachedKlines(symbol, timeframe, result.data); } } From 3aeb2a566c3e0c2e0d971bf43dcd624378bbc604 Mon Sep 17 00:00:00 2001 From: jameslappin Date: Wed, 19 Nov 2025 20:26:22 +1000 Subject: [PATCH 13/30] Add volume histogram to TradingView chart with toggle --- src/components/TradingViewChart.tsx | 74 ++++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 2 deletions(-) diff --git a/src/components/TradingViewChart.tsx b/src/components/TradingViewChart.tsx index a5550a1..1863640 100644 --- a/src/components/TradingViewChart.tsx +++ b/src/components/TradingViewChart.tsx @@ -2,7 +2,7 @@ import React, { useRef, useEffect, useState, useCallback, useMemo } from 'react'; import orderStore from '@/lib/services/orderStore'; -import { createChart, IChartApi, ISeriesApi, CandlestickData, Time } from 'lightweight-charts'; +import { createChart, IChartApi, ISeriesApi, CandlestickData, HistogramData, Time } from 'lightweight-charts'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { getCachedKlines, setCachedKlines, updateCachedKlines, getCandlesFor7Days, prependHistoricalKlines } from '@/lib/klineCache'; @@ -121,6 +121,7 @@ export default function TradingViewChart({ const chartRef = useRef(null); const candlestickSeriesRef = useRef | null>(null); + const volumeSeriesRef = useRef | null>(null); const positionLinesRef = useRef([]); const vwapLineRef = useRef(null); const orderMarkersRef = useRef([]); @@ -130,6 +131,7 @@ export default function TradingViewChart({ const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [klineData, setKlineData] = useState([]); + const [volumeData, setVolumeData] = useState([]); const [dbLiquidations, setDbLiquidations] = useState([]); const [showLiquidations, setShowLiquidations] = useState(true); const [liquidationGrouping, setLiquidationGrouping] = useState('5m'); @@ -137,6 +139,7 @@ export default function TradingViewChart({ const [showVWAP, setShowVWAP] = useState(false); const [showRecentOrders, setShowRecentOrders] = useState(false); const [showPositions, setShowPositions] = useState(true); // Show TP/SL lines + const [showVolume, setShowVolume] = useState(true); const [autoRefresh, setAutoRefresh] = useState(false); const [refreshInterval, setRefreshInterval] = useState(30); // Default 30 seconds const [lastUpdate, setLastUpdate] = useState(null); @@ -525,6 +528,12 @@ export default function TradingViewChart({ } } + // Reset error counter on success + if (consecutiveErrorsRef.current > 0) { + consecutiveErrorsRef.current = 0; + setApiConnectionError(false); + } + setIsRefreshing(false); setLastUpdate(new Date()); return; @@ -615,13 +624,29 @@ export default function TradingViewChart({ }; }); + // Transform volume data (quote asset volume in USDT) + const transformedVolume: HistogramData[] = result.data.map((kline: any[]) => { + const timestamp = typeof kline[0] === 'number' ? kline[0] : parseInt(kline[0]); + const open = parseFloat(kline[1]); + const close = parseFloat(kline[4]); + const volume = parseFloat(kline[7]); // Quote asset volume (USDT) + + return { + time: timestamp as Time, + value: volume, + color: close >= open ? '#26a69a' : '#ef5350' // Green for bullish, red for bearish + }; + }); + // Sort data by time (TradingView requires chronological order) transformedData.sort((a, b) => (a.time as number) - (b.time as number)); + transformedVolume.sort((a, b) => (a.time as number) - (b.time as number)); // Cache the data setCachedKlines(symbol, timeframe, result.data); setKlineData(transformedData); + setVolumeData(transformedVolume); } catch (error) { console.error('[TradingViewChart] Error fetching kline data:', error); setError(error instanceof Error ? error.message : 'Failed to fetch chart data'); @@ -680,8 +705,26 @@ export default function TradingViewChart({ wickDownColor: '#ef5350', }); + // Add volume histogram series + const volumeSeries = chart.addHistogramSeries({ + color: '#26a69a', + priceFormat: { + type: 'volume', + }, + priceScaleId: 'volume', // Separate scale for volume + }); + + // Configure volume scale to be at bottom 20% of chart + volumeSeries.priceScale().applyOptions({ + scaleMargins: { + top: 0.8, // Volume takes bottom 20% + bottom: 0, + }, + }); + chartRef.current = chart; candlestickSeriesRef.current = candlestickSeries; + volumeSeriesRef.current = volumeSeries; // Track user interactions (scrolling, zooming) const handleVisibleLogicalRangeChange = debounce((newRange: any) => { @@ -710,6 +753,7 @@ export default function TradingViewChart({ chartRef.current.remove(); chartRef.current = null; candlestickSeriesRef.current = null; + volumeSeriesRef.current = null; } }; }, [loading, error, isVisible, chartHeight]); // Re-initialize when loading/error/visibility states change @@ -750,6 +794,11 @@ export default function TradingViewChart({ if (candlestickSeriesRef.current && klineData.length > 0) { candlestickSeriesRef.current.setData(klineData); + // Update volume data if available + if (volumeSeriesRef.current && volumeData.length > 0) { + volumeSeriesRef.current.setData(volumeData); + } + // Only set visible range on initial load or if user hasn't interacted if (chartRef.current && klineData.length > 0 && !hasUserInteracted) { const totalBars = klineData.length; @@ -775,7 +824,16 @@ export default function TradingViewChart({ isInitialLoadRef.current = false; } } - }, [klineData, hasUserInteracted]); + }, [klineData, volumeData, hasUserInteracted]); + + // Toggle volume visibility + useEffect(() => { + if (volumeSeriesRef.current) { + volumeSeriesRef.current.applyOptions({ + visible: showVolume, + }); + } + }, [showVolume]); // Update position indicators when positions change or toggle changes useEffect(() => { @@ -1154,6 +1212,18 @@ export default function TradingViewChart({ VWAP
+ +
+ setShowVolume(checked as boolean)} + className="h-4 w-4" + /> + +
From 0164ead6fd28e66f6d26333e8fa9fb32ccf6b509 Mon Sep 17 00:00:00 2001 From: jameslappin Date: Wed, 19 Nov 2025 20:29:44 +1000 Subject: [PATCH 14/30] Fix ReferenceError and reduce WebSocket console spam --- src/components/TradingViewChart.tsx | 7 +------ src/lib/services/websocketService.ts | 6 ++++-- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/components/TradingViewChart.tsx b/src/components/TradingViewChart.tsx index 1863640..39fac09 100644 --- a/src/components/TradingViewChart.tsx +++ b/src/components/TradingViewChart.tsx @@ -528,12 +528,6 @@ export default function TradingViewChart({ } } - // Reset error counter on success - if (consecutiveErrorsRef.current > 0) { - consecutiveErrorsRef.current = 0; - setApiConnectionError(false); - } - setIsRefreshing(false); setLastUpdate(new Date()); return; @@ -796,6 +790,7 @@ export default function TradingViewChart({ // Update volume data if available if (volumeSeriesRef.current && volumeData.length > 0) { + console.log('[TradingViewChart] Setting volume data, count:', volumeData.length, 'sample:', volumeData[0]); volumeSeriesRef.current.setData(volumeData); } diff --git a/src/lib/services/websocketService.ts b/src/lib/services/websocketService.ts index f1c35a0..935cc30 100644 --- a/src/lib/services/websocketService.ts +++ b/src/lib/services/websocketService.ts @@ -172,8 +172,10 @@ class WebSocketService { this.isIntentionalDisconnect = true; } - // Broadcast to all handlers - console.log(`[WebSocketService] Broadcasting ${message.type} to ${this.handlers.size} handlers`); + // Broadcast to all handlers (spam filtered - only log important events) + if (['liquidation', 'shutdown', 'error'].includes(message.type)) { + console.log(`[WebSocketService] Broadcasting ${message.type} to ${this.handlers.size} handlers`); + } this.handlers.forEach(handler => { try { handler(message); From 42657ffea0beaeadcd25a68adef96af0d1d75ea7 Mon Sep 17 00:00:00 2001 From: jameslappin Date: Wed, 19 Nov 2025 22:25:07 +1000 Subject: [PATCH 15/30] Add debug logging to track Hunter event listener accumulation --- src/bot/index.ts | 5 ++- src/components/LiquidationSidebar.tsx | 11 ++++++ src/components/TradingViewChart.tsx | 51 +++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 1 deletion(-) diff --git a/src/bot/index.ts b/src/bot/index.ts index 200e2fc..a522c54 100644 --- a/src/bot/index.ts +++ b/src/bot/index.ts @@ -464,8 +464,11 @@ logErrorWithTimestamp('โš ๏ธ Position Manager failed to start:', error.message // Initialize Hunter (or reuse existing instance to prevent duplicate listeners) if (!this.hunter) { this.hunter = new Hunter(this.config, this.isHedgeMode); + console.log('[Bot] Created new Hunter instance'); } else { // Remove all old listeners before re-attaching to prevent duplicates + const listenerCount = this.hunter.listenerCount('liquidationDetected'); + console.log(`[Bot] Existing Hunter has ${listenerCount} liquidationDetected listeners`); this.hunter.removeAllListeners(); console.log('[Bot] Removed all old hunter event listeners to prevent duplicates'); } @@ -480,7 +483,7 @@ logErrorWithTimestamp('โš ๏ธ Position Manager failed to start:', error.message // Connect hunter events to position manager and status broadcaster this.hunter.on('liquidationDetected', (liquidationEvent: any) => { - console.log(`[Bot] liquidationDetected event received for ${liquidationEvent.symbol}`); + console.log(`[Bot] liquidationDetected event received for ${liquidationEvent.symbol} (${this.hunter.listenerCount('liquidationDetected')} total listeners)`); // Broadcast to UI and log activity (don't log to console - already logged in hunter.ts) this.statusBroadcaster.broadcastLiquidation(liquidationEvent); this.statusBroadcaster.logActivity(`Liquidation: ${liquidationEvent.symbol} ${liquidationEvent.side} ${liquidationEvent.quantity}`); diff --git a/src/components/LiquidationSidebar.tsx b/src/components/LiquidationSidebar.tsx index 3b53b00..dc55bbe 100644 --- a/src/components/LiquidationSidebar.tsx +++ b/src/components/LiquidationSidebar.tsx @@ -123,6 +123,17 @@ export default function LiquidationSidebar({ volumeThresholds = {}, maxEvents = const eventId = `${liquidationData.symbol}-${liquidationData.eventTime}`; setEvents(prev => { + // Check if this liquidation already exists (deduplicate) + const isDuplicate = prev.some(e => + e.symbol === liquidationData.symbol && + e.eventTime === liquidationData.eventTime + ); + + if (isDuplicate) { + console.log(`[LiquidationSidebar:${instanceId.current}] Duplicate liquidation detected, skipping:`, eventId); + return prev; + } + // Mark as new for animation setNewEventIds(prevIds => new Set([...prevIds, eventId])); diff --git a/src/components/TradingViewChart.tsx b/src/components/TradingViewChart.tsx index 39fac09..92a73f8 100644 --- a/src/components/TradingViewChart.tsx +++ b/src/components/TradingViewChart.tsx @@ -487,10 +487,23 @@ export default function TradingViewChart({ high: parseFloat(kline[2]), low: parseFloat(kline[3]), close: parseFloat(kline[4]) + }; }); + + const transformedVolume: HistogramData[] = result.data.map((kline: any[]) => { + const timestamp = typeof kline[0] === 'number' ? kline[0] : parseInt(kline[0]); + const open = parseFloat(kline[1]); + const close = parseFloat(kline[4]); + const volume = parseFloat(kline[7]); + + return { + time: timestamp as Time, + value: volume, + color: close >= open ? '#26a69a' : '#ef5350' }; }); transformedData.sort((a, b) => (a.time as number) - (b.time as number)); + transformedVolume.sort((a, b) => (a.time as number) - (b.time as number)); // Only update if data has actually changed setKlineData(prev => { @@ -500,6 +513,7 @@ export default function TradingViewChart({ } return transformedData; }); + setVolumeData(transformedVolume); } } } else { @@ -520,8 +534,23 @@ export default function TradingViewChart({ }; }); + const transformedVolume: HistogramData[] = result.data.map((kline: any[]) => { + const timestamp = typeof kline[0] === 'number' ? kline[0] : parseInt(kline[0]); + const open = parseFloat(kline[1]); + const close = parseFloat(kline[4]); + const volume = parseFloat(kline[7]); + + return { + time: timestamp as Time, + value: volume, + color: close >= open ? '#26a69a' : '#ef5350' + }; + }); + transformedData.sort((a, b) => (a.time as number) - (b.time as number)); + transformedVolume.sort((a, b) => (a.time as number) - (b.time as number)); setKlineData(transformedData); + setVolumeData(transformedVolume); // Cache the data setCachedKlines(symbol, timeframe, result.data); @@ -549,9 +578,25 @@ export default function TradingViewChart({ }; }); + // Transform cached volume data + const transformedVolume: HistogramData[] = cached.data.map((kline: any[]) => { + const timestamp = typeof kline[0] === 'number' ? kline[0] : parseInt(kline[0]); + const open = parseFloat(kline[1]); + const close = parseFloat(kline[4]); + const volume = parseFloat(kline[7]); // Quote asset volume (USDT) + + return { + time: timestamp as Time, + value: volume, + color: close >= open ? '#26a69a' : '#ef5350' + }; + }); + // Sort data by time (TradingView requires chronological order) transformedData.sort((a, b) => (a.time as number) - (b.time as number)); + transformedVolume.sort((a, b) => (a.time as number) - (b.time as number)); setKlineData(transformedData); + setVolumeData(transformedVolume); // Check if we need to fetch recent updates (cache older than 2 minutes) const cacheAge = Date.now() - cached.lastUpdate; @@ -625,6 +670,12 @@ export default function TradingViewChart({ const close = parseFloat(kline[4]); const volume = parseFloat(kline[7]); // Quote asset volume (USDT) + // Debug first item + if (timestamp === result.data[0][0]) { + console.log('[TradingViewChart] First kline data:', kline); + console.log('[TradingViewChart] Volume at index 7:', kline[7], 'parsed:', volume); + } + return { time: timestamp as Time, value: volume, From de4a23063e80cd73797babfba5864c1146dcb94b Mon Sep 17 00:00:00 2001 From: jameslappin Date: Wed, 19 Nov 2025 22:58:18 +1000 Subject: [PATCH 16/30] Revert "Add volume histogram to TradingView chart with toggle" This reverts commit 3aeb2a566c3e0c2e0d971bf43dcd624378bbc604. --- src/bot/index.ts | 5 +- src/components/LiquidationSidebar.tsx | 11 --- src/components/TradingViewChart.tsx | 120 +------------------------- src/lib/services/websocketService.ts | 6 +- 4 files changed, 5 insertions(+), 137 deletions(-) diff --git a/src/bot/index.ts b/src/bot/index.ts index a522c54..200e2fc 100644 --- a/src/bot/index.ts +++ b/src/bot/index.ts @@ -464,11 +464,8 @@ logErrorWithTimestamp('โš ๏ธ Position Manager failed to start:', error.message // Initialize Hunter (or reuse existing instance to prevent duplicate listeners) if (!this.hunter) { this.hunter = new Hunter(this.config, this.isHedgeMode); - console.log('[Bot] Created new Hunter instance'); } else { // Remove all old listeners before re-attaching to prevent duplicates - const listenerCount = this.hunter.listenerCount('liquidationDetected'); - console.log(`[Bot] Existing Hunter has ${listenerCount} liquidationDetected listeners`); this.hunter.removeAllListeners(); console.log('[Bot] Removed all old hunter event listeners to prevent duplicates'); } @@ -483,7 +480,7 @@ logErrorWithTimestamp('โš ๏ธ Position Manager failed to start:', error.message // Connect hunter events to position manager and status broadcaster this.hunter.on('liquidationDetected', (liquidationEvent: any) => { - console.log(`[Bot] liquidationDetected event received for ${liquidationEvent.symbol} (${this.hunter.listenerCount('liquidationDetected')} total listeners)`); + console.log(`[Bot] liquidationDetected event received for ${liquidationEvent.symbol}`); // Broadcast to UI and log activity (don't log to console - already logged in hunter.ts) this.statusBroadcaster.broadcastLiquidation(liquidationEvent); this.statusBroadcaster.logActivity(`Liquidation: ${liquidationEvent.symbol} ${liquidationEvent.side} ${liquidationEvent.quantity}`); diff --git a/src/components/LiquidationSidebar.tsx b/src/components/LiquidationSidebar.tsx index dc55bbe..3b53b00 100644 --- a/src/components/LiquidationSidebar.tsx +++ b/src/components/LiquidationSidebar.tsx @@ -123,17 +123,6 @@ export default function LiquidationSidebar({ volumeThresholds = {}, maxEvents = const eventId = `${liquidationData.symbol}-${liquidationData.eventTime}`; setEvents(prev => { - // Check if this liquidation already exists (deduplicate) - const isDuplicate = prev.some(e => - e.symbol === liquidationData.symbol && - e.eventTime === liquidationData.eventTime - ); - - if (isDuplicate) { - console.log(`[LiquidationSidebar:${instanceId.current}] Duplicate liquidation detected, skipping:`, eventId); - return prev; - } - // Mark as new for animation setNewEventIds(prevIds => new Set([...prevIds, eventId])); diff --git a/src/components/TradingViewChart.tsx b/src/components/TradingViewChart.tsx index 92a73f8..a5550a1 100644 --- a/src/components/TradingViewChart.tsx +++ b/src/components/TradingViewChart.tsx @@ -2,7 +2,7 @@ import React, { useRef, useEffect, useState, useCallback, useMemo } from 'react'; import orderStore from '@/lib/services/orderStore'; -import { createChart, IChartApi, ISeriesApi, CandlestickData, HistogramData, Time } from 'lightweight-charts'; +import { createChart, IChartApi, ISeriesApi, CandlestickData, Time } from 'lightweight-charts'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { getCachedKlines, setCachedKlines, updateCachedKlines, getCandlesFor7Days, prependHistoricalKlines } from '@/lib/klineCache'; @@ -121,7 +121,6 @@ export default function TradingViewChart({ const chartRef = useRef(null); const candlestickSeriesRef = useRef | null>(null); - const volumeSeriesRef = useRef | null>(null); const positionLinesRef = useRef([]); const vwapLineRef = useRef(null); const orderMarkersRef = useRef([]); @@ -131,7 +130,6 @@ export default function TradingViewChart({ const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [klineData, setKlineData] = useState([]); - const [volumeData, setVolumeData] = useState([]); const [dbLiquidations, setDbLiquidations] = useState([]); const [showLiquidations, setShowLiquidations] = useState(true); const [liquidationGrouping, setLiquidationGrouping] = useState('5m'); @@ -139,7 +137,6 @@ export default function TradingViewChart({ const [showVWAP, setShowVWAP] = useState(false); const [showRecentOrders, setShowRecentOrders] = useState(false); const [showPositions, setShowPositions] = useState(true); // Show TP/SL lines - const [showVolume, setShowVolume] = useState(true); const [autoRefresh, setAutoRefresh] = useState(false); const [refreshInterval, setRefreshInterval] = useState(30); // Default 30 seconds const [lastUpdate, setLastUpdate] = useState(null); @@ -487,23 +484,10 @@ export default function TradingViewChart({ high: parseFloat(kline[2]), low: parseFloat(kline[3]), close: parseFloat(kline[4]) - }; }); - - const transformedVolume: HistogramData[] = result.data.map((kline: any[]) => { - const timestamp = typeof kline[0] === 'number' ? kline[0] : parseInt(kline[0]); - const open = parseFloat(kline[1]); - const close = parseFloat(kline[4]); - const volume = parseFloat(kline[7]); - - return { - time: timestamp as Time, - value: volume, - color: close >= open ? '#26a69a' : '#ef5350' }; }); transformedData.sort((a, b) => (a.time as number) - (b.time as number)); - transformedVolume.sort((a, b) => (a.time as number) - (b.time as number)); // Only update if data has actually changed setKlineData(prev => { @@ -513,7 +497,6 @@ export default function TradingViewChart({ } return transformedData; }); - setVolumeData(transformedVolume); } } } else { @@ -534,23 +517,8 @@ export default function TradingViewChart({ }; }); - const transformedVolume: HistogramData[] = result.data.map((kline: any[]) => { - const timestamp = typeof kline[0] === 'number' ? kline[0] : parseInt(kline[0]); - const open = parseFloat(kline[1]); - const close = parseFloat(kline[4]); - const volume = parseFloat(kline[7]); - - return { - time: timestamp as Time, - value: volume, - color: close >= open ? '#26a69a' : '#ef5350' - }; - }); - transformedData.sort((a, b) => (a.time as number) - (b.time as number)); - transformedVolume.sort((a, b) => (a.time as number) - (b.time as number)); setKlineData(transformedData); - setVolumeData(transformedVolume); // Cache the data setCachedKlines(symbol, timeframe, result.data); @@ -578,25 +546,9 @@ export default function TradingViewChart({ }; }); - // Transform cached volume data - const transformedVolume: HistogramData[] = cached.data.map((kline: any[]) => { - const timestamp = typeof kline[0] === 'number' ? kline[0] : parseInt(kline[0]); - const open = parseFloat(kline[1]); - const close = parseFloat(kline[4]); - const volume = parseFloat(kline[7]); // Quote asset volume (USDT) - - return { - time: timestamp as Time, - value: volume, - color: close >= open ? '#26a69a' : '#ef5350' - }; - }); - // Sort data by time (TradingView requires chronological order) transformedData.sort((a, b) => (a.time as number) - (b.time as number)); - transformedVolume.sort((a, b) => (a.time as number) - (b.time as number)); setKlineData(transformedData); - setVolumeData(transformedVolume); // Check if we need to fetch recent updates (cache older than 2 minutes) const cacheAge = Date.now() - cached.lastUpdate; @@ -663,35 +615,13 @@ export default function TradingViewChart({ }; }); - // Transform volume data (quote asset volume in USDT) - const transformedVolume: HistogramData[] = result.data.map((kline: any[]) => { - const timestamp = typeof kline[0] === 'number' ? kline[0] : parseInt(kline[0]); - const open = parseFloat(kline[1]); - const close = parseFloat(kline[4]); - const volume = parseFloat(kline[7]); // Quote asset volume (USDT) - - // Debug first item - if (timestamp === result.data[0][0]) { - console.log('[TradingViewChart] First kline data:', kline); - console.log('[TradingViewChart] Volume at index 7:', kline[7], 'parsed:', volume); - } - - return { - time: timestamp as Time, - value: volume, - color: close >= open ? '#26a69a' : '#ef5350' // Green for bullish, red for bearish - }; - }); - // Sort data by time (TradingView requires chronological order) transformedData.sort((a, b) => (a.time as number) - (b.time as number)); - transformedVolume.sort((a, b) => (a.time as number) - (b.time as number)); // Cache the data setCachedKlines(symbol, timeframe, result.data); setKlineData(transformedData); - setVolumeData(transformedVolume); } catch (error) { console.error('[TradingViewChart] Error fetching kline data:', error); setError(error instanceof Error ? error.message : 'Failed to fetch chart data'); @@ -750,26 +680,8 @@ export default function TradingViewChart({ wickDownColor: '#ef5350', }); - // Add volume histogram series - const volumeSeries = chart.addHistogramSeries({ - color: '#26a69a', - priceFormat: { - type: 'volume', - }, - priceScaleId: 'volume', // Separate scale for volume - }); - - // Configure volume scale to be at bottom 20% of chart - volumeSeries.priceScale().applyOptions({ - scaleMargins: { - top: 0.8, // Volume takes bottom 20% - bottom: 0, - }, - }); - chartRef.current = chart; candlestickSeriesRef.current = candlestickSeries; - volumeSeriesRef.current = volumeSeries; // Track user interactions (scrolling, zooming) const handleVisibleLogicalRangeChange = debounce((newRange: any) => { @@ -798,7 +710,6 @@ export default function TradingViewChart({ chartRef.current.remove(); chartRef.current = null; candlestickSeriesRef.current = null; - volumeSeriesRef.current = null; } }; }, [loading, error, isVisible, chartHeight]); // Re-initialize when loading/error/visibility states change @@ -839,12 +750,6 @@ export default function TradingViewChart({ if (candlestickSeriesRef.current && klineData.length > 0) { candlestickSeriesRef.current.setData(klineData); - // Update volume data if available - if (volumeSeriesRef.current && volumeData.length > 0) { - console.log('[TradingViewChart] Setting volume data, count:', volumeData.length, 'sample:', volumeData[0]); - volumeSeriesRef.current.setData(volumeData); - } - // Only set visible range on initial load or if user hasn't interacted if (chartRef.current && klineData.length > 0 && !hasUserInteracted) { const totalBars = klineData.length; @@ -870,16 +775,7 @@ export default function TradingViewChart({ isInitialLoadRef.current = false; } } - }, [klineData, volumeData, hasUserInteracted]); - - // Toggle volume visibility - useEffect(() => { - if (volumeSeriesRef.current) { - volumeSeriesRef.current.applyOptions({ - visible: showVolume, - }); - } - }, [showVolume]); + }, [klineData, hasUserInteracted]); // Update position indicators when positions change or toggle changes useEffect(() => { @@ -1258,18 +1154,6 @@ export default function TradingViewChart({ VWAP
- -
- setShowVolume(checked as boolean)} - className="h-4 w-4" - /> - -
diff --git a/src/lib/services/websocketService.ts b/src/lib/services/websocketService.ts index 935cc30..f1c35a0 100644 --- a/src/lib/services/websocketService.ts +++ b/src/lib/services/websocketService.ts @@ -172,10 +172,8 @@ class WebSocketService { this.isIntentionalDisconnect = true; } - // Broadcast to all handlers (spam filtered - only log important events) - if (['liquidation', 'shutdown', 'error'].includes(message.type)) { - console.log(`[WebSocketService] Broadcasting ${message.type} to ${this.handlers.size} handlers`); - } + // Broadcast to all handlers + console.log(`[WebSocketService] Broadcasting ${message.type} to ${this.handlers.size} handlers`); this.handlers.forEach(handler => { try { handler(message); From 56556b47df29539850fb0632bb9caf810eee53d8 Mon Sep 17 00:00:00 2001 From: jameslappin Date: Thu, 20 Nov 2025 00:05:05 +1000 Subject: [PATCH 17/30] Reduce WebSocket broadcast spam and add eventTime logging --- src/components/LiquidationSidebar.tsx | 13 ++++++++++++- src/lib/services/websocketService.ts | 6 ++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/components/LiquidationSidebar.tsx b/src/components/LiquidationSidebar.tsx index 3b53b00..7b494fd 100644 --- a/src/components/LiquidationSidebar.tsx +++ b/src/components/LiquidationSidebar.tsx @@ -105,7 +105,7 @@ export default function LiquidationSidebar({ volumeThresholds = {}, maxEvents = const handleMessage = (message: any) => { if (message.type === 'liquidation') { - console.log(`[LiquidationSidebar:${instanceId.current}] Received liquidation message:`, message.data?.symbol); + console.log(`[LiquidationSidebar:${instanceId.current}] Received liquidation:`, message.data?.symbol, 'eventTime:', message.data?.eventTime); const liquidationData = message.data; // Calculate volume and determine if high volume (use ref for latest threshold) @@ -123,6 +123,17 @@ export default function LiquidationSidebar({ volumeThresholds = {}, maxEvents = const eventId = `${liquidationData.symbol}-${liquidationData.eventTime}`; setEvents(prev => { + // Check if this liquidation already exists (deduplicate) + const isDuplicate = prev.some(e => + e.symbol === liquidationData.symbol && + e.eventTime === liquidationData.eventTime + ); + + if (isDuplicate) { + console.log(`[LiquidationSidebar:${instanceId.current}] Duplicate liquidation detected, skipping:`, eventId); + return prev; + } + // Mark as new for animation setNewEventIds(prevIds => new Set([...prevIds, eventId])); diff --git a/src/lib/services/websocketService.ts b/src/lib/services/websocketService.ts index f1c35a0..6a5b353 100644 --- a/src/lib/services/websocketService.ts +++ b/src/lib/services/websocketService.ts @@ -172,8 +172,10 @@ class WebSocketService { this.isIntentionalDisconnect = true; } - // Broadcast to all handlers - console.log(`[WebSocketService] Broadcasting ${message.type} to ${this.handlers.size} handlers`); + // Broadcast to all handlers (only log important events to reduce spam) + if (['liquidation', 'shutdown', 'error'].includes(message.type)) { + console.log(`[WebSocketService] Broadcasting ${message.type} to ${this.handlers.size} handlers`); + } this.handlers.forEach(handler => { try { handler(message); From bcc201cb6dbfcf79842cd6dbed7563c1b852bfc9 Mon Sep 17 00:00:00 2001 From: birdbathd Date: Thu, 20 Nov 2025 12:05:39 +1000 Subject: [PATCH 18/30] fix: prevent duplicate liquidations with database UNIQUE constraint - Add UNIQUE constraint on (symbol, event_time) to liquidations table - Change INSERT to INSERT OR IGNORE to silently skip duplicate events - Addresses root cause at database level instead of UI-level workarounds Testing in progress for duplicate issues and potential bugs. --- .gitignore | 7 +++++++ [WEB] | 0 src/lib/db/database.ts | 3 ++- src/lib/services/liquidationStorage.ts | 2 +- 4 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 [WEB] diff --git a/.gitignore b/.gitignore index 9f62f6a..8096359 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/[WEB] b/[WEB] new file mode 100644 index 0000000..e69de29 diff --git a/src/lib/db/database.ts b/src/lib/db/database.ts index c6b00b5..c7468ac 100644 --- a/src/lib/db/database.ts +++ b/src/lib/db/database.ts @@ -48,7 +48,8 @@ export class Database { order_trade_time INTEGER, event_time INTEGER NOT NULL, created_at INTEGER DEFAULT (strftime('%s', 'now')), - metadata TEXT + metadata TEXT, + UNIQUE(symbol, event_time) ); CREATE INDEX IF NOT EXISTS idx_liquidations_event_time diff --git a/src/lib/services/liquidationStorage.ts b/src/lib/services/liquidationStorage.ts index d497b0a..1cafcb5 100644 --- a/src/lib/services/liquidationStorage.ts +++ b/src/lib/services/liquidationStorage.ts @@ -42,7 +42,7 @@ export interface LiquidationStats { export class LiquidationStorage { async saveLiquidation(event: LiquidationEvent, volumeUSDT: number): Promise { const sql = ` - INSERT INTO liquidations ( + INSERT OR IGNORE INTO liquidations ( symbol, side, order_type, quantity, price, average_price, volume_usdt, order_status, order_last_filled_quantity, order_filled_accumulated_quantity, order_trade_time, From 87ea43ccacbd63881eac350c57a8fe370a04a8ee Mon Sep 17 00:00:00 2001 From: birdbathd Date: Thu, 20 Nov 2025 12:29:14 +1000 Subject: [PATCH 19/30] chore: restore tranche implementation and docs accidentally deleted by revert - Restored tranche docs and all related source files from dev branch - Ensures TradingView feature branch does not remove unrelated tranche features --- docs/TRANCHE_IMPLEMENTATION_PLAN.md | 2154 +++++++++++++++++++++ docs/TRANCHE_TESTING.md | 433 +++++ docs/TRANCHE_USER_GUIDE.md | 730 +++++++ src/app/api/paper-mode/positions/route.ts | 53 + src/app/api/tranches/route.ts | 89 + src/components/ShareConfigModal.tsx | 236 +++ src/lib/db/trancheDb.ts | 457 +++++ src/lib/services/paperModeSimulator.ts | 335 ++++ tests/tranche-integration-test.ts | 766 ++++++++ tests/tranche-system-test.ts | 355 ++++ 10 files changed, 5608 insertions(+) create mode 100644 docs/TRANCHE_IMPLEMENTATION_PLAN.md create mode 100644 docs/TRANCHE_TESTING.md create mode 100644 docs/TRANCHE_USER_GUIDE.md create mode 100644 src/app/api/paper-mode/positions/route.ts create mode 100644 src/app/api/tranches/route.ts create mode 100644 src/components/ShareConfigModal.tsx create mode 100644 src/lib/db/trancheDb.ts create mode 100644 src/lib/services/paperModeSimulator.ts create mode 100644 tests/tranche-integration-test.ts create mode 100644 tests/tranche-system-test.ts diff --git a/docs/TRANCHE_IMPLEMENTATION_PLAN.md b/docs/TRANCHE_IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000..19ea89d --- /dev/null +++ b/docs/TRANCHE_IMPLEMENTATION_PLAN.md @@ -0,0 +1,2154 @@ +# Multi-Tranche Position Management - Implementation Plan + +## โœ… IMPLEMENTATION COMPLETE + +**Status:** All 8 phases completed and tested +**Completion Date:** 2025-10-12 +**Branch:** `feature/tranche-management` +**Test Results:** 19/19 tests passing (100% pass rate) + +### Quick Summary + +The multi-tranche position management system has been successfully implemented with: +- โœ… Virtual tranche tracking layer with SQLite persistence +- โœ… Automatic isolation of underwater positions (>5% loss) +- โœ… Configurable closing strategies (FIFO/LIFO/WORST_FIRST/BEST_FIRST) +- โœ… Exchange synchronization and drift detection +- โœ… Real-time WebSocket updates and UI dashboard +- โœ… Comprehensive automated test suite +- โœ… Full documentation (user guide + technical docs) + +### Implementation Phases + +| Phase | Status | Tests | Notes | +|-------|--------|-------|-------| +| Phase 1: Foundation | โœ… Complete | N/A | Types, database schema, initialization | +| Phase 2: Core Service | โœ… Complete | 8/8 passing | TrancheManager with 700+ LOC | +| Phase 3: Hunter Integration | โœ… Complete | 2/2 passing | Pre-trade checks, post-order creation | +| Phase 4: Position Manager | โœ… Complete | 4/4 passing | Exit logic, SL/TP, exchange sync | +| Phase 5: Real-time Updates | โœ… Complete | 2/2 passing | WebSocket broadcasting, isolation monitoring | +| Phase 6: UI Dashboard | โœ… Complete | 1/1 passing | Tranche breakdown, timeline, config UI | +| Phase 7: Testing | โœ… Complete | 19/19 passing | System tests + integration tests | +| Phase 8: Documentation | โœ… Complete | N/A | README, CLAUDE.md, user guide | + +--- + +## Overview + +This document provides a step-by-step implementation plan for adding multi-tranche position management to the Aster Lick Hunter bot. The system will allow tracking multiple "virtual" position entries (tranches) while the exchange only sees a single combined position per symbol+side. + +### Core Problem +When a position goes underwater (>5% loss), we currently can't place new trades on the same symbol without adding to the losing position. This locks up margin and prevents us from taking advantage of new opportunities. + +### Solution Architecture +Implement a **virtual tranche tracking layer** that: +- Tracks multiple position entries locally as separate "tranches" +- Syncs with the single exchange position (reconciliation layer) +- Manages SL/TP orders intelligently across all tranches +- Allows isolation of underwater positions while opening fresh tranches + +--- + +## Phase 1: Foundation - Data Models & Database + +### 1.1 Type Definitions (`src/lib/types.ts`) + +- [ ] **Add Tranche Interface** + ```typescript + export interface Tranche { + // Identity + id: string; // UUID v4 + symbol: string; // e.g., "BTCUSDT" + side: 'LONG' | 'SHORT'; // Position direction + positionSide: 'LONG' | 'SHORT' | 'BOTH'; // Exchange position side + + // Entry details + entryPrice: number; // Average entry price for this tranche + quantity: number; // Position size in base asset (BTC, ETH, etc.) + marginUsed: number; // USDT margin allocated + leverage: number; // Leverage used (1-125) + entryTime: number; // Unix timestamp + entryOrderId?: string; // Exchange order ID that created this tranche + + // Exit details + exitPrice?: number; // Average exit price (when closed) + exitTime?: number; // Unix timestamp + exitOrderId?: string; // Exchange order ID that closed this tranche + + // P&L tracking + unrealizedPnl: number; // Current unrealized P&L (updated real-time) + realizedPnl: number; // Final realized P&L (on close) + + // Risk management (inherited from SymbolConfig at entry time) + tpPercent: number; // Take profit % + slPercent: number; // Stop loss % + tpPrice: number; // Calculated TP price + slPrice: number; // Calculated SL price + + // Status tracking + status: 'active' | 'closed' | 'liquidated'; + isolated: boolean; // True if underwater > isolation threshold + isolationTime?: number; // When it became isolated + isolationPrice?: number; // Price when isolated + + // Metadata + notes?: string; // Optional notes (e.g., "manual entry", "recovered from restart") + } + ``` + +- [ ] **Add TrancheGroup Interface** (manages all tranches for a symbol+side) + ```typescript + export interface TrancheGroup { + symbol: string; + side: 'LONG' | 'SHORT'; + positionSide: 'LONG' | 'SHORT' | 'BOTH'; + + // Tranche tracking + tranches: Tranche[]; // All tranches (active + closed) + activeTranches: Tranche[]; // Currently open tranches + isolatedTranches: Tranche[]; // Underwater tranches + + // Aggregated metrics (sum of active tranches) + totalQuantity: number; // Total position size + totalMarginUsed: number; // Total margin allocated + weightedAvgEntry: number; // Weighted average entry price + totalUnrealizedPnl: number; // Sum of all unrealized P&L + + // Exchange sync + lastExchangeQuantity: number; // Last known exchange position size + lastExchangeSync: number; // Last sync timestamp + syncStatus: 'synced' | 'drift' | 'conflict'; // Sync health + + // Order management + activeSlOrderId?: number; // Current exchange SL order + activeTpOrderId?: number; // Current exchange TP order + targetSlPrice?: number; // Target SL price + targetTpPrice?: number; // Target TP price + } + ``` + +- [ ] **Add TrancheStrategy Interface** (defines tranche behavior) + ```typescript + export interface TrancheStrategy { + // Closing priority when SL/TP hits + closingStrategy: 'FIFO' | 'LIFO' | 'WORST_FIRST' | 'BEST_FIRST'; + + // SL/TP calculation method + slTpStrategy: 'NEWEST' | 'OLDEST' | 'BEST_ENTRY' | 'AVERAGE'; + + // Isolation behavior + isolationAction: 'HOLD' | 'REDUCE_LEVERAGE' | 'PARTIAL_CLOSE'; + } + ``` + +- [ ] **Extend SymbolConfig Interface** + ```typescript + export interface SymbolConfig { + // ... existing fields ... + + // Tranche management settings + enableTrancheManagement?: boolean; // Enable multi-tranche system + trancheIsolationThreshold?: number; // % loss to isolate (default: 5) + maxTranches?: number; // Max active tranches (default: 3) + maxIsolatedTranches?: number; // Max isolated tranches before blocking (default: 2) + trancheAllocation?: 'equal' | 'dynamic'; // How to size new tranches + trancheStrategy?: TrancheStrategy; // Tranche behavior settings + + // Advanced tranche settings + allowTrancheWhileIsolated?: boolean; // Allow new tranches when some are isolated (default: true) + isolatedTrancheMinMargin?: number; // Min margin to keep in isolated tranches (USDT) + trancheAutoCloseIsolated?: boolean; // Auto-close isolated tranches at breakeven (default: false) + } + ``` + +### 1.2 Database Schema (`src/lib/db/trancheDb.ts`) + +- [ ] **Create Tranches Table** + ```sql + CREATE TABLE IF NOT EXISTS tranches ( + -- Identity + id TEXT PRIMARY KEY, + symbol TEXT NOT NULL, + side TEXT NOT NULL, -- 'LONG' | 'SHORT' + position_side TEXT NOT NULL, -- 'LONG' | 'SHORT' | 'BOTH' + + -- Entry details + entry_price REAL NOT NULL, + quantity REAL NOT NULL, + margin_used REAL NOT NULL, + leverage INTEGER NOT NULL, + entry_time INTEGER NOT NULL, + entry_order_id TEXT, + + -- Exit details + exit_price REAL, + exit_time INTEGER, + exit_order_id TEXT, + + -- P&L tracking + unrealized_pnl REAL DEFAULT 0, + realized_pnl REAL DEFAULT 0, + + -- Risk management + tp_percent REAL NOT NULL, + sl_percent REAL NOT NULL, + tp_price REAL NOT NULL, + sl_price REAL NOT NULL, + + -- Status + status TEXT DEFAULT 'active', -- 'active' | 'closed' | 'liquidated' + isolated BOOLEAN DEFAULT 0, + isolation_time INTEGER, + isolation_price REAL, + + -- Metadata + notes TEXT, + created_at INTEGER DEFAULT (strftime('%s', 'now')), + updated_at INTEGER DEFAULT (strftime('%s', 'now')) + ); + + -- Indexes for performance + CREATE INDEX IF NOT EXISTS idx_tranches_symbol_side_status + ON tranches(symbol, side, status); + CREATE INDEX IF NOT EXISTS idx_tranches_status + ON tranches(status); + CREATE INDEX IF NOT EXISTS idx_tranches_entry_time + ON tranches(entry_time DESC); + CREATE INDEX IF NOT EXISTS idx_tranches_isolated + ON tranches(isolated, status) WHERE isolated = 1; + ``` + +- [ ] **Create Tranche Events Table** (audit trail) + ```sql + CREATE TABLE IF NOT EXISTS tranche_events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + tranche_id TEXT NOT NULL, + event_type TEXT NOT NULL, -- 'created' | 'isolated' | 'closed' | 'liquidated' | 'updated' + event_time INTEGER NOT NULL, + + -- Event details + price REAL, -- Price at event time + quantity REAL, -- Quantity affected + pnl REAL, -- P&L at event (if applicable) + + -- Context + trigger TEXT, -- What triggered the event + metadata TEXT, -- JSON with additional details + + FOREIGN KEY (tranche_id) REFERENCES tranches(id) + ); + + CREATE INDEX IF NOT EXISTS idx_tranche_events_tranche_id + ON tranche_events(tranche_id); + CREATE INDEX IF NOT EXISTS idx_tranche_events_time + ON tranche_events(event_time DESC); + ``` + +- [ ] **Implement Database Methods** + ```typescript + // Create + export async function createTranche(tranche: Tranche): Promise + + // Read + export async function getTranche(id: string): Promise + export async function getActiveTranches(symbol: string, side: string): Promise + export async function getIsolatedTranches(symbol: string, side: string): Promise + export async function getAllTranchesForSymbol(symbol: string): Promise + + // Update + export async function updateTranche(id: string, updates: Partial): Promise + export async function updateTrancheUnrealizedPnl(id: string, pnl: number): Promise + export async function isolateTranche(id: string, price: number): Promise + + // Delete/Close + export async function closeTranche(id: string, exitPrice: number, realizedPnl: number, orderId?: string): Promise + export async function liquidateTranche(id: string, liquidationPrice: number): Promise + + // Events + export async function logTrancheEvent(trancheId: string, eventType: string, data: any): Promise + export async function getTrancheHistory(trancheId: string): Promise + + // Cleanup + export async function cleanupOldTranches(daysToKeep: number = 30): Promise + ``` + +- [ ] **Add Database Initialization** to `src/lib/db/initDb.ts` + - Import and call tranche table creation + - Add to cleanup scheduler for old closed tranches + +--- + +## Phase 2: Core Service - Tranche Manager + +### 2.1 Tranche Manager Service (`src/lib/services/trancheManager.ts`) + +- [ ] **Service Structure** + ```typescript + class TrancheManagerService extends EventEmitter { + private trancheGroups: Map = new Map(); // key: "BTCUSDT_LONG" + private config: Config; + private priceService: any; // For real-time price updates + + constructor(config: Config) { + super(); + this.config = config; + } + } + ``` + +- [ ] **Initialization Methods** + ```typescript + // Initialize from database on startup + public async initialize(): Promise { + // Load all active tranches from DB + // Reconstruct TrancheGroups + // Subscribe to price updates + // Validate against exchange positions (sync check) + } + + // Check if tranche management is enabled for a symbol + public isEnabled(symbol: string): boolean { + return this.config.symbols[symbol]?.enableTrancheManagement === true; + } + ``` + +- [ ] **Tranche Creation Methods** + ```typescript + // Create a new tranche when opening a position + public async createTranche(params: { + symbol: string; + side: 'BUY' | 'SELL'; // Order side + positionSide: 'LONG' | 'SHORT' | 'BOTH'; + entryPrice: number; + quantity: number; + marginUsed: number; + leverage: number; + orderId?: string; + }): Promise { + const symbolConfig = this.config.symbols[params.symbol]; + const trancheSide = params.side === 'BUY' ? 'LONG' : 'SHORT'; + + // Calculate TP/SL prices + const tpPrice = this.calculateTpPrice(params.entryPrice, symbolConfig.tpPercent, trancheSide); + const slPrice = this.calculateSlPrice(params.entryPrice, symbolConfig.slPercent, trancheSide); + + const tranche: Tranche = { + id: uuidv4(), + symbol: params.symbol, + side: trancheSide, + positionSide: params.positionSide, + entryPrice: params.entryPrice, + quantity: params.quantity, + marginUsed: params.marginUsed, + leverage: params.leverage, + entryTime: Date.now(), + entryOrderId: params.orderId, + unrealizedPnl: 0, + realizedPnl: 0, + tpPercent: symbolConfig.tpPercent, + slPercent: symbolConfig.slPercent, + tpPrice, + slPrice, + status: 'active', + isolated: false, + }; + + // Save to database + await createTranche(tranche); + + // Add to in-memory tracking + const groupKey = this.getGroupKey(params.symbol, trancheSide); + let group = this.trancheGroups.get(groupKey); + if (!group) { + group = this.createTrancheGroup(params.symbol, trancheSide, params.positionSide); + this.trancheGroups.set(groupKey, group); + } + + group.tranches.push(tranche); + group.activeTranches.push(tranche); + this.recalculateGroupMetrics(group); + + // Log event + await logTrancheEvent(tranche.id, 'created', { + entryPrice: params.entryPrice, + quantity: params.quantity, + orderId: params.orderId, + }); + + // Emit event + this.emit('trancheCreated', tranche); + + return tranche; + } + ``` + +- [ ] **Tranche Isolation Methods** + ```typescript + // Check if a tranche should be isolated (P&L < threshold) + public shouldIsolateTranche(tranche: Tranche, currentPrice: number): boolean { + if (tranche.isolated || tranche.status !== 'active') { + return false; + } + + const symbolConfig = this.config.symbols[tranche.symbol]; + const threshold = symbolConfig?.trancheIsolationThreshold || 5; + + // Calculate unrealized P&L % + const pnlPercent = this.calculatePnlPercent( + tranche.entryPrice, + currentPrice, + tranche.side + ); + + return pnlPercent <= -threshold; // Negative = loss + } + + // Isolate a tranche (mark as underwater) + public async isolateTranche(trancheId: string, currentPrice?: number): Promise { + const tranche = await getTranche(trancheId); + if (!tranche || tranche.isolated) return; + + const price = currentPrice || await this.getCurrentPrice(tranche.symbol); + + await isolateTranche(trancheId, price); + + // Update in-memory + tranche.isolated = true; + tranche.isolationTime = Date.now(); + tranche.isolationPrice = price; + + const groupKey = this.getGroupKey(tranche.symbol, tranche.side); + const group = this.trancheGroups.get(groupKey); + if (group) { + // Move from active to isolated + group.activeTranches = group.activeTranches.filter(t => t.id !== trancheId); + group.isolatedTranches.push(tranche); + this.recalculateGroupMetrics(group); + } + + // Log event + await logTrancheEvent(trancheId, 'isolated', { + price, + unrealizedPnl: tranche.unrealizedPnl, + }); + + // Emit event + this.emit('trancheIsolated', tranche); + + logWithTimestamp(`TrancheManager: Isolated tranche ${trancheId.substring(0, 8)} for ${tranche.symbol} at ${price} (P&L: ${tranche.unrealizedPnl.toFixed(2)} USDT)`); + } + + // Monitor all active tranches and isolate if needed + public async checkIsolationConditions(): Promise { + for (const [_key, group] of this.trancheGroups) { + const currentPrice = await this.getCurrentPrice(group.symbol); + + for (const tranche of group.activeTranches) { + if (this.shouldIsolateTranche(tranche, currentPrice)) { + await this.isolateTranche(tranche.id, currentPrice); + } + } + } + } + ``` + +- [ ] **Tranche Closing Methods** + ```typescript + // Select which tranche(s) to close based on strategy + public selectTranchesToClose( + symbol: string, + side: 'LONG' | 'SHORT', + quantityToClose: number + ): Tranche[] { + const groupKey = this.getGroupKey(symbol, side); + const group = this.trancheGroups.get(groupKey); + if (!group) return []; + + const symbolConfig = this.config.symbols[symbol]; + const strategy = symbolConfig?.trancheStrategy?.closingStrategy || 'FIFO'; + + const tranchesToClose: Tranche[] = []; + let remainingQty = quantityToClose; + + // Sort tranches based on strategy + let sortedTranches = [...group.activeTranches]; + switch (strategy) { + case 'FIFO': + sortedTranches.sort((a, b) => a.entryTime - b.entryTime); // Oldest first + break; + case 'LIFO': + sortedTranches.sort((a, b) => b.entryTime - a.entryTime); // Newest first + break; + case 'WORST_FIRST': + sortedTranches.sort((a, b) => a.unrealizedPnl - b.unrealizedPnl); // Most negative first + break; + case 'BEST_FIRST': + sortedTranches.sort((a, b) => b.unrealizedPnl - a.unrealizedPnl); // Most positive first + break; + } + + // Select tranches until we have enough quantity + for (const tranche of sortedTranches) { + if (remainingQty <= 0) break; + + tranchesToClose.push(tranche); + remainingQty -= tranche.quantity; + } + + return tranchesToClose; + } + + // Close a tranche (fully or partially) + public async closeTranche(params: { + trancheId: string; + exitPrice: number; + quantityClosed?: number; // If partial close + realizedPnl: number; + orderId?: string; + }): Promise { + const tranche = await getTranche(params.trancheId); + if (!tranche) return; + + const isFullClose = !params.quantityClosed || params.quantityClosed >= tranche.quantity; + + if (isFullClose) { + // Full close + await closeTranche(params.trancheId, params.exitPrice, params.realizedPnl, params.orderId); + + // Update in-memory + tranche.status = 'closed'; + tranche.exitPrice = params.exitPrice; + tranche.exitTime = Date.now(); + tranche.exitOrderId = params.orderId; + tranche.realizedPnl = params.realizedPnl; + + const groupKey = this.getGroupKey(tranche.symbol, tranche.side); + const group = this.trancheGroups.get(groupKey); + if (group) { + group.activeTranches = group.activeTranches.filter(t => t.id !== params.trancheId); + group.isolatedTranches = group.isolatedTranches.filter(t => t.id !== params.trancheId); + this.recalculateGroupMetrics(group); + } + + await logTrancheEvent(params.trancheId, 'closed', { + exitPrice: params.exitPrice, + realizedPnl: params.realizedPnl, + orderId: params.orderId, + }); + + this.emit('trancheClosed', tranche); + + logWithTimestamp(`TrancheManager: Closed tranche ${params.trancheId.substring(0, 8)} for ${tranche.symbol} at ${params.exitPrice} (P&L: ${params.realizedPnl.toFixed(2)} USDT)`); + } else { + // Partial close - reduce quantity + const newQuantity = tranche.quantity - params.quantityClosed; + const proportionalPnl = params.realizedPnl * (params.quantityClosed / tranche.quantity); + + await updateTranche(params.trancheId, { + quantity: newQuantity, + realizedPnl: tranche.realizedPnl + proportionalPnl, + }); + + // Update in-memory + tranche.quantity = newQuantity; + tranche.realizedPnl += proportionalPnl; + + await logTrancheEvent(params.trancheId, 'updated', { + exitPrice: params.exitPrice, + quantityClosed: params.quantityClosed, + partialPnl: proportionalPnl, + }); + + this.emit('tranchePartialClose', tranche); + + logWithTimestamp(`TrancheManager: Partially closed tranche ${params.trancheId.substring(0, 8)} - ${params.quantityClosed} of ${tranche.quantity} (P&L: ${proportionalPnl.toFixed(2)} USDT)`); + } + } + + // Process order fill and close appropriate tranches + public async processOrderFill(params: { + symbol: string; + side: 'BUY' | 'SELL'; + positionSide: 'LONG' | 'SHORT' | 'BOTH'; + quantityFilled: number; + fillPrice: number; + realizedPnl: number; + orderId: string; + }): Promise { + const trancheSide = params.side === 'BUY' ? 'SHORT' : 'LONG'; // Closing side is opposite + + const tranchesToClose = this.selectTranchesToClose( + params.symbol, + trancheSide, + params.quantityFilled + ); + + let remainingQty = params.quantityFilled; + let remainingPnl = params.realizedPnl; + + for (const tranche of tranchesToClose) { + const qtyToClose = Math.min(remainingQty, tranche.quantity); + const proportionalPnl = remainingPnl * (qtyToClose / params.quantityFilled); + + await this.closeTranche({ + trancheId: tranche.id, + exitPrice: params.fillPrice, + quantityClosed: qtyToClose, + realizedPnl: proportionalPnl, + orderId: params.orderId, + }); + + remainingQty -= qtyToClose; + remainingPnl -= proportionalPnl; + + if (remainingQty <= 0) break; + } + } + ``` + +- [ ] **Exchange Sync Methods** + ```typescript + // Sync local tranches with exchange position + public async syncWithExchange( + symbol: string, + side: 'LONG' | 'SHORT', + exchangePosition: ExchangePosition + ): Promise { + const groupKey = this.getGroupKey(symbol, side); + const group = this.trancheGroups.get(groupKey); + + const exchangeQty = Math.abs(parseFloat(exchangePosition.positionAmt)); + + if (!group) { + if (exchangeQty > 0) { + // Exchange has position but we have no tranches - create "unknown" tranche + logWarnWithTimestamp(`TrancheManager: Found untracked position ${symbol} ${side}, creating recovery tranche`); + await this.createTranche({ + symbol, + side: side === 'LONG' ? 'BUY' : 'SELL', + positionSide: exchangePosition.positionSide as any, + entryPrice: parseFloat(exchangePosition.entryPrice), + quantity: exchangeQty, + marginUsed: exchangeQty * parseFloat(exchangePosition.entryPrice) / parseFloat(exchangePosition.leverage), + leverage: parseFloat(exchangePosition.leverage), + }); + } + return; + } + + // Compare quantities + const localQty = group.totalQuantity; + const drift = Math.abs(localQty - exchangeQty); + const driftPercent = (drift / Math.max(exchangeQty, 0.00001)) * 100; + + if (driftPercent > 1) { // More than 1% drift + logWarnWithTimestamp(`TrancheManager: Quantity drift detected for ${symbol} ${side} - Local: ${localQty}, Exchange: ${exchangeQty} (${driftPercent.toFixed(2)}% drift)`); + group.syncStatus = 'drift'; + + if (exchangeQty === 0 && localQty > 0) { + // Exchange position closed but we still have tranches - close all + logWarnWithTimestamp(`TrancheManager: Exchange position closed, closing all local tranches`); + for (const tranche of group.activeTranches) { + await this.closeTranche({ + trancheId: tranche.id, + exitPrice: parseFloat(exchangePosition.markPrice), + realizedPnl: 0, // Unknown - already realized on exchange + }); + } + } else if (exchangeQty > 0 && localQty === 0) { + // Exchange has position but we have no tranches + logWarnWithTimestamp(`TrancheManager: Creating recovery tranche for untracked position`); + await this.createTranche({ + symbol, + side: side === 'LONG' ? 'BUY' : 'SELL', + positionSide: exchangePosition.positionSide as any, + entryPrice: parseFloat(exchangePosition.entryPrice), + quantity: exchangeQty, + marginUsed: exchangeQty * parseFloat(exchangePosition.entryPrice) / parseFloat(exchangePosition.leverage), + leverage: parseFloat(exchangePosition.leverage), + }); + } else if (exchangeQty < localQty) { + // Partial close on exchange - close oldest tranches to match + const qtyToClose = localQty - exchangeQty; + const tranchesToClose = this.selectTranchesToClose(symbol, side, qtyToClose); + + for (const tranche of tranchesToClose) { + await this.closeTranche({ + trancheId: tranche.id, + exitPrice: parseFloat(exchangePosition.markPrice), + quantityClosed: Math.min(tranche.quantity, qtyToClose), + realizedPnl: 0, // Unknown + }); + } + } + } else { + group.syncStatus = 'synced'; + } + + group.lastExchangeQuantity = exchangeQty; + group.lastExchangeSync = Date.now(); + } + ``` + +- [ ] **Position Limit Checks** + ```typescript + // Check if we can open a new tranche + public canOpenNewTranche(symbol: string, side: 'LONG' | 'SHORT'): { + allowed: boolean; + reason?: string; + } { + const symbolConfig = this.config.symbols[symbol]; + if (!symbolConfig?.enableTrancheManagement) { + return { allowed: true }; // Not using tranche system + } + + const groupKey = this.getGroupKey(symbol, side); + const group = this.trancheGroups.get(groupKey); + + if (!group) { + return { allowed: true }; // First tranche + } + + // Check max active tranches + const maxTranches = symbolConfig.maxTranches || 3; + if (group.activeTranches.length >= maxTranches) { + return { + allowed: false, + reason: `Max active tranches (${maxTranches}) reached for ${symbol}`, + }; + } + + // Check max isolated tranches + const maxIsolated = symbolConfig.maxIsolatedTranches || 2; + if (group.isolatedTranches.length >= maxIsolated) { + if (!symbolConfig.allowTrancheWhileIsolated) { + return { + allowed: false, + reason: `Max isolated tranches (${maxIsolated}) reached for ${symbol}`, + }; + } + } + + return { allowed: true }; + } + ``` + +- [ ] **P&L Update Methods** + ```typescript + // Update unrealized P&L for all active tranches + public async updateUnrealizedPnl(symbol: string, currentPrice: number): Promise { + const groups = [ + this.trancheGroups.get(this.getGroupKey(symbol, 'LONG')), + this.trancheGroups.get(this.getGroupKey(symbol, 'SHORT')), + ]; + + for (const group of groups) { + if (!group) continue; + + for (const tranche of group.activeTranches) { + const pnl = this.calculateUnrealizedPnl( + tranche.entryPrice, + currentPrice, + tranche.quantity, + tranche.side + ); + + tranche.unrealizedPnl = pnl; + + // Update in DB (batch update for performance) + await updateTrancheUnrealizedPnl(tranche.id, pnl); + } + + this.recalculateGroupMetrics(group); + } + + // Check isolation conditions after P&L update + await this.checkIsolationConditions(); + } + + // Calculate unrealized P&L for a tranche + private calculateUnrealizedPnl( + entryPrice: number, + currentPrice: number, + quantity: number, + side: 'LONG' | 'SHORT' + ): number { + if (side === 'LONG') { + return (currentPrice - entryPrice) * quantity; + } else { + return (entryPrice - currentPrice) * quantity; + } + } + + // Calculate P&L percentage + private calculatePnlPercent( + entryPrice: number, + currentPrice: number, + side: 'LONG' | 'SHORT' + ): number { + if (side === 'LONG') { + return ((currentPrice - entryPrice) / entryPrice) * 100; + } else { + return ((entryPrice - currentPrice) / entryPrice) * 100; + } + } + ``` + +- [ ] **Helper Methods** + ```typescript + private getGroupKey(symbol: string, side: 'LONG' | 'SHORT'): string { + return `${symbol}_${side}`; + } + + private createTrancheGroup( + symbol: string, + side: 'LONG' | 'SHORT', + positionSide: 'LONG' | 'SHORT' | 'BOTH' + ): TrancheGroup { + return { + symbol, + side, + positionSide, + tranches: [], + activeTranches: [], + isolatedTranches: [], + totalQuantity: 0, + totalMarginUsed: 0, + weightedAvgEntry: 0, + totalUnrealizedPnl: 0, + lastExchangeQuantity: 0, + lastExchangeSync: Date.now(), + syncStatus: 'synced', + }; + } + + private recalculateGroupMetrics(group: TrancheGroup): void { + // Sum quantities and margins + let totalQty = 0; + let totalMargin = 0; + let weightedEntry = 0; + let totalPnl = 0; + + for (const tranche of group.activeTranches) { + totalQty += tranche.quantity; + totalMargin += tranche.marginUsed; + weightedEntry += tranche.entryPrice * tranche.quantity; + totalPnl += tranche.unrealizedPnl; + } + + group.totalQuantity = totalQty; + group.totalMarginUsed = totalMargin; + group.weightedAvgEntry = totalQty > 0 ? weightedEntry / totalQty : 0; + group.totalUnrealizedPnl = totalPnl; + } + + private async getCurrentPrice(symbol: string): Promise { + if (this.priceService) { + const price = this.priceService.getPrice(symbol); + if (price) return price; + } + + // Fallback to API + const markPriceData = await getMarkPrice(symbol); + return parseFloat(Array.isArray(markPriceData) ? markPriceData[0].markPrice : markPriceData.markPrice); + } + + private calculateTpPrice(entryPrice: number, tpPercent: number, side: 'LONG' | 'SHORT'): number { + if (side === 'LONG') { + return entryPrice * (1 + tpPercent / 100); + } else { + return entryPrice * (1 - tpPercent / 100); + } + } + + private calculateSlPrice(entryPrice: number, slPercent: number, side: 'LONG' | 'SHORT'): number { + if (side === 'LONG') { + return entryPrice * (1 - slPercent / 100); + } else { + return entryPrice * (1 + slPercent / 100); + } + } + + // Public getters + public getTranches(symbol: string, side: 'LONG' | 'SHORT'): Tranche[] { + const groupKey = this.getGroupKey(symbol, side); + return this.trancheGroups.get(groupKey)?.activeTranches || []; + } + + public getTrancheGroup(symbol: string, side: 'LONG' | 'SHORT'): TrancheGroup | undefined { + const groupKey = this.getGroupKey(symbol, side); + return this.trancheGroups.get(groupKey); + } + + public getAllTrancheGroups(): TrancheGroup[] { + return Array.from(this.trancheGroups.values()); + } + ``` + +- [ ] **Export Singleton Instance** + ```typescript + let trancheManager: TrancheManagerService | null = null; + + export function initializeTrancheManager(config: Config): TrancheManagerService { + trancheManager = new TrancheManagerService(config); + return trancheManager; + } + + export function getTrancheManager(): TrancheManagerService { + if (!trancheManager) { + throw new Error('TrancheManager not initialized'); + } + return trancheManager; + } + ``` + +--- + +## Phase 3: Hunter Integration (Entry Logic) + +### 3.1 Modify Hunter to Use Tranche Manager + +- [ ] **Import Tranche Manager in `src/lib/bot/hunter.ts`** + ```typescript + import { getTrancheManager } from '../services/trancheManager'; + ``` + +- [ ] **Update `placeTrade()` Method - Pre-Trade Checks** + ```typescript + // Add BEFORE existing position limit checks (around line 758) + + // Check tranche management + if (this.config.symbols[symbol]?.enableTrancheManagement) { + const trancheManager = getTrancheManager(); + const trancheSide = side === 'BUY' ? 'LONG' : 'SHORT'; + + // Update P&L and check isolation conditions + const currentPrice = await getMarkPrice(symbol); + const price = parseFloat(Array.isArray(currentPrice) ? currentPrice[0].markPrice : currentPrice.markPrice); + await trancheManager.updateUnrealizedPnl(symbol, price); + + // Check if we can open a new tranche + const canOpen = trancheManager.canOpenNewTranche(symbol, trancheSide); + if (!canOpen.allowed) { + logWithTimestamp(`Hunter: ${canOpen.reason}`); + + // Broadcast to UI + if (this.statusBroadcaster) { + this.statusBroadcaster.broadcastTradingError( + `Tranche Limit Reached - ${symbol}`, + canOpen.reason || 'Cannot open new tranche', + { + component: 'Hunter', + symbol, + details: { + activeTranches: trancheManager.getTranches(symbol, trancheSide).length, + maxTranches: this.config.symbols[symbol].maxTranches || 3, + } + } + ); + } + + return; // Block the trade + } + } + ``` + +- [ ] **Update `placeTrade()` Method - Post-Order Creation** + ```typescript + // Add AFTER order is successfully placed (around line 1151) + + // Only broadcast and emit if order was successfully placed + if (order && order.orderId) { + // Create tranche if tranche management is enabled + if (this.config.symbols[symbol]?.enableTrancheManagement) { + const trancheManager = getTrancheManager(); + const trancheSide = side === 'BUY' ? 'LONG' : 'SHORT'; + + try { + const tranche = await trancheManager.createTranche({ + symbol, + side, + positionSide: getPositionSide(this.isHedgeMode, side) as any, + entryPrice: orderType === 'LIMIT' ? orderPrice : entryPrice, + quantity, + marginUsed: tradeSizeUSDT, + leverage: symbolConfig.leverage, + orderId: order.orderId.toString(), + }); + + logWithTimestamp(`Hunter: Created tranche ${tranche.id.substring(0, 8)} for ${symbol} ${side}`); + } catch (error) { + logErrorWithTimestamp('Hunter: Failed to create tranche:', error); + // Don't fail the trade, just log the error + } + } + + // Existing broadcast and emit code... + } + ``` + +--- + +## Phase 4: Position Manager Integration (Exit Logic) + +### 4.1 Modify Position Manager for Tranche Tracking + +- [ ] **Import Tranche Manager in `src/lib/bot/positionManager.ts`** + ```typescript + import { getTrancheManager } from '../services/trancheManager'; + ``` + +- [ ] **Update `syncWithExchange()` Method** + ```typescript + // Add AFTER processing each position (around line 432) + + if (symbolConfig && symbolConfig.enableTrancheManagement) { + const trancheManager = getTrancheManager(); + const trancheSide = posAmt > 0 ? 'LONG' : 'SHORT'; + + try { + await trancheManager.syncWithExchange(symbol, trancheSide, position); + } catch (error) { + logErrorWithTimestamp(`PositionManager: Failed to sync tranches for ${symbol}:`, error); + } + } + ``` + +- [ ] **Update `handleOrderUpdate()` Method - Process Fills** + ```typescript + // Add when order fills with realized P&L (around line 997) + + if (orderStatus === 'FILLED' && order.rp) { + const symbol = order.s; + const symbolConfig = this.config.symbols[symbol]; + + // Check if tranche management is enabled + if (symbolConfig?.enableTrancheManagement) { + const trancheManager = getTrancheManager(); + const reduceOnlyFill = order.R === true || order.R === 'true'; + + if (reduceOnlyFill) { + // This is a closing order (SL or TP) + const quantityFilled = parseFloat(order.z); // Cumulative filled qty + const fillPrice = parseFloat(order.ap); // Average price + const realizedPnl = parseFloat(order.rp); // Realized profit + const orderId = order.i.toString(); + + try { + await trancheManager.processOrderFill({ + symbol, + side: order.S, + positionSide: order.ps || 'BOTH', + quantityFilled, + fillPrice, + realizedPnl, + orderId, + }); + + logWithTimestamp(`PositionManager: Processed tranche close for ${symbol}, qty: ${quantityFilled}, P&L: ${realizedPnl.toFixed(2)} USDT`); + } catch (error) { + logErrorWithTimestamp(`PositionManager: Failed to process tranche fill for ${symbol}:`, error); + } + } + } + } + ``` + +### 4.2 SL/TP Order Management Strategy + +**Critical Challenge**: The exchange only allows ONE SL and ONE TP order per position, but we have multiple tranches with different targets. + +**Solution Strategy**: Use the NEWEST (most favorable) tranche's TP/SL targets + +- [ ] **Create Helper Method for Tranche-Based SL/TP Calculation** + ```typescript + // Add to PositionManager class + + private async calculateTrancheBasedTargets( + symbol: string, + side: 'LONG' | 'SHORT', + totalQuantity: number + ): Promise<{ slPrice: number; tpPrice: number; targetTranche: Tranche } | null> { + const symbolConfig = this.config.symbols[symbol]; + if (!symbolConfig?.enableTrancheManagement) { + return null; + } + + const trancheManager = getTrancheManager(); + const activeTranches = trancheManager.getTranches(symbol, side); + + if (activeTranches.length === 0) { + return null; + } + + // Get strategy + const strategy = symbolConfig.trancheStrategy?.slTpStrategy || 'NEWEST'; + + let targetTranche: Tranche; + + switch (strategy) { + case 'NEWEST': + // Use newest tranche (most favorable entry) + targetTranche = activeTranches.sort((a, b) => b.entryTime - a.entryTime)[0]; + break; + + case 'OLDEST': + // Use oldest tranche + targetTranche = activeTranches.sort((a, b) => a.entryTime - b.entryTime)[0]; + break; + + case 'BEST_ENTRY': + // Use tranche with best entry price + if (side === 'LONG') { + targetTranche = activeTranches.sort((a, b) => a.entryPrice - b.entryPrice)[0]; // Lowest entry + } else { + targetTranche = activeTranches.sort((a, b) => b.entryPrice - a.entryPrice)[0]; // Highest entry + } + break; + + case 'AVERAGE': + // Use weighted average of all tranches + const group = trancheManager.getTrancheGroup(symbol, side); + if (!group) return null; + + const avgEntry = group.weightedAvgEntry; + const avgTpPercent = activeTranches.reduce((sum, t) => sum + t.tpPercent, 0) / activeTranches.length; + const avgSlPercent = activeTranches.reduce((sum, t) => sum + t.slPercent, 0) / activeTranches.length; + + const slPrice = side === 'LONG' + ? avgEntry * (1 - avgSlPercent / 100) + : avgEntry * (1 + avgSlPercent / 100); + + const tpPrice = side === 'LONG' + ? avgEntry * (1 + avgTpPercent / 100) + : avgEntry * (1 - avgTpPercent / 100); + + return { + slPrice: symbolPrecision.formatPrice(symbol, slPrice), + tpPrice: symbolPrecision.formatPrice(symbol, tpPrice), + targetTranche: activeTranches[0], // Use first tranche for reference + }; + + default: + targetTranche = activeTranches[0]; + } + + logWithTimestamp(`PositionManager: Using ${strategy} tranche for SL/TP - Entry: ${targetTranche.entryPrice}, SL: ${targetTranche.slPrice}, TP: ${targetTranche.tpPrice}`); + + return { + slPrice: targetTranche.slPrice, + tpPrice: targetTranche.tpPrice, + targetTranche, + }; + } + ``` + +- [ ] **Update `placeProtectiveOrdersWithLock()` Method** + ```typescript + // Modify around line 1000 (inside try block of placeProtectiveOrdersWithLock) + + // Calculate SL/TP prices + let slPrice: number; + let tpPrice: number; + + // Check if tranche management is enabled + const trancheTargets = await this.calculateTrancheBasedTargets( + position.symbol, + isLong ? 'LONG' : 'SHORT', + positionQty + ); + + if (trancheTargets) { + // Use tranche-based targets + slPrice = trancheTargets.slPrice; + tpPrice = trancheTargets.tpPrice; + + logWithTimestamp(`PositionManager: Using tranche-based targets for ${symbol} - SL: ${slPrice}, TP: ${tpPrice}`); + } else { + // Use traditional calculation (existing code) + const entryPrice = parseFloat(position.entryPrice); + const slPercent = symbolConfig.slPercent; + const tpPercent = symbolConfig.tpPercent; + + slPrice = isLong + ? entryPrice * (1 - slPercent / 100) + : entryPrice * (1 + slPercent / 100); + + tpPrice = isLong + ? entryPrice * (1 + tpPercent / 100) + : entryPrice * (1 - tpPercent / 100); + + // Format prices + slPrice = symbolPrecision.formatPrice(position.symbol, slPrice); + tpPrice = symbolPrecision.formatPrice(position.symbol, tpPrice); + } + + // Continue with existing order placement logic... + ``` + +- [ ] **Update `adjustProtectiveOrders()` Method** + ```typescript + // Add at the start of adjustProtectiveOrders method + + // Recalculate targets based on tranche strategy + const trancheTargets = await this.calculateTrancheBasedTargets( + position.symbol, + isLong ? 'LONG' : 'SHORT', + positionQty + ); + + if (trancheTargets) { + // Use tranche-based targets for adjustment + // (Update the calculation to use trancheTargets.slPrice and trancheTargets.tpPrice) + } + ``` + +--- + +## Phase 5: Real-Time Updates & Monitoring + +### 5.1 Price Update Integration + +- [ ] **Subscribe to Price Updates in Tranche Manager** + ```typescript + // In trancheManager.initialize() + + const priceService = getPriceService(); + if (priceService) { + // Subscribe to all symbols with active tranches + const symbols = new Set(); + for (const group of this.trancheGroups.values()) { + if (group.activeTranches.length > 0) { + symbols.add(group.symbol); + } + } + + if (symbols.size > 0) { + priceService.subscribeToSymbols(Array.from(symbols)); + } + + // Listen for price updates + priceService.on('priceUpdate', async (data: { symbol: string; price: number }) => { + await this.updateUnrealizedPnl(data.symbol, data.price); + }); + } + ``` + +- [ ] **Periodic Isolation Check** + ```typescript + // In trancheManager class + + private isolationCheckInterval?: NodeJS.Timeout; + + public startIsolationMonitoring(intervalMs: number = 10000): void { + this.stopIsolationMonitoring(); + + this.isolationCheckInterval = setInterval(async () => { + try { + await this.checkIsolationConditions(); + } catch (error) { + logErrorWithTimestamp('TrancheManager: Isolation check failed:', error); + } + }, intervalMs); + + logWithTimestamp(`TrancheManager: Started isolation monitoring (every ${intervalMs / 1000}s)`); + } + + public stopIsolationMonitoring(): void { + if (this.isolationCheckInterval) { + clearInterval(this.isolationCheckInterval); + this.isolationCheckInterval = undefined; + logWithTimestamp('TrancheManager: Stopped isolation monitoring'); + } + } + ``` + +### 5.2 WebSocket Event Broadcasting + +- [ ] **Add Tranche Events to Status Broadcaster** + ```typescript + // In src/bot/websocketServer.ts + + // Add new broadcast methods + public broadcastTrancheCreated(tranche: Tranche): void { + this.broadcast('tranche_created', { + id: tranche.id, + symbol: tranche.symbol, + side: tranche.side, + entryPrice: tranche.entryPrice, + quantity: tranche.quantity, + marginUsed: tranche.marginUsed, + leverage: tranche.leverage, + timestamp: tranche.entryTime, + }); + } + + public broadcastTrancheIsolated(tranche: Tranche): void { + this.broadcast('tranche_isolated', { + id: tranche.id, + symbol: tranche.symbol, + side: tranche.side, + isolationPrice: tranche.isolationPrice, + unrealizedPnl: tranche.unrealizedPnl, + timestamp: tranche.isolationTime, + }); + } + + public broadcastTrancheClosed(tranche: Tranche): void { + this.broadcast('tranche_closed', { + id: tranche.id, + symbol: tranche.symbol, + side: tranche.side, + exitPrice: tranche.exitPrice, + realizedPnl: tranche.realizedPnl, + timestamp: tranche.exitTime, + }); + } + + public broadcastTrancheUpdate(group: TrancheGroup): void { + this.broadcast('tranche_update', { + symbol: group.symbol, + side: group.side, + activeTranches: group.activeTranches.length, + isolatedTranches: group.isolatedTranches.length, + totalQuantity: group.totalQuantity, + totalMarginUsed: group.totalMarginUsed, + weightedAvgEntry: group.weightedAvgEntry, + totalUnrealizedPnl: group.totalUnrealizedPnl, + syncStatus: group.syncStatus, + }); + } + ``` + +- [ ] **Connect Tranche Manager Events to Broadcaster** + ```typescript + // In src/bot/index.ts (AsterBot initialization) + + // After initializing tranche manager + const trancheManager = getTrancheManager(); + + trancheManager.on('trancheCreated', (tranche) => { + this.statusBroadcaster.broadcastTrancheCreated(tranche); + }); + + trancheManager.on('trancheIsolated', (tranche) => { + this.statusBroadcaster.broadcastTrancheIsolated(tranche); + }); + + trancheManager.on('trancheClosed', (tranche) => { + this.statusBroadcaster.broadcastTrancheClosed(tranche); + }); + + trancheManager.on('tranchePartialClose', (tranche) => { + this.statusBroadcaster.broadcastTrancheUpdate( + trancheManager.getTrancheGroup(tranche.symbol, tranche.side)! + ); + }); + ``` + +--- + +## Phase 6: UI Dashboard Integration + +### 6.1 Tranche Breakdown Component + +- [ ] **Create `src/components/TrancheBreakdownCard.tsx`** + ```typescript + import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; + import { Badge } from '@/components/ui/badge'; + import { Button } from '@/components/ui/button'; + import { TrendingUp, TrendingDown, AlertTriangle } from 'lucide-react'; + + interface Tranche { + id: string; + side: 'LONG' | 'SHORT'; + entryPrice: number; + quantity: number; + marginUsed: number; + leverage: number; + unrealizedPnl: number; + isolated: boolean; + entryTime: number; + tpPrice: number; + slPrice: number; + } + + interface TrancheBreakdownProps { + symbol: string; + tranches: Tranche[]; + currentPrice: number; + onCloseTranche?: (trancheId: string) => void; + } + + export function TrancheBreakdownCard({ symbol, tranches, currentPrice, onCloseTranche }: TrancheBreakdownProps) { + const activeTranches = tranches.filter(t => !t.isolated); + const isolatedTranches = tranches.filter(t => t.isolated); + + const totalPnl = tranches.reduce((sum, t) => sum + t.unrealizedPnl, 0); + const totalMargin = tranches.reduce((sum, t) => sum + t.marginUsed, 0); + + return ( + + + + {symbol} Tranches +
+ = 0 ? "success" : "destructive"}> + {totalPnl >= 0 ? '+' : ''}{totalPnl.toFixed(2)} USDT + + + {tranches.length} Total + +
+
+
+ + {/* Active Tranches */} + {activeTranches.length > 0 && ( +
+

Active Tranches

+
+ {activeTranches.map(tranche => ( + + ))} +
+
+ )} + + {/* Isolated Tranches */} + {isolatedTranches.length > 0 && ( +
+

+ + Isolated Tranches +

+
+ {isolatedTranches.map(tranche => ( + + ))} +
+
+ )} + + {/* Summary */} +
+
+ Total Margin: + {totalMargin.toFixed(2)} USDT +
+
+
+
+ ); + } + + function TrancheRow({ tranche, currentPrice, isolated, onClose }: { + tranche: Tranche; + currentPrice: number; + isolated?: boolean; + onClose?: (id: string) => void; + }) { + const pnlPercent = ((currentPrice - tranche.entryPrice) / tranche.entryPrice) * 100 * (tranche.side === 'LONG' ? 1 : -1); + const isProfitable = tranche.unrealizedPnl >= 0; + + return ( +
+
+
+ {tranche.side === 'LONG' ? ( + + ) : ( + + )} + + {tranche.side} + + + {new Date(tranche.entryTime).toLocaleTimeString()} + +
+ + {isProfitable ? '+' : ''}{pnlPercent.toFixed(2)}% + +
+ +
+
+ Entry: + ${tranche.entryPrice.toFixed(4)} +
+
+ Size: + {tranche.quantity.toFixed(4)} +
+
+ Margin: + {tranche.marginUsed.toFixed(2)} USDT +
+
+ P&L: + + {isProfitable ? '+' : ''}{tranche.unrealizedPnl.toFixed(2)} USDT + +
+
+ TP: + ${tranche.tpPrice.toFixed(4)} +
+
+ SL: + ${tranche.slPrice.toFixed(4)} +
+
+ + {onClose && ( + + )} +
+ ); + } + ``` + +- [ ] **Create API Route for Tranche Data (`src/app/api/tranches/route.ts`)** + ```typescript + import { NextResponse } from 'next/server'; + import { getTrancheManager } from '@/lib/services/trancheManager'; + + export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const symbol = searchParams.get('symbol'); + const side = searchParams.get('side') as 'LONG' | 'SHORT' | null; + + const trancheManager = getTrancheManager(); + + if (symbol && side) { + const tranches = trancheManager.getTranches(symbol, side); + return NextResponse.json({ tranches }); + } else if (symbol) { + const longTranches = trancheManager.getTranches(symbol, 'LONG'); + const shortTranches = trancheManager.getTranches(symbol, 'SHORT'); + return NextResponse.json({ + long: longTranches, + short: shortTranches, + }); + } else { + const allGroups = trancheManager.getAllTrancheGroups(); + return NextResponse.json({ groups: allGroups }); + } + } catch (error) { + return NextResponse.json({ error: 'Failed to fetch tranches' }, { status: 500 }); + } + } + + export async function POST(request: Request) { + try { + const { action, trancheId, price } = await request.json(); + const trancheManager = getTrancheManager(); + + if (action === 'isolate' && trancheId) { + await trancheManager.isolateTranche(trancheId, price); + return NextResponse.json({ success: true }); + } + + if (action === 'close' && trancheId && price) { + // Manual close - would need to place order on exchange + // For now, just return error + return NextResponse.json({ error: 'Manual close not implemented' }, { status: 501 }); + } + + return NextResponse.json({ error: 'Invalid action' }, { status: 400 }); + } catch (error) { + return NextResponse.json({ error: 'Action failed' }, { status: 500 }); + } + } + ``` + +- [ ] **Add Tranche Breakdown to Dashboard (`src/app/page.tsx`)** + ```typescript + // Import + import { TrancheBreakdownCard } from '@/components/TrancheBreakdownCard'; + + // Add WebSocket listener for tranche updates + useEffect(() => { + if (!ws) return; + + const handleTrancheUpdate = (data: any) => { + // Update tranche state + setTrancheGroups(prev => ({ + ...prev, + [`${data.symbol}_${data.side}`]: data, + })); + }; + + ws.addEventListener('message', (event) => { + const data = JSON.parse(event.data); + if (data.type === 'tranche_update') { + handleTrancheUpdate(data.data); + } + if (data.type === 'tranche_created') { + // Refresh tranche data + } + if (data.type === 'tranche_isolated') { + // Show notification + } + if (data.type === 'tranche_closed') { + // Show notification + } + }); + }, [ws]); + + // Render tranche cards for each symbol with active tranches + ``` + +### 6.2 Tranche Timeline Component + +- [ ] **Create `src/components/TrancheTimeline.tsx`** + ```typescript + import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; + import { Badge } from '@/components/ui/badge'; + + interface TrancheEvent { + id: string; + trancheId: string; + eventType: 'created' | 'isolated' | 'closed' | 'liquidated'; + eventTime: number; + price: number; + pnl?: number; + } + + interface TrancheTimelineProps { + symbol: string; + events: TrancheEvent[]; + } + + export function TrancheTimeline({ symbol, events }: TrancheTimelineProps) { + const sortedEvents = [...events].sort((a, b) => b.eventTime - a.eventTime); + + return ( + + + {symbol} Tranche History + + +
+ {/* Timeline line */} +
+ + {/* Events */} +
+ {sortedEvents.map(event => ( +
+ {/* Timeline dot */} +
+ + {/* Event content */} +
+
+ + {event.eventType.toUpperCase()} + + + {new Date(event.eventTime).toLocaleString()} + +
+
+ Price: + ${event.price.toFixed(4)} + {event.pnl !== undefined && ( + <> + P&L: + = 0 ? 'text-green-600' : 'text-red-600'}`}> + {event.pnl >= 0 ? '+' : ''}{event.pnl.toFixed(2)} USDT + + + )} +
+
+
+ ))} +
+
+ + + ); + } + + function getEventColor(type: string): string { + switch (type) { + case 'created': return 'bg-blue-500'; + case 'isolated': return 'bg-yellow-500'; + case 'closed': return 'bg-green-500'; + case 'liquidated': return 'bg-red-500'; + default: return 'bg-gray-500'; + } + } + + function getEventVariant(type: string): 'default' | 'success' | 'destructive' | 'warning' { + switch (type) { + case 'created': return 'default'; + case 'isolated': return 'warning'; + case 'closed': return 'success'; + case 'liquidated': return 'destructive'; + default: return 'default'; + } + } + ``` + +### 6.3 Configuration UI Updates + +- [ ] **Add Tranche Settings to `src/components/SymbolConfigForm.tsx`** + ```typescript + // Add new section for tranche management +
+

Tranche Management

+ +
+ handleChange('enableTrancheManagement', e.target.checked)} + /> + +
+ + {config.enableTrancheManagement && ( + <> +
+
+ + handleChange('trancheIsolationThreshold', parseFloat(e.target.value))} + min={1} + max={50} + step={0.5} + /> +

% loss to isolate tranche

+
+ +
+ + handleChange('maxTranches', parseInt(e.target.value))} + min={1} + max={10} + /> +
+ +
+ + handleChange('maxIsolatedTranches', parseInt(e.target.value))} + min={0} + max={5} + /> +
+ +
+ + +
+ +
+ + +
+
+ +
+ handleChange('allowTrancheWhileIsolated', e.target.checked)} + /> + +
+ + )} +
+ ``` + +--- + +## Phase 7: Testing & Validation + +### 7.1 Unit Tests + +- [ ] **Create `tests/services/trancheManager.test.ts`** + ```typescript + import { describe, it, expect, beforeEach } from '@jest/globals'; + import { TrancheManagerService } from '@/lib/services/trancheManager'; + import { Config } from '@/lib/types'; + + describe('TrancheManager', () => { + let trancheManager: TrancheManagerService; + let config: Config; + + beforeEach(() => { + config = { + // Mock config + }; + trancheManager = new TrancheManagerService(config); + }); + + describe('Tranche Creation', () => { + it('should create a new tranche', async () => { + // Test tranche creation + }); + + it('should calculate correct TP/SL prices', async () => { + // Test TP/SL calculation + }); + + it('should enforce max tranche limits', async () => { + // Test limits + }); + }); + + describe('Tranche Isolation', () => { + it('should isolate tranche when P&L drops below threshold', async () => { + // Test isolation + }); + + it('should not isolate if already isolated', async () => { + // Test duplicate isolation prevention + }); + }); + + describe('Tranche Closing', () => { + it('should close tranche fully', async () => { + // Test full close + }); + + it('should close tranche partially', async () => { + // Test partial close + }); + + it('should select correct tranches based on strategy', async () => { + // Test FIFO, LIFO, etc. + }); + }); + + describe('Exchange Sync', () => { + it('should sync with exchange position', async () => { + // Test sync + }); + + it('should detect and handle drift', async () => { + // Test drift handling + }); + + it('should create recovery tranche for untracked positions', async () => { + // Test recovery + }); + }); + + describe('P&L Calculations', () => { + it('should calculate unrealized P&L correctly for LONG', async () => { + // Test LONG P&L + }); + + it('should calculate unrealized P&L correctly for SHORT', async () => { + // Test SHORT P&L + }); + + it('should update group metrics correctly', async () => { + // Test aggregation + }); + }); + }); + ``` + +- [ ] **Create `tests/db/trancheDb.test.ts`** + ```typescript + import { describe, it, expect, beforeEach } from '@jest/globals'; + import { + createTranche, + getTranche, + getActiveTranches, + closeTranche, + isolateTranche, + } from '@/lib/db/trancheDb'; + + describe('Tranche Database', () => { + beforeEach(async () => { + // Setup test database + }); + + it('should create and retrieve tranche', async () => { + // Test CRUD operations + }); + + it('should query active tranches', async () => { + // Test queries + }); + + it('should update tranche status', async () => { + // Test updates + }); + }); + ``` + +### 7.2 Integration Tests + +- [ ] **Create `tests/integration/tranche-flow.test.ts`** + ```typescript + import { describe, it, expect } from '@jest/globals'; + + describe('Tranche Flow Integration', () => { + it('should complete full tranche lifecycle', async () => { + // 1. Create tranche on entry + // 2. Update P&L + // 3. Isolate when underwater + // 4. Open new tranche + // 5. Close profitable tranche + // 6. Verify state + }); + + it('should sync with exchange correctly', async () => { + // Test sync scenarios + }); + + it('should handle SL/TP fills correctly', async () => { + // Test order fills + }); + }); + ``` + +### 7.3 Manual Testing Checklist + +- [ ] **Basic Tranche Operations** + - [ ] Open position with tranche management enabled + - [ ] Verify tranche created in database + - [ ] Check tranche appears in UI + - [ ] Update price and verify P&L calculation + - [ ] Trigger isolation by price drop >5% + - [ ] Verify isolated tranche shown separately in UI + +- [ ] **Multiple Tranches** + - [ ] Open 2nd tranche while 1st is active + - [ ] Verify both show in UI + - [ ] Check SL/TP orders use correct strategy (newest/oldest/etc) + - [ ] Trigger TP and verify correct tranche closes (FIFO/LIFO) + +- [ ] **Edge Cases** + - [ ] Restart bot with active tranches + - [ ] Verify tranches recovered from database + - [ ] Sync with exchange position + - [ ] Place manual trade on exchange + - [ ] Verify "unknown" tranche created + - [ ] Test with max tranches reached + +- [ ] **UI Testing** + - [ ] Check tranche breakdown card displays correctly + - [ ] Verify timeline shows events + - [ ] Test configuration settings save/load + - [ ] Check WebSocket updates in real-time + +--- + +## Phase 8: Documentation & Deployment + +### 8.1 Documentation + +- [ ] **Update `CLAUDE.md`** + - Add tranche management overview + - Document configuration options + - Add troubleshooting section + +- [ ] **Create `docs/TRANCHE_SYSTEM.md`** + - Detailed architecture explanation + - Usage guide + - FAQ section + +- [ ] **Update `README.md`** + - Add tranche management to features list + - Link to detailed documentation + +### 8.2 Configuration Defaults + +- [ ] **Update `config.default.json`** + ```json + { + "symbols": { + "BTCUSDT": { + "enableTrancheManagement": false, + "trancheIsolationThreshold": 5, + "maxTranches": 3, + "maxIsolatedTranches": 2, + "allowTrancheWhileIsolated": true, + "trancheStrategy": { + "closingStrategy": "FIFO", + "slTpStrategy": "NEWEST" + } + } + } + } + ``` + +### 8.3 Migration & Deployment + +- [ ] **Create Migration Script** (`scripts/migrate-to-tranches.js`) + - Scan existing positions + - Create "recovery" tranches for untracked positions + - Verify data integrity + +- [ ] **Deployment Checklist** + - [ ] Backup current database + - [ ] Run database migrations + - [ ] Test with paper mode first + - [ ] Gradually enable for live symbols + - [ ] Monitor for issues + +--- + +## Risk Mitigation & Monitoring + +### Known Risks + +1. **Exchange Sync Issues** + - **Risk**: Local tranches drift from exchange position + - **Mitigation**: Regular sync checks, drift detection, automatic reconciliation + - **Monitoring**: Log sync status, alert on drift >2% + +2. **SL/TP Order Coordination** + - **Risk**: Single exchange SL/TP doesn't protect all tranches optimally + - **Mitigation**: Use configurable strategy (NEWEST/AVERAGE/etc) + - **Monitoring**: Track which tranches hit SL/TP, adjust strategy if needed + +3. **Database Corruption** + - **Risk**: Tranche data lost or corrupted + - **Mitigation**: Regular backups, recovery from exchange state + - **Monitoring**: Validate data integrity on startup + +4. **Performance Impact** + - **Risk**: Tranche management adds processing overhead + - **Mitigation**: Efficient DB queries, in-memory caching, batch updates + - **Monitoring**: Track latency, optimize slow queries + +5. **Complexity Bugs** + - **Risk**: Edge cases cause unexpected behavior + - **Mitigation**: Comprehensive testing, logging, fail-safes + - **Monitoring**: Error tracking, user reports + +### Monitoring Dashboard + +- [ ] **Add Tranche Metrics to Dashboard** + - Total active tranches across all symbols + - Total isolated tranches + - Average tranche duration + - Sync health status + - P&L attribution accuracy + +--- + +## Success Criteria + +### Functional Requirements +- โœ… Create multiple virtual tranches per symbol+side +- โœ… Isolate underwater tranches automatically +- โœ… Allow new trades while holding isolated positions +- โœ… Sync virtual tranches with single exchange position +- โœ… Close tranches based on configurable strategy (FIFO/LIFO/etc) +- โœ… Calculate and display per-tranche P&L +- โœ… Persist tranches to database for recovery + +### Performance Requirements +- โœ… P&L updates complete in <100ms +- โœ… Tranche creation adds <50ms to trade execution +- โœ… UI updates render in <500ms +- โœ… Database queries return in <50ms + +### User Experience +- โœ… Clear visualization of all tranches +- โœ… Easy configuration in UI +- โœ… Helpful error messages and warnings +- โœ… Accurate real-time P&L tracking + +--- + +## Timeline Estimate + +| Phase | Estimated Time | Dependencies | +|-------|---------------|--------------| +| Phase 1: Foundation | 1-2 days | None | +| Phase 2: Core Service | 2-3 days | Phase 1 | +| Phase 3: Hunter Integration | 0.5 day | Phase 2 | +| Phase 4: Position Manager | 1 day | Phase 2 | +| Phase 5: Real-time Updates | 0.5 day | Phase 2-4 | +| Phase 6: UI Dashboard | 2 days | Phase 5 | +| Phase 7: Testing | 1-2 days | Phase 6 | +| Phase 8: Docs & Deploy | 0.5 day | Phase 7 | +| **Total** | **8-11 days** | | + +--- + +## Next Steps + +1. Review this plan and get approval +2. Set up development branch: `git checkout -b feature/tranche-management` +3. Start with Phase 1 (Foundation) +4. Implement incrementally with testing at each phase +5. Deploy to paper mode for validation +6. Gradual rollout to live trading + +--- + +## Questions & Decisions Needed + +- [ ] **Tranche Naming**: Should users be able to name/tag tranches? +- [ ] **Manual Tranche Management**: Allow manual tranche creation/closure via UI? +- [ ] **Tranche Limits**: Global max tranches across all symbols? +- [ ] **Isolation Actions**: What to do with isolated tranches? (Hold, reduce leverage, partial close?) +- [ ] **Reporting**: Export tranche history to CSV/JSON? +- [ ] **Advanced Features**: DCA into isolated tranches? Tranche merging? + +--- + +*This implementation plan provides a comprehensive roadmap for adding multi-tranche position management. Each checkbox represents a discrete, completable task. Follow the phases sequentially for best results.* diff --git a/docs/TRANCHE_TESTING.md b/docs/TRANCHE_TESTING.md new file mode 100644 index 0000000..90b6f6f --- /dev/null +++ b/docs/TRANCHE_TESTING.md @@ -0,0 +1,433 @@ +# Multi-Tranche Position Management - Testing Guide + +## Overview + +This guide provides comprehensive testing procedures for the multi-tranche position management system. The system allows tracking multiple virtual position entries (tranches) per symbol while syncing with a single exchange position. + +## Prerequisites + +Before testing, ensure: +- [ ] TypeScript compilation passes: `npx tsc --noEmit` โœ… +- [ ] All Phase 1-5 code is committed to `feature/multi-tranche-management` branch +- [ ] Database is initialized with tranche tables +- [ ] Configuration includes tranche-enabled symbols + +## Test Environment Setup + +### 1. Configuration Setup + +Add tranche management settings to your test symbol in `config.user.json`: + +```json +{ + "symbols": { + "BTCUSDT": { + "enableTrancheManagement": true, + "trancheIsolationThreshold": 5, + "maxTranches": 3, + "maxIsolatedTranches": 2, + "trancheStrategy": { + "closingStrategy": "FIFO", + "slTpStrategy": "NEWEST", + "isolationAction": "HOLD" + }, + "allowTrancheWhileIsolated": true, + "trancheAutoCloseIsolated": false + } + }, + "global": { + "paperMode": true + } +} +``` + +### 2. Database Verification + +Check that tranche tables were created: + +```bash +# Open database +sqlite3 liquidations.db + +# Verify tables exist +.tables +# Should show: tranches, tranche_events + +# Check tranche table schema +.schema tranches + +# Check events table schema +.schema tranche_events + +# Exit +.exit +``` + +Expected `tranches` table columns: +- id, symbol, side, positionSide, entryPrice, quantity, marginUsed, leverage +- entryTime, entryOrderId, exitPrice, exitTime, exitOrderId +- unrealizedPnl, realizedPnl, tpPercent, slPercent, tpPrice, slPrice +- status, isolated, isolationTime, isolationPrice, notes + +## Manual Testing Checklist + +### Phase 1: Database Layer Tests + +#### Test 1.1: Database Initialization +- [ ] Start bot: `npm run dev:bot` +- [ ] Verify log: `โœ… Database initialized` +- [ ] Check for tranche table creation logs +- [ ] No database errors in console + +#### Test 1.2: Database CRUD Operations +```bash +# Test creating a tranche record directly +node -e " +const { createTranche } = require('./src/lib/db/trancheDb'); +createTranche({ + id: 'test-uuid-001', + symbol: 'BTCUSDT', + side: 'LONG', + positionSide: 'LONG', + entryPrice: 50000, + quantity: 0.001, + marginUsed: 5, + leverage: 10, + entryTime: Date.now(), + entryOrderId: '123456', + unrealizedPnl: 0, + realizedPnl: 0, + tpPercent: 5, + slPercent: 2, + tpPrice: 52500, + slPrice: 49000, + status: 'active', + isolated: false +}).then(() => console.log('โœ… Tranche created')).catch(e => console.error('โŒ Error:', e)); +" +``` + +Expected: `โœ… Tranche created` + +Verify in database: +```bash +sqlite3 liquidations.db "SELECT * FROM tranches WHERE id='test-uuid-001';" +``` + +### Phase 2: TrancheManager Service Tests + +#### Test 2.1: TrancheManager Initialization +- [ ] Enable tranche management for BTCUSDT in config +- [ ] Start bot: `npm run dev:bot` +- [ ] Look for log: `โœ… Tranche Manager initialized for 1 symbol(s): BTCUSDT` +- [ ] Verify no initialization errors + +#### Test 2.2: Tranche Creation via Manager +```bash +# Create test script +node -e " +const { loadConfig } = require('./src/lib/bot/config'); +const { initializeTrancheManager } = require('./src/lib/services/trancheManager'); + +(async () => { + const config = await loadConfig(); + const tm = initializeTrancheManager(config); + await tm.initialize(); + + const tranche = await tm.createTranche({ + symbol: 'BTCUSDT', + side: 'BUY', + positionSide: 'LONG', + entryPrice: 50000, + quantity: 0.001, + marginUsed: 5, + leverage: 10, + orderId: 'test-order-001' + }); + + console.log('โœ… Tranche created:', tranche.id.substring(0, 8)); + console.log('Entry:', tranche.entryPrice, 'TP:', tranche.tpPrice, 'SL:', tranche.slPrice); +})(); +" +``` + +Expected output: +- `โœ… Tranche created: xxxxxxxx` +- Entry, TP, and SL prices calculated correctly + +#### Test 2.3: Isolation Logic +```bash +# Test isolation threshold calculation +node -e " +const { loadConfig } = require('./src/lib/bot/config'); +const { initializeTrancheManager } = require('./src/lib/services/trancheManager'); + +(async () => { + const config = await loadConfig(); + const tm = initializeTrancheManager(config); + await tm.initialize(); + + // Create tranche at 50000 + const tranche = await tm.createTranche({ + symbol: 'BTCUSDT', + side: 'BUY', + positionSide: 'LONG', + entryPrice: 50000, + quantity: 0.001, + marginUsed: 5, + leverage: 10, + orderId: 'test-order-002' + }); + + console.log('Tranche created at entry:', tranche.entryPrice); + + // Test isolation at 47500 (5% loss) + const shouldIsolate = tm.shouldIsolateTranche(tranche, 47500); + console.log('Should isolate at 47500 (5% loss)?', shouldIsolate); + + // Test at 48000 (4% loss) + const shouldNotIsolate = tm.shouldIsolateTranche(tranche, 48000); + console.log('Should isolate at 48000 (4% loss)?', shouldNotIsolate); +})(); +" +``` + +Expected: +- Should isolate at 47500: `true` โœ… +- Should isolate at 48000: `false` โœ… + +### Phase 3: Hunter Integration Tests + +#### Test 3.1: Pre-Trade Tranche Checks +- [ ] Enable paper mode and tranche management +- [ ] Set `maxTranches: 2` for BTCUSDT +- [ ] Start bot and wait for liquidation opportunities +- [ ] Observe logs for tranche limit checks +- [ ] After 2 tranches created, verify 3rd trade is blocked + +Expected logs: +``` +Hunter: Tranche Limit Reached - BTCUSDT +Hunter: Active tranches (2) >= maxTranches (2) +``` + +#### Test 3.2: Tranche Creation on Order Fill +- [ ] Clear existing tranches from database +- [ ] Start bot with paper mode enabled +- [ ] Wait for a liquidation opportunity and order placement +- [ ] Check logs for: `Hunter: Created tranche xxxxxxxx for BTCUSDT BUY` +- [ ] Verify tranche in database: + +```bash +sqlite3 liquidations.db "SELECT id, symbol, side, entryPrice, quantity, status FROM tranches ORDER BY entryTime DESC LIMIT 1;" +``` + +Expected: New tranche record with correct details + +### Phase 4: PositionManager Integration Tests + +#### Test 4.1: Tranche Closing on SL/TP Fill +This test requires actual positions to be closed. Best tested in paper mode with mock fills: + +- [ ] Create 2 tranches for BTCUSDT LONG (via Hunter) +- [ ] Simulate SL/TP order fill (requires live trading or paper mode simulation) +- [ ] Check logs for: `PositionManager: Processed tranche close for BTCUSDT LONG` +- [ ] Verify tranches marked as closed in database + +```bash +sqlite3 liquidations.db "SELECT id, status, exitPrice, realizedPnl FROM tranches WHERE status='closed' ORDER BY exitTime DESC LIMIT 5;" +``` + +#### Test 4.2: Exchange Synchronization +- [ ] Create 2 tranches manually in database (total quantity 0.002 BTC) +- [ ] Open position on exchange with quantity 0.002 BTC +- [ ] Trigger ACCOUNT_UPDATE event +- [ ] Check logs for: `PositionManager: Synced tranches for BTCUSDT LONG with exchange` +- [ ] Verify sync status in TrancheGroup is 'synced' + +### Phase 5: Real-Time Broadcasting Tests + +#### Test 5.1: WebSocket Tranche Events +- [ ] Start bot: `npm run dev` +- [ ] Open dashboard: http://localhost:3000 +- [ ] Open browser console (F12) +- [ ] Look for WebSocket connection: `ws://localhost:8080` +- [ ] Create a tranche (via liquidation opportunity) +- [ ] Verify WebSocket messages received: + - `tranche_created` with tranche details + - `tranche_pnl_update` with P&L updates + +Expected WebSocket message format: +```json +{ + "type": "tranche_created", + "data": { + "trancheId": "uuid-here", + "symbol": "BTCUSDT", + "side": "LONG", + "entryPrice": 50000, + "quantity": 0.001, + "marginUsed": 5, + "leverage": 10, + "tpPrice": 52500, + "slPrice": 49000, + "timestamp": "2025-10-12T..." + } +} +``` + +#### Test 5.2: Isolation Broadcasting +- [ ] Create tranche at entry price (e.g., 50000) +- [ ] Wait for price to drop >5% OR manually trigger isolation +- [ ] Check browser console for `tranche_isolated` WebSocket event +- [ ] Verify log: `โš ๏ธ Tranche isolated: xxxxxxxx for BTCUSDT (-5.XX% loss)` + +#### Test 5.3: Closing Broadcasting +- [ ] Have active tranche +- [ ] Close position (SL/TP hit or manual close) +- [ ] Check browser console for `tranche_closed` WebSocket event +- [ ] Verify log: `๐Ÿ’ฐ Tranche closed: xxxxxxxx for BTCUSDT (PnL: $X.XX)` + +## Integration Testing Scenarios + +### Scenario 1: Full Lifecycle - Profitable Trade +1. Enable tranche management for BTCUSDT +2. Wait for liquidation opportunity (LONG) +3. Hunter places order โ†’ Tranche created +4. Price moves up 5% โ†’ TP hit +5. PositionManager closes tranche +6. Verify tranche status='closed' with positive realizedPnl + +### Scenario 2: Isolation Flow +1. Create tranche at entry 50000 (LONG) +2. Price drops to 47500 (5% loss) +3. Isolation monitor detects threshold breach +4. Tranche marked as isolated +5. New liquidation opportunity occurs +6. New tranche created (old one still isolated) +7. Price recovers to 51000 +8. Both tranches profitable, close together + +### Scenario 3: Multi-Tranche Position +1. Create 3 tranches for BTCUSDT LONG: + - Tranche 1: Entry 50000, qty 0.001 + - Tranche 2: Entry 49500, qty 0.001 + - Tranche 3: Entry 49000, qty 0.001 +2. Total exchange position: 0.003 BTC +3. Price moves to 52000 +4. Verify all tranches show unrealized profit +5. Close position (SL/TP or manual) +6. Verify FIFO closing: Tranche 1 closes first + +### Scenario 4: Exchange Sync with Drift +1. Create 2 tranches (total 0.002 BTC) +2. Manually close 0.001 BTC on exchange +3. Trigger ACCOUNT_UPDATE +4. Verify sync detects drift (>1%) +5. Check logs for quantity mismatch warning +6. Verify appropriate tranche closed + +## Performance Testing + +### Test 1: Database Performance +```bash +# Insert 100 tranches +for i in {1..100}; do + sqlite3 liquidations.db "INSERT INTO tranches (id, symbol, side, positionSide, entryPrice, quantity, marginUsed, leverage, entryTime, unrealizedPnl, realizedPnl, tpPercent, slPercent, tpPrice, slPrice, status, isolated) VALUES ('test-$i', 'BTCUSDT', 'LONG', 'LONG', 50000, 0.001, 5, 10, $(date +%s)000, 0, 0, 5, 2, 52500, 49000, 'active', 0);" +done + +# Query performance +time sqlite3 liquidations.db "SELECT * FROM tranches WHERE symbol='BTCUSDT' AND status='active';" +``` + +Expected: Query completes in <100ms + +### Test 2: Isolation Monitoring Performance +- [ ] Create 10 active tranches across multiple symbols +- [ ] Start isolation monitoring (10s interval) +- [ ] Monitor CPU usage during checks +- [ ] Verify no performance degradation + +### Test 3: Concurrent Tranche Operations +- [ ] Multiple trades happening simultaneously +- [ ] Verify no race conditions +- [ ] Check database locks handled correctly +- [ ] No duplicate tranches created + +## Error Handling Tests + +### Test 1: TrancheManager Not Initialized +- [ ] Disable tranche management in config +- [ ] Start bot +- [ ] Trigger trade +- [ ] Verify log: `TrancheManager check failed (not initialized?), continuing with trade` +- [ ] Trade completes normally + +### Test 2: Database Error Handling +- [ ] Corrupt database file +- [ ] Start bot +- [ ] Verify error logged but bot continues +- [ ] Database recreated on next start + +### Test 3: Invalid Configuration +- [ ] Set `maxTranches: 0` +- [ ] Start bot +- [ ] Verify validation error or warning +- [ ] Bot uses safe default (3) + +## Success Criteria + +The multi-tranche system passes testing if: +- โœ… All database operations complete without errors +- โœ… Tranches created automatically on order fills +- โœ… Isolation threshold correctly triggers at configured % +- โœ… Exchange synchronization detects and handles drift +- โœ… Position closes respect closing strategy (FIFO/LIFO/etc) +- โœ… WebSocket broadcasts all tranche events to UI +- โœ… No memory leaks or performance degradation +- โœ… Error handling gracefully degrades (continues trading) +- โœ… Database persists tranches across bot restarts +- โœ… All TypeScript compilation passes + +## Known Limitations & Edge Cases + +### Limitations: +1. Exchange only allows one SL/TP per position (handled via strategies) +2. Tranche tracking is local - not visible to exchange +3. Position mode must be HEDGE for best results +4. Requires paper mode for full testing without real funds + +### Edge Cases to Test: +- [ ] Position closed manually on exchange (not via bot) +- [ ] Network interruption during tranche creation +- [ ] Multiple tranches closing simultaneously +- [ ] Isolated tranche never recovers (stays isolated) +- [ ] Max tranches reached, then one closes, then new trade + +## Next Steps After Testing + +Once manual testing is complete: +1. Document any bugs found โ†’ create GitHub issues +2. Proceed to Phase 6: UI Dashboard Components +3. Create automated unit tests for critical paths +4. Prepare for merge to `dev` branch +5. Update user documentation + +## Test Execution Log + +Date: _____________ +Tester: _____________ + +| Test | Status | Notes | +|------|--------|-------| +| Database Init | โฌœ Pass / โฌœ Fail | | +| Tranche Creation | โฌœ Pass / โฌœ Fail | | +| Isolation Logic | โฌœ Pass / โฌœ Fail | | +| Exchange Sync | โฌœ Pass / โฌœ Fail | | +| WebSocket Events | โฌœ Pass / โฌœ Fail | | +| Full Lifecycle | โฌœ Pass / โฌœ Fail | | +| Error Handling | โฌœ Pass / โฌœ Fail | | + +--- + +**Important**: Always test in **paper mode** first before enabling live trading with tranche management! diff --git a/docs/TRANCHE_USER_GUIDE.md b/docs/TRANCHE_USER_GUIDE.md new file mode 100644 index 0000000..c3afd8e --- /dev/null +++ b/docs/TRANCHE_USER_GUIDE.md @@ -0,0 +1,730 @@ +# Multi-Tranche Position Management - User Guide + +## Table of Contents + +1. [Introduction](#introduction) +2. [What Are Tranches?](#what-are-tranches) +3. [Why Use Multi-Tranche Management?](#why-use-multi-tranche-management) +4. [Getting Started](#getting-started) +5. [Configuration Guide](#configuration-guide) +6. [Using the Tranche Dashboard](#using-the-tranche-dashboard) +7. [Trading Strategies](#trading-strategies) +8. [Monitoring & Troubleshooting](#monitoring--troubleshooting) +9. [Best Practices](#best-practices) +10. [FAQ](#faq) + +--- + +## Introduction + +The **Multi-Tranche Position Management System** is an advanced feature that allows the bot to track multiple independent position entries (tranches) within the same trading pair. This enables you to: + +- Isolate losing positions automatically +- Continue trading fresh entries without adding to underwater positions +- Generate consistent profits while bad positions recover +- Maximize margin efficiency and avoid locked capital + +This guide will help you understand, configure, and use the tranche system effectively. + +--- + +## What Are Tranches? + +Think of **tranches** as individual "sub-positions" within the same trading symbol. + +### Traditional Position Management + +Normally, when you trade a symbol multiple times, your positions stack together: + +``` +Entry #1: LONG BTCUSDT @ $50,000 (0.01 BTC) +Entry #2: LONG BTCUSDT @ $49,000 (0.01 BTC) +Combined Position: LONG BTCUSDT @ $49,500 (0.02 BTC) - Average entry +``` + +**Problem:** If the first entry is losing, you can't exit it without closing the entire combined position. + +### Multi-Tranche Management + +With tranches, each entry is tracked separately: + +``` +Tranche #1: LONG BTCUSDT @ $50,000 (0.01 BTC) โ†’ Down 5% โ†’ ISOLATED +Tranche #2: LONG BTCUSDT @ $49,000 (0.01 BTC) โ†’ Up 2% โ†’ CLOSE (+profit) +Tranche #3: LONG BTCUSDT @ $48,500 (0.01 BTC) โ†’ Up 3% โ†’ CLOSE (+profit) + +Exchange sees: One combined position (updated as tranches close) +Bot tracks: Three separate entries with independent P&L +``` + +**Solution:** You can close profitable tranches individually while holding losing tranches for recovery. + +--- + +## Why Use Multi-Tranche Management? + +### Key Benefits + +| Feature | Without Tranches | With Tranches | +|---------|-----------------|---------------| +| **Losing Position** | Must hold entire position or take full loss | Isolate loser, trade fresh entries | +| **Profit Opportunities** | Blocked until position recovers | Continue trading and profiting | +| **Margin Efficiency** | Capital locked in underwater position | Only isolated tranches locked | +| **Risk Management** | All-or-nothing closes | Granular control per entry | +| **Profitability** | Wait for breakeven/profit | Generate profits while holding losers | + +### Real-World Example + +**Scenario:** BTCUSDT liquidation hunting with 5% isolation threshold + +``` +09:00 - Enter LONG @ $50,000 (Tranche #1) +09:15 - Price drops to $47,500 (-5%) + โ†’ Tranche #1 ISOLATED automatically +09:30 - New liquidation spike + โ†’ Enter LONG @ $47,800 (Tranche #2) +09:45 - Price hits $48,700 (+1.8%) + โ†’ Close Tranche #2 for +1.8% profit +10:00 - Another liquidation spike + โ†’ Enter LONG @ $48,200 (Tranche #3) +10:15 - Price hits $49,300 (+2.3%) + โ†’ Close Tranche #3 for +2.3% profit +10:30 - Price recovers to $50,500 + โ†’ Close Tranche #1 for +1% profit + +Result: +5.1% total profit vs -5% loss without tranches +``` + +--- + +## Getting Started + +### Prerequisites + +1. Bot must be installed and running +2. Access to web dashboard at `http://localhost:3000` +3. At least one symbol configured in your config +4. Understanding of basic trading concepts (leverage, SL/TP) + +### Quick Setup (5 Minutes) + +1. **Enable Tranches:** + - Open http://localhost:3000/config + - Select your trading symbol (e.g., BTCUSDT) + - Find "Tranche Management Settings" + - Toggle **"Enable Multi-Tranche Management"** to ON + +2. **Start with Defaults:** + - Isolation Threshold: 5% + - Max Tranches: 3 + - Max Isolated: 2 + - Closing Strategy: FIFO (First In, First Out) + +3. **Test in Paper Mode:** + - Ensure "Paper Mode" is enabled + - Monitor the `/tranches` dashboard + - Watch how tranches are created and isolated + +4. **Go Live (When Ready):** + - Disable paper mode + - Start with small position sizes + - Monitor closely for the first few trades + +--- + +## Configuration Guide + +### Access Configuration + +**Via Web UI:** +1. Navigate to http://localhost:3000/config +2. Select your symbol from the list +3. Scroll to "Tranche Management Settings" + +### Core Settings + +#### 1. Enable Multi-Tranche Management +- **Type:** Toggle (ON/OFF) +- **Default:** OFF +- **Description:** Master switch for tranche system +- **Recommendation:** Start OFF in paper mode, enable after testing + +#### 2. Isolation Threshold +- **Type:** Percentage (0-100%) +- **Default:** 5% +- **Description:** Unrealized loss % that triggers automatic isolation +- **Examples:** + - **3%**: Aggressive isolation (more tranches, quicker isolation) + - **5%**: Balanced (recommended for most strategies) + - **10%**: Conservative (fewer isolations, higher tolerance) +- **Formula:** `(currentPrice - entryPrice) / entryPrice * 100` + +#### 3. Max Tranches +- **Type:** Number (1-10) +- **Default:** 3 +- **Description:** Maximum active tranches per symbol/side +- **Recommendations:** + - **1-2**: Conservative, minimal complexity + - **3-5**: Balanced, good for most strategies + - **6+**: Aggressive, requires more monitoring + +#### 4. Max Isolated Tranches +- **Type:** Number (1-10) +- **Default:** 2 +- **Description:** Max underwater tranches before blocking new trades +- **Safety:** Prevents accumulating too many losing positions +- **Formula:** `max_isolated = max_tranches - 1` (keep at least 1 slot for profitable trading) + +#### 5. Allow Tranche While Isolated +- **Type:** Toggle (ON/OFF) +- **Default:** ON +- **Description:** Allow new tranches even when some are isolated +- **Use Cases:** + - **ON**: Continue trading despite isolated tranches (recommended) + - **OFF**: Block all new trades until isolated tranches close + +### Strategy Settings + +The tranche system uses optimized strategies that are hardcoded for best performance: + +#### 1. Closing Strategy: LIFO (Last In, First Out) +**Automatically configured** - closes newest tranches first. + +**Why LIFO?** +- Perfect for liquidation hunting strategies +- Quick profit-taking on recent entries +- Keeps older positions for potential recovery +- Minimizes complexity + +**Example:** +``` +Tranches: +#1: LONG @ $50,000 โ†’ -5% (oldest, underwater) +#2: LONG @ $48,000 โ†’ +2% (middle, profitable) +#3: LONG @ $49,000 โ†’ +1% (newest, profitable) + +SL/TP triggers โ†’ LIFO closes #3 first, then #2, then #1 +``` + +#### 2. Best Entry Tracking +The bot tracks which tranche has the most favorable entry price: +- **For LONG positions:** Lowest entry price +- **For SHORT positions:** Highest entry price + +This is used for display purposes and P&L tracking to help you understand your best positions. + +#### 3. Isolation Action +Determines what happens when a tranche is isolated. + +| Action | Description | Status | +|--------|-------------|--------| +| **HOLD** | Keep position, wait for recovery | โœ… Implemented | +| **REDUCE_LEVERAGE** | Lower leverage to reduce risk | ๐Ÿ”œ Future | +| **PARTIAL_CLOSE** | Close portion to reduce exposure | ๐Ÿ”œ Future | + +**Currently:** Only HOLD is implemented. Future versions will add dynamic risk management. + +--- + +## Using the Tranche Dashboard + +Access the dashboard at **http://localhost:3000/tranches** + +### Dashboard Overview + +The tranche dashboard provides real-time visibility into all your tranches: + +1. **Symbol Selector** + - Choose which symbol to view + - Select side (LONG/SHORT) + - Auto-refreshes every 5 seconds + +2. **Summary Metrics** + - Total Active Tranches + - Total Isolated Tranches + - Total Closed Tranches + - Combined Unrealized P&L + - Combined Realized P&L + +3. **Tranche Breakdown Tab** + - **Active Tranches:** Currently open positions + - **Isolated Tranches:** Underwater positions (>threshold) + - **Closed Tranches:** Historical completed trades + - Color-coded status indicators + +4. **Event Timeline Tab** + - Real-time event stream + - Tranche creation notifications + - Isolation events + - Close events with P&L + - Sync updates from exchange + +### Reading Tranche Cards + +Each tranche displays: + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Tranche #abc123 | LONG โ”‚ +โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ +โ”‚ Entry: $50,000.00 | Time: 10:30:15 AM โ”‚ +โ”‚ Quantity: 0.01 BTC | Margin: $100 USDT โ”‚ +โ”‚ Leverage: 10x | Unrealized P&L: -$5.00โ”‚ +โ”‚ TP: $50,500 (1%) | SL: $49,000 (2%) โ”‚ +โ”‚ Status: ๐Ÿ”ด ISOLATED โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**Status Colors:** +- ๐ŸŸข **GREEN**: Active (profitable or within threshold) +- ๐Ÿ”ด **RED**: Isolated (underwater > threshold) +- โšซ **GRAY**: Closed (historical) + +### Timeline Events + +Events appear in real-time and show: +- โœ… **Tranche Created**: New entry opened +- โš ๏ธ **Tranche Isolated**: Position went underwater +- ๐Ÿ’ฐ **Tranche Closed**: Exit with P&L +- ๐Ÿ”„ **Exchange Sync**: Reconciliation with exchange +- ๐Ÿ“Š **P&L Update**: Unrealized P&L changed + +--- + +## Trading Strategies + +The tranche system automatically uses **LIFO closing** for all strategies. Configure these parameters to match your trading style: + +### Strategy 1: Aggressive Scalping + +**Goal:** Fast in-and-out trades with minimal isolation time + +**Configuration:** +```json +{ + "trancheIsolationThreshold": 3, + "maxTranches": 5, + "maxIsolatedTranches": 2, + "allowTrancheWhileIsolated": true +} +``` + +**Characteristics:** +- Low 3% isolation threshold โ†’ quick isolation +- High max tranches (5) โ†’ more opportunities +- LIFO automatically takes profits on newest entries +- Good for high-volatility, liquid pairs + +**Pros:** Maximum trading frequency, quick profit generation +**Cons:** More isolated tranches, requires active monitoring + +--- + +### Strategy 2: Hold & Recover + +**Goal:** Hold losing positions long-term while scalping profits + +**Configuration:** +```json +{ + "trancheIsolationThreshold": 10, + "maxTranches": 3, + "maxIsolatedTranches": 2, + "allowTrancheWhileIsolated": true +} +``` + +**Characteristics:** +- High 10% isolation threshold โ†’ rare isolation +- Moderate max tranches (3) โ†’ balanced +- LIFO lets profitable new entries close first +- Good for trending, less volatile pairs + +**Pros:** Fewer isolations, simpler management +**Cons:** Takes longer to recover underwater positions + +--- + +### Strategy 3: Balanced Approach + +**Goal:** Balance between quick profits and position recovery + +**Configuration:** +```json +{ + "trancheIsolationThreshold": 5, + "maxTranches": 4, + "maxIsolatedTranches": 2, + "allowTrancheWhileIsolated": true +} +``` + +**Characteristics:** +- Balanced 5% isolation threshold +- Moderate max tranches (4) +- LIFO closes newest (often most profitable) +- Good for mixed market conditions + +**Pros:** Good balance of profit-taking and recovery +**Cons:** Middle-ground complexity + +--- + +### Strategy 4: Conservative Risk Management + +**Goal:** Minimal complexity, tight risk control + +**Configuration:** +```json +{ + "trancheIsolationThreshold": 7, + "maxTranches": 2, + "maxIsolatedTranches": 1, + "allowTrancheWhileIsolated": false +} +``` + +**Characteristics:** +- Moderate 7% isolation threshold +- Low max tranches (2) โ†’ simple tracking +- Block new trades when isolated โ†’ no compounding losses +- LIFO minimizes exposure time + +**Pros:** Simple, controlled risk +**Cons:** Fewer trading opportunities + +--- + +## Monitoring & Troubleshooting + +### Normal Operation Indicators + +โœ… **Healthy Tranche System:** +- Active tranches cycling (opening/closing regularly) +- Isolated tranches recovering over time +- Positive net realized P&L trend +- Dashboard updates every 5 seconds +- Timeline shows regular events + +### Warning Signs + +โš ๏ธ **Potential Issues:** +- Max isolated tranches reached frequently +- Tranches not closing for extended periods +- Large negative unrealized P&L building up +- Sync status showing "drift" or "conflict" +- No new tranches being created + +### Common Issues & Solutions + +#### Issue 1: Too Many Isolated Tranches + +**Symptom:** Max isolated limit reached, new trades blocked + +**Causes:** +- Isolation threshold too low +- Market moving strongly against positions +- Max tranches set too high + +**Solutions:** +1. Increase isolation threshold (5% โ†’ 7% or 10%) +2. Reduce max tranches (5 โ†’ 3) +3. Wait for market recovery +4. Manually close worst tranches via exchange + +--- + +#### Issue 2: Tranches Not Being Created + +**Symptom:** No new tranches appearing despite liquidation signals + +**Causes:** +- `enableTrancheManagement` not enabled +- Max tranches limit reached +- Max isolated tranches blocking new entries +- TrancheManager initialization failed + +**Solutions:** +1. Check config UI: Tranche Management toggle ON +2. View current tranche count in dashboard +3. Check bot console for TrancheManager errors +4. Restart bot if initialization failed + +--- + +#### Issue 3: Sync Drift Detected + +**Symptom:** Timeline shows "Exchange sync drift detected" + +**Causes:** +- Manual trades made outside bot +- Partial fills not tracked correctly +- Database/memory state mismatch + +**Solutions:** +1. Let TrancheManager auto-reconcile (happens automatically) +2. Check exchange position size matches tranche totals +3. If persistent, restart bot to re-sync from exchange + +--- + +#### Issue 4: Unrealized P&L Not Updating + +**Symptom:** P&L values frozen or stale + +**Causes:** +- WebSocket connection lost +- Price service not updating +- Dashboard auto-refresh stopped + +**Solutions:** +1. Check WebSocket connection status (top of timeline tab) +2. Refresh browser page +3. Check bot console for WebSocket errors +4. Verify `priceService` is running + +--- + +### Logs to Check + +**Bot Console:** +``` +TrancheManager: Created tranche [ID] for BTCUSDT LONG +TrancheManager: Isolated tranche [ID] (P&L: -5.2%) +TrancheManager: Closed tranche [ID] with P&L: $12.50 +``` + +**Database Queries:** +```sql +-- View all active tranches +SELECT * FROM tranches WHERE status = 'active'; + +-- View isolated tranches +SELECT * FROM tranches WHERE isolated = 1; + +-- View tranche events (audit trail) +SELECT * FROM tranche_events ORDER BY event_time DESC LIMIT 20; +``` + +--- + +## Best Practices + +### 1. Start in Paper Mode +- Enable tranches in paper mode first +- Monitor for at least 24 hours +- Understand how isolation/closing works +- Adjust settings based on simulated results + +### 2. Conservative Initial Settings +```json +{ + "trancheIsolationThreshold": 5, // Balanced threshold + "maxTranches": 3, // Moderate complexity + "maxIsolatedTranches": 2, // Safety buffer + "allowTrancheWhileIsolated": true // Continue trading +} +``` +Note: LIFO closing and best entry tracking are automatically configured. + +### 3. Monitor Regularly +- Check `/tranches` dashboard daily +- Review timeline events for patterns +- Watch for repeated isolations (adjust threshold) +- Track realized P&L trends + +### 4. Adjust Based on Market Conditions + +**Trending Market (Strong Direction):** +- Increase isolation threshold (7-10%) +- Use FIFO closing (ride trend) +- Higher max tranches (4-5) + +**Choppy Market (Range-Bound):** +- Decrease isolation threshold (3-5%) +- Use LIFO closing (quick exits) +- Moderate max tranches (3-4) + +**High Volatility:** +- Increase isolation threshold (8-12%) +- Reduce max tranches (2-3) +- Use WORST_FIRST closing (cut losses) + +### 5. Risk Management Rules + +**Position Sizing:** +- Each tranche should be manageable in isolation +- Total margin across all tranches โ‰ค max position margin +- Don't overleverage individual tranches + +**Isolation Management:** +- Don't let isolated tranches exceed 50% of total margin +- If >2 tranches isolated, reduce new trade frequency +- Consider manual intervention if isolation persists >24h + +**Leverage Control:** +- Lower leverage (5-10x) when using tranches +- Higher leverage increases isolation risk +- Balance between profit potential and safety + +### 6. Testing New Strategies + +Before deploying a new tranche strategy: + +1. **Backtest (Manual):** + - Review historical data + - Estimate isolation frequency + - Calculate expected P&L + +2. **Paper Trade (1-2 weeks):** + - Enable in paper mode + - Monitor actual isolation rate + - Adjust settings as needed + +3. **Small Live Test (1 week):** + - Start with minimal position sizes + - One symbol only + - Monitor closely + +4. **Full Deployment:** + - Increase position sizes gradually + - Add more symbols one at a time + - Maintain monitoring routine + +--- + +## FAQ + +### General Questions + +**Q: Do I need special API permissions for tranches?** +A: No, tranches are tracked locally by the bot. Standard trading API permissions are sufficient. + +**Q: Will tranches work with paper mode?** +A: Yes! Paper mode fully supports tranches with simulated fills and P&L. + +**Q: Can I use tranches on multiple symbols simultaneously?** +A: Yes, each symbol has independent tranche tracking and configuration. + +**Q: What happens if the bot restarts?** +A: Tranches are persisted in the SQLite database and automatically reloaded on startup. + +--- + +### Configuration Questions + +**Q: What's the best isolation threshold?** +A: Start with 5%. Adjust based on your risk tolerance and market volatility: +- Aggressive: 3% +- Balanced: 5-7% +- Conservative: 10%+ + +**Q: How many max tranches should I allow?** +A: Recommended: 3-5 for most strategies. More tranches = more complexity and monitoring. + +**Q: Should I allow tranches while isolated?** +A: Generally YES. This lets you keep trading while bad positions recover. Set to NO if you want stricter risk control. + +**Q: Can I change the closing strategy?** +A: The closing strategy is automatically set to LIFO (Last In, First Out), which is optimal for liquidation hunting. LIFO closes newest tranches first, allowing quick profit-taking while letting older positions recover. This is hardcoded for simplicity and best performance. + +--- + +### Technical Questions + +**Q: How does the bot track tranches vs exchange positions?** +A: The bot maintains a local "virtual" tracking layer while the exchange sees one combined position. The bot reconciles differences automatically. + +**Q: What if I manually close a position on the exchange?** +A: TrancheManager detects the close and reconciles local tranches accordingly. Check timeline for sync events. + +**Q: Can I manually close a specific tranche?** +A: Not directly. The bot's closing strategy determines which tranches close. You can close the entire exchange position manually if needed. + +**Q: What happens if quantities drift (bot vs exchange)?** +A: TrancheManager auto-syncs every 10 seconds and detects drift >1%. It creates recovery tranches or adjusts existing ones as needed. + +--- + +### Troubleshooting Questions + +**Q: My tranches aren't being created. Why?** +A: Check: +1. Is `enableTrancheManagement` enabled in config? +2. Have you reached max tranches limit? +3. Are too many tranches isolated (blocking new entries)? +4. Check bot console for TrancheManager errors + +**Q: Why is my P&L not updating?** +A: Check: +1. WebSocket connection status (timeline tab) +2. Refresh browser page +3. Verify bot is running and connected to exchange + +**Q: What does "sync drift" mean?** +A: Exchange position quantity doesn't match sum of local tranches (>1% difference). Usually auto-reconciles within 10 seconds. + +**Q: Can I delete old closed tranches?** +A: Yes, closed tranches are automatically cleaned up after a configurable retention period. You can also manually delete from database: +```sql +DELETE FROM tranches WHERE status = 'closed' AND exit_time < [timestamp]; +``` + +--- + +### Advanced Questions + +**Q: Can I implement custom closing strategies?** +A: Yes, modify `selectTranchesToClose()` in `src/lib/services/trancheManager.ts`. Requires TypeScript knowledge. + +**Q: How do I export tranche data for analysis?** +A: Query the database: +```sql +SELECT * FROM tranches WHERE symbol = 'BTCUSDT' ORDER BY entry_time DESC; +``` +Or use the `/api/tranches` API endpoint. + +**Q: Can I disable tranches for specific symbols only?** +A: Yes, set `enableTrancheManagement: false` for that symbol in config. Other symbols remain unaffected. + +**Q: Does the tranche system support hedging mode?** +A: Yes, tranches work with both ONE_WAY and HEDGE position modes. In HEDGE mode, LONG and SHORT sides have independent tranche tracking. + +--- + +## Support & Resources + +### Documentation +- **Implementation Plan:** `docs/TRANCHE_IMPLEMENTATION_PLAN.md` +- **Testing Guide:** `docs/TRANCHE_TESTING.md` +- **Technical Docs:** `CLAUDE.md` (Multi-Tranche section) + +### Community +- **Discord:** [Join Server](https://discord.gg/P8Ev3Up) +- **GitHub Issues:** [Report Problems](https://github.com/CryptoGnome/aster_lick_hunter_node/issues) + +### Code References +- **TrancheManager:** `src/lib/services/trancheManager.ts` +- **Database Layer:** `src/lib/db/trancheDb.ts` +- **UI Dashboard:** `src/app/tranches/page.tsx` +- **Types:** `src/lib/types.ts` (Tranche interfaces) + +--- + +## Conclusion + +The multi-tranche system is a powerful tool for managing complex trading scenarios. By isolating losing positions and continuing to trade fresh entries, you can: + +โœ… Generate consistent profits even when some positions are underwater +โœ… Maximize margin efficiency and capital utilization +โœ… Maintain trading velocity without adding to losers +โœ… Implement sophisticated strategies with granular control + +**Remember:** +- Start in paper mode +- Use conservative settings initially +- Monitor regularly via `/tranches` dashboard +- Adjust based on market conditions +- Test new strategies thoroughly before deployment + +Happy trading! ๐Ÿš€ diff --git a/src/app/api/paper-mode/positions/route.ts b/src/app/api/paper-mode/positions/route.ts new file mode 100644 index 0000000..44dc7a9 --- /dev/null +++ b/src/app/api/paper-mode/positions/route.ts @@ -0,0 +1,53 @@ +import { NextResponse } from 'next/server'; +import { paperModeSimulator } from '@/lib/services/paperModeSimulator'; +import { loadConfig } from '@/lib/bot/config'; + +/** + * GET /api/paper-mode/positions + * + * Returns all active paper mode positions + */ +export async function GET() { + try { + const config = await loadConfig(); + + // Only return positions if in paper mode + if (!config.global.paperMode) { + return NextResponse.json({ + positions: [], + paperMode: false, + message: 'Not in paper mode' + }); + } + + const positions = paperModeSimulator.getPositions(); + + return NextResponse.json({ + positions: positions.map(pos => ({ + symbol: pos.symbol, + side: pos.side, + quantity: pos.quantity, + entryPrice: pos.entryPrice, + markPrice: pos.lastMarkPrice, + slPrice: pos.slPrice, + tpPrice: pos.tpPrice, + leverage: pos.leverage, + pnlPercent: pos.lastPnL, + openTime: pos.openTime, + unrealizedPnl: (pos.lastPnL / 100) * pos.quantity * pos.entryPrice * pos.leverage, + })), + paperMode: true, + count: positions.length + }); + } catch (error: any) { + console.error('Error fetching paper mode positions:', error); + return NextResponse.json( + { + error: `Failed to fetch paper mode positions: ${error.message}`, + positions: [], + paperMode: true + }, + { status: 500 } + ); + } +} diff --git a/src/app/api/tranches/route.ts b/src/app/api/tranches/route.ts new file mode 100644 index 0000000..5d0d339 --- /dev/null +++ b/src/app/api/tranches/route.ts @@ -0,0 +1,89 @@ +import { NextResponse } from 'next/server'; +import { getAllTranchesForSymbol, getActiveTranches, getIsolatedTranches } from '@/lib/db/trancheDb'; + +/** + * GET /api/tranches - Fetch tranche data + * Query params: + * - symbol: Filter by symbol (optional) + * - side: Filter by side (optional) + * - status: 'active', 'isolated', 'all' (default: 'all') + */ +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const symbol = searchParams.get('symbol'); + const side = searchParams.get('side'); + const status = searchParams.get('status') || 'all'; + + let tranches = []; + + if (symbol && side) { + // Fetch specific symbol and side + if (status === 'active') { + const activeTranches = await getActiveTranches(symbol, side); + tranches = activeTranches.filter(t => !t.isolated); + } else if (status === 'isolated') { + tranches = await getIsolatedTranches(symbol, side); + } else { + tranches = await getAllTranchesForSymbol(symbol); + tranches = tranches.filter(t => t.side === side); + } + } else if (symbol) { + // Fetch all sides for symbol + tranches = await getAllTranchesForSymbol(symbol); + + if (status === 'active') { + tranches = tranches.filter(t => t.status === 'active' && !t.isolated); + } else if (status === 'isolated') { + tranches = tranches.filter(t => t.isolated); + } + } else { + // Return error - need at least symbol + return NextResponse.json( + { error: 'Symbol parameter is required' }, + { status: 400 } + ); + } + + // Calculate aggregated metrics + const activeTranches = tranches.filter(t => t.status === 'active' && !t.isolated); + const isolatedTranches = tranches.filter(t => t.isolated); + const closedTranches = tranches.filter(t => t.status === 'closed'); + + const totalQuantity = activeTranches.reduce((sum, t) => sum + t.quantity, 0); + const totalMarginUsed = activeTranches.reduce((sum, t) => sum + t.marginUsed, 0); + const totalUnrealizedPnl = activeTranches.reduce((sum, t) => sum + t.unrealizedPnl, 0); + const totalRealizedPnl = closedTranches.reduce((sum, t) => sum + t.realizedPnl, 0); + + // Calculate weighted average entry + let weightedAvgEntry = 0; + if (totalQuantity > 0) { + const weightedSum = activeTranches.reduce( + (sum, t) => sum + t.entryPrice * t.quantity, + 0 + ); + weightedAvgEntry = weightedSum / totalQuantity; + } + + return NextResponse.json({ + tranches, + metrics: { + total: tranches.length, + active: activeTranches.length, + isolated: isolatedTranches.length, + closed: closedTranches.length, + totalQuantity, + totalMarginUsed, + totalUnrealizedPnl, + totalRealizedPnl, + weightedAvgEntry, + }, + }); + } catch (error: any) { + console.error('Error fetching tranches:', error); + return NextResponse.json( + { error: 'Failed to fetch tranches', details: error.message }, + { status: 500 } + ); + } +} diff --git a/src/components/ShareConfigModal.tsx b/src/components/ShareConfigModal.tsx new file mode 100644 index 0000000..361d2bb --- /dev/null +++ b/src/components/ShareConfigModal.tsx @@ -0,0 +1,236 @@ +'use client'; + +import React, { useRef } from 'react'; +import { Download, X } from 'lucide-react'; +import { toPng } from 'html-to-image'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { toast } from 'sonner'; +import type { Config } from '@/lib/config/types'; + +interface ShareConfigModalProps { + isOpen: boolean; + onClose: () => void; + config: Config; +} + +export default function ShareConfigModal({ isOpen, onClose, config }: ShareConfigModalProps) { + const contentRef = useRef(null); + + const handleExport = async () => { + if (!contentRef.current) return; + + try { + toast.info('Generating screenshot...'); + + const dataUrl = await toPng(contentRef.current, { + quality: 1.0, + pixelRatio: 2, + backgroundColor: '#ffffff', + }); + + const link = document.createElement('a'); + link.download = `aster-config-${new Date().toISOString().split('T')[0]}.png`; + link.href = dataUrl; + link.click(); + + toast.success('Configuration exported successfully!'); + } catch (error) { + console.error('Failed to export configuration:', error); + toast.error('Failed to export configuration'); + } + }; + + const symbols = Object.entries(config.symbols); + + return ( + + +
+ + Share Configuration + +
+ + +
+
+ +
+ {symbols.map(([symbol, symbolConfig]) => ( +
+ {/* Symbol Header */} +
+

{symbol}

+ + {symbolConfig.leverage}x + + + {symbolConfig.orderType || 'LIMIT'} + +
+ + {/* Settings Grid */} +
+
+ {/* Volume Thresholds */} +
+ Long Vol: + + ${(symbolConfig.longVolumeThresholdUSDT || symbolConfig.volumeThresholdUSDT || 0).toLocaleString()} + +
+
+ Short Vol: + + ${(symbolConfig.shortVolumeThresholdUSDT || symbolConfig.volumeThresholdUSDT || 0).toLocaleString()} + +
+ + {/* Position Sizing */} +
+ Base Size: + + {symbolConfig.tradeSize} + +
+ {symbolConfig.longTradeSize !== undefined && ( +
+ Long Size: + + ${symbolConfig.longTradeSize} + +
+ )} + {symbolConfig.shortTradeSize !== undefined && ( +
+ Short Size: + + ${symbolConfig.shortTradeSize} + +
+ )} + {symbolConfig.maxPositionMarginUSDT !== undefined && ( +
+ Max Margin: + + ${symbolConfig.maxPositionMarginUSDT} + +
+ )} + + {/* Risk Parameters */} +
+ Take Profit: + + {symbolConfig.tpPercent}% + +
+
+ Stop Loss: + + {symbolConfig.slPercent}% + +
+ + {/* Order Settings */} + {symbolConfig.priceOffsetBps !== undefined && ( +
+ Price Offset: + + {symbolConfig.priceOffsetBps} bps + +
+ )} + {symbolConfig.maxSlippageBps !== undefined && ( +
+ Max Slippage: + + {symbolConfig.maxSlippageBps} bps + +
+ )} + {symbolConfig.usePostOnly !== undefined && ( +
+ Post-Only: + + {symbolConfig.usePostOnly ? 'Yes' : 'No'} + +
+ )} + {symbolConfig.forceMarketEntry !== undefined && ( +
+ Force Market: + + {symbolConfig.forceMarketEntry ? 'Yes' : 'No'} + +
+ )} + + {/* VWAP Protection */} +
+ VWAP: + + {symbolConfig.vwapProtection ? 'On' : 'Off'} + +
+ {symbolConfig.vwapProtection && ( + <> +
+ Timeframe: + + {symbolConfig.vwapTimeframe || '5m'} + +
+
+ Lookback: + + {symbolConfig.vwapLookback || 200} + +
+ + )} + + {/* Threshold System */} +
+ Threshold: + + {symbolConfig.useThreshold ? 'On' : 'Off'} + +
+ {symbolConfig.useThreshold && ( + <> +
+ Window: + + {((symbolConfig.thresholdTimeWindow || 60000) / 1000).toFixed(0)}s + +
+
+ Cooldown: + + {((symbolConfig.thresholdCooldown || 30000) / 1000).toFixed(0)}s + +
+ + )} +
+
+
+ ))} +
+
+
+ ); +} diff --git a/src/lib/db/trancheDb.ts b/src/lib/db/trancheDb.ts new file mode 100644 index 0000000..f9623ae --- /dev/null +++ b/src/lib/db/trancheDb.ts @@ -0,0 +1,457 @@ +import { db } from './database'; +import { Tranche, TrancheEvent } from '../types'; + +// Initialize tranche tables +export async function initTrancheTables(): Promise { + // Tranches table + await db.run(` + CREATE TABLE IF NOT EXISTS tranches ( + -- Identity + id TEXT PRIMARY KEY, + symbol TEXT NOT NULL, + side TEXT NOT NULL, + position_side TEXT NOT NULL, + + -- Entry details + entry_price REAL NOT NULL, + quantity REAL NOT NULL, + margin_used REAL NOT NULL, + leverage INTEGER NOT NULL, + entry_time INTEGER NOT NULL, + entry_order_id TEXT, + + -- Exit details + exit_price REAL, + exit_time INTEGER, + exit_order_id TEXT, + + -- P&L tracking + unrealized_pnl REAL DEFAULT 0, + realized_pnl REAL DEFAULT 0, + + -- Risk management + tp_percent REAL NOT NULL, + sl_percent REAL NOT NULL, + tp_price REAL NOT NULL, + sl_price REAL NOT NULL, + + -- Status + status TEXT DEFAULT 'active', + isolated INTEGER DEFAULT 0, + isolation_time INTEGER, + isolation_price REAL, + + -- Metadata + notes TEXT, + created_at INTEGER DEFAULT (strftime('%s', 'now')), + updated_at INTEGER DEFAULT (strftime('%s', 'now')) + ) + `); + + // Indexes for performance + await db.run(` + CREATE INDEX IF NOT EXISTS idx_tranches_symbol_side_status + ON tranches(symbol, side, status) + `); + + await db.run(` + CREATE INDEX IF NOT EXISTS idx_tranches_status + ON tranches(status) + `); + + await db.run(` + CREATE INDEX IF NOT EXISTS idx_tranches_entry_time + ON tranches(entry_time DESC) + `); + + await db.run(` + CREATE INDEX IF NOT EXISTS idx_tranches_isolated + ON tranches(isolated, status) + `); + + // Tranche events table (audit trail) + await db.run(` + CREATE TABLE IF NOT EXISTS tranche_events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + tranche_id TEXT NOT NULL, + event_type TEXT NOT NULL, + event_time INTEGER NOT NULL, + + -- Event details + price REAL, + quantity REAL, + pnl REAL, + + -- Context + trigger TEXT, + metadata TEXT, + + FOREIGN KEY (tranche_id) REFERENCES tranches(id) + ) + `); + + await db.run(` + CREATE INDEX IF NOT EXISTS idx_tranche_events_tranche_id + ON tranche_events(tranche_id) + `); + + await db.run(` + CREATE INDEX IF NOT EXISTS idx_tranche_events_time + ON tranche_events(event_time DESC) + `); +} + +// Helper to convert DB row to Tranche object +function rowToTranche(row: any): Tranche { + return { + id: row.id, + symbol: row.symbol, + side: row.side as 'LONG' | 'SHORT', + positionSide: row.position_side as 'LONG' | 'SHORT' | 'BOTH', + entryPrice: row.entry_price, + quantity: row.quantity, + marginUsed: row.margin_used, + leverage: row.leverage, + entryTime: row.entry_time, + entryOrderId: row.entry_order_id || undefined, + exitPrice: row.exit_price || undefined, + exitTime: row.exit_time || undefined, + exitOrderId: row.exit_order_id || undefined, + unrealizedPnl: row.unrealized_pnl, + realizedPnl: row.realized_pnl, + tpPercent: row.tp_percent, + slPercent: row.sl_percent, + tpPrice: row.tp_price, + slPrice: row.sl_price, + status: row.status as 'active' | 'closed' | 'liquidated', + isolated: Boolean(row.isolated), + isolationTime: row.isolation_time || undefined, + isolationPrice: row.isolation_price || undefined, + notes: row.notes || undefined, + }; +} + +// Create a new tranche +export async function createTranche(tranche: Tranche): Promise { + await db.run( + ` + INSERT INTO tranches ( + id, symbol, side, position_side, + entry_price, quantity, margin_used, leverage, entry_time, entry_order_id, + exit_price, exit_time, exit_order_id, + unrealized_pnl, realized_pnl, + tp_percent, sl_percent, tp_price, sl_price, + status, isolated, isolation_time, isolation_price, + notes + ) VALUES ( + ?, ?, ?, ?, + ?, ?, ?, ?, ?, ?, + ?, ?, ?, + ?, ?, + ?, ?, ?, ?, + ?, ?, ?, ?, + ? + ) + `, + [ + tranche.id, + tranche.symbol, + tranche.side, + tranche.positionSide, + tranche.entryPrice, + tranche.quantity, + tranche.marginUsed, + tranche.leverage, + tranche.entryTime, + tranche.entryOrderId || null, + tranche.exitPrice || null, + tranche.exitTime || null, + tranche.exitOrderId || null, + tranche.unrealizedPnl, + tranche.realizedPnl, + tranche.tpPercent, + tranche.slPercent, + tranche.tpPrice, + tranche.slPrice, + tranche.status, + tranche.isolated ? 1 : 0, + tranche.isolationTime || null, + tranche.isolationPrice || null, + tranche.notes || null, + ] + ); +} + +// Get a single tranche by ID +export async function getTranche(id: string): Promise { + const row = await db.get('SELECT * FROM tranches WHERE id = ?', [id]); + return row ? rowToTranche(row) : null; +} + +// Get all active tranches for a symbol and side +export async function getActiveTranches(symbol: string, side: string): Promise { + const rows = await db.all( + ` + SELECT * FROM tranches + WHERE symbol = ? AND side = ? AND status = 'active' + ORDER BY entry_time ASC + `, + [symbol, side] + ); + + return rows.map(rowToTranche); +} + +// Get all isolated tranches for a symbol and side +export async function getIsolatedTranches(symbol: string, side: string): Promise { + const rows = await db.all( + ` + SELECT * FROM tranches + WHERE symbol = ? AND side = ? AND status = 'active' AND isolated = 1 + ORDER BY isolation_time ASC + `, + [symbol, side] + ); + + return rows.map(rowToTranche); +} + +// Get all tranches (active and closed) for a symbol +export async function getAllTranchesForSymbol(symbol: string): Promise { + const rows = await db.all( + ` + SELECT * FROM tranches + WHERE symbol = ? + ORDER BY entry_time DESC + `, + [symbol] + ); + + return rows.map(rowToTranche); +} + +// Update a tranche +export async function updateTranche(id: string, updates: Partial): Promise { + const fields: string[] = []; + const values: any[] = []; + + // Build dynamic UPDATE statement + if (updates.quantity !== undefined) { + fields.push('quantity = ?'); + values.push(updates.quantity); + } + if (updates.marginUsed !== undefined) { + fields.push('margin_used = ?'); + values.push(updates.marginUsed); + } + if (updates.unrealizedPnl !== undefined) { + fields.push('unrealized_pnl = ?'); + values.push(updates.unrealizedPnl); + } + if (updates.realizedPnl !== undefined) { + fields.push('realized_pnl = ?'); + values.push(updates.realizedPnl); + } + if (updates.exitPrice !== undefined) { + fields.push('exit_price = ?'); + values.push(updates.exitPrice); + } + if (updates.exitTime !== undefined) { + fields.push('exit_time = ?'); + values.push(updates.exitTime); + } + if (updates.exitOrderId !== undefined) { + fields.push('exit_order_id = ?'); + values.push(updates.exitOrderId); + } + if (updates.status !== undefined) { + fields.push('status = ?'); + values.push(updates.status); + } + if (updates.isolated !== undefined) { + fields.push('isolated = ?'); + values.push(updates.isolated ? 1 : 0); + } + if (updates.isolationTime !== undefined) { + fields.push('isolation_time = ?'); + values.push(updates.isolationTime); + } + if (updates.isolationPrice !== undefined) { + fields.push('isolation_price = ?'); + values.push(updates.isolationPrice); + } + if (updates.notes !== undefined) { + fields.push('notes = ?'); + values.push(updates.notes); + } + + if (fields.length === 0) return; // No updates + + // Always update timestamp + fields.push('updated_at = strftime("%s", "now")'); + + values.push(id); // Add ID for WHERE clause + + const sql = `UPDATE tranches SET ${fields.join(', ')} WHERE id = ?`; + await db.run(sql, values); +} + +// Update unrealized P&L for a tranche (fast path for frequent updates) +export async function updateTrancheUnrealizedPnl(id: string, pnl: number): Promise { + await db.run( + ` + UPDATE tranches + SET unrealized_pnl = ?, updated_at = strftime('%s', 'now') + WHERE id = ? + `, + [pnl, id] + ); +} + +// Isolate a tranche +export async function isolateTranche(id: string, price: number): Promise { + await db.run( + ` + UPDATE tranches + SET isolated = 1, isolation_time = ?, isolation_price = ?, updated_at = strftime('%s', 'now') + WHERE id = ? + `, + [Date.now(), price, id] + ); +} + +// Close a tranche +export async function closeTranche( + id: string, + exitPrice: number, + realizedPnl: number, + orderId?: string +): Promise { + await db.run( + ` + UPDATE tranches + SET status = 'closed', exit_price = ?, exit_time = ?, exit_order_id = ?, + realized_pnl = ?, updated_at = strftime('%s', 'now') + WHERE id = ? + `, + [exitPrice, Date.now(), orderId || null, realizedPnl, id] + ); +} + +// Liquidate a tranche +export async function liquidateTranche(id: string, liquidationPrice: number): Promise { + await db.run( + ` + UPDATE tranches + SET status = 'liquidated', exit_price = ?, exit_time = ?, updated_at = strftime('%s', 'now') + WHERE id = ? + `, + [liquidationPrice, Date.now(), id] + ); +} + +// Log a tranche event +export async function logTrancheEvent( + trancheId: string, + eventType: 'created' | 'isolated' | 'closed' | 'liquidated' | 'updated', + data: { + price?: number; + quantity?: number; + pnl?: number; + trigger?: string; + metadata?: any; + } +): Promise { + await db.run( + ` + INSERT INTO tranche_events ( + tranche_id, event_type, event_time, price, quantity, pnl, trigger, metadata + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `, + [ + trancheId, + eventType, + Date.now(), + data.price || null, + data.quantity || null, + data.pnl || null, + data.trigger || null, + data.metadata ? JSON.stringify(data.metadata) : null, + ] + ); +} + +// Get event history for a tranche +export async function getTrancheHistory(trancheId: string): Promise { + const rows = await db.all( + ` + SELECT * FROM tranche_events + WHERE tranche_id = ? + ORDER BY event_time DESC + `, + [trancheId] + ); + + return rows.map((row) => ({ + id: row.id, + trancheId: row.tranche_id, + eventType: row.event_type, + eventTime: row.event_time, + price: row.price || undefined, + quantity: row.quantity || undefined, + pnl: row.pnl || undefined, + trigger: row.trigger || undefined, + metadata: row.metadata || undefined, + })); +} + +// Clean up old closed tranches +export async function cleanupOldTranches(daysToKeep: number = 30): Promise { + const cutoffTime = Date.now() - daysToKeep * 24 * 60 * 60 * 1000; + + await db.run( + ` + DELETE FROM tranches + WHERE status IN ('closed', 'liquidated') AND exit_time < ? + `, + [cutoffTime] + ); + + // Return approximate count (sqlite3 doesn't support RETURNING) + const result = await db.get<{ count: number }>( + ` + SELECT COUNT(*) as count FROM tranches + WHERE status IN ('closed', 'liquidated') AND exit_time < ? + `, + [cutoffTime] + ); + + return result?.count || 0; +} + +// Get statistics +export async function getTrancheStats(): Promise<{ + totalActive: number; + totalIsolated: number; + totalClosed: number; + totalLiquidated: number; + totalPnl: number; +}> { + const row = await db.get(` + SELECT + SUM(CASE WHEN status = 'active' AND isolated = 0 THEN 1 ELSE 0 END) as active, + SUM(CASE WHEN status = 'active' AND isolated = 1 THEN 1 ELSE 0 END) as isolated, + SUM(CASE WHEN status = 'closed' THEN 1 ELSE 0 END) as closed, + SUM(CASE WHEN status = 'liquidated' THEN 1 ELSE 0 END) as liquidated, + SUM(CASE WHEN status IN ('closed', 'liquidated') THEN realized_pnl ELSE 0 END) as total_pnl + FROM tranches + `); + + return { + totalActive: row?.active || 0, + totalIsolated: row?.isolated || 0, + totalClosed: row?.closed || 0, + totalLiquidated: row?.liquidated || 0, + totalPnl: row?.total_pnl || 0, + }; +} diff --git a/src/lib/services/paperModeSimulator.ts b/src/lib/services/paperModeSimulator.ts new file mode 100644 index 0000000..2e9f116 --- /dev/null +++ b/src/lib/services/paperModeSimulator.ts @@ -0,0 +1,335 @@ +import { EventEmitter } from 'events'; +import { Config } from '../types'; +import { getMarkPrice } from '../api/market'; +import { logWithTimestamp, logErrorWithTimestamp } from '../utils/timestamp'; + +/** + * Paper Mode Position Simulator + * + * Simulates the full position lifecycle in paper mode: + * - Tracks simulated positions with real market prices + * - Monitors SL/TP triggers based on actual market data + * - Calculates realistic P&L + * - Broadcasts events to UI for real-time updates + * + * This service runs ONLY in paper mode and does not affect live trading. + */ + +interface SimulatedPosition { + symbol: string; + side: 'LONG' | 'SHORT'; + quantity: number; + entryPrice: number; + leverage: number; + slPrice: number; + tpPrice: number; + openTime: number; + lastPnL: number; + lastMarkPrice: number; +} + +export class PaperModeSimulator extends EventEmitter { + private positions: Map = new Map(); + private config: Config | null = null; + private monitorInterval: NodeJS.Timeout | null = null; + private isRunning = false; + + constructor() { + super(); + } + + /** + * Initialize the paper mode simulator with config + */ + public initialize(config: Config): void { + this.config = config; + logWithTimestamp('PaperModeSimulator: Initialized'); + } + + /** + * Update configuration + */ + public updateConfig(config: Config): void { + this.config = config; + logWithTimestamp('PaperModeSimulator: Configuration updated'); + } + + /** + * Start monitoring simulated positions + */ + public start(): void { + if (this.isRunning) return; + if (!this.config) { + logErrorWithTimestamp('PaperModeSimulator: Cannot start - no config loaded'); + return; + } + + this.isRunning = true; + logWithTimestamp('PaperModeSimulator: Starting position monitoring...'); + + // Monitor positions every 5 seconds + this.monitorInterval = setInterval(() => { + this.monitorPositions(); + }, 5000); + + logWithTimestamp('PaperModeSimulator: Monitoring active (checking every 5s)'); + } + + /** + * Stop monitoring + */ + public stop(): void { + if (!this.isRunning) return; + + this.isRunning = false; + + if (this.monitorInterval) { + clearInterval(this.monitorInterval); + this.monitorInterval = null; + } + + logWithTimestamp('PaperModeSimulator: Stopped'); + } + + /** + * Open a new simulated position + */ + public async openPosition(data: { + symbol: string; + side: 'BUY' | 'SELL'; + quantity: number; + leverage: number; + slPercent: number; + tpPercent: number; + }): Promise { + try { + // Fetch current market price for accurate entry + const markPriceData = await getMarkPrice(data.symbol); + const entryPrice = parseFloat( + Array.isArray(markPriceData) ? markPriceData[0].markPrice : markPriceData.markPrice + ); + + const isLong = data.side === 'BUY'; + const positionSide = isLong ? 'LONG' : 'SHORT'; + + // Calculate SL and TP prices + const slPrice = isLong + ? entryPrice * (1 - data.slPercent / 100) + : entryPrice * (1 + data.slPercent / 100); + + const tpPrice = isLong + ? entryPrice * (1 + data.tpPercent / 100) + : entryPrice * (1 - data.tpPercent / 100); + + const position: SimulatedPosition = { + symbol: data.symbol, + side: positionSide, + quantity: data.quantity, + entryPrice, + leverage: data.leverage, + slPrice, + tpPrice, + openTime: Date.now(), + lastPnL: 0, + lastMarkPrice: entryPrice, + }; + + const key = `${data.symbol}_${positionSide}`; + this.positions.set(key, position); + + logWithTimestamp( + `PaperModeSimulator: Opened ${positionSide} position for ${data.symbol} ` + + `at $${entryPrice.toFixed(2)} (SL: $${slPrice.toFixed(2)}, TP: $${tpPrice.toFixed(2)})` + ); + + // Emit position opened event + this.emit('positionOpened', { + symbol: data.symbol, + side: positionSide, + quantity: data.quantity, + entryPrice, + slPrice, + tpPrice, + leverage: data.leverage, + }); + } catch (error) { + logErrorWithTimestamp(`PaperModeSimulator: Failed to open position for ${data.symbol}:`, error); + } + } + + /** + * Close a simulated position + */ + public async closePosition(symbol: string, side: 'LONG' | 'SHORT', reason: string = 'Manual close'): Promise { + const key = `${symbol}_${side}`; + const position = this.positions.get(key); + + if (!position) { + logErrorWithTimestamp(`PaperModeSimulator: No position found for ${symbol} ${side}`); + return false; + } + + try { + // Fetch current market price for accurate exit + const markPriceData = await getMarkPrice(symbol); + const exitPrice = parseFloat( + Array.isArray(markPriceData) ? markPriceData[0].markPrice : markPriceData.markPrice + ); + + // Calculate final P&L + const isLong = side === 'LONG'; + const pnlPercent = isLong + ? ((exitPrice - position.entryPrice) / position.entryPrice) * 100 + : ((position.entryPrice - exitPrice) / position.entryPrice) * 100; + + const pnlUSDT = (pnlPercent / 100) * position.quantity * position.entryPrice * position.leverage; + const holdTime = Date.now() - position.openTime; + + logWithTimestamp( + `PaperModeSimulator: Closed ${side} position for ${symbol} ` + + `at $${exitPrice.toFixed(2)} (Entry: $${position.entryPrice.toFixed(2)}) ` + + `P&L: ${pnlPercent.toFixed(2)}% ($${pnlUSDT.toFixed(2)} USDT) ` + + `Hold: ${(holdTime / 1000).toFixed(0)}s - ${reason}` + ); + + // Emit position closed event + this.emit('positionClosed', { + symbol, + side, + entryPrice: position.entryPrice, + exitPrice, + pnlPercent, + pnlUSDT, + holdTime, + reason, + }); + + // Remove position + this.positions.delete(key); + return true; + } catch (error) { + logErrorWithTimestamp(`PaperModeSimulator: Failed to close position ${symbol} ${side}:`, error); + return false; + } + } + + /** + * Monitor all open positions and check SL/TP triggers + */ + private async monitorPositions(): Promise { + if (this.positions.size === 0) return; + + for (const [key, position] of this.positions.entries()) { + try { + // Fetch current market price + const markPriceData = await getMarkPrice(position.symbol); + const markPrice = parseFloat( + Array.isArray(markPriceData) ? markPriceData[0].markPrice : markPriceData.markPrice + ); + + position.lastMarkPrice = markPrice; + + // Calculate current P&L + const isLong = position.side === 'LONG'; + const pnlPercent = isLong + ? ((markPrice - position.entryPrice) / position.entryPrice) * 100 + : ((position.entryPrice - markPrice) / position.entryPrice) * 100; + + const pnlUSDT = (pnlPercent / 100) * position.quantity * position.entryPrice * position.leverage; + + // Only log if P&L changed significantly (> 0.1%) + if (Math.abs(pnlPercent - position.lastPnL) > 0.1) { + logWithTimestamp( + `PaperModeSimulator: ${position.symbol} ${position.side} @ $${markPrice.toFixed(2)} ` + + `P&L: ${pnlPercent.toFixed(2)}% ($${pnlUSDT.toFixed(2)} USDT)` + ); + position.lastPnL = pnlPercent; + } + + // Emit P&L update for UI + this.emit('pnlUpdate', { + symbol: position.symbol, + side: position.side, + markPrice, + pnlPercent, + pnlUSDT, + }); + + // Check SL trigger + const slTriggered = isLong + ? markPrice <= position.slPrice + : markPrice >= position.slPrice; + + if (slTriggered) { + logWithTimestamp( + `PaperModeSimulator: ๐Ÿ›‘ STOP LOSS triggered for ${position.symbol} ${position.side} ` + + `at $${markPrice.toFixed(2)} (SL: $${position.slPrice.toFixed(2)})` + ); + await this.closePosition(position.symbol, position.side, 'Stop Loss triggered'); + continue; + } + + // Check TP trigger + const tpTriggered = isLong + ? markPrice >= position.tpPrice + : markPrice <= position.tpPrice; + + if (tpTriggered) { + logWithTimestamp( + `PaperModeSimulator: ๐ŸŽฏ TAKE PROFIT triggered for ${position.symbol} ${position.side} ` + + `at $${markPrice.toFixed(2)} (TP: $${position.tpPrice.toFixed(2)})` + ); + await this.closePosition(position.symbol, position.side, 'Take Profit triggered'); + continue; + } + } catch (error) { + logErrorWithTimestamp(`PaperModeSimulator: Error monitoring ${key}:`, error); + } + } + } + + /** + * Get all open positions + */ + public getPositions(): SimulatedPosition[] { + return Array.from(this.positions.values()); + } + + /** + * Get specific position + */ + public getPosition(symbol: string, side: 'LONG' | 'SHORT'): SimulatedPosition | undefined { + return this.positions.get(`${symbol}_${side}`); + } + + /** + * Check if position exists + */ + public hasPosition(symbol: string, side: 'LONG' | 'SHORT'): boolean { + return this.positions.has(`${symbol}_${side}`); + } + + /** + * Get position count + */ + public getPositionCount(): number { + return this.positions.size; + } + + /** + * Close all positions + */ + public async closeAllPositions(): Promise { + logWithTimestamp(`PaperModeSimulator: Closing all ${this.positions.size} position(s)...`); + + const positions = Array.from(this.positions.values()); + for (const position of positions) { + await this.closePosition(position.symbol, position.side, 'Close all requested'); + } + + logWithTimestamp('PaperModeSimulator: All positions closed'); + } +} + +// Export singleton instance +export const paperModeSimulator = new PaperModeSimulator(); diff --git a/tests/tranche-integration-test.ts b/tests/tranche-integration-test.ts new file mode 100644 index 0000000..2294ef8 --- /dev/null +++ b/tests/tranche-integration-test.ts @@ -0,0 +1,766 @@ +/** + * Multi-Tranche Position Management - Integration Tests + * + * Comprehensive automated tests for all integration points: + * - Hunter integration (entry logic) + * - PositionManager integration (exit logic) + * - Exchange synchronization + * - WebSocket broadcasting + * - Full lifecycle scenarios + */ + +import { EventEmitter } from 'events'; +import { initTrancheTables, createTranche, getTranche, getActiveTranches, getAllTranchesForSymbol, closeTranche as dbCloseTranche } from '../src/lib/db/trancheDb'; +import { initializeTrancheManager, getTrancheManager } from '../src/lib/services/trancheManager'; +import { Config } from '../src/lib/types'; +import { db } from '../src/lib/db/database'; + +const TEST_SYMBOL = 'BTCUSDT'; +const TEST_ENTRY_PRICE = 50000; +const TEST_QUANTITY = 0.001; +const TEST_MARGIN = 5; +const TEST_LEVERAGE = 10; + +// Test configuration +const testConfig: Config = { + api: { + apiKey: 'test-key', + secretKey: 'test-secret', + }, + symbols: { + [TEST_SYMBOL]: { + longVolumeThresholdUSDT: 10000, + shortVolumeThresholdUSDT: 10000, + tradeSize: 0.001, + maxPositionMarginUSDT: 200, + leverage: TEST_LEVERAGE, + tpPercent: 5, + slPercent: 2, + priceOffsetBps: 2, + maxSlippageBps: 50, + orderType: 'LIMIT', + postOnly: false, + forceMarketOrders: false, + vwapProtection: false, + vwapTimeframe: '5m', + vwapLookback: 200, + useThreshold: false, + thresholdTimeWindow: 60000, + thresholdCooldown: 30000, + enableTrancheManagement: true, + trancheIsolationThreshold: 5, + maxTranches: 3, + maxIsolatedTranches: 2, + trancheStrategy: { + closingStrategy: 'FIFO', + slTpStrategy: 'NEWEST', + isolationAction: 'HOLD', + }, + allowTrancheWhileIsolated: true, + trancheAutoCloseIsolated: false, + }, + }, + global: { + paperMode: true, + riskPercent: 90, + positionMode: 'HEDGE', + maxOpenPositions: 5, + useThresholdSystem: false, + server: { + dashboardPassword: 'test', + dashboardPort: 3000, + websocketPort: 8080, + useRemoteWebSocket: false, + websocketHost: null, + }, + rateLimit: { + maxRequestWeight: 2400, + maxOrderCount: 1200, + reservePercent: 30, + enableBatching: true, + queueTimeout: 30000, + enableDeduplication: true, + deduplicationWindowMs: 1000, + parallelProcessing: true, + maxConcurrentRequests: 3, + }, + }, + version: '1.1.0', +}; + +// Mock StatusBroadcaster for testing +class MockStatusBroadcaster extends EventEmitter { + public broadcastedEvents: any[] = []; + + broadcastTrancheCreated(data: any) { + this.broadcastedEvents.push({ type: 'tranche_created', data }); + this.emit('tranche_created', data); + } + + broadcastTrancheIsolated(data: any) { + this.broadcastedEvents.push({ type: 'tranche_isolated', data }); + this.emit('tranche_isolated', data); + } + + broadcastTrancheClosed(data: any) { + this.broadcastedEvents.push({ type: 'tranche_closed', data }); + this.emit('tranche_closed', data); + } + + broadcastTrancheSyncUpdate(data: any) { + this.broadcastedEvents.push({ type: 'tranche_sync', data }); + this.emit('tranche_sync', data); + } + + broadcastTradingError(title: string, message: string, details?: any) { + this.broadcastedEvents.push({ type: 'trading_error', title, message, details }); + this.emit('trading_error', { title, message, details }); + } + + clearEvents() { + this.broadcastedEvents = []; + } + + getEventsByType(type: string) { + return this.broadcastedEvents.filter(e => e.type === type); + } +} + +// Helper to clean up test data +async function cleanupTestData() { + // Delete events first (foreign key constraint) + await db.run(` + DELETE FROM tranche_events + WHERE tranche_id IN (SELECT id FROM tranches WHERE symbol = ?) + `, [TEST_SYMBOL]); + + // Then delete tranches + await db.run('DELETE FROM tranches WHERE symbol = ?', [TEST_SYMBOL]); +} + +async function runIntegrationTests() { + console.log('๐Ÿงช Multi-Tranche Integration Tests\n'); + console.log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•\n'); + + let testsPassed = 0; + let testsFailed = 0; + + // Initialize database + await db.initialize(); + await initTrancheTables(); + + // Test Suite 1: Hunter Integration Tests + console.log('๐Ÿ“‹ Test Suite 1: Hunter Integration\n'); + + // Test 1.1: Pre-trade tranche limit check + console.log('Test 1.1: Pre-trade Tranche Limit Check'); + try { + await cleanupTestData(); + const trancheManager = initializeTrancheManager(testConfig); + await trancheManager.initialize(); + + // Create max tranches (3) + for (let i = 0; i < 3; i++) { + await trancheManager.createTranche({ + symbol: TEST_SYMBOL, + side: 'BUY', + positionSide: 'LONG', + entryPrice: TEST_ENTRY_PRICE + i * 100, + quantity: TEST_QUANTITY, + marginUsed: TEST_MARGIN, + leverage: TEST_LEVERAGE, + orderId: `test-hunter-${i}`, + }); + } + + // Verify we have 3 active tranches + const activeTranches = await getActiveTranches(TEST_SYMBOL, 'LONG'); + const activeCount = activeTranches.filter(t => !t.isolated).length; + + // Verify limit is reached + const canOpen = trancheManager.canOpenNewTranche(TEST_SYMBOL, 'LONG'); + + if (activeCount === 3 && !canOpen.allowed && (canOpen.reason?.includes('maxTranches') || canOpen.reason?.includes('Max active tranches'))) { + console.log('โœ… Pre-trade limit check blocks new trades correctly'); + console.log(` Active tranches: ${activeCount}/3`); + console.log(` Can open new: ${canOpen.allowed} โœ“\n`); + testsPassed++; + } else { + throw new Error(`Limit check failed: activeCount=${activeCount}, canOpen=${canOpen.allowed}, reason=${canOpen.reason}`); + } + } catch (error) { + console.error('โŒ Test failed:', error); + testsFailed++; + } + + // Test 1.2: Post-order tranche creation + console.log('Test 1.2: Post-order Tranche Creation'); + try { + await cleanupTestData(); + const trancheManager = initializeTrancheManager(testConfig); + await trancheManager.initialize(); + + const tranchesBefore = await getActiveTranches(TEST_SYMBOL, 'LONG'); + const countBefore = tranchesBefore.length; + + // Simulate Hunter creating tranche after order filled + await trancheManager.createTranche({ + symbol: TEST_SYMBOL, + side: 'BUY', + positionSide: 'LONG', + entryPrice: TEST_ENTRY_PRICE, + quantity: TEST_QUANTITY, + marginUsed: TEST_MARGIN, + leverage: TEST_LEVERAGE, + orderId: 'hunter-order-123', + }); + + const tranchesAfter = await getActiveTranches(TEST_SYMBOL, 'LONG'); + const countAfter = tranchesAfter.length; + + if (countAfter === countBefore + 1 && tranchesAfter[0].entryOrderId === 'hunter-order-123') { + console.log('โœ… Tranche created correctly after order fill\n'); + testsPassed++; + } else { + throw new Error('Tranche not created or order ID mismatch'); + } + } catch (error) { + console.error('โŒ Test failed:', error); + testsFailed++; + } + + // Test Suite 2: PositionManager Integration Tests + console.log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•'); + console.log('๐Ÿ“‹ Test Suite 2: PositionManager Integration\n'); + + // Test 2.1: Tranche closing on SL/TP fill (FIFO strategy) + console.log('Test 2.1: Tranche Closing with FIFO Strategy'); + try { + await cleanupTestData(); + const trancheManager = initializeTrancheManager(testConfig); + await trancheManager.initialize(); + + // Create 3 tranches at different entry prices + const tranche1 = await trancheManager.createTranche({ + symbol: TEST_SYMBOL, + side: 'BUY', + positionSide: 'LONG', + entryPrice: 50000, + quantity: 0.001, + marginUsed: 5, + leverage: 10, + orderId: 'order-1', + }); + + await new Promise(resolve => setTimeout(resolve, 10)); + + const tranche2 = await trancheManager.createTranche({ + symbol: TEST_SYMBOL, + side: 'BUY', + positionSide: 'LONG', + entryPrice: 50100, + quantity: 0.001, + marginUsed: 5, + leverage: 10, + orderId: 'order-2', + }); + + await new Promise(resolve => setTimeout(resolve, 10)); + + const tranche3 = await trancheManager.createTranche({ + symbol: TEST_SYMBOL, + side: 'BUY', + positionSide: 'LONG', + entryPrice: 50200, + quantity: 0.001, + marginUsed: 5, + leverage: 10, + orderId: 'order-3', + }); + + // Simulate position manager closing order (SELL = closing LONG) + await trancheManager.processOrderFill({ + symbol: TEST_SYMBOL, + side: 'SELL', + positionSide: 'LONG', + quantityFilled: 0.001, + fillPrice: 52000, + realizedPnl: 2.0, + orderId: 'close-order-1', + }); + + // Verify FIFO: First tranche should be closed + const tranche1After = await getTranche(tranche1.id); + const tranche2After = await getTranche(tranche2.id); + + if (tranche1After?.status === 'closed' && tranche2After?.status === 'active') { + console.log('โœ… FIFO closing strategy works correctly'); + console.log(` Tranche 1 (oldest): closed โœ“`); + console.log(` Tranche 2 (middle): active โœ“\n`); + testsPassed++; + } else { + throw new Error('FIFO strategy not working correctly'); + } + } catch (error) { + console.error('โŒ Test failed:', error); + testsFailed++; + } + + // Test 2.2: Partial position close + console.log('Test 2.2: Partial Position Close'); + try { + await cleanupTestData(); + const trancheManager = initializeTrancheManager(testConfig); + await trancheManager.initialize(); + + // Create tranche with 0.003 BTC + await trancheManager.createTranche({ + symbol: TEST_SYMBOL, + side: 'BUY', + positionSide: 'LONG', + entryPrice: 50000, + quantity: 0.003, + marginUsed: 15, + leverage: 10, + orderId: 'large-order', + }); + + // Close only 0.001 BTC (partial) + await trancheManager.processOrderFill({ + symbol: TEST_SYMBOL, + side: 'SELL', + positionSide: 'LONG', + quantityFilled: 0.001, + fillPrice: 52000, + realizedPnl: 2.0, + orderId: 'partial-close-1', + }); + + const tranches = await getActiveTranches(TEST_SYMBOL, 'LONG'); + const remainingQty = tranches.reduce((sum, t) => sum + t.quantity, 0); + + if (Math.abs(remainingQty - 0.002) < 0.0001) { + console.log('โœ… Partial close handled correctly'); + console.log(` Original: 0.003 BTC, Closed: 0.001 BTC`); + console.log(` Remaining: ${remainingQty.toFixed(4)} BTC โœ“\n`); + testsPassed++; + } else { + throw new Error(`Partial close quantity mismatch: ${remainingQty}`); + } + } catch (error) { + console.error('โŒ Test failed:', error); + testsFailed++; + } + + // Test Suite 3: Exchange Synchronization + console.log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•'); + console.log('๐Ÿ“‹ Test Suite 3: Exchange Synchronization\n'); + + // Test 3.1: Sync with matching quantities + console.log('Test 3.1: Exchange Sync - Matching Quantities'); + try { + await cleanupTestData(); + const trancheManager = initializeTrancheManager(testConfig); + await trancheManager.initialize(); + + // Create 2 tranches (total 0.002 BTC) + await trancheManager.createTranche({ + symbol: TEST_SYMBOL, + side: 'BUY', + positionSide: 'LONG', + entryPrice: 50000, + quantity: 0.001, + marginUsed: 5, + leverage: 10, + orderId: 'sync-1', + }); + + await trancheManager.createTranche({ + symbol: TEST_SYMBOL, + side: 'BUY', + positionSide: 'LONG', + entryPrice: 50100, + quantity: 0.001, + marginUsed: 5, + leverage: 10, + orderId: 'sync-2', + }); + + // Simulate exchange position with matching quantity + const mockExchangePosition = { + symbol: TEST_SYMBOL, + positionAmt: '0.002', + entryPrice: '50050', + markPrice: '50500', + unRealizedProfit: '0.9', + liquidationPrice: '45000', + leverage: '10', + marginType: 'cross', + isolatedMargin: '0', + isAutoAddMargin: 'false', + positionSide: 'LONG', + updateTime: Date.now(), + }; + + await trancheManager.syncWithExchange(TEST_SYMBOL, 'LONG', mockExchangePosition); + + const group = trancheManager.getTrancheGroup(TEST_SYMBOL, 'LONG'); + + if (group && group.syncStatus === 'synced') { + console.log('โœ… Exchange sync successful with matching quantities'); + console.log(` Local: ${group.totalQuantity.toFixed(4)} BTC`); + console.log(` Exchange: 0.002 BTC`); + console.log(` Status: ${group.syncStatus} โœ“\n`); + testsPassed++; + } else { + throw new Error(`Sync status incorrect: ${group?.syncStatus}`); + } + } catch (error) { + console.error('โŒ Test failed:', error); + testsFailed++; + } + + // Test 3.2: Sync with quantity drift + console.log('Test 3.2: Exchange Sync - Quantity Drift Detection'); + try { + await cleanupTestData(); + const trancheManager = initializeTrancheManager(testConfig); + await trancheManager.initialize(); + + // Create tranches totaling 0.003 BTC + for (let i = 0; i < 3; i++) { + await trancheManager.createTranche({ + symbol: TEST_SYMBOL, + side: 'BUY', + positionSide: 'LONG', + entryPrice: 50000 + i * 50, + quantity: 0.001, + marginUsed: 5, + leverage: 10, + orderId: `drift-${i}`, + }); + } + + // Simulate exchange position with less quantity (drift) + const mockExchangePosition = { + symbol: TEST_SYMBOL, + positionAmt: '0.002', // 0.001 less than local + entryPrice: '50050', + markPrice: '50500', + unRealizedProfit: '0.9', + liquidationPrice: '45000', + leverage: '10', + marginType: 'cross', + isolatedMargin: '0', + isAutoAddMargin: 'false', + positionSide: 'LONG', + updateTime: Date.now(), + }; + + await trancheManager.syncWithExchange(TEST_SYMBOL, 'LONG', mockExchangePosition); + + const group = trancheManager.getTrancheGroup(TEST_SYMBOL, 'LONG'); + + if (group && group.syncStatus === 'drift') { + console.log('โœ… Quantity drift detected correctly'); + console.log(` Local: ${group.totalQuantity.toFixed(4)} BTC`); + console.log(` Exchange: 0.002 BTC`); + console.log(` Status: ${group.syncStatus} โœ“`); + console.log(` Drift: ${((group.totalQuantity - 0.002) * 100).toFixed(1)}%\n`); + testsPassed++; + } else { + throw new Error(`Drift not detected: ${group?.syncStatus}`); + } + } catch (error) { + console.error('โŒ Test failed:', error); + testsFailed++; + } + + // Test Suite 4: Isolation Logic + console.log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•'); + console.log('๐Ÿ“‹ Test Suite 4: Isolation Logic\n'); + + // Test 4.1: Isolation threshold detection + console.log('Test 4.1: Isolation Threshold Detection'); + try { + await cleanupTestData(); + const trancheManager = initializeTrancheManager(testConfig); + await trancheManager.initialize(); + + const tranche = await trancheManager.createTranche({ + symbol: TEST_SYMBOL, + side: 'BUY', + positionSide: 'LONG', + entryPrice: 50000, + quantity: 0.001, + marginUsed: 5, + leverage: 10, + orderId: 'iso-test', + }); + + // Update P&L at 47500 (5% loss - at threshold) + await trancheManager.updateUnrealizedPnl(TEST_SYMBOL, 47500); + + // Test shouldIsolateTranche logic + const shouldIsolate5 = trancheManager.shouldIsolateTranche(tranche, 47500); // 5% loss + const shouldIsolate4 = trancheManager.shouldIsolateTranche(tranche, 48000); // 4% loss + + if (shouldIsolate5 && !shouldIsolate4) { + console.log('โœ… Isolation threshold detection correct'); + console.log(` Entry: $50000`); + console.log(` At $47500 (5% loss): Should isolate = ${shouldIsolate5} โœ“`); + console.log(` At $48000 (4% loss): Should isolate = ${shouldIsolate4} โœ“\n`); + testsPassed++; + } else { + throw new Error(`Threshold detection failed: shouldIsolate5=${shouldIsolate5}, shouldIsolate4=${shouldIsolate4}`); + } + } catch (error) { + console.error('โŒ Test failed:', error); + testsFailed++; + } + + // Test 4.2: Manual tranche isolation + console.log('Test 4.2: Manual Tranche Isolation'); + try { + await cleanupTestData(); + const trancheManager = initializeTrancheManager(testConfig); + await trancheManager.initialize(); + + // Create tranche + const tranche1 = await trancheManager.createTranche({ + symbol: TEST_SYMBOL, + side: 'BUY', + positionSide: 'LONG', + entryPrice: 50000, + quantity: 0.001, + marginUsed: 5, + leverage: 10, + orderId: 'iso-manual', + }); + + // Manually isolate tranche + await trancheManager.isolateTranche(tranche1.id, 47500); + + // Verify isolation + const tranche1After = await getTranche(tranche1.id); + + // Create new tranche (should be allowed if allowTrancheWhileIsolated) + const canOpen = trancheManager.canOpenNewTranche(TEST_SYMBOL, 'LONG'); + + if (canOpen.allowed && tranche1After?.isolated) { + await trancheManager.createTranche({ + symbol: TEST_SYMBOL, + side: 'BUY', + positionSide: 'LONG', + entryPrice: 48000, + quantity: 0.001, + marginUsed: 5, + leverage: 10, + orderId: 'new-after-iso', + }); + + const group = trancheManager.getTrancheGroup(TEST_SYMBOL, 'LONG'); + + if (group && group.activeTranches.length === 1 && group.isolatedTranches.length === 1) { + console.log('โœ… New tranche created successfully with isolated tranche'); + console.log(` Active tranches: ${group.activeTranches.length}`); + console.log(` Isolated tranches: ${group.isolatedTranches.length} โœ“\n`); + testsPassed++; + } else { + throw new Error(`Tranche counts incorrect: active=${group?.activeTranches.length}, isolated=${group?.isolatedTranches.length}`); + } + } else { + throw new Error(`Cannot open new tranche: canOpen=${canOpen.allowed}, isolated=${tranche1After?.isolated}`); + } + } catch (error) { + console.error('โŒ Test failed:', error); + testsFailed++; + } + + // Test Suite 5: Event Broadcasting + console.log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•'); + console.log('๐Ÿ“‹ Test Suite 5: Event Broadcasting\n'); + + // Test 5.1: Tranche lifecycle events + console.log('Test 5.1: Tranche Lifecycle Events'); + try { + await cleanupTestData(); + const trancheManager = initializeTrancheManager(testConfig); + await trancheManager.initialize(); + + let createdEvent = false; + let isolatedEvent = false; + let closedEvent = false; + + trancheManager.on('trancheCreated', () => { createdEvent = true; }); + trancheManager.on('trancheIsolated', () => { isolatedEvent = true; }); + trancheManager.on('trancheClosed', () => { closedEvent = true; }); + + // Create tranche + const tranche = await trancheManager.createTranche({ + symbol: TEST_SYMBOL, + side: 'BUY', + positionSide: 'LONG', + entryPrice: 50000, + quantity: 0.001, + marginUsed: 5, + leverage: 10, + orderId: 'event-test', + }); + + // Isolate + await trancheManager.updateUnrealizedPnl(TEST_SYMBOL, 47500); + await trancheManager.isolateTranche(tranche.id, 47500); + + // Close + await trancheManager.closeTranche({ + trancheId: tranche.id, + exitPrice: 48000, + realizedPnl: -2.0, + orderId: 'close-event', + }); + + if (createdEvent && isolatedEvent && closedEvent) { + console.log('โœ… All lifecycle events emitted correctly'); + console.log(` Created: ${createdEvent} โœ“`); + console.log(` Isolated: ${isolatedEvent} โœ“`); + console.log(` Closed: ${closedEvent} โœ“\n`); + testsPassed++; + } else { + throw new Error(`Events missing: created=${createdEvent}, isolated=${isolatedEvent}, closed=${closedEvent}`); + } + } catch (error) { + console.error('โŒ Test failed:', error); + testsFailed++; + } + + // Test Suite 6: Full Lifecycle Scenarios + console.log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•'); + console.log('๐Ÿ“‹ Test Suite 6: Full Lifecycle Scenarios\n'); + + // Test 6.1: Profitable trade full lifecycle + console.log('Test 6.1: Profitable Trade - Entry to Exit'); + try { + await cleanupTestData(); + const trancheManager = initializeTrancheManager(testConfig); + await trancheManager.initialize(); + + // Entry + const tranche = await trancheManager.createTranche({ + symbol: TEST_SYMBOL, + side: 'BUY', + positionSide: 'LONG', + entryPrice: 50000, + quantity: 0.001, + marginUsed: 5, + leverage: 10, + orderId: 'profit-trade', + }); + + // Price moves up 5% (TP hit) + const tpPrice = 52500; + await trancheManager.processOrderFill({ + symbol: TEST_SYMBOL, + side: 'SELL', + positionSide: 'LONG', + quantityFilled: 0.001, + fillPrice: tpPrice, + realizedPnl: 2.5, + orderId: 'tp-fill', + }); + + const closedTranche = await getTranche(tranche.id); + + if (closedTranche?.status === 'closed' && closedTranche.realizedPnl > 0) { + console.log('โœ… Profitable trade lifecycle complete'); + console.log(` Entry: $${closedTranche.entryPrice}`); + console.log(` Exit: $${closedTranche.exitPrice}`); + console.log(` P&L: $${closedTranche.realizedPnl.toFixed(2)} โœ“\n`); + testsPassed++; + } else { + throw new Error('Trade lifecycle incomplete or not profitable'); + } + } catch (error) { + console.error('โŒ Test failed:', error); + testsFailed++; + } + + // Test 6.2: Multi-tranche P&L tracking + console.log('Test 6.2: Multi-Tranche P&L Tracking'); + try { + await cleanupTestData(); + const trancheManager = initializeTrancheManager(testConfig); + await trancheManager.initialize(); + + // Create 3 tranches at different prices + const entries = [50000, 49500, 49000]; + const trancheIds = []; + for (const entry of entries) { + const t = await trancheManager.createTranche({ + symbol: TEST_SYMBOL, + side: 'BUY', + positionSide: 'LONG', + entryPrice: entry, + quantity: 0.001, + marginUsed: 5, + leverage: 10, + orderId: `multi-${entry}`, + }); + trancheIds.push(t.id); + } + + // Update P&L at profitable price (51000) + await trancheManager.updateUnrealizedPnl(TEST_SYMBOL, 51000); + + const group = trancheManager.getTrancheGroup(TEST_SYMBOL, 'LONG'); + const allProfitable = group?.tranches.every(t => t.unrealizedPnl > 0); + const totalPnL = group?.totalUnrealizedPnl || 0; + + // All tranches should be profitable at 51000 + if (allProfitable && totalPnL > 0 && group.tranches.length === 3) { + console.log('โœ… Multi-tranche P&L tracking successful'); + console.log(` Total tranches: ${group.tranches.length}`); + console.log(` All profitable: ${allProfitable} โœ“`); + console.log(` Total unrealized P&L: $${totalPnL.toFixed(2)}\n`); + testsPassed++; + } else { + throw new Error(`P&L tracking failed: allProfitable=${allProfitable}, totalPnL=${totalPnL}, count=${group?.tranches.length}`); + } + } catch (error) { + console.error('โŒ Test failed:', error); + testsFailed++; + } + + // Summary + console.log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•'); + console.log('๐Ÿ“Š Integration Test Summary'); + console.log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•'); + console.log(`โœ… Tests Passed: ${testsPassed}`); + console.log(`โŒ Tests Failed: ${testsFailed}`); + console.log(`๐Ÿ“ˆ Success Rate: ${((testsPassed / (testsPassed + testsFailed)) * 100).toFixed(1)}%`); + console.log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•\n'); + + if (testsFailed === 0) { + console.log('๐ŸŽ‰ All integration tests passed!'); + console.log('โœ… Hunter integration working'); + console.log('โœ… PositionManager integration working'); + console.log('โœ… Exchange synchronization working'); + console.log('โœ… Isolation logic working'); + console.log('โœ… Event broadcasting working'); + console.log('โœ… Full lifecycle scenarios working\n'); + } else { + console.log('โš ๏ธ Some integration tests failed. Please review the errors above.\n'); + } + + // Cleanup + await cleanupTestData(); + await db.close(); + + process.exit(testsFailed > 0 ? 1 : 0); +} + +// Run tests +runIntegrationTests().catch(error => { + console.error('๐Ÿ’ฅ Integration test suite crashed:', error); + process.exit(1); +}); diff --git a/tests/tranche-system-test.ts b/tests/tranche-system-test.ts new file mode 100644 index 0000000..17e345d --- /dev/null +++ b/tests/tranche-system-test.ts @@ -0,0 +1,355 @@ +/** + * Multi-Tranche Position Management - System Test + * + * This test verifies the core functionality of the tranche management system: + * - Database initialization + * - Tranche creation and retrieval + * - Isolation logic + * - P&L calculations + * - Exchange synchronization + */ + +import { initTrancheTables, createTranche, getTranche, getActiveTranches, updateTrancheUnrealizedPnl, isolateTranche, closeTranche } from '../src/lib/db/trancheDb'; +import { initializeTrancheManager } from '../src/lib/services/trancheManager'; +import { Config } from '../src/lib/types'; +import { db } from '../src/lib/db/database'; + +const TEST_SYMBOL = 'BTCUSDT'; +const TEST_ENTRY_PRICE = 50000; +const TEST_QUANTITY = 0.001; +const TEST_MARGIN = 5; +const TEST_LEVERAGE = 10; + +// Test configuration +const testConfig: Config = { + api: { + apiKey: 'test-key', + secretKey: 'test-secret', + }, + symbols: { + [TEST_SYMBOL]: { + longVolumeThresholdUSDT: 10000, + shortVolumeThresholdUSDT: 10000, + tradeSize: 0.001, + maxPositionMarginUSDT: 200, + leverage: TEST_LEVERAGE, + tpPercent: 5, + slPercent: 2, + priceOffsetBps: 2, + maxSlippageBps: 50, + orderType: 'LIMIT', + postOnly: false, + forceMarketOrders: false, + vwapProtection: false, + vwapTimeframe: '5m', + vwapLookback: 200, + useThreshold: false, + thresholdTimeWindow: 60000, + thresholdCooldown: 30000, + // Tranche management settings + enableTrancheManagement: true, + trancheIsolationThreshold: 5, + maxTranches: 3, + maxIsolatedTranches: 2, + trancheStrategy: { + closingStrategy: 'FIFO', + slTpStrategy: 'NEWEST', + isolationAction: 'HOLD', + }, + allowTrancheWhileIsolated: true, + trancheAutoCloseIsolated: false, + }, + }, + global: { + paperMode: true, + riskPercent: 90, + positionMode: 'HEDGE', + maxOpenPositions: 5, + useThresholdSystem: false, + server: { + dashboardPassword: 'test', + dashboardPort: 3000, + websocketPort: 8080, + useRemoteWebSocket: false, + websocketHost: null, + }, + rateLimit: { + maxRequestWeight: 2400, + maxOrderCount: 1200, + reservePercent: 30, + enableBatching: true, + queueTimeout: 30000, + enableDeduplication: true, + deduplicationWindowMs: 1000, + parallelProcessing: true, + maxConcurrentRequests: 3, + }, + }, + version: '1.1.0', +}; + +async function runTests() { + console.log('๐Ÿงช Starting Multi-Tranche System Tests\n'); + + let testsPassed = 0; + let testsFailed = 0; + + // Test 1: Database Initialization + console.log('Test 1: Database Initialization'); + try { + await db.initialize(); + await initTrancheTables(); + console.log('โœ… Database and tranche tables initialized\n'); + testsPassed++; + } catch (error) { + console.error('โŒ Database initialization failed:', error); + testsFailed++; + return; // Can't continue without database + } + + // Test 2: Tranche Creation (Database Layer) + console.log('Test 2: Tranche Creation (Database Layer)'); + const testTrancheId = `test-${Date.now()}`; + try { + await createTranche({ + id: testTrancheId, + symbol: TEST_SYMBOL, + side: 'LONG', + positionSide: 'LONG', + entryPrice: TEST_ENTRY_PRICE, + quantity: TEST_QUANTITY, + marginUsed: TEST_MARGIN, + leverage: TEST_LEVERAGE, + entryTime: Date.now(), + entryOrderId: 'test-order-001', + unrealizedPnl: 0, + realizedPnl: 0, + tpPercent: 5, + slPercent: 2, + tpPrice: TEST_ENTRY_PRICE * 1.05, + slPrice: TEST_ENTRY_PRICE * 0.98, + status: 'active', + isolated: false, + }); + + const retrieved = await getTranche(testTrancheId); + if (retrieved && retrieved.entryPrice === TEST_ENTRY_PRICE) { + console.log('โœ… Tranche created and retrieved successfully'); + console.log(` ID: ${testTrancheId.substring(0, 8)}...`); + console.log(` Entry: $${retrieved.entryPrice}, TP: $${retrieved.tpPrice}, SL: $${retrieved.slPrice}\n`); + testsPassed++; + } else { + throw new Error('Retrieved tranche does not match'); + } + } catch (error) { + console.error('โŒ Tranche creation failed:', error); + testsFailed++; + } + + // Test 3: TrancheManager Service Initialization + console.log('Test 3: TrancheManager Service Initialization'); + try { + const trancheManager = initializeTrancheManager(testConfig); + await trancheManager.initialize(); + console.log('โœ… TrancheManager initialized successfully\n'); + testsPassed++; + } catch (error) { + console.error('โŒ TrancheManager initialization failed:', error); + testsFailed++; + } + + // Test 4: Tranche Creation via Manager + console.log('Test 4: Tranche Creation via TrancheManager'); + try { + const trancheManager = initializeTrancheManager(testConfig); + await trancheManager.initialize(); + + const tranche = await trancheManager.createTranche({ + symbol: TEST_SYMBOL, + side: 'BUY', + positionSide: 'LONG', + entryPrice: TEST_ENTRY_PRICE, + quantity: TEST_QUANTITY, + marginUsed: TEST_MARGIN, + leverage: TEST_LEVERAGE, + orderId: 'test-order-002', + }); + + if (tranche.tpPrice > TEST_ENTRY_PRICE && tranche.slPrice < TEST_ENTRY_PRICE) { + console.log('โœ… Tranche created via manager with correct TP/SL'); + console.log(` Entry: $${tranche.entryPrice}`); + console.log(` TP: $${tranche.tpPrice} (+5%)`); + console.log(` SL: $${tranche.slPrice} (-2%)\n`); + testsPassed++; + } else { + throw new Error('TP/SL calculation incorrect'); + } + } catch (error) { + console.error('โŒ Tranche creation via manager failed:', error); + testsFailed++; + } + + // Test 5: Isolation Threshold Logic + console.log('Test 5: Isolation Threshold Logic'); + try { + const trancheManager = initializeTrancheManager(testConfig); + await trancheManager.initialize(); + + // Create test tranche + const tranche = await trancheManager.createTranche({ + symbol: TEST_SYMBOL, + side: 'BUY', + positionSide: 'LONG', + entryPrice: TEST_ENTRY_PRICE, + quantity: TEST_QUANTITY, + marginUsed: TEST_MARGIN, + leverage: TEST_LEVERAGE, + orderId: 'test-order-003', + }); + + // Test at 5% loss (should isolate) + const priceAt5PercentLoss = TEST_ENTRY_PRICE * 0.95; + const shouldIsolate = trancheManager.shouldIsolateTranche(tranche, priceAt5PercentLoss); + + // Test at 4% loss (should NOT isolate) + const priceAt4PercentLoss = TEST_ENTRY_PRICE * 0.96; + const shouldNotIsolate = trancheManager.shouldIsolateTranche(tranche, priceAt4PercentLoss); + + if (shouldIsolate && !shouldNotIsolate) { + console.log('โœ… Isolation threshold logic correct'); + console.log(` Entry: $${TEST_ENTRY_PRICE}`); + console.log(` At $${priceAt5PercentLoss} (5% loss): Should isolate = ${shouldIsolate} โœ“`); + console.log(` At $${priceAt4PercentLoss} (4% loss): Should isolate = ${shouldNotIsolate} โœ“\n`); + testsPassed++; + } else { + throw new Error(`Isolation logic failed: shouldIsolate=${shouldIsolate}, shouldNotIsolate=${shouldNotIsolate}`); + } + } catch (error) { + console.error('โŒ Isolation threshold test failed:', error); + testsFailed++; + } + + // Test 6: P&L Calculation + console.log('Test 6: Unrealized P&L Calculation'); + try { + const trancheManager = initializeTrancheManager(testConfig); + await trancheManager.initialize(); + + // Create LONG tranche at 50000 + const tranche = await trancheManager.createTranche({ + symbol: TEST_SYMBOL, + side: 'BUY', + positionSide: 'LONG', + entryPrice: TEST_ENTRY_PRICE, + quantity: TEST_QUANTITY, + marginUsed: TEST_MARGIN, + leverage: TEST_LEVERAGE, + orderId: 'test-order-004', + }); + + // Update P&L at 52000 (4% profit) + await trancheManager.updateUnrealizedPnl(TEST_SYMBOL, 52000); + + const updated = await getTranche(tranche.id); + const expectedPnl = (52000 - TEST_ENTRY_PRICE) * TEST_QUANTITY; // Should be ~$2 + + if (updated && Math.abs(updated.unrealizedPnl - expectedPnl) < 0.01) { + console.log('โœ… P&L calculation correct'); + console.log(` Entry: $${TEST_ENTRY_PRICE}, Current: $52000`); + console.log(` Expected P&L: $${expectedPnl.toFixed(2)}`); + console.log(` Actual P&L: $${updated.unrealizedPnl.toFixed(2)}\n`); + testsPassed++; + } else { + throw new Error(`P&L mismatch: expected ${expectedPnl}, got ${updated?.unrealizedPnl}`); + } + } catch (error) { + console.error('โŒ P&L calculation test failed:', error); + testsFailed++; + } + + // Test 7: Position Limits + console.log('Test 7: Position Limit Checks'); + try { + const trancheManager = initializeTrancheManager(testConfig); + await trancheManager.initialize(); + + // Get current active tranches + const existingTranches = trancheManager.getTranches(TEST_SYMBOL, 'LONG'); + const activeCount = existingTranches.filter(t => !t.isolated).length; + console.log(` Existing active tranches: ${activeCount}`); + + // Create tranches up to the limit + const maxTranches = testConfig.symbols[TEST_SYMBOL].maxTranches || 3; + const tranchesToCreate = Math.max(0, maxTranches - activeCount); + + for (let i = 0; i < tranchesToCreate; i++) { + await trancheManager.createTranche({ + symbol: TEST_SYMBOL, + side: 'BUY', + positionSide: 'LONG', + entryPrice: TEST_ENTRY_PRICE, + quantity: TEST_QUANTITY, + marginUsed: TEST_MARGIN, + leverage: TEST_LEVERAGE, + orderId: `test-order-limit-${i}`, + }); + } + + // Try to create one more (should be blocked) + const canOpen = trancheManager.canOpenNewTranche(TEST_SYMBOL, 'LONG'); + + if (!canOpen.allowed && canOpen.reason?.includes('maxTranches')) { + console.log('โœ… Position limit enforcement correct'); + console.log(` Max tranches: ${maxTranches}`); + console.log(` Current active: ${maxTranches}`); + console.log(` Can open new: ${canOpen.allowed} โœ“`); + console.log(` Reason: ${canOpen.reason}\n`); + testsPassed++; + } else { + throw new Error(`Position limit not enforced: allowed=${canOpen.allowed}, reason=${canOpen.reason}`); + } + } catch (error) { + console.error('โŒ Position limit test failed:', error); + testsFailed++; + } + + // Test 8: Tranche Retrieval + console.log('Test 8: Tranche Retrieval'); + try { + const activeTranches = await getActiveTranches(TEST_SYMBOL, 'LONG'); + if (activeTranches.length > 0) { + console.log(`โœ… Retrieved ${activeTranches.length} active tranches for ${TEST_SYMBOL} LONG`); + console.log(` Sample: ${activeTranches[0].id.substring(0, 8)}... at $${activeTranches[0].entryPrice}\n`); + testsPassed++; + } else { + throw new Error('No active tranches found'); + } + } catch (error) { + console.error('โŒ Tranche retrieval test failed:', error); + testsFailed++; + } + + // Summary + console.log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•'); + console.log('๐Ÿ“Š Test Summary'); + console.log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•'); + console.log(`โœ… Tests Passed: ${testsPassed}`); + console.log(`โŒ Tests Failed: ${testsFailed}`); + console.log(`๐Ÿ“ˆ Success Rate: ${((testsPassed / (testsPassed + testsFailed)) * 100).toFixed(1)}%`); + console.log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•\n'); + + if (testsFailed === 0) { + console.log('๐ŸŽ‰ All tests passed! The multi-tranche system is ready for integration testing.'); + } else { + console.log('โš ๏ธ Some tests failed. Please review the errors above.'); + } + + // Cleanup + await db.close(); +} + +// Run tests +runTests().catch(error => { + console.error('๐Ÿ’ฅ Test suite crashed:', error); + process.exit(1); +}); From 6bb2ce237c62277c09663afe17aefc3a5a496b95 Mon Sep 17 00:00:00 2001 From: birdbathd Date: Thu, 20 Nov 2025 12:53:36 +1000 Subject: [PATCH 20/30] docs: merge back with dev - restore tranche documentation --- CLAUDE.md | 74 ++++++++++++++++++++++++++++---- README.md | 124 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 190 insertions(+), 8 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 6e4dc98..77c59e3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -39,14 +39,17 @@ npm run lint # Run ESLint npx tsc --noEmit # Check TypeScript types # Testing -npm test # Run all tests -npm run test:hunter # Test Hunter component -npm run test:position # Test PositionManager -npm run test:rate # Test rate limiting -npm run test:ws # Test WebSocket functionality -npm run test:errors # Test error logging -npm run test:integration # Test trading flow integration -npm run test:watch # Run tests in watch mode +npm test # Run all tests +npm run test:hunter # Test Hunter component +npm run test:position # Test PositionManager +npm run test:rate # Test rate limiting +npm run test:ws # Test WebSocket functionality +npm run test:errors # Test error logging +npm run test:integration # Test trading flow integration +npm run test:tranche # Test tranche system (basic) +npm run test:tranche:integration # Test tranche integration (comprehensive) +npm run test:tranche:all # Run all tranche tests +npm run test:watch # Run tests in watch mode # Utilities npm run optimize:ui # Run configuration optimizer @@ -80,10 +83,57 @@ npm run optimize:ui # Run configuration optimizer |-----------|----------|---------| | **Hunter** | `src/lib/bot/hunter.ts` | Monitors liquidation streams, triggers trades | | **PositionManager** | `src/lib/bot/positionManager.ts` | Manages positions, SL/TP orders, user data streams | +| **TrancheManager** | `src/lib/services/trancheManager.ts` | Tracks multiple position entries (tranches) per symbol | | **AsterBot** | `src/bot/index.ts` | Main orchestrator coordinating Hunter and PositionManager | | **StatusBroadcaster** | `src/bot/websocketServer.ts` | WebSocket server for real-time UI updates | | **ProcessManager** | `scripts/process-manager.js` | Cross-platform process lifecycle management | +### Multi-Tranche Position Management + +The bot includes an advanced **multi-tranche system** that tracks multiple virtual position entries per symbol: + +**What are Tranches?** +- Virtual position entries tracked locally while exchange sees one combined position +- Allows isolation of underwater positions (>5% loss by default) +- Continue trading fresh positions without adding to losers +- Better margin utilization and risk management + +**Key Components:** +- **Database Layer** (`src/lib/db/trancheDb.ts`): Tranche and event storage with SQLite +- **TrancheManager Service** (`src/lib/services/trancheManager.ts`): Core tranche lifecycle management +- **Hunter Integration**: Pre-trade limit checks, post-order tranche creation +- **PositionManager Integration**: Tranche closing on SL/TP fills, exchange synchronization +- **UI Dashboard** (`/tranches`): Real-time tranche visualization and management + +**Configuration (per symbol):** +```json +{ + "enableTrancheManagement": true, + "trancheIsolationThreshold": 5, // % loss before isolation + "maxTranches": 3, // Max active tranches + "maxIsolatedTranches": 2, // Max isolated tranches + "trancheStrategy": { + "closingStrategy": "FIFO", // FIFO, LIFO, WORST_FIRST, BEST_FIRST + "slTpStrategy": "NEWEST", // NEWEST, OLDEST, BEST_ENTRY, AVERAGE + "isolationAction": "HOLD" // Action when isolated + }, + "allowTrancheWhileIsolated": true, // Continue trading with isolated tranches + "trancheAutoCloseIsolated": false // Auto-close when recovered +} +``` + +**Testing:** +```bash +npm run test:tranche # Basic system tests +npm run test:tranche:integration # Full integration tests (100% passing) +npm run test:tranche:all # Run all tranche tests +``` + +**Documentation:** +- Implementation Plan: `docs/TRANCHE_IMPLEMENTATION_PLAN.md` +- Testing Guide: `docs/TRANCHE_TESTING.md` +- User Guide: `docs/TRANCHE_USER_GUIDE.md` (for end users) + ### Services (`src/lib/services/`) - **balanceService.ts**: Real-time balance tracking via WebSocket @@ -93,6 +143,7 @@ npm run optimize:ui # Run configuration optimizer - **configManager.ts**: Hot-reload configuration management - **pnlService.ts**: Real-time P&L tracking and session metrics - **thresholdMonitor.ts**: 60-second rolling volume threshold tracking +- **trancheManager.ts**: Multi-tranche position tracking and lifecycle management ### API Layer (`src/lib/api/`) @@ -265,6 +316,13 @@ config.default.json # Default configuration template - Includes stack traces, timestamps, and trading data - Accessible via web UI at `/errors` +**Tranche Database** (`src/lib/db/trancheDb.ts`): +- Stores all tranche entries and lifecycle events +- Tracks active, isolated, and closed tranches +- Audit trail via `tranche_events` table +- Indexed for performance (symbol, side, status, entry_time) +- Automatic cleanup of old closed tranches + ## Error Handling ### Custom Error Types (`src/lib/errors/TradingErrors.ts`) diff --git a/README.md b/README.md index 076db4c..849c2c9 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ A smart trading bot that monitors and trades liquidation events on Aster DEX. Fe - ๐Ÿ“ˆ **Real-time Liquidation Hunting** - Monitors and instantly trades liquidation events - ๐Ÿ’ฐ **Smart Position Management** - Automatic stop-loss and take-profit on every trade +- ๐ŸŽฏ **Multi-Tranche System** - Isolate losing positions while continuing to trade fresh entries - ๐Ÿงช **Paper Trading Mode** - Test strategies safely with simulated trades - ๐ŸŽจ **Beautiful Web Dashboard** - Monitor everything from a clean, modern UI - โšก **One-Click Setup** - Get running in under 2 minutes @@ -75,6 +76,7 @@ Access at http://localhost:3000 - **Dashboard** - Monitor positions and P&L - **Config** - Adjust all settings via UI +- **Tranches** - View and manage multi-tranche positions - **History** - View past trades ## โš™๏ธ Commands @@ -178,11 +180,133 @@ Found a bug in the dev branch? Help us improve! **Note**: Always start with paper mode when testing new beta features! +## ๐ŸŽฏ Advanced Features + +### Multi-Tranche Position Management + +The bot includes an intelligent **multi-tranche system** that dramatically improves trading performance when positions move against you: + +#### What are Tranches? + +Think of tranches as separate "sub-positions" within the same trading pair. Instead of one large position that you keep adding to, the bot tracks multiple independent entries: + +- **Position goes underwater (>5% loss)?** โ†’ Bot automatically **isolates** it +- **Continue trading?** โ†’ Bot opens **new tranches** without adding to the loser +- **Keep making profits?** โ†’ Trade fresh entries while holding positions recover +- **Better margin usage** โ†’ Don't let one bad position lock up all your capital + +#### Why Use Multi-Tranche? + +**Traditional Trading Problem:** +``` +Enter BTCUSDT LONG @ $50,000 +Price drops to $47,500 (-5%) +You're stuck: Can't trade more without adding to losing position +Miss opportunities while waiting for recovery +``` + +**With Multi-Tranche System:** +``` +Tranche #1: LONG @ $50,000 โ†’ Down 5% โ†’ ISOLATED (held separately) +Tranche #2: LONG @ $47,500 โ†’ Up 2% โ†’ CLOSE (+profit!) +Tranche #3: LONG @ $48,000 โ†’ Up 3% โ†’ CLOSE (+profit!) +Meanwhile, Tranche #1 recovers โ†’ Eventually closes at breakeven or profit +``` + +**Result:** You keep making money on new trades while bad positions recover naturally. + +#### Key Benefits + +โœ… **Isolate Losing Positions** - Underwater positions tracked separately +โœ… **Continue Trading** - Open fresh positions without adding to losers +โœ… **Better Margin Efficiency** - Don't lock up capital in losing trades +โœ… **Automatic Management** - Bot handles everything automatically +โœ… **Configurable Strategies** - Choose FIFO, LIFO, or close best/worst first +โœ… **Real-Time Monitoring** - Dashboard shows all tranches and their P&L + +#### How to Enable + +1. **Via Web UI** (Recommended): + - Go to http://localhost:3000/config + - Find your trading pair (e.g., BTCUSDT) + - Scroll to "Tranche Management Settings" + - Toggle "Enable Multi-Tranche Management" + - Configure settings: + - **Isolation Threshold**: When to isolate (default: 5% loss) + - **Max Tranches**: Max active positions (default: 3) + - **Max Isolated**: Max underwater positions before blocking new trades (default: 2) + - **Closing Strategy**: FIFO (oldest first), LIFO (newest first), WORST_FIRST, BEST_FIRST + - **SL/TP Strategy**: Which tranche's targets to use (NEWEST, OLDEST, BEST_ENTRY, AVERAGE) + +2. **Monitor Your Tranches**: + - Visit http://localhost:3000/tranches + - See all active, isolated, and closed tranches + - Real-time P&L tracking + - Event timeline showing tranche lifecycle + +#### Configuration Example + +```json +{ + "symbols": { + "BTCUSDT": { + "enableTrancheManagement": true, + "trancheIsolationThreshold": 5, + "maxTranches": 3, + "maxIsolatedTranches": 2, + "allowTrancheWhileIsolated": true, + "trancheStrategy": { + "closingStrategy": "FIFO", + "slTpStrategy": "NEWEST", + "isolationAction": "HOLD" + } + } + } +} +``` + +#### Safety & Risk Management + +The multi-tranche system includes built-in safety features: + +- **Position Limits**: Won't exceed max tranches per symbol +- **Isolation Blocking**: Stops new trades if too many positions are underwater +- **Exchange Sync**: Reconciles local tracking with exchange positions +- **Automatic Monitoring**: Checks every 10 seconds for positions needing isolation +- **Event Audit Trail**: Full history of every tranche action in database + +**โš ๏ธ Important Notes:** +- Start with **paper mode** to understand how tranches work +- Set conservative limits (3 max tranches, 2 max isolated is recommended) +- Higher isolation threshold (5-10%) prevents over-isolation +- Monitor the `/tranches` dashboard regularly + +#### Advanced Use Cases + +**Scalping Strategy:** +- Low isolation threshold (3%) +- High max tranches (5) +- LIFO closing (close newest first) +- Works great for quick in-and-out trades + +**Hold & Recover Strategy:** +- High isolation threshold (10%) +- Moderate max tranches (3) +- FIFO closing (close oldest first) +- Good for trending markets + +**Best Trade First:** +- BEST_FIRST closing strategy +- Take profits on winners quickly +- Hold losers for recovery +- Maximizes realized gains + ## ๐Ÿ›ก๏ธ Safety Features - Paper mode for testing - Automatic stop-loss/take-profit - Position size limits +- Multi-tranche isolation system - WebSocket auto-reconnection ## ๐ŸŒ Remote Access Configuration From 2159acccb303c8e22e5efb2a04430fb4206df85e Mon Sep 17 00:00:00 2001 From: birdbathd Date: Fri, 21 Nov 2025 11:46:26 +1000 Subject: [PATCH 21/30] fix: prevent duplicate liquidation events from inflating threshold counts - Properly close and remove listeners from old WebSocket before creating new connection - Add deduplication logic in thresholdMonitor based on eventTime, quantity, and price - Prevents multiple WebSocket connections from processing the same liquidation event - Fixes issue where single liquidations were being counted 50+ times due to duplicate events --- src/lib/bot/hunter.ts | 18 ++++++++++++++++++ src/lib/services/thresholdMonitor.ts | 13 +++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/lib/bot/hunter.ts b/src/lib/bot/hunter.ts index af36736..c0a261c 100644 --- a/src/lib/bot/hunter.ts +++ b/src/lib/bot/hunter.ts @@ -354,6 +354,24 @@ logWithTimestamp('Hunter: Running in paper mode without API keys - simulating li clearTimeout(this.wsInactivityTimeout); this.wsInactivityTimeout = null; } + if (this.statusLogInterval) { + clearInterval(this.statusLogInterval); + this.statusLogInterval = null; + } + + // CRITICAL: Close and remove all listeners from old WebSocket before creating new one + // This prevents duplicate event handlers from accumulating + if (this.ws) { + try { + this.ws.removeAllListeners(); + if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) { + this.ws.close(); + } + } catch (error) { + logErrorWithTimestamp('Hunter: Error closing old WebSocket:', error); + } + this.ws = null; + } this.ws = new WebSocket('wss://fstream.asterdex.com/ws/!forceOrder@arr'); diff --git a/src/lib/services/thresholdMonitor.ts b/src/lib/services/thresholdMonitor.ts index d8c6516..5e59de7 100644 --- a/src/lib/services/thresholdMonitor.ts +++ b/src/lib/services/thresholdMonitor.ts @@ -158,6 +158,19 @@ export class ThresholdMonitor extends EventEmitter { // BUY liquidation means shorts are getting liquidated, we might want to SELL (short) const isLongOpportunity = liquidation.side === 'SELL'; + // DEDUPLICATION: Check if this exact liquidation already exists based on eventTime and quantity + // This prevents duplicate WebSocket events from inflating the threshold count + const liquidationKey = `${liquidation.eventTime}_${liquidation.quantity}_${liquidation.price}`; + const targetArray = isLongOpportunity ? status.recentLiquidations.long : status.recentLiquidations.short; + const isDuplicate = targetArray.some(liq => + `${liq.eventTime}_${liq.quantity}_${liq.price}` === liquidationKey + ); + + if (isDuplicate) { + // Skip duplicate liquidation - don't add to threshold count + return status; + } + if (isLongOpportunity) { status.recentLiquidations.long.push(liquidation); } else { From 03032197dee5eef8677069a61e280a6e1af167c1 Mon Sep 17 00:00:00 2001 From: birdbathd Date: Fri, 21 Nov 2025 11:52:04 +1000 Subject: [PATCH 22/30] refactor: improve WebSocket reconnection logic to prevent reconnection loops - Add shouldReconnect flag to control automatic reconnection behavior - Prevent close handler from reconnecting when manually disconnecting - Track reconnection timeouts to avoid scheduling multiple reconnections - Temporarily disable auto-reconnect during intentional disconnects (inactivity, config changes) - Prevents reconnection cascades that could cause duplicate connections - Maintains deduplication safety mechanisms from previous commit --- src/lib/bot/hunter.ts | 44 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/src/lib/bot/hunter.ts b/src/lib/bot/hunter.ts index c0a261c..46f8641 100644 --- a/src/lib/bot/hunter.ts +++ b/src/lib/bot/hunter.ts @@ -40,6 +40,8 @@ export class Hunter extends EventEmitter { private wsInactivityTimeout: NodeJS.Timeout | null = null; // WebSocket inactivity detector private lastLiquidationTime: number = Date.now(); // Track last liquidation received private statusLogInterval: NodeJS.Timeout | null = null; // Periodic status logging + private shouldReconnect: boolean = true; // Flag to control automatic reconnection + private reconnectTimeout: NodeJS.Timeout | null = null; // Track scheduled reconnection constructor(config: Config, isHedgeMode: boolean = false) { super(); @@ -321,6 +323,13 @@ logWithTimestamp('Hunter: Running in paper mode without API keys - simulating li stop(): void { this.isRunning = false; + this.shouldReconnect = false; // Disable auto-reconnect on shutdown + + // Cancel any scheduled reconnections + if (this.reconnectTimeout) { + clearTimeout(this.reconnectTimeout); + this.reconnectTimeout = null; + } // Stop periodic cleanup this.stopPeriodicCleanup(); @@ -345,6 +354,12 @@ logWithTimestamp('Hunter: Running in paper mode without API keys - simulating li } private connectWebSocket(): void { + // Cancel any pending reconnection attempts + if (this.reconnectTimeout) { + clearTimeout(this.reconnectTimeout); + this.reconnectTimeout = null; + } + // Clean up any existing keepalive/inactivity timers if (this.wsKeepAliveInterval) { clearInterval(this.wsKeepAliveInterval); @@ -363,10 +378,17 @@ logWithTimestamp('Hunter: Running in paper mode without API keys - simulating li // This prevents duplicate event handlers from accumulating if (this.ws) { try { + // Temporarily disable auto-reconnect to prevent close event from triggering reconnection + const wasAutoReconnectEnabled = this.shouldReconnect; + this.shouldReconnect = false; + this.ws.removeAllListeners(); if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) { this.ws.close(); } + + // Restore auto-reconnect flag + this.shouldReconnect = wasAutoReconnectEnabled; } catch (error) { logErrorWithTimestamp('Hunter: Error closing old WebSocket:', error); } @@ -467,8 +489,10 @@ logWithTimestamp('Hunter: Running in paper mode without API keys - simulating li } // Clean up timers before reconnecting this.cleanupWebSocketTimers(); - // Reconnect after delay - setTimeout(() => this.connectWebSocket(), 5000); + // Reconnect after delay (only if auto-reconnect is enabled) + if (this.shouldReconnect && this.isRunning) { + this.reconnectTimeout = setTimeout(() => this.connectWebSocket(), 5000); + } }); this.ws.on('close', () => { @@ -476,9 +500,10 @@ logWithTimestamp('Hunter: Running in paper mode without API keys - simulating li // Clean up timers this.cleanupWebSocketTimers(); - if (this.isRunning) { - // Reconnect silently - close events are often normal (like during inactivity reconnect) - setTimeout(() => this.connectWebSocket(), 5000); + // Only reconnect if auto-reconnect is enabled and bot is running + // This prevents reconnection loops during manual disconnects + if (this.shouldReconnect && this.isRunning) { + this.reconnectTimeout = setTimeout(() => this.connectWebSocket(), 5000); } }); } @@ -496,10 +521,13 @@ logWithTimestamp('Hunter: Running in paper mode without API keys - simulating li logWarnWithTimestamp(`โš ๏ธ Hunter: No liquidations for ${minutesInactive} minutes. Reconnecting stream...`); - // Force reconnection + // Force reconnection (this is intentional, so we allow it) if (this.ws) { + // Temporarily disable auto-reconnect to prevent close handler from double-reconnecting + this.shouldReconnect = false; this.ws.close(); this.ws = null; + this.shouldReconnect = true; } this.connectWebSocket(); }, 5 * 60 * 1000); // 5 minutes @@ -518,6 +546,10 @@ logWithTimestamp('Hunter: Running in paper mode without API keys - simulating li clearInterval(this.statusLogInterval); this.statusLogInterval = null; } + if (this.reconnectTimeout) { + clearTimeout(this.reconnectTimeout); + this.reconnectTimeout = null; + } } private async handleLiquidationEvent(event: any): Promise { From 703fe2f22481f553fc4004808e47c349c55a9429 Mon Sep 17 00:00:00 2001 From: birdbathd Date: Sun, 23 Nov 2025 16:14:59 +1000 Subject: [PATCH 23/30] feat: enable tranche management UI configuration - Add tranche management fields to SymbolConfig type - Import and render TrancheSettingsSection in symbol config form - Add default tranche configuration values - Allows configuring isolation threshold, max tranches, and recovery settings per symbol --- src/components/SymbolConfigForm.tsx | 19 +++++++++++++++++++ src/lib/types.ts | 9 +++++++++ 2 files changed, 28 insertions(+) diff --git a/src/components/SymbolConfigForm.tsx b/src/components/SymbolConfigForm.tsx index 37e8389..77fa9d5 100644 --- a/src/components/SymbolConfigForm.tsx +++ b/src/components/SymbolConfigForm.tsx @@ -27,6 +27,7 @@ import { Database, } from 'lucide-react'; import { toast } from 'sonner'; +import { TrancheSettingsSection } from './TrancheSettingsSection'; interface SymbolConfigFormProps { onSave: (config: Config) => void; @@ -144,6 +145,14 @@ export default function SymbolConfigForm({ onSave, currentConfig }: SymbolConfig vwapProtection: false, // VWAP protection disabled by default vwapTimeframe: '1m', // Default to 1 minute timeframe vwapLookback: 100, // Default to 100 candles + // Multi-Tranche defaults (disabled by default) + enableTrancheManagement: false, + trancheIsolationThreshold: 5, + maxTranches: 3, + maxIsolatedTranches: 2, + allowTrancheWhileIsolated: true, + trancheAutoCloseIsolated: false, + trancheRecoveryThreshold: 0.5, }; }; @@ -1448,6 +1457,16 @@ export default function SymbolConfigForm({ onSave, currentConfig }: SymbolConfig
)} + + {/* Multi-Tranche Position Management */} +
+ + +
)} diff --git a/src/lib/types.ts b/src/lib/types.ts index bb20043..b9bb699 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -31,6 +31,15 @@ export interface SymbolConfig { useThreshold?: boolean; // Enable threshold-based triggering for this symbol (default: false) thresholdTimeWindow?: number; // Time window in ms for volume accumulation (default: 60000) thresholdCooldown?: number; // Cooldown period in ms between triggers (default: 30000) + + // Multi-Tranche Position Management + enableTrancheManagement?: boolean; // Enable tracking of multiple independent position entries + trancheIsolationThreshold?: number; // P&L % threshold to isolate underwater tranches (e.g., 5 for -5%) + maxTranches?: number; // Maximum number of active tranches per symbol/side (e.g., 3) + maxIsolatedTranches?: number; // Maximum number of isolated tranches allowed before blocking new trades + allowTrancheWhileIsolated?: boolean; // Allow opening new tranches while some are isolated + trancheAutoCloseIsolated?: boolean; // Automatically close isolated tranches when they recover + trancheRecoveryThreshold?: number; // P&L % threshold to auto-close recovered tranches (e.g., 0.5 for +0.5%) } export interface ApiCredentials { From f045004024ec58c4d86ea24911604cbbbae337c7 Mon Sep 17 00:00:00 2001 From: birdbathd Date: Sun, 23 Nov 2025 16:31:52 +1000 Subject: [PATCH 24/30] feat: enable tranche management system (untested) - Add tranche configuration fields to SymbolConfig type - Integrate TrancheSettingsSection into symbol config form - Initialize tranche database tables on startup - Add Tranches page to sidebar navigation with DashboardLayout - Fix symbol dropdown to show configured symbols from config - Fix onChange binding and null coalescing for trade size fields Note: Tranche system is implemented but requires testing with live positions --- src/app/tranches/page.tsx | 50 +++++++++++++++++++---------- src/components/SymbolConfigForm.tsx | 6 ++-- src/components/app-sidebar.tsx | 6 ++++ src/lib/db/database.ts | 12 +++++++ 4 files changed, 54 insertions(+), 20 deletions(-) diff --git a/src/app/tranches/page.tsx b/src/app/tranches/page.tsx index 7ff7d2d..35cf44c 100644 --- a/src/app/tranches/page.tsx +++ b/src/app/tranches/page.tsx @@ -1,6 +1,7 @@ 'use client'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; +import { DashboardLayout } from '@/components/dashboard-layout'; import { TrancheBreakdownCard } from '@/components/TrancheBreakdownCard'; import { TrancheTimeline } from '@/components/TrancheTimeline'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; @@ -8,28 +9,42 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Label } from '@/components/ui/label'; import { Badge } from '@/components/ui/badge'; -import { Layers, TrendingUp, AlertTriangle, Info } from 'lucide-react'; +import { TrendingUp, AlertTriangle, Info } from 'lucide-react'; export default function TranchesPage() { const [selectedSymbol, setSelectedSymbol] = useState('BTCUSDT'); const [selectedSide, setSelectedSide] = useState<'LONG' | 'SHORT'>('LONG'); + const [symbols, setSymbols] = useState(['BTCUSDT', 'ETHUSDT', 'BNBUSDT', 'SOLUSDT', 'XRPUSDT']); - // Common trading symbols - const symbols = ['BTCUSDT', 'ETHUSDT', 'BNBUSDT', 'SOLUSDT', 'XRPUSDT']; + // Fetch configured symbols from the config + useEffect(() => { + async function fetchConfiguredSymbols() { + try { + const response = await fetch('/api/config'); + if (response.ok) { + const config = await response.json(); + const configuredSymbols = Object.keys(config.symbols || {}); + if (configuredSymbols.length > 0) { + setSymbols(configuredSymbols); + // Set first configured symbol as default if current selection isn't in list + if (!configuredSymbols.includes(selectedSymbol)) { + setSelectedSymbol(configuredSymbols[0]); + } + } + } + } catch (error) { + console.error('Failed to fetch configured symbols:', error); + // Keep default symbols if fetch fails + } + } + fetchConfiguredSymbols(); + }, []); return ( -
- {/* Page Header */} -
-
- -
-

Multi-Tranche Management

-

- Track multiple position entries for better margin utilization -

-
-
+ +
+ {/* Info Card */} +
{/* Info Card */} @@ -173,6 +188,7 @@ export default function TranchesPage() {
-
+
+ ); } diff --git a/src/components/SymbolConfigForm.tsx b/src/components/SymbolConfigForm.tsx index 77fa9d5..d56c99c 100644 --- a/src/components/SymbolConfigForm.tsx +++ b/src/components/SymbolConfigForm.tsx @@ -294,8 +294,8 @@ export default function SymbolConfigForm({ onSave, currentConfig }: SymbolConfig [selectedSymbol]: hasLongSize || hasShortSize })); - setLongTradeSizeInput((hasLongSize && symbolConfig.longTradeSize !== undefined ? symbolConfig.longTradeSize : symbolConfig.tradeSize).toString()); - setShortTradeSizeInput((hasShortSize && symbolConfig.shortTradeSize !== undefined ? symbolConfig.shortTradeSize : symbolConfig.tradeSize).toString()); + setLongTradeSizeInput((hasLongSize && symbolConfig.longTradeSize !== undefined ? symbolConfig.longTradeSize : symbolConfig.tradeSize ?? 100).toString()); + setShortTradeSizeInput((hasShortSize && symbolConfig.shortTradeSize !== undefined ? symbolConfig.shortTradeSize : symbolConfig.tradeSize ?? 100).toString()); } else { setSymbolDetails(null); } @@ -1464,7 +1464,7 @@ export default function SymbolConfigForm({ onSave, currentConfig }: SymbolConfig handleSymbolChange(selectedSymbol, field, value)} />
diff --git a/src/components/app-sidebar.tsx b/src/components/app-sidebar.tsx index 9d3ab36..890c3bc 100644 --- a/src/components/app-sidebar.tsx +++ b/src/components/app-sidebar.tsx @@ -14,6 +14,7 @@ import { RefreshCw, Bug, Target, + Layers, } from "lucide-react" import { RateLimitSidebar } from "@/components/RateLimitSidebar" @@ -50,6 +51,11 @@ const navigation = [ icon: Settings, href: "/config", }, + { + title: "Tranches", + icon: Layers, + href: "/tranches", + }, { title: "Optimizer", icon: Target, diff --git a/src/lib/db/database.ts b/src/lib/db/database.ts index c7468ac..ce929e9 100644 --- a/src/lib/db/database.ts +++ b/src/lib/db/database.ts @@ -70,10 +70,22 @@ export class Database { console.error('Error creating schema:', err); } else { console.log('Database schema initialized'); + // Initialize tranche tables after main schema is ready + this.initTrancheTables(); } }); } + private async initTrancheTables(): Promise { + try { + const { initTrancheTables } = await import('./trancheDb'); + await initTrancheTables(); + console.log('Tranche tables initialized'); + } catch (error) { + console.error('Error initializing tranche tables:', error); + } + } + async run(sql: string, params: any[] = []): Promise { return new Promise((resolve, reject) => { this.db.run(sql, params, function(err) { From c5277623cd15923632465fdf55c702b425bdea89 Mon Sep 17 00:00:00 2001 From: birdbathd Date: Sun, 23 Nov 2025 16:39:46 +1000 Subject: [PATCH 25/30] feat: implement protective orders system Configuration: - Add protective order fields to SymbolConfig type - Create ProtectiveOrdersSection UI component - Support breakeven trim with configurable offset - Support multiple trim levels at different P&L targets Backend Implementation: - Create ProtectiveOrderService to manage protective orders - Place LIMIT orders with 'po_' prefix to avoid TP/SL conflicts - Integrate with PositionManager for automatic triggers - Monitor positions and place orders when price levels hit - Handle order fills and position closures - Filter protective orders from orphaned order cleanup Features: - Breakeven protection: Trim X% when price returns to entry - Multi-level trims: Set multiple profit/loss targets - Non-interfering: Uses separate order IDs, won't conflict with TP/SL - Auto-cleanup: Removes orders when positions close Note: Untested - requires live position testing --- src/components/ProtectiveOrdersSection.tsx | 228 +++++++++++++ src/components/SymbolConfigForm.tsx | 11 + src/lib/bot/positionManager.ts | 68 +++- src/lib/services/protectiveOrderService.ts | 372 +++++++++++++++++++++ src/lib/types.ts | 12 + 5 files changed, 690 insertions(+), 1 deletion(-) create mode 100644 src/components/ProtectiveOrdersSection.tsx create mode 100644 src/lib/services/protectiveOrderService.ts diff --git a/src/components/ProtectiveOrdersSection.tsx b/src/components/ProtectiveOrdersSection.tsx new file mode 100644 index 0000000..ab316c5 --- /dev/null +++ b/src/components/ProtectiveOrdersSection.tsx @@ -0,0 +1,228 @@ +'use client'; + +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Label } from '@/components/ui/label'; +import { Input } from '@/components/ui/input'; +import { Switch } from '@/components/ui/switch'; +import { Separator } from '@/components/ui/separator'; +import { Button } from '@/components/ui/button'; +import { Info, Plus, Trash2, Shield } from 'lucide-react'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; +import { Alert, AlertDescription } from '@/components/ui/alert'; + +interface ProtectiveOrdersSectionProps { + symbol: string; + config: any; + onChange: (field: string, value: any) => void; +} + +export function ProtectiveOrdersSection({ symbol, config, onChange }: ProtectiveOrdersSectionProps) { + const enabled = config.enableProtectiveOrders ?? false; + const breakeven = config.protectiveBreakeven ?? { enabled: false, triggerOffset: 0, trimPercent: 50 }; + const trimLevels = config.protectiveTrimLevels ?? []; + + const handleBreakevenChange = (field: string, value: any) => { + onChange('protectiveBreakeven', { + ...breakeven, + [field]: value, + }); + }; + + const addTrimLevel = () => { + const newLevel = { triggerPercent: 2, trimPercent: 25 }; + onChange('protectiveTrimLevels', [...trimLevels, newLevel]); + }; + + const removeTrimLevel = (index: number) => { + const updated = trimLevels.filter((_: any, i: number) => i !== index); + onChange('protectiveTrimLevels', updated); + }; + + const updateTrimLevel = (index: number, field: string, value: number) => { + const updated = [...trimLevels]; + updated[index] = { ...updated[index], [field]: value }; + onChange('protectiveTrimLevels', updated); + }; + + return ( + + + + + Protective Orders + + + Automatically trim portions of positions at specific price levels before TP/SL + + + + {/* Enable/Disable Toggle */} +
+
+ +

+ Place LIMIT orders to trim position size at breakeven or profit levels +

+
+ onChange('enableProtectiveOrders', checked)} + /> +
+ + {enabled && ( + <> + + + {/* Breakeven Protection */} +
+
+
+ +

+ Automatically trim position when price returns near entry +

+
+ handleBreakevenChange('enabled', checked)} + /> +
+ + {breakeven.enabled && ( +
+
+
+ + + + + + + +

+ 0 = exact breakeven, 1 = 1% profit, -1 = 1% loss from entry +

+
+
+
+
+ handleBreakevenChange('triggerOffset', parseFloat(e.target.value) || 0)} + placeholder="0" + /> +

+ Default: 0% (exact breakeven) +

+
+ +
+ + handleBreakevenChange('trimPercent', parseFloat(e.target.value) || 50)} + placeholder="50" + /> +

+ % of position to close (1-100%) +

+
+
+ )} +
+ + + + {/* Multi-Level Trims */} +
+
+
+ +

+ Set multiple profit/loss levels for position trimming +

+
+ +
+ + {trimLevels.length > 0 && ( +
+ {trimLevels.map((level: any, index: number) => ( +
+
+
+ + updateTrimLevel(index, 'triggerPercent', parseFloat(e.target.value) || 0)} + placeholder="2" + /> +
+
+ + updateTrimLevel(index, 'trimPercent', parseFloat(e.target.value) || 25)} + placeholder="25" + /> +
+
+ +
+ ))} +
+ )} +
+ + + + + How it works: Protective orders use LIMIT orders with the po_ prefix. + They won't interfere with your main TP/SL orders. These are complementary safety measures that + execute before your main exit targets. + + + + )} +
+
+ ); +} diff --git a/src/components/SymbolConfigForm.tsx b/src/components/SymbolConfigForm.tsx index d56c99c..4801ffc 100644 --- a/src/components/SymbolConfigForm.tsx +++ b/src/components/SymbolConfigForm.tsx @@ -28,6 +28,7 @@ import { } from 'lucide-react'; import { toast } from 'sonner'; import { TrancheSettingsSection } from './TrancheSettingsSection'; +import { ProtectiveOrdersSection } from './ProtectiveOrdersSection'; interface SymbolConfigFormProps { onSave: (config: Config) => void; @@ -1467,6 +1468,16 @@ export default function SymbolConfigForm({ onSave, currentConfig }: SymbolConfig onChange={(field, value) => handleSymbolChange(selectedSymbol, field, value)} />
+ + {/* Protective Orders */} +
+ + handleSymbolChange(selectedSymbol, field, value)} + /> +
)} diff --git a/src/lib/bot/positionManager.ts b/src/lib/bot/positionManager.ts index 1a8eede..2f81b50 100644 --- a/src/lib/bot/positionManager.ts +++ b/src/lib/bot/positionManager.ts @@ -12,6 +12,7 @@ import { errorLogger } from '../services/errorLogger'; import { getPriceService } from '../services/priceService'; import { invalidateIncomeCache } from '../api/income'; import { logWithTimestamp, logErrorWithTimestamp, logWarnWithTimestamp } from '../utils/timestamp'; +import { getProtectiveOrderService } from '../services/protectiveOrderService'; // Minimal local state - only track order IDs linked to positions interface PositionOrders { @@ -860,6 +861,11 @@ logErrorWithTimestamp(`PositionManager: Failed to ensure protection for ${symbol }); } + // Check for protective orders if enabled + this.checkProtectiveOrders(position).catch(error => { +logErrorWithTimestamp(`PositionManager: Failed to check protective orders for ${symbol}:`, error?.message); + }); + // Trigger balance refresh if position size changed if (sizeChanged) { this.refreshBalance(); @@ -919,6 +925,12 @@ logWithTimestamp(`PositionManager: Order cancellation already in progress for ${ this.positionOrders.delete(key); this.previousPositionSizes.delete(key); + // Clear protective orders for this position + const protectiveService = getProtectiveOrderService(); + if (protectiveService) { + protectiveService.clearProtectiveOrders(symbol, position.positionSide); + } + // Trigger balance refresh after position closure this.refreshBalance(); } @@ -950,6 +962,16 @@ logWithTimestamp(`PositionManager: ORDER_TRADE_UPDATE - Symbol: ${symbol}, Order // Check if this is a filled order that affects positions (SL/TP fills) if (orderStatus === 'FILLED' && order.rp) { // rp = realized profit (from exchange API) logWithTimestamp(`PositionManager: Reduce-only order filled for ${symbol}`); + + // Check if this was a protective order + const clientOrderId = order.c; + if (clientOrderId && clientOrderId.startsWith('po_')) { + const protectiveService = getProtectiveOrderService(); + if (protectiveService) { + protectiveService.handleOrderFilled(orderId); + } + } + // Trigger balance refresh after SL/TP execution this.refreshBalance(); } @@ -1944,6 +1966,11 @@ logWithTimestamp('PositionManager: Checking for orphaned and duplicate orders... const openOrders = await this.getOpenOrdersFromExchange(); const positions = await this.getPositionsFromExchange(); + // Filter out protective orders (they are managed by ProtectiveOrderService) + const managedOrders = openOrders.filter(order => { + return !order.clientOrderId || !order.clientOrderId.startsWith('po_'); + }); + // Create map of active positions with their position details const activePositions = new Map(); @@ -1975,7 +2002,7 @@ logWithTimestamp('PositionManager: Checking for orphaned and duplicate orders... // Find orphaned orders (reduce-only orders without matching positions) // Enhanced check considers order quantity matching - const orphanedOrders = openOrders.filter(order => { + const orphanedOrders = managedOrders.filter(order => { if (!order.reduceOnly) return false; const symbolDetails = symbolPositionDetails.get(order.symbol); @@ -2563,6 +2590,45 @@ logWithTimestamp('PositionManager: Manual cleanup triggered'); await this.cleanupOrphanedOrders(); } + // Check and place protective orders for a position + private async checkProtectiveOrders(position: ExchangePosition): Promise { + const protectiveService = getProtectiveOrderService(); + if (!protectiveService) { + return; // Service not initialized + } + + const symbol = position.symbol; + const symbolConfig = this.config.symbols[symbol]; + + if (!symbolConfig?.enableProtectiveOrders) { + return; // Not enabled for this symbol + } + + // Get current market price + const priceService = getPriceService(); + let currentPrice = parseFloat(position.markPrice); + + // If markPrice is 0 or stale, get from price service + if (currentPrice <= 0 || !priceService) { + try { + const priceData = priceService?.getMarkPrice(symbol); + if (priceData && priceData.markPrice) { + currentPrice = parseFloat(priceData.markPrice); + } + } catch (error) { + logErrorWithTimestamp(`PositionManager: Failed to get current price for ${symbol}:`, error); + return; + } + } + + if (currentPrice <= 0) { + return; // Invalid price + } + + // Check if protective orders should be placed + await protectiveService.checkPositionForProtectiveOrders(position, currentPrice); + } + // Manual methods public async closePosition(symbol: string, side: string): Promise { // Find the position in our current positions map diff --git a/src/lib/services/protectiveOrderService.ts b/src/lib/services/protectiveOrderService.ts new file mode 100644 index 0000000..35e292a --- /dev/null +++ b/src/lib/services/protectiveOrderService.ts @@ -0,0 +1,372 @@ +import { EventEmitter } from 'events'; +import { Config } from '../types'; +import { placeOrder } from '../api/orders'; +import { symbolPrecision } from '../utils/symbolPrecision'; +import { logWithTimestamp, logErrorWithTimestamp, logWarnWithTimestamp } from '../utils/timestamp'; +import { errorLogger } from './errorLogger'; + +// Exchange position interface (from positionManager) +interface ExchangePosition { + symbol: string; + positionAmt: string; + entryPrice: string; + markPrice: string; + unRealizedProfit: string; + liquidationPrice: string; + leverage: string; + marginType: string; + isolatedMargin: string; + isAutoAddMargin: string; + positionSide: string; + updateTime: number; +} + +interface ProtectiveOrder { + orderId: number; + symbol: string; + side: 'BUY' | 'SELL'; + positionSide: string; + triggerType: 'breakeven' | 'trim_level'; + triggerPercent: number; + quantity: number; + price: number; + createdAt: number; +} + +export class ProtectiveOrderService extends EventEmitter { + private config: Config; + private activeOrders: Map = new Map(); // key: "BTCUSDT_LONG" + private isRunning = false; + private monitorInterval?: NodeJS.Timeout; + + constructor(config: Config) { + super(); + this.config = config; + } + + public updateConfig(newConfig: Config): void { + this.config = newConfig; + } + + public start(): void { + if (this.isRunning) return; + this.isRunning = true; + + // Monitor positions every 10 seconds to place/update protective orders + this.monitorInterval = setInterval(() => { + this.checkAndPlaceProtectiveOrders().catch(error => { + logErrorWithTimestamp('ProtectiveOrderService: Error in monitor interval:', error); + }); + }, 10000); + + logWithTimestamp('ProtectiveOrderService: Started'); + } + + public stop(): void { + if (!this.isRunning) return; + this.isRunning = false; + + if (this.monitorInterval) { + clearInterval(this.monitorInterval); + this.monitorInterval = undefined; + } + + logWithTimestamp('ProtectiveOrderService: Stopped'); + } + + // Check if protective orders should be placed for a position + public async checkPositionForProtectiveOrders( + position: ExchangePosition, + currentPrice: number + ): Promise { + const symbol = position.symbol; + const symbolConfig = this.config.symbols[symbol]; + + if (!symbolConfig?.enableProtectiveOrders) { + return; // Protective orders not enabled for this symbol + } + + const posAmt = parseFloat(position.positionAmt); + if (Math.abs(posAmt) < 0.0001) { + return; // No position + } + + const entryPrice = parseFloat(position.entryPrice); + const isLong = posAmt > 0; + const key = this.getPositionKey(symbol, position.positionSide); + + // Calculate current P&L percentage + const pnlPercent = isLong + ? ((currentPrice - entryPrice) / entryPrice) * 100 + : ((entryPrice - currentPrice) / entryPrice) * 100; + + // Check if we should place breakeven protective order + if (symbolConfig.protectiveBreakeven?.enabled) { + await this.checkBreakevenOrder(position, currentPrice, pnlPercent, key); + } + + // Check if we should place trim level orders + if (symbolConfig.protectiveTrimLevels && symbolConfig.protectiveTrimLevels.length > 0) { + await this.checkTrimLevelOrders(position, currentPrice, pnlPercent, key); + } + } + + private async checkBreakevenOrder( + position: ExchangePosition, + currentPrice: number, + pnlPercent: number, + key: string + ): Promise { + const symbol = position.symbol; + const symbolConfig = this.config.symbols[symbol]; + const breakeven = symbolConfig.protectiveBreakeven!; + const entryPrice = parseFloat(position.entryPrice); + const posAmt = parseFloat(position.positionAmt); + const isLong = posAmt > 0; + + // Check if we already have a breakeven order + const existingOrders = this.activeOrders.get(key) || []; + const hasBreakevenOrder = existingOrders.some(o => o.triggerType === 'breakeven'); + + if (hasBreakevenOrder) { + return; // Already placed + } + + // Calculate trigger price with offset + const offsetMultiplier = 1 + (breakeven.triggerOffset / 100); + const triggerPrice = entryPrice * offsetMultiplier; + + // Check if current price has crossed the trigger + const shouldTrigger = isLong + ? currentPrice >= triggerPrice + : currentPrice <= triggerPrice; + + if (!shouldTrigger) { + return; // Not at trigger price yet + } + + // Calculate quantity to trim + const trimQuantity = Math.abs(posAmt) * (breakeven.trimPercent / 100); + const formattedQty = symbolPrecision.formatQuantity(symbol, trimQuantity); + + // Place protective order + try { + const side = isLong ? 'SELL' : 'BUY'; + const clientOrderId = `po_be_${symbol}_${Date.now()}`; + + const orderParams: any = { + symbol, + side, + type: 'LIMIT', + quantity: formattedQty, + price: symbolPrecision.formatPrice(symbol, triggerPrice), + timeInForce: 'GTC', + positionSide: position.positionSide, + reduceOnly: true, + newClientOrderId: clientOrderId, + }; + + const order = await placeOrder(orderParams, this.config.api); + + const protectiveOrder: ProtectiveOrder = { + orderId: order.orderId, + symbol, + side, + positionSide: position.positionSide, + triggerType: 'breakeven', + triggerPercent: breakeven.triggerOffset, + quantity: trimQuantity, + price: triggerPrice, + createdAt: Date.now(), + }; + + // Track the order + if (!this.activeOrders.has(key)) { + this.activeOrders.set(key, []); + } + this.activeOrders.get(key)!.push(protectiveOrder); + + logWithTimestamp( + `ProtectiveOrderService: Placed breakeven trim order for ${symbol} at ${triggerPrice.toFixed(2)} (${breakeven.trimPercent}% of position)` + ); + + this.emit('protectiveOrderPlaced', protectiveOrder); + } catch (error: any) { + logErrorWithTimestamp( + `ProtectiveOrderService: Failed to place breakeven order for ${symbol}:`, + error?.response?.data || error?.message + ); + + await errorLogger.logError(error instanceof Error ? error : new Error(String(error)), { + type: 'trading', + severity: 'medium', + context: { + component: 'ProtectiveOrderService', + symbol, + userAction: 'Place breakeven protective order', + }, + }); + } + } + + private async checkTrimLevelOrders( + position: ExchangePosition, + currentPrice: number, + pnlPercent: number, + key: string + ): Promise { + const symbol = position.symbol; + const symbolConfig = this.config.symbols[symbol]; + const trimLevels = symbolConfig.protectiveTrimLevels!; + const entryPrice = parseFloat(position.entryPrice); + const posAmt = parseFloat(position.positionAmt); + const isLong = posAmt > 0; + + const existingOrders = this.activeOrders.get(key) || []; + + // Check each trim level + for (const level of trimLevels) { + // Skip if we already have an order for this level + const hasLevelOrder = existingOrders.some( + o => o.triggerType === 'trim_level' && o.triggerPercent === level.triggerPercent + ); + + if (hasLevelOrder) { + continue; + } + + // Check if we've reached this P&L level + const shouldTrigger = isLong + ? pnlPercent >= level.triggerPercent + : pnlPercent >= level.triggerPercent; + + if (!shouldTrigger) { + continue; + } + + // Calculate trigger price based on P&L percentage + const priceMultiplier = 1 + (level.triggerPercent / 100); + const triggerPrice = isLong + ? entryPrice * priceMultiplier + : entryPrice * (2 - priceMultiplier); + + // Calculate quantity to trim (percentage of current position) + const currentPosQty = Math.abs(posAmt); + const trimQuantity = currentPosQty * (level.trimPercent / 100); + const formattedQty = symbolPrecision.formatQuantity(symbol, trimQuantity); + + // Place protective order + try { + const side = isLong ? 'SELL' : 'BUY'; + const clientOrderId = `po_tl_${symbol}_${level.triggerPercent}_${Date.now()}`; + + const orderParams: any = { + symbol, + side, + type: 'LIMIT', + quantity: formattedQty, + price: symbolPrecision.formatPrice(symbol, triggerPrice), + timeInForce: 'GTC', + positionSide: position.positionSide, + reduceOnly: true, + newClientOrderId: clientOrderId, + }; + + const order = await placeOrder(orderParams, this.config.api); + + const protectiveOrder: ProtectiveOrder = { + orderId: order.orderId, + symbol, + side, + positionSide: position.positionSide, + triggerType: 'trim_level', + triggerPercent: level.triggerPercent, + quantity: trimQuantity, + price: triggerPrice, + createdAt: Date.now(), + }; + + // Track the order + if (!this.activeOrders.has(key)) { + this.activeOrders.set(key, []); + } + this.activeOrders.get(key)!.push(protectiveOrder); + + logWithTimestamp( + `ProtectiveOrderService: Placed trim level order for ${symbol} at ${triggerPrice.toFixed(2)} (${level.trimPercent}% at ${level.triggerPercent}% P&L)` + ); + + this.emit('protectiveOrderPlaced', protectiveOrder); + } catch (error: any) { + logErrorWithTimestamp( + `ProtectiveOrderService: Failed to place trim level order for ${symbol}:`, + error?.response?.data || error?.message + ); + + await errorLogger.logError(error instanceof Error ? error : new Error(String(error)), { + type: 'trading', + severity: 'medium', + context: { + component: 'ProtectiveOrderService', + symbol, + userAction: 'Place trim level protective order', + }, + }); + } + } + } + + // Remove protective orders when position closes + public clearProtectiveOrders(symbol: string, positionSide: string): void { + const key = this.getPositionKey(symbol, positionSide); + this.activeOrders.delete(key); + logWithTimestamp(`ProtectiveOrderService: Cleared protective orders for ${key}`); + } + + // Handle order fill events to remove from tracking + public handleOrderFilled(orderId: number): void { + for (const [key, orders] of this.activeOrders.entries()) { + const index = orders.findIndex(o => o.orderId === orderId); + if (index !== -1) { + const order = orders[index]; + orders.splice(index, 1); + logWithTimestamp( + `ProtectiveOrderService: Protective order filled - ${order.symbol} ${order.triggerType} at ${order.price.toFixed(2)}` + ); + this.emit('protectiveOrderFilled', order); + break; + } + } + } + + private async checkAndPlaceProtectiveOrders(): Promise { + // This will be called by position manager when it has position updates + // For now, it's a placeholder for future integration + } + + private getPositionKey(symbol: string, positionSide: string): string { + return `${symbol}_${positionSide}`; + } + + // Get all active protective orders for a position + public getProtectiveOrders(symbol: string, positionSide: string): ProtectiveOrder[] { + const key = this.getPositionKey(symbol, positionSide); + return this.activeOrders.get(key) || []; + } +} + +// Singleton instance +let protectiveOrderServiceInstance: ProtectiveOrderService | null = null; + +export function getProtectiveOrderService(): ProtectiveOrderService | null { + return protectiveOrderServiceInstance; +} + +export function initializeProtectiveOrderService(config: Config): ProtectiveOrderService { + if (!protectiveOrderServiceInstance) { + protectiveOrderServiceInstance = new ProtectiveOrderService(config); + } else { + protectiveOrderServiceInstance.updateConfig(config); + } + return protectiveOrderServiceInstance; +} diff --git a/src/lib/types.ts b/src/lib/types.ts index b9bb699..aae66a4 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -40,6 +40,18 @@ export interface SymbolConfig { allowTrancheWhileIsolated?: boolean; // Allow opening new tranches while some are isolated trancheAutoCloseIsolated?: boolean; // Automatically close isolated tranches when they recover trancheRecoveryThreshold?: number; // P&L % threshold to auto-close recovered tranches (e.g., 0.5 for +0.5%) + + // Protective Orders (partial position trimming) + enableProtectiveOrders?: boolean; // Enable automatic position trimming at specific levels + protectiveBreakeven?: { + enabled: boolean; // Trim at breakeven + triggerOffset: number; // % offset from entry (0 = exact breakeven, 1 = 1% profit, -1 = 1% loss) + trimPercent: number; // % of position to close (e.g., 50 for 50%) + }; + protectiveTrimLevels?: Array<{ + triggerPercent: number; // PnL % to trigger (can be negative for loss protection) + trimPercent: number; // % of remaining position to close + }>; } export interface ApiCredentials { From 2d97d315c44ed6963564fbd7f36a079217a9ccb5 Mon Sep 17 00:00:00 2001 From: birdbathd Date: Sun, 23 Nov 2025 18:01:29 +1000 Subject: [PATCH 26/30] Add timestamps to logs UI and initialize ProtectiveOrderService - Fixed log parsing to include proper timestamps with milliseconds - Generate unique log IDs for each entry - Added ProtectiveOrderService initialization in bot startup - Service now starts when any symbol has enableProtectiveOrders=true - Both TrancheManager and ProtectiveOrderService log their startup status - Logs now show HH:MM:SS.mmm format for easy reading --- src/app/api/logs/route.ts | 153 +++++++++ src/app/api/positions/protect/route.ts | 103 +++++++ src/app/logs/page.tsx | 341 +++++++++++++++++++++ src/bot/index.ts | 21 ++ src/components/PositionTable.tsx | 134 +++++++- src/components/ProtectPositionModal.tsx | 215 +++++++++++++ src/components/app-sidebar.tsx | 6 + src/lib/services/logStore.ts | 124 ++++++++ src/lib/services/protectiveOrderService.ts | 228 +++++++++++++- src/lib/utils/timestamp.ts | 72 +++++ src/middleware.ts | 2 +- 11 files changed, 1383 insertions(+), 16 deletions(-) create mode 100644 src/app/api/logs/route.ts create mode 100644 src/app/api/positions/protect/route.ts create mode 100644 src/app/logs/page.tsx create mode 100644 src/components/ProtectPositionModal.tsx create mode 100644 src/lib/services/logStore.ts diff --git a/src/app/api/logs/route.ts b/src/app/api/logs/route.ts new file mode 100644 index 0000000..8fcbfc7 --- /dev/null +++ b/src/app/api/logs/route.ts @@ -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 } + ); + } +} diff --git a/src/app/api/positions/protect/route.ts b/src/app/api/positions/protect/route.ts new file mode 100644 index 0000000..54ddf58 --- /dev/null +++ b/src/app/api/positions/protect/route.ts @@ -0,0 +1,103 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { protectiveOrderService } from '@/lib/services/protectiveOrderService'; + +export const dynamic = 'force-dynamic'; + +/** + * POST /api/positions/protect + * Activate protective orders (breakeven + trim levels) for a specific position + */ +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { symbol, side, entryPrice, quantity, settings } = body; + + if (!symbol || !side) { + return NextResponse.json( + { success: false, error: 'Symbol and side are required' }, + { status: 400 } + ); + } + + if (typeof entryPrice !== 'number' || entryPrice <= 0) { + return NextResponse.json( + { success: false, error: 'Valid entry price is required' }, + { status: 400 } + ); + } + + if (typeof quantity !== 'number' || quantity <= 0) { + return NextResponse.json( + { success: false, error: 'Valid quantity is required' }, + { status: 400 } + ); + } + + if (!settings) { + return NextResponse.json( + { success: false, error: 'Protection settings are required' }, + { status: 400 } + ); + } + + // Validate settings structure + if (typeof settings.enableBreakeven !== 'boolean') { + return NextResponse.json( + { success: false, error: 'Invalid protection settings: enableBreakeven must be boolean' }, + { status: 400 } + ); + } + + if (!Array.isArray(settings.trimLevels)) { + return NextResponse.json( + { success: false, error: 'Invalid protection settings: trimLevels must be an array' }, + { status: 400 } + ); + } + + // Validate trim levels + for (const level of settings.trimLevels) { + if (typeof level.profitPercent !== 'number' || level.profitPercent <= 0) { + return NextResponse.json( + { success: false, error: 'Invalid trim level: profitPercent must be a positive number' }, + { status: 400 } + ); + } + if (typeof level.trimPercent !== 'number' || level.trimPercent <= 0 || level.trimPercent > 100) { + return NextResponse.json( + { success: false, error: 'Invalid trim level: trimPercent must be between 0 and 100' }, + { status: 400 } + ); + } + } + + // Activate protection via the service + await protectiveOrderService.activateProtection( + symbol, + side, + entryPrice, + quantity, + settings + ); + + return NextResponse.json({ + success: true, + message: 'Protection activated successfully', + details: { + symbol, + side, + breakeven: settings.enableBreakeven, + trimLevels: settings.trimLevels.length, + }, + }); + } catch (error) { + console.error('[API] Error activating protection:', error); + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Failed to activate protection', + }, + { status: 500 } + ); + } +} diff --git a/src/app/logs/page.tsx b/src/app/logs/page.tsx new file mode 100644 index 0000000..1d3d41c --- /dev/null +++ b/src/app/logs/page.tsx @@ -0,0 +1,341 @@ +'use client'; + +import React, { useEffect, useState, useRef } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Input } from '@/components/ui/input'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { + ArrowLeft, + Trash2, + RefreshCw, + Search, + Info, + AlertTriangle, + XCircle, + Pause, + Play, +} from 'lucide-react'; +import { useRouter } from 'next/navigation'; +import { toast } from 'sonner'; +import { DashboardLayout } from '@/components/dashboard-layout'; + +interface LogEntry { + id: string; + timestamp: number; + timestampFormatted: string; + level: 'info' | 'warn' | 'error'; + component: string; + message: string; + data?: any; +} + +export default function LogsPage() { + const router = useRouter(); + const [logs, setLogs] = useState([]); + const [components, setComponents] = useState([]); + const [loading, setLoading] = useState(true); + const [filters, setFilters] = useState({ + component: '', + level: '', + }); + const [searchQuery, setSearchQuery] = useState(''); + const [autoScroll, setAutoScroll] = useState(true); + const [isPaused, setIsPaused] = useState(false); + const logsEndRef = useRef(null); + const lastTimestamp = useRef(0); + + const fetchLogs = async (since?: number) => { + try { + const params = new URLSearchParams(); + if (filters.component) params.append('component', filters.component); + if (filters.level) params.append('level', filters.level); + if (since) params.append('since', since.toString()); + + const response = await fetch(`/api/logs?${params}`); + const data = await response.json(); + + if (data.success) { + if (since) { + // Append new logs + setLogs(prev => { + const combined = [...data.logs.reverse(), ...prev]; + // Keep max 1000 logs in UI + return combined.slice(0, 1000); + }); + } else { + // Full refresh + setLogs(data.logs.reverse()); + } + setComponents(data.components); + + // Update last timestamp + if (data.logs.length > 0) { + lastTimestamp.current = Math.max(...data.logs.map((l: LogEntry) => l.timestamp)); + } + } + } catch (error) { + console.error('Failed to fetch logs:', error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchLogs(); + }, [filters]); + + useEffect(() => { + if (isPaused) return; + + // Poll for new logs every 2 seconds + const interval = setInterval(() => { + if (lastTimestamp.current > 0) { + fetchLogs(lastTimestamp.current); + } + }, 2000); + + return () => clearInterval(interval); + }, [isPaused, filters]); + + useEffect(() => { + if (autoScroll && !isPaused) { + logsEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + } + }, [logs, autoScroll, isPaused]); + + const handleClearLogs = async () => { + if (!confirm('Are you sure you want to clear all logs?')) return; + + try { + const response = await fetch('/api/logs', { method: 'DELETE' }); + const data = await response.json(); + + if (data.success) { + setLogs([]); + lastTimestamp.current = 0; + toast.success('Logs cleared'); + } + } catch (error) { + console.error('Failed to clear logs:', error); + toast.error('Failed to clear logs'); + } + }; + + const handleRefresh = () => { + lastTimestamp.current = 0; + setLoading(true); + fetchLogs(); + }; + + const getLevelIcon = (level: string) => { + switch (level) { + case 'error': + return ; + case 'warn': + return ; + case 'info': + default: + return ; + } + }; + + const getLevelBadgeVariant = (level: string) => { + switch (level) { + case 'error': + return 'destructive'; + case 'warn': + return 'outline'; + case 'info': + default: + return 'secondary'; + } + }; + + const filteredLogs = logs.filter(log => { + if (searchQuery) { + const query = searchQuery.toLowerCase(); + return ( + log.message.toLowerCase().includes(query) || + log.component.toLowerCase().includes(query) + ); + } + return true; + }); + + return ( + +
+
+
+ +

System Logs

+
+
+ + + +
+
+ +
+
+ + setSearchQuery(e.target.value)} + className="pl-8" + /> +
+ + + + + +
+ setAutoScroll(e.target.checked)} + className="cursor-pointer" + /> + +
+
+ + + + + Logs ({filteredLogs.length}) + {isPaused && ( + + Paused + + )} + + + +
+ {loading && logs.length === 0 ? ( +
Loading logs...
+ ) : filteredLogs.length === 0 ? ( +
+ No logs found. {searchQuery && 'Try adjusting your search.'} +
+ ) : ( +
+ {filteredLogs.map((log) => ( +
+ + {log.timestampFormatted} + + {getLevelIcon(log.level)} + + {log.component} + + {log.message} + {log.data && ( +
+ data +
+                            {JSON.stringify(log.data, null, 2)}
+                          
+
+ )} +
+ ))} +
+
+ )} +
+ + +
+ + ); +} diff --git a/src/bot/index.ts b/src/bot/index.ts index 200e2fc..36f56c4 100644 --- a/src/bot/index.ts +++ b/src/bot/index.ts @@ -461,6 +461,27 @@ logErrorWithTimestamp('โš ๏ธ Position Manager failed to start:', error.message logWithTimestamp('โ„น๏ธ Tranche Management disabled for all symbols'); } + // Initialize Protective Order Service (if enabled for any symbol) + const protectiveEnabledSymbols = Object.entries(this.config.symbols).filter( + ([_symbol, config]) => config.enableProtectiveOrders + ); + + if (protectiveEnabledSymbols.length > 0) { + try { + const { initializeProtectiveOrderService } = await import('../lib/services/protectiveOrderService'); + const protectiveOrderService = initializeProtectiveOrderService(this.config); + protectiveOrderService.start(); + + logWithTimestamp(`โœ… Protective Order Service initialized for ${protectiveEnabledSymbols.length} symbol(s): ${protectiveEnabledSymbols.map(([s]) => s).join(', ')}`); + } catch (error: any) { + logErrorWithTimestamp('โš ๏ธ Protective Order Service failed to start:', error.message); + this.statusBroadcaster.addError(`Protective Order Service: ${error.message}`); + // Continue without protective orders + } + } else { + logWithTimestamp('โ„น๏ธ Protective Orders disabled for all symbols'); + } + // Initialize Hunter (or reuse existing instance to prevent duplicate listeners) if (!this.hunter) { this.hunter = new Hunter(this.config, this.isHedgeMode); diff --git a/src/components/PositionTable.tsx b/src/components/PositionTable.tsx index f469504..cbeb4a3 100644 --- a/src/components/PositionTable.tsx +++ b/src/components/PositionTable.tsx @@ -10,6 +10,7 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/comp import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { toast } from 'sonner'; +import { ProtectPositionModal, ProtectiveSettings } from '@/components/ProtectPositionModal'; import websocketService from '@/lib/services/websocketService'; import { useConfig } from '@/components/ConfigProvider'; import { useSymbolPrecision } from '@/hooks/useSymbolPrecision'; @@ -69,6 +70,19 @@ export default function PositionTable({ quantity: 0, }); const [isClosingPosition, setIsClosingPosition] = useState(false); + const [protectPositionModal, setProtectPositionModal] = useState<{ + isOpen: boolean; + position: { + symbol: string; + side: 'LONG' | 'SHORT'; + quantity: number; + entryPrice: number; + markPrice: number; + } | null; + }>({ + isOpen: false, + position: null, + }); const { config } = useConfig(); const { formatPrice, formatQuantity, formatPriceWithCommas } = useSymbolPrecision(); @@ -327,6 +341,78 @@ export default function PositionTable({ }); }, []); + // Handle protect position + const handleProtectPosition = useCallback((position: Position) => { + setProtectPositionModal({ + isOpen: true, + position: { + symbol: position.symbol, + side: position.side, + quantity: position.quantity, + entryPrice: position.entryPrice, + markPrice: position.markPrice, + }, + }); + }, []); + + const handleProtectConfirm = useCallback(async (settings: ProtectiveSettings) => { + if (!protectPositionModal.position) return; + + try { + const response = await fetch('/api/positions/protect', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + symbol: protectPositionModal.position.symbol, + side: protectPositionModal.position.side, + entryPrice: protectPositionModal.position.entryPrice, + quantity: protectPositionModal.position.quantity, + settings, + }), + }); + + const result = await response.json(); + + if (result.success) { + toast.success(`Protection activated for ${protectPositionModal.position.symbol}`, { + description: settings.enableBreakeven + ? `Breakeven order and ${settings.trimLevels.length} trim level(s) set` + : `${settings.trimLevels.length} trim level(s) set`, + duration: 5000, + }); + } else { + showTradingError( + 'Failed to activate protection', + result.error || 'An unknown error occurred', + { + symbol: protectPositionModal.position.symbol, + component: 'PositionTable', + rawError: result, + } + ); + } + } catch (error) { + console.error('Error activating protection:', error); + showApiError( + 'Network error', + 'Failed to connect to the server', + { + symbol: protectPositionModal.position.symbol, + component: 'PositionTable', + rawError: error, + } + ); + } finally { + setProtectPositionModal({ isOpen: false, position: null }); + } + }, [protectPositionModal]); + + const handleProtectCancel = useCallback(() => { + setProtectPositionModal({ isOpen: false, position: null }); + }, []); + // Use passed positions if available, otherwise use fetched positions // Apply live mark prices to calculate real-time PnL const displayPositions = (positions.length > 0 ? positions : realPositions).map(position => { @@ -613,18 +699,32 @@ export default function PositionTable({
- +
+ + +
); @@ -702,6 +802,16 @@ export default function PositionTable({ + + {/* Protect Position Modal */} + {protectPositionModal.position && ( + + )} ); } \ No newline at end of file diff --git a/src/components/ProtectPositionModal.tsx b/src/components/ProtectPositionModal.tsx new file mode 100644 index 0000000..2498a73 --- /dev/null +++ b/src/components/ProtectPositionModal.tsx @@ -0,0 +1,215 @@ +'use client'; + +import { useState } from 'react'; +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Label } from '@/components/ui/label'; +import { Input } from '@/components/ui/input'; +import { Switch } from '@/components/ui/switch'; +import { Separator } from '@/components/ui/separator'; +import { Shield, Plus, Trash2, Info } from 'lucide-react'; +import { Alert, AlertDescription } from '@/components/ui/alert'; + +interface ProtectPositionModalProps { + isOpen: boolean; + onClose: () => void; + position: { + symbol: string; + side: 'LONG' | 'SHORT'; + quantity: number; + entryPrice: number; + markPrice: number; + } | null; + onConfirm: (settings: ProtectiveSettings) => Promise; +} + +export interface ProtectiveSettings { + enableBreakeven: boolean; + breakevenTrimPercent?: number; + trimLevels: Array<{ + profitPercent: number; + trimPercent: number; + }>; +} + +export function ProtectPositionModal({ isOpen, onClose, position, onConfirm }: ProtectPositionModalProps) { + const [isSubmitting, setIsSubmitting] = useState(false); + const [breakevenEnabled, setBreakevenEnabled] = useState(true); + const [breakevenTrim, setBreakevenTrim] = useState(50); + const [trimLevels, setTrimLevels] = useState>([]); + + const handleAddTrimLevel = () => { + setTrimLevels([...trimLevels, { profitPercent: 2, trimPercent: 25 }]); + }; + + const handleRemoveTrimLevel = (index: number) => { + setTrimLevels(trimLevels.filter((_, i) => i !== index)); + }; + + const handleUpdateTrimLevel = (index: number, field: 'profitPercent' | 'trimPercent', value: number) => { + const updated = [...trimLevels]; + updated[index][field] = value; + setTrimLevels(updated); + }; + + const handleSubmit = async () => { + setIsSubmitting(true); + try { + await onConfirm({ + enableBreakeven: breakevenEnabled, + breakevenTrimPercent: breakevenTrim, + trimLevels, + }); + onClose(); + } catch (error) { + console.error('Failed to activate protection:', error); + } finally { + setIsSubmitting(false); + } + }; + + if (!position) return null; + + return ( + + + + + + Protect Position - {position.symbol} + + + Set protective trim levels to automatically reduce position size at specific price points + + + +
+ {/* Position Info */} +
+
+ Side: + {position.side} +
+
+ Quantity: + {position.quantity} +
+
+ Entry: + ${position.entryPrice.toFixed(2)} +
+
+ Current: + ${position.markPrice.toFixed(2)} +
+
+ + + + {/* Breakeven Protection */} +
+
+
+ +

Trim position when price returns near entry

+
+ +
+ + {breakevenEnabled && ( +
+
+ + setBreakevenTrim(parseFloat(e.target.value) || 50)} + placeholder="50" + /> +

% of position to close

+
+
+ )} +
+ + + + {/* Additional Trim Levels */} +
+
+
+ +

Set multiple profit/loss targets

+
+ +
+ + {trimLevels.length > 0 && ( +
+ {trimLevels.map((level, index) => ( +
+
+
+ + handleUpdateTrimLevel(index, 'profitPercent', parseFloat(e.target.value) || 0)} + placeholder="2" + /> +
+
+ + handleUpdateTrimLevel(index, 'trimPercent', parseFloat(e.target.value) || 25)} + placeholder="25" + /> +
+
+ +
+ ))} +
+ )} +
+ + + + + Protective orders will be placed as LIMIT orders that execute when price hits your targets. + They won't interfere with your existing TP/SL orders. + + +
+ + + + + +
+
+ ); +} diff --git a/src/components/app-sidebar.tsx b/src/components/app-sidebar.tsx index 890c3bc..1e26dd3 100644 --- a/src/components/app-sidebar.tsx +++ b/src/components/app-sidebar.tsx @@ -15,6 +15,7 @@ import { Bug, Target, Layers, + FileText, } from "lucide-react" import { RateLimitSidebar } from "@/components/RateLimitSidebar" @@ -66,6 +67,11 @@ const navigation = [ icon: BookOpen, href: "/wiki", }, + { + title: "System Logs", + icon: FileText, + href: "/logs", + }, { title: "Error Logs", icon: Bug, diff --git a/src/lib/services/logStore.ts b/src/lib/services/logStore.ts new file mode 100644 index 0000000..41adf82 --- /dev/null +++ b/src/lib/services/logStore.ts @@ -0,0 +1,124 @@ +/** + * In-memory log storage service for UI consumption + * Stores recent logs in a circular buffer with categorization + */ + +export interface LogEntry { + id: string; + timestamp: number; + timestampFormatted: string; + level: 'info' | 'warn' | 'error'; + component: string; + message: string; + data?: any; +} + +class LogStore { + private static instance: LogStore; + private logs: LogEntry[] = []; + private maxLogs = 1000; // Keep last 1000 logs + private logId = 0; + + private constructor() {} + + public static getInstance(): LogStore { + if (!LogStore.instance) { + LogStore.instance = new LogStore(); + } + return LogStore.instance; + } + + /** + * Add a log entry to the store + */ + public addLog( + level: 'info' | 'warn' | 'error', + component: string, + message: string, + data?: any + ): void { + const now = new Date(); + const entry: LogEntry = { + id: `${Date.now()}-${this.logId++}`, + timestamp: now.getTime(), + timestampFormatted: this.formatTimestamp(now), + level, + component, + message, + data, + }; + + this.logs.push(entry); + + // Maintain circular buffer + if (this.logs.length > this.maxLogs) { + this.logs.shift(); + } + } + + /** + * Get logs with optional filtering + */ + public getLogs(params?: { + component?: string; + level?: 'info' | 'warn' | 'error'; + limit?: number; + since?: number; // timestamp in ms + }): LogEntry[] { + let filtered = [...this.logs]; + + if (params?.component) { + const componentLower = params.component.toLowerCase(); + filtered = filtered.filter(log => + log.component.toLowerCase().includes(componentLower) + ); + } + + if (params?.level) { + filtered = filtered.filter(log => log.level === params.level); + } + + if (params?.since !== undefined) { + filtered = filtered.filter(log => log.timestamp >= params.since!); + } + + // Return most recent first + filtered.reverse(); + + if (params?.limit) { + filtered = filtered.slice(0, params.limit); + } + + return filtered; + } + + /** + * Get available components for filtering + */ + public getComponents(): string[] { + const components = new Set(); + this.logs.forEach(log => components.add(log.component)); + return Array.from(components).sort(); + } + + /** + * Clear all logs + */ + public clear(): void { + this.logs = []; + this.logId = 0; + } + + /** + * Format timestamp for display + */ + private formatTimestamp(date: Date): string { + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + const milliseconds = String(date.getMilliseconds()).padStart(3, '0'); + return `${hours}:${minutes}:${seconds}.${milliseconds}`; + } +} + +export const logStore = LogStore.getInstance(); diff --git a/src/lib/services/protectiveOrderService.ts b/src/lib/services/protectiveOrderService.ts index 35e292a..73f4d3c 100644 --- a/src/lib/services/protectiveOrderService.ts +++ b/src/lib/services/protectiveOrderService.ts @@ -22,7 +22,7 @@ interface ExchangePosition { } interface ProtectiveOrder { - orderId: number; + orderId: string; symbol: string; side: 'BUY' | 'SELL'; positionSide: string; @@ -74,6 +74,217 @@ export class ProtectiveOrderService extends EventEmitter { logWithTimestamp('ProtectiveOrderService: Stopped'); } + /** + * Activate protective orders for a specific position with custom settings + * This is used by the UI when a user manually activates protection + */ + public async activateProtection( + symbol: string, + side: 'LONG' | 'SHORT', + entryPrice: number, + currentQuantity: number, + settings: { + enableBreakeven: boolean; + breakevenTrimPercent?: number; + trimLevels: Array<{ profitPercent: number; trimPercent: number }>; + } + ): Promise { + if (currentQuantity <= 0) { + throw new Error('Invalid position quantity'); + } + + // Create a mock position for the protective order logic + const positionSide = side; // 'LONG' or 'SHORT' + const posAmt = side === 'LONG' ? currentQuantity : -currentQuantity; + + const mockPosition: ExchangePosition = { + symbol, + positionAmt: posAmt.toString(), + entryPrice: entryPrice.toString(), + markPrice: entryPrice.toString(), // We'll use current market price + unRealizedProfit: '0', + liquidationPrice: '0', + leverage: '10', + marginType: 'isolated', + isolatedMargin: '0', + isAutoAddMargin: 'false', + positionSide: positionSide, + updateTime: Date.now(), + }; + + const key = this.getPositionKey(symbol, positionSide); + + // Clear any existing protective orders for this position + this.clearProtectiveOrders(symbol, positionSide); + + logWithTimestamp( + `ProtectiveOrderService: Activating protection for ${symbol} ${side} - Breakeven: ${settings.enableBreakeven}, Trim levels: ${settings.trimLevels.length}` + ); + + // Place breakeven order if enabled + if (settings.enableBreakeven) { + const trimPercent = settings.breakevenTrimPercent || 25; // Default 25% + await this.placeBreakevenOrder(mockPosition, entryPrice, trimPercent, key); + } + + // Place trim level orders + for (const level of settings.trimLevels) { + await this.placeTrimLevelOrder( + mockPosition, + entryPrice, + level.profitPercent, + level.trimPercent, + key + ); + } + + logWithTimestamp( + `ProtectiveOrderService: Protection activated for ${symbol} ${side}` + ); + } + + /** + * Place a breakeven protective order + */ + private async placeBreakevenOrder( + position: ExchangePosition, + entryPrice: number, + trimPercent: number, + key: string + ): Promise { + const symbol = position.symbol; + const posAmt = parseFloat(position.positionAmt); + const isLong = posAmt > 0; + + // Breakeven is exactly at entry price + const triggerPrice = entryPrice; + + // Calculate quantity to trim + const trimQuantity = Math.abs(posAmt) * (trimPercent / 100); + const formattedQty = symbolPrecision.formatQuantity(symbol, trimQuantity); + + try { + const side = isLong ? 'SELL' : 'BUY'; + const clientOrderId = `po_be_${symbol}_${Date.now()}`; + + const orderParams: any = { + symbol, + side, + type: 'LIMIT', + quantity: formattedQty, + price: symbolPrecision.formatPrice(symbol, triggerPrice), + timeInForce: 'GTC', + positionSide: position.positionSide, + reduceOnly: true, + newClientOrderId: clientOrderId, + }; + + const order = await placeOrder(orderParams, this.config.api); + + const protectiveOrder: ProtectiveOrder = { + orderId: order.orderId, + symbol, + side, + positionSide: position.positionSide, + triggerType: 'breakeven', + triggerPercent: 0, + quantity: trimQuantity, + price: triggerPrice, + createdAt: Date.now(), + }; + + if (!this.activeOrders.has(key)) { + this.activeOrders.set(key, []); + } + this.activeOrders.get(key)!.push(protectiveOrder); + + logWithTimestamp( + `ProtectiveOrderService: Placed breakeven order for ${symbol} at ${triggerPrice.toFixed(2)} (${trimPercent}% of position)` + ); + + this.emit('protectiveOrderPlaced', protectiveOrder); + } catch (error: any) { + logErrorWithTimestamp( + `ProtectiveOrderService: Failed to place breakeven order for ${symbol}:`, + error?.response?.data || error?.message + ); + throw error; + } + } + + /** + * Place a trim level protective order at a specific profit percentage + */ + private async placeTrimLevelOrder( + position: ExchangePosition, + entryPrice: number, + profitPercent: number, + trimPercent: number, + key: string + ): Promise { + const symbol = position.symbol; + const posAmt = parseFloat(position.positionAmt); + const isLong = posAmt > 0; + + // Calculate trigger price + const priceMultiplier = isLong + ? 1 + profitPercent / 100 + : 1 - profitPercent / 100; + const triggerPrice = entryPrice * priceMultiplier; + + // Calculate quantity to trim + const trimQuantity = Math.abs(posAmt) * (trimPercent / 100); + const formattedQty = symbolPrecision.formatQuantity(symbol, trimQuantity); + + try { + const side = isLong ? 'SELL' : 'BUY'; + const clientOrderId = `po_trim_${symbol}_${profitPercent}_${Date.now()}`; + + const orderParams: any = { + symbol, + side, + type: 'LIMIT', + quantity: formattedQty, + price: symbolPrecision.formatPrice(symbol, triggerPrice), + timeInForce: 'GTC', + positionSide: position.positionSide, + reduceOnly: true, + newClientOrderId: clientOrderId, + }; + + const order = await placeOrder(orderParams, this.config.api); + + const protectiveOrder: ProtectiveOrder = { + orderId: order.orderId, + symbol, + side, + positionSide: position.positionSide, + triggerType: 'trim_level', + triggerPercent: profitPercent, + quantity: trimQuantity, + price: triggerPrice, + createdAt: Date.now(), + }; + + if (!this.activeOrders.has(key)) { + this.activeOrders.set(key, []); + } + this.activeOrders.get(key)!.push(protectiveOrder); + + logWithTimestamp( + `ProtectiveOrderService: Placed trim order for ${symbol} at ${triggerPrice.toFixed(2)} (+${profitPercent}%, ${trimPercent}% of position)` + ); + + this.emit('protectiveOrderPlaced', protectiveOrder); + } catch (error: any) { + logErrorWithTimestamp( + `ProtectiveOrderService: Failed to place trim order for ${symbol} at ${profitPercent}%:`, + error?.response?.data || error?.message + ); + throw error; + } + } + // Check if protective orders should be placed for a position public async checkPositionForProtectiveOrders( position: ExchangePosition, @@ -324,9 +535,10 @@ export class ProtectiveOrderService extends EventEmitter { } // Handle order fill events to remove from tracking - public handleOrderFilled(orderId: number): void { + public handleOrderFilled(orderId: string | number): void { + const orderIdStr = String(orderId); for (const [key, orders] of this.activeOrders.entries()) { - const index = orders.findIndex(o => o.orderId === orderId); + const index = orders.findIndex(o => o.orderId === orderIdStr); if (index !== -1) { const order = orders[index]; orders.splice(index, 1); @@ -370,3 +582,13 @@ export function initializeProtectiveOrderService(config: Config): ProtectiveOrde } return protectiveOrderServiceInstance; } + +// Export singleton for API usage +export const protectiveOrderService = new Proxy({} as ProtectiveOrderService, { + get(_target, prop) { + if (!protectiveOrderServiceInstance) { + throw new Error('ProtectiveOrderService not initialized. Call initializeProtectiveOrderService() first.'); + } + return (protectiveOrderServiceInstance as any)[prop]; + }, +}); diff --git a/src/lib/utils/timestamp.ts b/src/lib/utils/timestamp.ts index 41c6c53..659f50f 100644 --- a/src/lib/utils/timestamp.ts +++ b/src/lib/utils/timestamp.ts @@ -3,6 +3,69 @@ * Provides formatted timestamps for terminal output */ +// Server-side log buffer (Node.js only) +interface ServerLogEntry { + timestamp: string; + level: 'info' | 'warn' | 'error'; + component: string; + message: string; +} + +const MAX_SERVER_LOGS = 1000; +const serverLogBuffer: ServerLogEntry[] = []; + +export function getServerLogs(limit?: number): ServerLogEntry[] { + return limit ? serverLogBuffer.slice(-limit) : [...serverLogBuffer]; +} + +export function clearServerLogs(): void { + serverLogBuffer.length = 0; +} + +function addToServerBuffer(level: 'info' | 'warn' | 'error', args: any[]): void { + // Only buffer logs on server-side + if (typeof window !== 'undefined') return; + + const component = extractComponent(args); + const message = formatMessage(args); + + serverLogBuffer.push({ + timestamp: new Date().toISOString(), + level, + component, + message + }); + + // Keep only last MAX_SERVER_LOGS entries + if (serverLogBuffer.length > MAX_SERVER_LOGS) { + serverLogBuffer.shift(); + } +} + +/** + * Extract component name from log message + * Looks for patterns like "ComponentName: message" + */ +function extractComponent(args: any[]): string { + const firstArg = String(args[0] || ''); + const match = firstArg.match(/^([A-Za-z]+(?:Manager|Service|Bot)?)\s*:/); + return match ? match[1] : 'System'; +} + +/** + * Format args into a single message string + */ +function formatMessage(args: any[]): string { + return args + .map(arg => { + if (typeof arg === 'string') return arg; + if (arg instanceof Error) return arg.message; + if (typeof arg === 'object') return JSON.stringify(arg); + return String(arg); + }) + .join(' '); +} + /** * Get current timestamp in ISO 8601 format with milliseconds * Example: 2025-10-11 09:05:29.736 @@ -43,6 +106,9 @@ export function getTimeOnly(): string { export function logWithTimestamp(...args: any[]): void { const timestamp = getTimeOnly(); console.log(`[${timestamp}]`, ...args); + + // Store in server-side buffer + addToServerBuffer('info', args); } /** @@ -52,6 +118,9 @@ export function logWithTimestamp(...args: any[]): void { export function logErrorWithTimestamp(...args: any[]): void { const timestamp = getTimeOnly(); console.error(`[${timestamp}]`, ...args); + + // Store in server-side buffer + addToServerBuffer('error', args); } /** @@ -61,4 +130,7 @@ export function logErrorWithTimestamp(...args: any[]): void { export function logWarnWithTimestamp(...args: any[]): void { const timestamp = getTimeOnly(); console.warn(`[${timestamp}]`, ...args); + + // Store in server-side buffer + addToServerBuffer('warn', args); } diff --git a/src/middleware.ts b/src/middleware.ts index 0cb9570..78b9d1f 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -12,7 +12,7 @@ export default withAuth( const pathname = req.nextUrl.pathname; // Allow public paths - const PUBLIC_PATHS = ['/login', '/api/auth', '/api/health', '/api/klines']; + const PUBLIC_PATHS = ['/login', '/api/auth', '/api/health', '/api/klines', '/api/logs']; if (PUBLIC_PATHS.some(path => pathname.startsWith(path))) { return true; } From 6882f5b9e61e52bcebdc12b3c65726cb1b9f3a50 Mon Sep 17 00:00:00 2001 From: birdbathd Date: Sun, 23 Nov 2025 18:18:31 +1000 Subject: [PATCH 27/30] Remove per-symbol protective order config and fix duplicate service starts - Removed enableProtectiveOrders, protectiveBreakeven, protectiveTrimLevels from SymbolConfig types - Removed ProtectiveOrdersSection from symbol config UI - Removed automatic protective order checking from PositionManager - ProtectiveOrderService now starts once in on-demand mode (no monitoring interval) - Added duplicate start protection to prevent log spam - Commented out old config-based methods (kept helpers for per-position activation) - Protective orders now exclusively activated via 'Protect' button on positions --- src/bot/index.ts | 29 +- src/components/SymbolConfigForm.tsx | 11 - src/lib/bot/positionManager.ts | 44 -- src/lib/services/protectiveOrderService.ts | 441 +++++++++++---------- src/lib/types.ts | 12 - 5 files changed, 231 insertions(+), 306 deletions(-) diff --git a/src/bot/index.ts b/src/bot/index.ts index 36f56c4..8ef542b 100644 --- a/src/bot/index.ts +++ b/src/bot/index.ts @@ -461,25 +461,16 @@ logErrorWithTimestamp('โš ๏ธ Position Manager failed to start:', error.message logWithTimestamp('โ„น๏ธ Tranche Management disabled for all symbols'); } - // Initialize Protective Order Service (if enabled for any symbol) - const protectiveEnabledSymbols = Object.entries(this.config.symbols).filter( - ([_symbol, config]) => config.enableProtectiveOrders - ); - - if (protectiveEnabledSymbols.length > 0) { - try { - const { initializeProtectiveOrderService } = await import('../lib/services/protectiveOrderService'); - const protectiveOrderService = initializeProtectiveOrderService(this.config); - protectiveOrderService.start(); - - logWithTimestamp(`โœ… Protective Order Service initialized for ${protectiveEnabledSymbols.length} symbol(s): ${protectiveEnabledSymbols.map(([s]) => s).join(', ')}`); - } catch (error: any) { - logErrorWithTimestamp('โš ๏ธ Protective Order Service failed to start:', error.message); - this.statusBroadcaster.addError(`Protective Order Service: ${error.message}`); - // Continue without protective orders - } - } else { - logWithTimestamp('โ„น๏ธ Protective Orders disabled for all symbols'); + // Initialize Protective Order Service (always available for on-demand protection via UI) + try { + const { initializeProtectiveOrderService } = await import('../lib/services/protectiveOrderService'); + const protectiveOrderService = initializeProtectiveOrderService(this.config); + protectiveOrderService.start(); + logWithTimestamp('โœ… Protective Order Service ready (activated per-position via UI)'); + } catch (error: any) { + logErrorWithTimestamp('โš ๏ธ Protective Order Service failed to start:', error.message); + this.statusBroadcaster.addError(`Protective Order Service: ${error.message}`); + // Continue without protective orders } // Initialize Hunter (or reuse existing instance to prevent duplicate listeners) diff --git a/src/components/SymbolConfigForm.tsx b/src/components/SymbolConfigForm.tsx index 4801ffc..d56c99c 100644 --- a/src/components/SymbolConfigForm.tsx +++ b/src/components/SymbolConfigForm.tsx @@ -28,7 +28,6 @@ import { } from 'lucide-react'; import { toast } from 'sonner'; import { TrancheSettingsSection } from './TrancheSettingsSection'; -import { ProtectiveOrdersSection } from './ProtectiveOrdersSection'; interface SymbolConfigFormProps { onSave: (config: Config) => void; @@ -1468,16 +1467,6 @@ export default function SymbolConfigForm({ onSave, currentConfig }: SymbolConfig onChange={(field, value) => handleSymbolChange(selectedSymbol, field, value)} />
- - {/* Protective Orders */} -
- - handleSymbolChange(selectedSymbol, field, value)} - /> -
)} diff --git a/src/lib/bot/positionManager.ts b/src/lib/bot/positionManager.ts index 2f81b50..cbdf786 100644 --- a/src/lib/bot/positionManager.ts +++ b/src/lib/bot/positionManager.ts @@ -861,11 +861,6 @@ logErrorWithTimestamp(`PositionManager: Failed to ensure protection for ${symbol }); } - // Check for protective orders if enabled - this.checkProtectiveOrders(position).catch(error => { -logErrorWithTimestamp(`PositionManager: Failed to check protective orders for ${symbol}:`, error?.message); - }); - // Trigger balance refresh if position size changed if (sizeChanged) { this.refreshBalance(); @@ -2590,45 +2585,6 @@ logWithTimestamp('PositionManager: Manual cleanup triggered'); await this.cleanupOrphanedOrders(); } - // Check and place protective orders for a position - private async checkProtectiveOrders(position: ExchangePosition): Promise { - const protectiveService = getProtectiveOrderService(); - if (!protectiveService) { - return; // Service not initialized - } - - const symbol = position.symbol; - const symbolConfig = this.config.symbols[symbol]; - - if (!symbolConfig?.enableProtectiveOrders) { - return; // Not enabled for this symbol - } - - // Get current market price - const priceService = getPriceService(); - let currentPrice = parseFloat(position.markPrice); - - // If markPrice is 0 or stale, get from price service - if (currentPrice <= 0 || !priceService) { - try { - const priceData = priceService?.getMarkPrice(symbol); - if (priceData && priceData.markPrice) { - currentPrice = parseFloat(priceData.markPrice); - } - } catch (error) { - logErrorWithTimestamp(`PositionManager: Failed to get current price for ${symbol}:`, error); - return; - } - } - - if (currentPrice <= 0) { - return; // Invalid price - } - - // Check if protective orders should be placed - await protectiveService.checkPositionForProtectiveOrders(position, currentPrice); - } - // Manual methods public async closePosition(symbol: string, side: string): Promise { // Find the position in our current positions map diff --git a/src/lib/services/protectiveOrderService.ts b/src/lib/services/protectiveOrderService.ts index 73f4d3c..9c19b4d 100644 --- a/src/lib/services/protectiveOrderService.ts +++ b/src/lib/services/protectiveOrderService.ts @@ -49,17 +49,14 @@ export class ProtectiveOrderService extends EventEmitter { } public start(): void { - if (this.isRunning) return; + if (this.isRunning) { + logWithTimestamp('ProtectiveOrderService: Already running, skipping duplicate start'); + return; + } this.isRunning = true; - // Monitor positions every 10 seconds to place/update protective orders - this.monitorInterval = setInterval(() => { - this.checkAndPlaceProtectiveOrders().catch(error => { - logErrorWithTimestamp('ProtectiveOrderService: Error in monitor interval:', error); - }); - }, 10000); - - logWithTimestamp('ProtectiveOrderService: Started'); + // Note: No monitoring interval needed - protective orders are activated on-demand via UI + logWithTimestamp('ProtectiveOrderService: Started (on-demand mode)'); } public stop(): void { @@ -285,7 +282,8 @@ export class ProtectiveOrderService extends EventEmitter { } } - // Check if protective orders should be placed for a position + // DEPRECATED: Old config-based methods (not used - protective orders are now on-demand via UI) + /* public async checkPositionForProtectiveOrders( position: ExchangePosition, currentPrice: number @@ -321,212 +319,214 @@ export class ProtectiveOrderService extends EventEmitter { await this.checkTrimLevelOrders(position, currentPrice, pnlPercent, key); } } - - private async checkBreakevenOrder( - position: ExchangePosition, - currentPrice: number, - pnlPercent: number, - key: string - ): Promise { - const symbol = position.symbol; - const symbolConfig = this.config.symbols[symbol]; - const breakeven = symbolConfig.protectiveBreakeven!; - const entryPrice = parseFloat(position.entryPrice); - const posAmt = parseFloat(position.positionAmt); - const isLong = posAmt > 0; - - // Check if we already have a breakeven order - const existingOrders = this.activeOrders.get(key) || []; - const hasBreakevenOrder = existingOrders.some(o => o.triggerType === 'breakeven'); - - if (hasBreakevenOrder) { - return; // Already placed - } - - // Calculate trigger price with offset - const offsetMultiplier = 1 + (breakeven.triggerOffset / 100); - const triggerPrice = entryPrice * offsetMultiplier; - - // Check if current price has crossed the trigger - const shouldTrigger = isLong - ? currentPrice >= triggerPrice - : currentPrice <= triggerPrice; - - if (!shouldTrigger) { - return; // Not at trigger price yet - } - - // Calculate quantity to trim - const trimQuantity = Math.abs(posAmt) * (breakeven.trimPercent / 100); - const formattedQty = symbolPrecision.formatQuantity(symbol, trimQuantity); - - // Place protective order - try { - const side = isLong ? 'SELL' : 'BUY'; - const clientOrderId = `po_be_${symbol}_${Date.now()}`; - - const orderParams: any = { - symbol, - side, - type: 'LIMIT', - quantity: formattedQty, - price: symbolPrecision.formatPrice(symbol, triggerPrice), - timeInForce: 'GTC', - positionSide: position.positionSide, - reduceOnly: true, - newClientOrderId: clientOrderId, - }; - - const order = await placeOrder(orderParams, this.config.api); - - const protectiveOrder: ProtectiveOrder = { - orderId: order.orderId, - symbol, - side, - positionSide: position.positionSide, - triggerType: 'breakeven', - triggerPercent: breakeven.triggerOffset, - quantity: trimQuantity, - price: triggerPrice, - createdAt: Date.now(), - }; - - // Track the order - if (!this.activeOrders.has(key)) { - this.activeOrders.set(key, []); - } - this.activeOrders.get(key)!.push(protectiveOrder); - - logWithTimestamp( - `ProtectiveOrderService: Placed breakeven trim order for ${symbol} at ${triggerPrice.toFixed(2)} (${breakeven.trimPercent}% of position)` - ); - - this.emit('protectiveOrderPlaced', protectiveOrder); - } catch (error: any) { - logErrorWithTimestamp( - `ProtectiveOrderService: Failed to place breakeven order for ${symbol}:`, - error?.response?.data || error?.message - ); - - await errorLogger.logError(error instanceof Error ? error : new Error(String(error)), { - type: 'trading', - severity: 'medium', - context: { - component: 'ProtectiveOrderService', - symbol, - userAction: 'Place breakeven protective order', - }, - }); - } - } - - private async checkTrimLevelOrders( - position: ExchangePosition, - currentPrice: number, - pnlPercent: number, - key: string - ): Promise { - const symbol = position.symbol; - const symbolConfig = this.config.symbols[symbol]; - const trimLevels = symbolConfig.protectiveTrimLevels!; - const entryPrice = parseFloat(position.entryPrice); - const posAmt = parseFloat(position.positionAmt); - const isLong = posAmt > 0; - - const existingOrders = this.activeOrders.get(key) || []; - - // Check each trim level - for (const level of trimLevels) { - // Skip if we already have an order for this level - const hasLevelOrder = existingOrders.some( - o => o.triggerType === 'trim_level' && o.triggerPercent === level.triggerPercent - ); - - if (hasLevelOrder) { - continue; - } - - // Check if we've reached this P&L level - const shouldTrigger = isLong - ? pnlPercent >= level.triggerPercent - : pnlPercent >= level.triggerPercent; - - if (!shouldTrigger) { - continue; - } - - // Calculate trigger price based on P&L percentage - const priceMultiplier = 1 + (level.triggerPercent / 100); - const triggerPrice = isLong - ? entryPrice * priceMultiplier - : entryPrice * (2 - priceMultiplier); - - // Calculate quantity to trim (percentage of current position) - const currentPosQty = Math.abs(posAmt); - const trimQuantity = currentPosQty * (level.trimPercent / 100); - const formattedQty = symbolPrecision.formatQuantity(symbol, trimQuantity); - - // Place protective order - try { - const side = isLong ? 'SELL' : 'BUY'; - const clientOrderId = `po_tl_${symbol}_${level.triggerPercent}_${Date.now()}`; - - const orderParams: any = { - symbol, - side, - type: 'LIMIT', - quantity: formattedQty, - price: symbolPrecision.formatPrice(symbol, triggerPrice), - timeInForce: 'GTC', - positionSide: position.positionSide, - reduceOnly: true, - newClientOrderId: clientOrderId, - }; - - const order = await placeOrder(orderParams, this.config.api); - - const protectiveOrder: ProtectiveOrder = { - orderId: order.orderId, - symbol, - side, - positionSide: position.positionSide, - triggerType: 'trim_level', - triggerPercent: level.triggerPercent, - quantity: trimQuantity, - price: triggerPrice, - createdAt: Date.now(), - }; - - // Track the order - if (!this.activeOrders.has(key)) { - this.activeOrders.set(key, []); - } - this.activeOrders.get(key)!.push(protectiveOrder); - - logWithTimestamp( - `ProtectiveOrderService: Placed trim level order for ${symbol} at ${triggerPrice.toFixed(2)} (${level.trimPercent}% at ${level.triggerPercent}% P&L)` - ); - - this.emit('protectiveOrderPlaced', protectiveOrder); - } catch (error: any) { - logErrorWithTimestamp( - `ProtectiveOrderService: Failed to place trim level order for ${symbol}:`, - error?.response?.data || error?.message - ); - - await errorLogger.logError(error instanceof Error ? error : new Error(String(error)), { - type: 'trading', - severity: 'medium', - context: { - component: 'ProtectiveOrderService', - symbol, - userAction: 'Place trim level protective order', - }, - }); - } - } - } - + */ + +// +// private async checkBreakevenOrder( +// position: ExchangePosition, +// currentPrice: number, +// pnlPercent: number, +// key: string +// ): Promise { +// const symbol = position.symbol; +// const symbolConfig = this.config.symbols[symbol]; +// const breakeven = symbolConfig.protectiveBreakeven!; +// const entryPrice = parseFloat(position.entryPrice); +// const posAmt = parseFloat(position.positionAmt); +// const isLong = posAmt > 0; +// +// // Check if we already have a breakeven order +// const existingOrders = this.activeOrders.get(key) || []; +// const hasBreakevenOrder = existingOrders.some(o => o.triggerType === 'breakeven'); +// +// if (hasBreakevenOrder) { +// return; // Already placed +// } +// +// // Calculate trigger price with offset +// const offsetMultiplier = 1 + (breakeven.triggerOffset / 100); +// const triggerPrice = entryPrice * offsetMultiplier; +// +// // Check if current price has crossed the trigger +// const shouldTrigger = isLong +// ? currentPrice >= triggerPrice +// : currentPrice <= triggerPrice; +// +// if (!shouldTrigger) { +// return; // Not at trigger price yet +// } +// +// // Calculate quantity to trim +// const trimQuantity = Math.abs(posAmt) * (breakeven.trimPercent / 100); +// const formattedQty = symbolPrecision.formatQuantity(symbol, trimQuantity); +// +// // Place protective order +// try { +// const side = isLong ? 'SELL' : 'BUY'; +// const clientOrderId = `po_be_${symbol}_${Date.now()}`; +// +// const orderParams: any = { +// symbol, +// side, +// type: 'LIMIT', +// quantity: formattedQty, +// price: symbolPrecision.formatPrice(symbol, triggerPrice), +// timeInForce: 'GTC', +// positionSide: position.positionSide, +// reduceOnly: true, +// newClientOrderId: clientOrderId, +// }; +// +// const order = await placeOrder(orderParams, this.config.api); +// +// const protectiveOrder: ProtectiveOrder = { +// orderId: order.orderId, +// symbol, +// side, +// positionSide: position.positionSide, +// triggerType: 'breakeven', +// triggerPercent: breakeven.triggerOffset, +// quantity: trimQuantity, +// price: triggerPrice, +// createdAt: Date.now(), +// }; +// +// // Track the order +// if (!this.activeOrders.has(key)) { +// this.activeOrders.set(key, []); +// } +// this.activeOrders.get(key)!.push(protectiveOrder); +// +// logWithTimestamp( +// `ProtectiveOrderService: Placed breakeven trim order for ${symbol} at ${triggerPrice.toFixed(2)} (${breakeven.trimPercent}% of position)` +// ); +// +// this.emit('protectiveOrderPlaced', protectiveOrder); +// } catch (error: any) { +// logErrorWithTimestamp( +// `ProtectiveOrderService: Failed to place breakeven order for ${symbol}:`, +// error?.response?.data || error?.message +// ); +// +// await errorLogger.logError(error instanceof Error ? error : new Error(String(error)), { +// type: 'trading', +// severity: 'medium', +// context: { +// component: 'ProtectiveOrderService', +// symbol, +// userAction: 'Place breakeven protective order', +// }, +// }); +// } +// } +// +// private async checkTrimLevelOrders( +// position: ExchangePosition, +// currentPrice: number, +// pnlPercent: number, +// key: string +// ): Promise { +// const symbol = position.symbol; +// const symbolConfig = this.config.symbols[symbol]; +// const trimLevels = symbolConfig.protectiveTrimLevels!; +// const entryPrice = parseFloat(position.entryPrice); +// const posAmt = parseFloat(position.positionAmt); +// const isLong = posAmt > 0; +// +// const existingOrders = this.activeOrders.get(key) || []; +// +// // Check each trim level +// for (const level of trimLevels) { +// // Skip if we already have an order for this level +// const hasLevelOrder = existingOrders.some( +// o => o.triggerType === 'trim_level' && o.triggerPercent === level.triggerPercent +// ); +// +// if (hasLevelOrder) { +// continue; +// } +// +// // Check if we've reached this P&L level +// const shouldTrigger = isLong +// ? pnlPercent >= level.triggerPercent +// : pnlPercent >= level.triggerPercent; +// +// if (!shouldTrigger) { +// continue; +// } +// +// // Calculate trigger price based on P&L percentage +// const priceMultiplier = 1 + (level.triggerPercent / 100); +// const triggerPrice = isLong +// ? entryPrice * priceMultiplier +// : entryPrice * (2 - priceMultiplier); +// +// // Calculate quantity to trim (percentage of current position) +// const currentPosQty = Math.abs(posAmt); +// const trimQuantity = currentPosQty * (level.trimPercent / 100); +// const formattedQty = symbolPrecision.formatQuantity(symbol, trimQuantity); +// +// // Place protective order +// try { +// const side = isLong ? 'SELL' : 'BUY'; +// const clientOrderId = `po_tl_${symbol}_${level.triggerPercent}_${Date.now()}`; +// +// const orderParams: any = { +// symbol, +// side, +// type: 'LIMIT', +// quantity: formattedQty, +// price: symbolPrecision.formatPrice(symbol, triggerPrice), +// timeInForce: 'GTC', +// positionSide: position.positionSide, +// reduceOnly: true, +// newClientOrderId: clientOrderId, +// }; +// +// const order = await placeOrder(orderParams, this.config.api); +// +// const protectiveOrder: ProtectiveOrder = { +// orderId: order.orderId, +// symbol, +// side, +// positionSide: position.positionSide, +// triggerType: 'trim_level', +// triggerPercent: level.triggerPercent, +// quantity: trimQuantity, +// price: triggerPrice, +// createdAt: Date.now(), +// }; +// +// // Track the order +// if (!this.activeOrders.has(key)) { +// this.activeOrders.set(key, []); +// } +// this.activeOrders.get(key)!.push(protectiveOrder); +// +// logWithTimestamp( +// `ProtectiveOrderService: Placed trim level order for ${symbol} at ${triggerPrice.toFixed(2)} (${level.trimPercent}% at ${level.triggerPercent}% P&L)` +// ); +// +// this.emit('protectiveOrderPlaced', protectiveOrder); +// } catch (error: any) { +// logErrorWithTimestamp( +// `ProtectiveOrderService: Failed to place trim level order for ${symbol}:`, +// error?.response?.data || error?.message +// ); +// +// await errorLogger.logError(error instanceof Error ? error : new Error(String(error)), { +// type: 'trading', +// severity: 'medium', +// context: { +// component: 'ProtectiveOrderService', +// symbol, +// userAction: 'Place trim level protective order', +// }, +// }); +// } +// } +// } +// // Remove protective orders when position closes public clearProtectiveOrders(symbol: string, positionSide: string): void { const key = this.getPositionKey(symbol, positionSide); @@ -551,10 +551,11 @@ export class ProtectiveOrderService extends EventEmitter { } } - private async checkAndPlaceProtectiveOrders(): Promise { - // This will be called by position manager when it has position updates - // For now, it's a placeholder for future integration - } + // DEPRECATED: Placeholder for old automatic monitoring +// private async checkAndPlaceProtectiveOrders(): Promise { +// // This will be called by position manager when it has position updates +// // For now, it's a placeholder for future integration +// } private getPositionKey(symbol: string, positionSide: string): string { return `${symbol}_${positionSide}`; diff --git a/src/lib/types.ts b/src/lib/types.ts index aae66a4..b9bb699 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -40,18 +40,6 @@ export interface SymbolConfig { allowTrancheWhileIsolated?: boolean; // Allow opening new tranches while some are isolated trancheAutoCloseIsolated?: boolean; // Automatically close isolated tranches when they recover trancheRecoveryThreshold?: number; // P&L % threshold to auto-close recovered tranches (e.g., 0.5 for +0.5%) - - // Protective Orders (partial position trimming) - enableProtectiveOrders?: boolean; // Enable automatic position trimming at specific levels - protectiveBreakeven?: { - enabled: boolean; // Trim at breakeven - triggerOffset: number; // % offset from entry (0 = exact breakeven, 1 = 1% profit, -1 = 1% loss) - trimPercent: number; // % of position to close (e.g., 50 for 50%) - }; - protectiveTrimLevels?: Array<{ - triggerPercent: number; // PnL % to trigger (can be negative for loss protection) - trimPercent: number; // % of remaining position to close - }>; } export interface ApiCredentials { From 218f606c28079063cdc4783d984b288fe7e399a7 Mon Sep 17 00:00:00 2001 From: birdbathd Date: Sun, 23 Nov 2025 18:20:21 +1000 Subject: [PATCH 28/30] Fix ProtectiveOrderService initialization check in API - Use getProtectiveOrderService() instead of proxy export to avoid initialization errors - Return 503 error if service not available instead of throwing exception - Provides better error message when bot is not running --- src/app/api/positions/protect/route.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/app/api/positions/protect/route.ts b/src/app/api/positions/protect/route.ts index 54ddf58..42009fa 100644 --- a/src/app/api/positions/protect/route.ts +++ b/src/app/api/positions/protect/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server'; -import { protectiveOrderService } from '@/lib/services/protectiveOrderService'; +import { getProtectiveOrderService } from '@/lib/services/protectiveOrderService'; export const dynamic = 'force-dynamic'; @@ -9,6 +9,15 @@ export const dynamic = 'force-dynamic'; */ export async function POST(request: NextRequest) { try { + const protectiveOrderService = getProtectiveOrderService(); + + if (!protectiveOrderService) { + return NextResponse.json( + { success: false, error: 'Protective order service not available. Please ensure the bot is running.' }, + { status: 503 } + ); + } + const body = await request.json(); const { symbol, side, entryPrice, quantity, settings } = body; From 0e07ed4e43d6d5fca1ed9152e8f15708be7a2542 Mon Sep 17 00:00:00 2001 From: birdbathd Date: Sun, 23 Nov 2025 23:38:22 +1000 Subject: [PATCH 29/30] feat: Transform trailing stop to trailing TP with break-even protection and disable default TP/SL option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Major Changes: - Renamed all 'trailing stop' references to 'trailing take profit' (trailing TP) - Implemented break-even protection: TP never goes below entry price for LONG (never above for SHORT) - Added 'Disable Default TP/SL' option to scale out settings with intelligent warnings - Added DCA flag to trailing TP (integrates with existing liq hunter) - Fixed WebSocket connection error banner flashing on page load (5s grace period) Trailing TP Enhancements: - placeTrailingStop() โ†’ placeTrailingTakeProfit() with break-even enforcement - Monitoring enforces Math.max(idealTpPrice, entryPrice) for LONG positions - Monitoring enforces Math.min(idealTpPrice, entryPrice) for SHORT positions - Tracking now stores: entryPrice, enableDCA flag alongside trail data - Method names updated: startTrailingTakeProfitMonitoring(), checkAndAdjustTrailingTakeProfits() - Log messages updated to reflect 'trailing TP' instead of 'trailing stop' - UI description: 'Captures upside while protecting profits (exit never falls below break-even)' Disable Default TP/SL Feature: - New toggle in ScaleOutModal to disable bot's automatic TP/SL for specific positions - ProtectiveOrderService tracks disabled positions in disabledDefaultTPSL Set - Position Manager checks isDefaultTPSLDisabled() before placing/adjusting TP/SL - Cancels existing default TP/SL orders when option is enabled - Automatically resumes default TP/SL when scale out is deactivated - Preserves robustness: only skips disabled positions, monitors all others normally Smart Warning System: - No warning when breakeven 100% or any trim level is 100% (full position exit) - 'No exit protection' warning when no methods enabled - 'No stop loss protection' warning when only trailing TP (no downside protection) - 'Partial exit only' warning when no 100% trim levels configured - Confirmation prompts on submit for risky configurations - Prevents activating with disabled TP/SL and no exit methods UI/UX Improvements: - Added DCA toggle: 'DCA on Drop Below Entry' (continues liq hunting when enabled) - WebSocket error banner now waits 5 seconds before showing (prevents flash on page load) - Context-aware warnings based on configured exit methods - Validation ensures at least one exit method when default TP/SL disabled Technical Details: - ScaleOutSettings interface: added disableDefaultTPSL flag - ProtectiveOrder triggerType: 'trailing_stop' โ†’ 'trailing_tp' - positionManager.adjustProtectiveOrders(): checks isDefaultTPSLDisabled() before managing TP/SL - positionManager.placeProtectiveOrders(): checks isDefaultTPSLDisabled() before placing orders - cancelDefaultTPSL() method filters TAKE_PROFIT_MARKET, STOP_MARKET orders (excludes po_ prefix) - Cleanup on deactivation: removes from disabledDefaultTPSL Set to resume normal TP/SL management Files Modified: - src/lib/services/protectiveOrderService.ts (trailing TP transformation, disable TP/SL tracking) - src/components/ScaleOutModal.tsx (UI updates, smart warnings, validation) - src/lib/bot/positionManager.ts (skip disabled positions in TP/SL management) - src/components/PersistentErrorBanner.tsx (5s delay for WebSocket errors) - src/bot/index.ts (event handlers for scale out) - src/app/api/positions/scale-out/*.ts (API routes) --- src/app/api/bot/control/route.ts | 5 +- src/app/api/positions/protect/route.ts | 112 ----- .../positions/scale-out/deactivate/route.ts | 117 +++++ src/app/api/positions/scale-out/route.ts | 181 +++++++ .../api/positions/scale-out/status/route.ts | 102 ++++ src/bot/index.ts | 72 +++ src/bot/websocketServer.ts | 25 + src/components/PersistentErrorBanner.tsx | 15 +- src/components/PositionTable.tsx | 210 +++++--- src/components/ProtectPositionModal.tsx | 215 --------- src/components/ScaleOutModal.tsx | 435 +++++++++++++++++ src/hooks/useBotStatus.ts | 4 + src/lib/bot/positionManager.ts | 61 ++- src/lib/services/protectiveOrderService.ts | 450 +++++++++++++++++- src/lib/services/websocketService.ts | 2 - 15 files changed, 1611 insertions(+), 395 deletions(-) delete mode 100644 src/app/api/positions/protect/route.ts create mode 100644 src/app/api/positions/scale-out/deactivate/route.ts create mode 100644 src/app/api/positions/scale-out/route.ts create mode 100644 src/app/api/positions/scale-out/status/route.ts delete mode 100644 src/components/ProtectPositionModal.tsx create mode 100644 src/components/ScaleOutModal.tsx diff --git a/src/app/api/bot/control/route.ts b/src/app/api/bot/control/route.ts index 629262f..57e392c 100644 --- a/src/app/api/bot/control/route.ts +++ b/src/app/api/bot/control/route.ts @@ -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' }); diff --git a/src/app/api/positions/protect/route.ts b/src/app/api/positions/protect/route.ts deleted file mode 100644 index 42009fa..0000000 --- a/src/app/api/positions/protect/route.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { getProtectiveOrderService } from '@/lib/services/protectiveOrderService'; - -export const dynamic = 'force-dynamic'; - -/** - * POST /api/positions/protect - * Activate protective orders (breakeven + trim levels) for a specific position - */ -export async function POST(request: NextRequest) { - try { - const protectiveOrderService = getProtectiveOrderService(); - - if (!protectiveOrderService) { - return NextResponse.json( - { success: false, error: 'Protective order service not available. Please ensure the bot is running.' }, - { status: 503 } - ); - } - - const body = await request.json(); - const { symbol, side, entryPrice, quantity, settings } = body; - - if (!symbol || !side) { - return NextResponse.json( - { success: false, error: 'Symbol and side are required' }, - { status: 400 } - ); - } - - if (typeof entryPrice !== 'number' || entryPrice <= 0) { - return NextResponse.json( - { success: false, error: 'Valid entry price is required' }, - { status: 400 } - ); - } - - if (typeof quantity !== 'number' || quantity <= 0) { - return NextResponse.json( - { success: false, error: 'Valid quantity is required' }, - { status: 400 } - ); - } - - if (!settings) { - return NextResponse.json( - { success: false, error: 'Protection settings are required' }, - { status: 400 } - ); - } - - // Validate settings structure - if (typeof settings.enableBreakeven !== 'boolean') { - return NextResponse.json( - { success: false, error: 'Invalid protection settings: enableBreakeven must be boolean' }, - { status: 400 } - ); - } - - if (!Array.isArray(settings.trimLevels)) { - return NextResponse.json( - { success: false, error: 'Invalid protection settings: trimLevels must be an array' }, - { status: 400 } - ); - } - - // Validate trim levels - for (const level of settings.trimLevels) { - if (typeof level.profitPercent !== 'number' || level.profitPercent <= 0) { - return NextResponse.json( - { success: false, error: 'Invalid trim level: profitPercent must be a positive number' }, - { status: 400 } - ); - } - if (typeof level.trimPercent !== 'number' || level.trimPercent <= 0 || level.trimPercent > 100) { - return NextResponse.json( - { success: false, error: 'Invalid trim level: trimPercent must be between 0 and 100' }, - { status: 400 } - ); - } - } - - // Activate protection via the service - await protectiveOrderService.activateProtection( - symbol, - side, - entryPrice, - quantity, - settings - ); - - return NextResponse.json({ - success: true, - message: 'Protection activated successfully', - details: { - symbol, - side, - breakeven: settings.enableBreakeven, - trimLevels: settings.trimLevels.length, - }, - }); - } catch (error) { - console.error('[API] Error activating protection:', error); - return NextResponse.json( - { - success: false, - error: error instanceof Error ? error.message : 'Failed to activate protection', - }, - { status: 500 } - ); - } -} diff --git a/src/app/api/positions/scale-out/deactivate/route.ts b/src/app/api/positions/scale-out/deactivate/route.ts new file mode 100644 index 0000000..b69bf01 --- /dev/null +++ b/src/app/api/positions/scale-out/deactivate/route.ts @@ -0,0 +1,117 @@ +import { NextRequest, NextResponse } from 'next/server'; +import WebSocket from 'ws'; +import { configLoader } from '@/lib/config/configLoader'; + +export const dynamic = 'force-dynamic'; + +/** + * Helper to send deactivate scale out command via WebSocket to the bot + */ +async function sendDeactivateCommand(data: any): Promise<{ success: boolean; error?: string }> { + return new Promise((resolve) => { + 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' }); + }, 10000); + + let responseReceived = false; + + ws.on('open', () => { + ws.send(JSON.stringify({ + type: 'deactivate_scale_out', + data + })); + }); + + ws.on('message', (message: Buffer) => { + try { + const response = JSON.parse(message.toString()); + + if (response.type === 'deactivate_scale_out_response') { + clearTimeout(timeout); + responseReceived = true; + ws.close(); + resolve({ success: true }); + } else if (response.type === 'deactivate_scale_out_success') { + clearTimeout(timeout); + responseReceived = true; + ws.close(); + resolve({ success: true }); + } else if (response.type === 'deactivate_scale_out_error') { + clearTimeout(timeout); + responseReceived = true; + ws.close(); + resolve({ success: false, error: response.data?.error || 'Unknown error' }); + } + } catch (error) { + // Ignore parse errors, keep waiting + } + }); + + ws.on('error', (error) => { + clearTimeout(timeout); + if (!responseReceived) { + resolve({ success: false, error: error.message }); + } + }); + + ws.on('close', () => { + clearTimeout(timeout); + if (!responseReceived) { + resolve({ success: false, error: 'Connection closed before response' }); + } + }); + }); +} + +/** + * POST /api/positions/scale-out/deactivate + * Deactivate scale out orders for a specific position + */ +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { symbol, side } = body; + + if (!symbol || !side) { + return NextResponse.json( + { success: false, error: 'Symbol and side are required' }, + { status: 400 } + ); + } + + // Send deactivate command to bot via WebSocket + const result = await sendDeactivateCommand({ symbol, side }); + + if (!result.success) { + if (result.error?.includes('ECONNREFUSED') || result.error?.includes('timeout')) { + return NextResponse.json( + { success: false, error: 'Bot is not running or not responding. Please ensure the bot is started.' }, + { status: 503 } + ); + } + + return NextResponse.json( + { success: false, error: result.error || 'Failed to deactivate scale out' }, + { status: 500 } + ); + } + + return NextResponse.json({ + success: true, + message: 'Scale out deactivated successfully', + }); + } catch (error) { + console.error('[API] Error deactivating scale out:', error); + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Failed to deactivate scale out', + }, + { status: 500 } + ); + } +} diff --git a/src/app/api/positions/scale-out/route.ts b/src/app/api/positions/scale-out/route.ts new file mode 100644 index 0000000..e28861f --- /dev/null +++ b/src/app/api/positions/scale-out/route.ts @@ -0,0 +1,181 @@ +import { NextRequest, NextResponse } from 'next/server'; +import WebSocket from 'ws'; +import { configLoader } from '@/lib/config/configLoader'; + +export const dynamic = 'force-dynamic'; + +/** + * Helper to send scale out command via WebSocket to the bot + */ +async function sendProtectCommand(data: any): Promise<{ success: boolean; error?: string }> { + return new Promise((resolve) => { + 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' }); + }, 10000); + + let responseReceived = false; + + ws.on('open', () => { + ws.send(JSON.stringify({ + type: 'scale_out_position', + data + })); + }); + + ws.on('message', (message: Buffer) => { + try { + const response = JSON.parse(message.toString()); + + if (response.type === 'scale_out_position_response') { + clearTimeout(timeout); + responseReceived = true; + ws.close(); + resolve({ success: true }); + } else if (response.type === 'scale_out_position_success') { + clearTimeout(timeout); + responseReceived = true; + ws.close(); + resolve({ success: true }); + } else if (response.type === 'scale_out_position_error') { + clearTimeout(timeout); + responseReceived = true; + ws.close(); + resolve({ success: false, error: response.data?.error || 'Unknown error' }); + } + } catch (error) { + // Ignore parse errors, keep waiting + } + }); + + ws.on('error', (error) => { + clearTimeout(timeout); + if (!responseReceived) { + resolve({ success: false, error: error.message }); + } + }); + + ws.on('close', () => { + clearTimeout(timeout); + if (!responseReceived) { + resolve({ success: false, error: 'Connection closed before response' }); + } + }); + }); +} + +/** + * POST /api/positions/scale-out + * Activate scale out orders (breakeven + trim levels) for a specific position + */ +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { symbol, side, entryPrice, quantity, settings } = body; + + if (!symbol || !side) { + return NextResponse.json( + { success: false, error: 'Symbol and side are required' }, + { status: 400 } + ); + } + + if (typeof entryPrice !== 'number' || entryPrice <= 0) { + return NextResponse.json( + { success: false, error: 'Valid entry price is required' }, + { status: 400 } + ); + } + + if (typeof quantity !== 'number' || quantity <= 0) { + return NextResponse.json( + { success: false, error: 'Valid quantity is required' }, + { status: 400 } + ); + } + + if (!settings) { + return NextResponse.json( + { success: false, error: 'Protection settings are required' }, + { status: 400 } + ); + } + + // Validate settings structure + if (typeof settings.enableBreakeven !== 'boolean') { + return NextResponse.json( + { success: false, error: 'Invalid protection settings: enableBreakeven must be boolean' }, + { status: 400 } + ); + } + + if (!Array.isArray(settings.trimLevels)) { + return NextResponse.json( + { success: false, error: 'Invalid protection settings: trimLevels must be an array' }, + { status: 400 } + ); + } + + // Validate trim levels + for (const level of settings.trimLevels) { + if (typeof level.profitPercent !== 'number' || level.profitPercent <= 0) { + return NextResponse.json( + { success: false, error: 'Invalid trim level: profitPercent must be a positive number' }, + { status: 400 } + ); + } + if (typeof level.trimPercent !== 'number' || level.trimPercent <= 0 || level.trimPercent > 100) { + return NextResponse.json( + { success: false, error: 'Invalid trim level: trimPercent must be between 0 and 100' }, + { status: 400 } + ); + } + } + + // Send protection command to bot via WebSocket + const result = await sendProtectCommand({ + symbol, + side, + entryPrice, + quantity, + settings + }); + + if (!result.success) { + if (result.error?.includes('ECONNREFUSED') || result.error?.includes('timeout')) { + return NextResponse.json( + { success: false, error: 'Bot is not running or not responding. Please ensure the bot is started.' }, + { status: 503 } + ); + } + + return NextResponse.json( + { success: false, error: result.error || 'Failed to activate protection' }, + { status: 500 } + ); + } + + return NextResponse.json({ + success: true, + message: 'Scale out activated successfully', + details: { + symbol, + side, + breakeven: settings.enableBreakeven, + trimLevels: settings.trimLevels.length, + }, + }); + } catch (error) { + console.error('[API] Error activating scale out:', error); + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Failed to activate scale out', + }, + { status: 500 } + ); + } +} diff --git a/src/app/api/positions/scale-out/status/route.ts b/src/app/api/positions/scale-out/status/route.ts new file mode 100644 index 0000000..2785a5a --- /dev/null +++ b/src/app/api/positions/scale-out/status/route.ts @@ -0,0 +1,102 @@ +import { NextRequest, NextResponse } from 'next/server'; +import WebSocket from 'ws'; +import { configLoader } from '@/lib/config/configLoader'; + +export const dynamic = 'force-dynamic'; + +/** + * GET /api/positions/scale-out/status + * Check if scale out is active for a specific position + */ +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const symbol = searchParams.get('symbol'); + const side = searchParams.get('side'); + + if (!symbol || !side) { + return NextResponse.json( + { success: false, error: 'Symbol and side are required' }, + { status: 400 } + ); + } + + // Send check request via WebSocket to bot + const result = await checkScaleOutStatus({ symbol, side }); + + if (!result.success) { + return NextResponse.json( + { success: false, isActive: false, error: result.error }, + { status: 200 } // Return 200 with isActive: false instead of error + ); + } + + return NextResponse.json({ + success: true, + isActive: result.isActive || false, + }); + } catch (error) { + console.error('[API] Error checking scale out status:', error); + return NextResponse.json( + { + success: false, + isActive: false, + error: error instanceof Error ? error.message : 'Failed to check status', + }, + { status: 200 } // Return 200 to avoid errors in UI + ); + } +} + +/** + * Helper to check scale out status via WebSocket + */ +async function checkScaleOutStatus(data: any): Promise<{ success: boolean; isActive?: boolean; error?: string }> { + return new Promise((resolve) => { + 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' }); + }, 5000); + + let responseReceived = false; + + ws.on('open', () => { + ws.send(JSON.stringify({ + type: 'check_scale_out_status', + data + })); + }); + + ws.on('message', (message: Buffer) => { + try { + const response = JSON.parse(message.toString()); + + if (response.type === 'scale_out_status_response') { + clearTimeout(timeout); + responseReceived = true; + ws.close(); + resolve({ success: true, isActive: response.data?.isActive || false }); + } + } catch (error) { + // Ignore parse errors, keep waiting + } + }); + + ws.on('error', (error) => { + clearTimeout(timeout); + if (!responseReceived) { + resolve({ success: false, error: error.message }); + } + }); + + ws.on('close', () => { + clearTimeout(timeout); + if (!responseReceived) { + resolve({ success: false, error: 'Connection closed before response' }); + } + }); + }); +} diff --git a/src/bot/index.ts b/src/bot/index.ts index 8ef542b..c51f564 100644 --- a/src/bot/index.ts +++ b/src/bot/index.ts @@ -467,6 +467,78 @@ logErrorWithTimestamp('โš ๏ธ Position Manager failed to start:', error.message const protectiveOrderService = initializeProtectiveOrderService(this.config); protectiveOrderService.start(); logWithTimestamp('โœ… Protective Order Service ready (activated per-position via UI)'); + + // Listen for scale_out_position commands from WebSocket + this.statusBroadcaster.removeAllListeners('scale_out_position'); + this.statusBroadcaster.on('scale_out_position', async (data: any) => { + try { + logWithTimestamp(`๐Ÿ›ก๏ธ Activating scale out for ${data.symbol} ${data.side}`); + await protectiveOrderService.activateProtection( + data.symbol, + data.side, + data.entryPrice, + data.quantity, + data.settings + ); + this.statusBroadcaster.broadcast('scale_out_position_success', { + symbol: data.symbol, + side: data.side, + timestamp: new Date() + }); + } catch (error: any) { + logErrorWithTimestamp(`โŒ Failed to activate scale out for ${data.symbol}:`, error.message); + this.statusBroadcaster.broadcast('scale_out_position_error', { + symbol: data.symbol, + side: data.side, + error: error.message, + timestamp: new Date() + }); + } + }); + + // Listen for deactivate_scale_out commands from WebSocket + this.statusBroadcaster.removeAllListeners('deactivate_scale_out'); + this.statusBroadcaster.on('deactivate_scale_out', async (data: any) => { + try { + logWithTimestamp(`๐Ÿ›ก๏ธ Deactivating scale out for ${data.symbol} ${data.side}`); + await protectiveOrderService.deactivateProtection(data.symbol, data.side); + + // Broadcast success and status update + this.statusBroadcaster.broadcast('deactivate_scale_out_success', { + symbol: data.symbol, + side: data.side, + timestamp: new Date() + }); + + // Immediately broadcast status update to UI + this.statusBroadcaster.broadcast('scale_out_status_update', { + symbol: data.symbol, + side: data.side, + isActive: false, + reason: 'manual_deactivation' + }); + } catch (error: any) { + logErrorWithTimestamp(`โŒ Failed to deactivate scale out for ${data.symbol}:`, error.message); + this.statusBroadcaster.broadcast('deactivate_scale_out_error', { + symbol: data.symbol, + side: data.side, + error: error.message, + timestamp: new Date() + }); + } + }); + + // Listen for check_scale_out_status commands from WebSocket + this.statusBroadcaster.removeAllListeners('check_scale_out_status'); + this.statusBroadcaster.on('check_scale_out_status', (data: any) => { + const isActive = protectiveOrderService.isProtectionActive(data.symbol, data.side); + this.statusBroadcaster.broadcast('scale_out_status_response', { + symbol: data.symbol, + side: data.side, + isActive, + timestamp: new Date() + }); + }); } catch (error: any) { logErrorWithTimestamp('โš ๏ธ Protective Order Service failed to start:', error.message); this.statusBroadcaster.addError(`Protective Order Service: ${error.message}`); diff --git a/src/bot/websocketServer.ts b/src/bot/websocketServer.ts index 471d8ea..57aa6f9 100644 --- a/src/bot/websocketServer.ts +++ b/src/bot/websocketServer.ts @@ -85,6 +85,31 @@ export class StatusBroadcaster extends EventEmitter { } break; + case 'scale_out_position': + console.log('๐Ÿ›ก๏ธ Scale out requested from web UI:', message.data); + this.emit('scale_out_position', message.data); + ws.send(JSON.stringify({ + type: 'scale_out_position_response', + success: true, + timestamp: Date.now() + })); + break; + + case 'deactivate_scale_out': + console.log('๐Ÿ›ก๏ธ Scale out deactivation requested from web UI:', message.data); + this.emit('deactivate_scale_out', message.data); + ws.send(JSON.stringify({ + type: 'deactivate_scale_out_response', + success: true, + timestamp: Date.now() + })); + break; + + case 'check_scale_out_status': + console.log('๐Ÿ›ก๏ธ Scale out status check requested from web UI:', message.data); + this.emit('check_scale_out_status', message.data); + break; + case 'ping': ws.send(JSON.stringify({ type: 'pong', timestamp: Date.now() })); break; diff --git a/src/components/PersistentErrorBanner.tsx b/src/components/PersistentErrorBanner.tsx index 95ac33b..98fb9aa 100644 --- a/src/components/PersistentErrorBanner.tsx +++ b/src/components/PersistentErrorBanner.tsx @@ -31,8 +31,15 @@ const ERROR_COLORS = { export function PersistentErrorBanner() { const [systemicErrors, setSystemicErrors] = useState>(new Map()); + const [hasShownInitialConnection, setHasShownInitialConnection] = useState(false); useEffect(() => { + // Wait 5 seconds before allowing websocket errors to be shown + // This prevents the banner from flashing on initial page load + const initialDelay = setTimeout(() => { + setHasShownInitialConnection(true); + }, 5000); + const cleanup = websocketService.addMessageHandler((message: any) => { if (!message.type || !message.type.endsWith('_error')) { return; @@ -50,6 +57,11 @@ export function PersistentErrorBanner() { return; } + // Skip websocket errors during initial 5 second grace period + if (message.type === 'websocket_error' && !hasShownInitialConnection) { + return; + } + // Determine error type let errorType: ErrorType = 'general'; if (message.type === 'websocket_error') errorType = 'websocket'; @@ -109,10 +121,11 @@ export function PersistentErrorBanner() { }, 10000); // Check every 10 seconds return () => { + clearTimeout(initialDelay); cleanup(); clearInterval(interval); }; - }, []); + }, [hasShownInitialConnection]); const dismissError = (key: string) => { setSystemicErrors(prev => { diff --git a/src/components/PositionTable.tsx b/src/components/PositionTable.tsx index cbeb4a3..8ed6a5f 100644 --- a/src/components/PositionTable.tsx +++ b/src/components/PositionTable.tsx @@ -10,7 +10,7 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/comp import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { toast } from 'sonner'; -import { ProtectPositionModal, ProtectiveSettings } from '@/components/ProtectPositionModal'; +import { ScaleOutModal, ScaleOutSettings } from '@/components/ScaleOutModal'; import websocketService from '@/lib/services/websocketService'; import { useConfig } from '@/components/ConfigProvider'; import { useSymbolPrecision } from '@/hooks/useSymbolPrecision'; @@ -53,6 +53,7 @@ export default function PositionTable({ const [isLoading, setIsLoading] = useState(true); const [markPrices, setMarkPrices] = useState>({}); const [vwapData, setVwapData] = useState>({}); + const [protectionStatus, setProtectionStatus] = useState>({}); const [isCollapsed, setIsCollapsed] = useState(false); const [closePositionModal, setClosePositionModal] = useState<{ isOpen: boolean; @@ -195,13 +196,18 @@ export default function PositionTable({ }); setVwapData(prev => ({ ...prev, ...vwapUpdates })); } + } else if (message.type === 'scale_out_status_update') { + // Update scale out button status when orders are filled/canceled + const { symbol, side, isActive } = message.data; + const key = `${symbol}_${side}`; + setProtectionStatus(prev => ({ ...prev, [key]: isActive })); } }; const cleanupWebSocket = websocketService.addMessageHandler(handleWebSocketMessage); - // Load initial VWAP data once - loadVWAPData(); + // Skip initial VWAP load - WebSocket will provide updates + // loadVWAPData(); // Cleanup on unmount return () => { @@ -249,6 +255,53 @@ export default function PositionTable({ } }, [positions.length, loadVWAPData]); // Include loadVWAPData dependency + // Load scale out status for all positions on mount and when position count changes + useEffect(() => { + const displayedPositions = positions.length > 0 ? positions : realPositions; + if (displayedPositions.length === 0) return; + + // Create a unique key for each position to track what we've checked + const positionKeys = displayedPositions.map(p => `${p.symbol}_${p.side}`); + const uncheckedPositions = displayedPositions.filter(p => { + const key = `${p.symbol}_${p.side}`; + return !(key in protectionStatus); + }); + + // Only check if we have new positions we haven't checked yet + if (uncheckedPositions.length === 0) return; + + // Request status for all positions in parallel (much faster) + const checkStatuses = async () => { + const statusPromises = uncheckedPositions.map(async (position) => { + const key = `${position.symbol}_${position.side}`; + try { + const response = await fetch(`/api/positions/scale-out/status?symbol=${position.symbol}&side=${position.side}`); + if (response.ok) { + const data = await response.json(); + return { key, isActive: data.isActive }; + } + } catch (error) { + console.error(`Failed to check scale out status for ${position.symbol}:`, error); + } + return null; + }); + + const results = await Promise.all(statusPromises); + const updates: Record = {}; + results.forEach(result => { + if (result) updates[result.key] = result.isActive; + }); + + if (Object.keys(updates).length > 0) { + setProtectionStatus(prev => ({ ...prev, ...updates })); + } + }; + + // Small delay to batch requests after component mounts + const timer = setTimeout(checkStatuses, 100); + return () => clearTimeout(timer); + }, [positions.length, realPositions.length]); // Removed protectionStatus from deps + // Handle close position const handleClosePosition = useCallback((position: Position) => { @@ -343,23 +396,61 @@ export default function PositionTable({ // Handle protect position const handleProtectPosition = useCallback((position: Position) => { - setProtectPositionModal({ - isOpen: true, - position: { - symbol: position.symbol, - side: position.side, - quantity: position.quantity, - entryPrice: position.entryPrice, - markPrice: position.markPrice, - }, - }); + const key = `${position.symbol}_${position.side}`; + const isProtected = protectionStatus[key]; + + // If already protected, deactivate instead of showing modal + if (isProtected) { + handleDeactivateProtection(position); + } else { + setProtectPositionModal({ + isOpen: true, + position: { + symbol: position.symbol, + side: position.side, + quantity: position.quantity, + entryPrice: position.entryPrice, + markPrice: position.markPrice, + }, + }); + } + }, [protectionStatus]); + + const handleDeactivateProtection = useCallback(async (position: Position) => { + try { + const response = await fetch('/api/positions/scale-out/deactivate', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + symbol: position.symbol, + side: position.side, + }), + }); + + const result = await response.json(); + + if (result.success) { + toast.success(`Scale out deactivated for ${position.symbol}`); + const key = `${position.symbol}_${position.side}`; + setProtectionStatus(prev => ({ ...prev, [key]: false })); + } else { + throw new Error(result.error || 'Failed to deactivate scale out'); + } + } catch (error: any) { + console.error('[PositionTable] Error deactivating scale out:', error); + toast.error(`Failed to deactivate scale out`, { + description: error.message || 'Unknown error occurred', + }); + } }, []); - const handleProtectConfirm = useCallback(async (settings: ProtectiveSettings) => { + const handleProtectConfirm = useCallback(async (settings: ScaleOutSettings) => { if (!protectPositionModal.position) return; try { - const response = await fetch('/api/positions/protect', { + const response = await fetch('/api/positions/scale-out', { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -376,15 +467,19 @@ export default function PositionTable({ const result = await response.json(); if (result.success) { - toast.success(`Protection activated for ${protectPositionModal.position.symbol}`, { + toast.success(`Scale out active for ${protectPositionModal.position.symbol}`, { description: settings.enableBreakeven ? `Breakeven order and ${settings.trimLevels.length} trim level(s) set` : `${settings.trimLevels.length} trim level(s) set`, duration: 5000, }); + + // Update protection status + const key = `${protectPositionModal.position.symbol}_${protectPositionModal.position.side}`; + setProtectionStatus(prev => ({ ...prev, [key]: true })); } else { showTradingError( - 'Failed to activate protection', + 'Failed to activate scale out', result.error || 'An unknown error occurred', { symbol: protectPositionModal.position.symbol, @@ -415,28 +510,31 @@ export default function PositionTable({ // Use passed positions if available, otherwise use fetched positions // Apply live mark prices to calculate real-time PnL - const displayPositions = (positions.length > 0 ? positions : realPositions).map(position => { - const liveMarkPrice = markPrices[position.symbol]; - if (liveMarkPrice && liveMarkPrice !== position.markPrice) { - // Calculate live PnL based on current mark price - const entryPrice = position.entryPrice; - const quantity = position.quantity; - const isLong = position.side === 'LONG'; - - const priceDiff = liveMarkPrice - entryPrice; - const livePnL = isLong ? priceDiff * quantity : -priceDiff * quantity; - const notionalValue = quantity * entryPrice; - const livePnLPercent = notionalValue > 0 ? (livePnL / notionalValue) * 100 : 0; - - return { - ...position, - markPrice: liveMarkPrice, - pnl: livePnL, - pnlPercent: livePnLPercent - }; - } - return position; - }); + // Memoized to avoid recalculating on every render + const displayPositions = useMemo(() => { + return (positions.length > 0 ? positions : realPositions).map(position => { + const liveMarkPrice = markPrices[position.symbol]; + if (liveMarkPrice && liveMarkPrice !== position.markPrice) { + // Calculate live PnL based on current mark price + const entryPrice = position.entryPrice; + const quantity = position.quantity; + const isLong = position.side === 'LONG'; + + const priceDiff = liveMarkPrice - entryPrice; + const livePnL = isLong ? priceDiff * quantity : -priceDiff * quantity; + const notionalValue = quantity * entryPrice; + const livePnLPercent = notionalValue > 0 ? (livePnL / notionalValue) * 100 : 0; + + return { + ...position, + markPrice: liveMarkPrice, + pnl: livePnL, + pnlPercent: livePnLPercent + }; + } + return position; + }); + }, [positions, realPositions, markPrices]); const _totalPnL = displayPositions.reduce((sum, p) => sum + p.pnl, 0); const _totalMargin = displayPositions.reduce((sum, p) => sum + p.margin, 0); @@ -700,18 +798,24 @@ export default function PositionTable({
- + {(() => { + const key = `${position.symbol}_${position.side}`; + const isProtected = protectionStatus[key]; + return ( + + ); + })()} -
- - {trimLevels.length > 0 && ( -
- {trimLevels.map((level, index) => ( -
-
-
- - handleUpdateTrimLevel(index, 'profitPercent', parseFloat(e.target.value) || 0)} - placeholder="2" - /> -
-
- - handleUpdateTrimLevel(index, 'trimPercent', parseFloat(e.target.value) || 25)} - placeholder="25" - /> -
-
- -
- ))} -
- )} -
- - - - - Protective orders will be placed as LIMIT orders that execute when price hits your targets. - They won't interfere with your existing TP/SL orders. - - -
- - - - - - - - ); -} diff --git a/src/components/ScaleOutModal.tsx b/src/components/ScaleOutModal.tsx new file mode 100644 index 0000000..dbfcd93 --- /dev/null +++ b/src/components/ScaleOutModal.tsx @@ -0,0 +1,435 @@ +'use client'; + +import { useState } from 'react'; +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Label } from '@/components/ui/label'; +import { Input } from '@/components/ui/input'; +import { Switch } from '@/components/ui/switch'; +import { Separator } from '@/components/ui/separator'; +import { Shield, Plus, Trash2, Info, AlertTriangle } from 'lucide-react'; +import { Alert, AlertDescription } from '@/components/ui/alert'; + +interface ScaleOutModalProps { + isOpen: boolean; + onClose: () => void; + position: { + symbol: string; + side: 'LONG' | 'SHORT'; + quantity: number; + entryPrice: number; + markPrice: number; + } | null; + onConfirm: (settings: ScaleOutSettings) => Promise; +} + +export interface ScaleOutSettings { + enableBreakeven: boolean; + breakevenTrimPercent?: number; + trimLevels: Array<{ + profitPercent: number; + trimPercent: number; + }>; + enableTrailingTakeProfit: boolean; + trailingTakeProfitPercent?: number; + trailingActivationPercent?: number; + enableDCAOnDrop?: boolean; + disableDefaultTPSL?: boolean; +} + +export function ScaleOutModal({ isOpen, onClose, position, onConfirm }: ScaleOutModalProps) { + const [isSubmitting, setIsSubmitting] = useState(false); + const [breakevenEnabled, setBreakevenEnabled] = useState(false); + const [breakevenTrim, setBreakevenTrim] = useState(50); + const [trimLevels, setTrimLevels] = useState>([]); + const [trailingTakeProfitEnabled, setTrailingTakeProfitEnabled] = useState(false); + const [trailingTakeProfitPercent, setTrailingTakeProfitPercent] = useState(2); + const [trailingActivationPercent, setTrailingActivationPercent] = useState(1); + const [enableDCAOnDrop, setEnableDCAOnDrop] = useState(false); // Activate when position is 1% profitable + const [disableDefaultTPSL, setDisableDefaultTPSL] = useState(false); + + // Check if any orders would trigger immediately + const getImmediateExecutionWarnings = (): string[] => { + if (!position) return []; + + const warnings: string[] = []; + const currentPnlPercent = ((position.markPrice - position.entryPrice) / position.entryPrice) * 100; + const isLong = position.side === 'LONG'; + + // Adjust for SHORT positions (negative PnL when price goes up) + const effectivePnl = isLong ? currentPnlPercent : -currentPnlPercent; + + // Check if breakeven would trigger immediately + if (breakevenEnabled && effectivePnl >= 0) { + warnings.push(`โš ๏ธ Breakeven order will execute immediately (position is ${effectivePnl.toFixed(2)}% profitable)`); + } + + // Check if any trim levels would trigger immediately + trimLevels.forEach((level, idx) => { + if (effectivePnl >= level.profitPercent) { + warnings.push(`โš ๏ธ Trim level #${idx + 1} (${level.profitPercent}%) will execute immediately`); + } + }); + + return warnings; + }; + + const immediateWarnings = getImmediateExecutionWarnings(); + + const handleAddTrimLevel = () => { + setTrimLevels([...trimLevels, { profitPercent: 2, trimPercent: 25 }]); + }; + + const handleRemoveTrimLevel = (index: number) => { + setTrimLevels(trimLevels.filter((_, i) => i !== index)); + }; + + const handleUpdateTrimLevel = (index: number, field: 'profitPercent' | 'trimPercent', value: number) => { + const updated = [...trimLevels]; + updated[index][field] = value; + setTrimLevels(updated); + }; + + const handleSubmit = async () => { + // Check if there's any full position exit method enabled + const hasFullExit = + (breakevenEnabled && breakevenTrim === 100) || + trimLevels.some(level => level.trimPercent === 100); + + // Check if only trailing TP is enabled (acts like no stop loss) + const onlyTrailingTP = !breakevenEnabled && trimLevels.length === 0 && trailingTakeProfitEnabled; + + // Validate: if disabling default TP/SL, must have full position exit OR understand the risk + if (disableDefaultTPSL) { + if (!breakevenEnabled && trimLevels.length === 0 && !trailingTakeProfitEnabled) { + alert('โš ๏ธ You must enable at least one exit method (Breakeven, Trim Levels, or Trailing TP) when disabling default TP/SL. Otherwise your position has no exit protection.'); + return; + } + + if (onlyTrailingTP) { + const confirmed = confirm( + 'โš ๏ธ Warning: Only Trailing TP enabled with no Stop Loss\n\n' + + 'Your position will have NO downside protection. If price moves against you, the position will remain open until liquidation.\n\n' + + 'Trailing TP only closes positions when profitable. Are you sure you want to continue?' + ); + if (!confirmed) return; + } else if (!hasFullExit) { + const confirmed = confirm( + 'โš ๏ธ Warning: Partial exit only, no full position close\n\n' + + 'Your scale out settings will only reduce the position size. The remaining position will have no exit protection and could remain open indefinitely.\n\n' + + 'Consider setting at least one trim level to 100% or enabling Breakeven with 100% trim. Continue anyway?' + ); + if (!confirmed) return; + } + } + + setIsSubmitting(true); + try { + await onConfirm({ + enableBreakeven: breakevenEnabled, + breakevenTrimPercent: breakevenTrim, + trimLevels, + enableTrailingTakeProfit: trailingTakeProfitEnabled, + trailingTakeProfitPercent: trailingTakeProfitPercent, + trailingActivationPercent: trailingActivationPercent, + enableDCAOnDrop: enableDCAOnDrop, + disableDefaultTPSL: disableDefaultTPSL, + }); + onClose(); + } catch (error) { + console.error('Failed to activate protection:', error); + } finally { + setIsSubmitting(false); + } + }; + + if (!position) return null; + + return ( + + + + + + Scale Out Strategy - {position.symbol} + + + Configure automated partial exits to reduce position size at specific profit levels + + + +
+ {/* Position Info */} +
+
+ Side: + {position.side} +
+
+ Quantity: + {position.quantity} +
+
+ Entry: + ${position.entryPrice.toFixed(2)} +
+
+ Current: + ${position.markPrice.toFixed(2)} +
+
+ + + + {/* Immediate Execution Warning */} + {immediateWarnings.length > 0 && ( + + + +
Orders will execute immediately:
+
    + {immediateWarnings.map((warning, idx) => ( +
  • {warning}
  • + ))} +
+
+
+ )} + + {/* Breakeven Protection */} +
+
+
+ +

Trim position when price returns near entry

+
+ +
+ + {breakevenEnabled && ( +
+
+ + setBreakevenTrim(parseFloat(e.target.value) || 50)} + placeholder="50" + /> +

% of position to close

+
+
+ )} +
+ + + + {/* Additional Trim Levels */} +
+
+
+ +

Set multiple profit/loss targets

+
+ +
+ + {trimLevels.length > 0 && ( +
+ {trimLevels.map((level, index) => ( +
+
+
+ + handleUpdateTrimLevel(index, 'profitPercent', parseFloat(e.target.value) || 0)} + placeholder="2" + /> +
+
+ + handleUpdateTrimLevel(index, 'trimPercent', parseFloat(e.target.value) || 25)} + placeholder="25" + /> +
+
+ +
+ ))} +
+ )} +
+ + + + {/* Trailing Take Profit */} +
+
+
+ +

Captures upside while protecting profits (exit never falls below break-even)

+
+ +
+ + {trailingTakeProfitEnabled && ( +
+
+ + setTrailingActivationPercent(parseFloat(e.target.value) || 1)} + placeholder="1" + /> +

+ Trailing activates when position reaches {trailingActivationPercent}% profit +

+
+
+ + setTrailingTakeProfitPercent(parseFloat(e.target.value) || 2)} + placeholder="2" + /> +

+ TP will be placed {trailingTakeProfitPercent}% {position.side === 'LONG' ? 'below' : 'above'} highest profitable price (never below entry) +

+
+
+
+ +

Continue adding to position if liquidations meet threshold

+
+ +
+
+ )} +
+ + + + {/* Disable Default TP/SL */} +
+
+
+ +

Remove bot's automatic stop loss and take profit orders for this position

+
+ +
+ + {disableDefaultTPSL && ( + <> + {(() => { + const hasFullExit = + (breakevenEnabled && breakevenTrim === 100) || + trimLevels.some(level => level.trimPercent === 100); + + const onlyTrailingTP = !breakevenEnabled && trimLevels.length === 0 && trailingTakeProfitEnabled; + const noExitMethods = !breakevenEnabled && trimLevels.length === 0 && !trailingTakeProfitEnabled; + + if (hasFullExit) { + // Full exit configured - no warning needed + return null; + } + + if (noExitMethods) { + return ( + + + +
โš ๏ธ Warning: No exit protection
+

+ You must enable at least one scale out method (breakeven, trim levels, or trailing TP). + Otherwise, your position will remain open indefinitely or until liquidation. +

+
+
+ ); + } + + if (onlyTrailingTP) { + return ( + + + +
โš ๏ธ Warning: No stop loss protection
+

+ Trailing TP only closes positions when profitable. If price moves against you, + there will be no downside protection and the position could remain open until liquidation. +

+
+
+ ); + } + + // Partial exits only + return ( + + + +
โš ๏ธ Warning: Partial exit only
+

+ Your scale out settings will only reduce position size. The remaining position will have no exit protection. + Consider setting at least one trim level to 100% for full position close. +

+
+
+ ); + })()} + + )} +
+ + + + + Protective orders will be placed as LIMIT orders that execute when price hits your targets. + They won't interfere with your existing TP/SL orders. + + +
+ + + + + +
+
+ ); +} diff --git a/src/hooks/useBotStatus.ts b/src/hooks/useBotStatus.ts index cec6a88..03d2442 100644 --- a/src/hooks/useBotStatus.ts +++ b/src/hooks/useBotStatus.ts @@ -55,6 +55,10 @@ export function useBotStatus(): UseBotStatusReturn { case 'sl_placed': case 'tp_placed': case 'threshold_update': + case 'scale_out_activated': + case 'scale_out_deactivated': + case 'scale_out_status_response': + case 'scale_out_status_update': // These messages are handled by other components, ignore silently break; default: diff --git a/src/lib/bot/positionManager.ts b/src/lib/bot/positionManager.ts index cbdf786..5f704f2 100644 --- a/src/lib/bot/positionManager.ts +++ b/src/lib/bot/positionManager.ts @@ -964,12 +964,54 @@ logWithTimestamp(`PositionManager: ORDER_TRADE_UPDATE - Symbol: ${symbol}, Order const protectiveService = getProtectiveOrderService(); if (protectiveService) { protectiveService.handleOrderFilled(orderId); + + // Broadcast status update to UI + const positionSide = order.ps || 'BOTH'; + const side = positionSide === 'LONG' ? 'LONG' : positionSide === 'SHORT' ? 'SHORT' : (order.S === 'BUY' ? 'SHORT' : 'LONG'); + const isActive = protectiveService.isProtectionActive(symbol, side); + if (this.statusBroadcaster) { + this.statusBroadcaster.broadcast('scale_out_status_update', { + symbol, + side, + isActive, + reason: 'order_filled' + }); + } } } // Trigger balance refresh after SL/TP execution this.refreshBalance(); } + + // Check if this is a canceled protective order + if (orderStatus === 'CANCELED') { + const clientOrderId = order.c; + if (clientOrderId && clientOrderId.startsWith('po_')) { + const protectiveService = getProtectiveOrderService(); + if (protectiveService) { + protectiveService.handleOrderFilled(orderId); // Same cleanup for canceled orders + + // Broadcast status update to UI (but skip if manually deactivating) + const positionSide = order.ps || 'BOTH'; + const side = positionSide === 'LONG' ? 'LONG' : positionSide === 'SHORT' ? 'SHORT' : (order.S === 'BUY' ? 'SHORT' : 'LONG'); + + // Skip status update if this position is being manually deactivated + // (the deactivation handler will send its own status update) + if (!protectiveService.isDeactivating(symbol, side)) { + const isActive = protectiveService.isProtectionActive(symbol, side); + if (this.statusBroadcaster) { + this.statusBroadcaster.broadcast('scale_out_status_update', { + symbol, + side, + isActive, + reason: 'order_canceled' + }); + } + } + } + } + } // Track our SL/TP order IDs when they're placed if (orderStatus === 'NEW' && (orderType === 'STOP_MARKET' || orderType === 'TAKE_PROFIT_MARKET')) { @@ -1270,6 +1312,13 @@ logWithTimestamp(`PositionManager: Notified of potential new position: ${data.sy const posAmt = parseFloat(position.positionAmt); const key = this.getPositionKey(symbol, position.positionSide, posAmt); + // Check if default TP/SL is disabled for this position via scale out settings + const protectiveOrderService = (await import('../services/protectiveOrderService')).getProtectiveOrderService(); + if (protectiveOrderService?.isDefaultTPSLDisabled(symbol, position.positionSide)) { +logWithTimestamp(`PositionManager: Skipping default TP/SL management for ${key} - disabled via scale out settings`); + return; + } + // Check if adjustment is already in progress for this position if (this.orderPlacementLocks.has(key)) { logWithTimestamp(`PositionManager: Order adjustment already in progress for ${key}, skipping`); @@ -1362,6 +1411,14 @@ logWarnWithTimestamp(`PositionManager: No config for symbol ${symbol}`); } const posAmt = parseFloat(position.positionAmt); + + // Check if default TP/SL is disabled for this position via scale out settings + const protectiveOrderService = (await import('../services/protectiveOrderService')).getProtectiveOrderService(); + if (protectiveOrderService?.isDefaultTPSLDisabled(symbol, position.positionSide)) { +logWithTimestamp(`PositionManager: Skipping default TP/SL placement for ${symbol} ${position.positionSide} - disabled via scale out settings`); + return; + } + const entryPrice = parseFloat(position.entryPrice); const quantity = Math.abs(posAmt); const isLong = posAmt > 0; @@ -2108,7 +2165,7 @@ logWithTimestamp(`PositionManager: Found stuck entry order for ${order.symbol} - } // Find all SL orders for this specific position - const slOrders = openOrders.filter(o => { + const slOrders = managedOrders.filter(o => { // Must match symbol if (o.symbol !== symbol) return false; // Must be a stop order type @@ -2125,7 +2182,7 @@ logWithTimestamp(`PositionManager: Evaluating SL order ${o.orderId} for position }); // Find all TP orders for this specific position - const tpOrders = openOrders.filter(o => { + const tpOrders = managedOrders.filter(o => { // Must match symbol if (o.symbol !== symbol) return false; // Must be a take profit or limit order type diff --git a/src/lib/services/protectiveOrderService.ts b/src/lib/services/protectiveOrderService.ts index 9c19b4d..b324caf 100644 --- a/src/lib/services/protectiveOrderService.ts +++ b/src/lib/services/protectiveOrderService.ts @@ -26,11 +26,12 @@ interface ProtectiveOrder { symbol: string; side: 'BUY' | 'SELL'; positionSide: string; - triggerType: 'breakeven' | 'trim_level'; + triggerType: 'breakeven' | 'trim_level' | 'trailing_tp'; triggerPercent: number; quantity: number; price: number; createdAt: number; + trailPercent?: number; // For trailing TP } export class ProtectiveOrderService extends EventEmitter { @@ -38,6 +39,10 @@ export class ProtectiveOrderService extends EventEmitter { private activeOrders: Map = new Map(); // key: "BTCUSDT_LONG" private isRunning = false; private monitorInterval?: NodeJS.Timeout; + private deactivatingKeys: Set = new Set(); // Track positions being manually deactivated + private trailingStops: Map = new Map(); + private trailingStopMonitor?: NodeJS.Timeout; + private disabledDefaultTPSL: Set = new Set(); // Track positions with disabled default TP/SL (key: "BTCUSDT_LONG") constructor(config: Config) { super(); @@ -55,7 +60,7 @@ export class ProtectiveOrderService extends EventEmitter { } this.isRunning = true; - // Note: No monitoring interval needed - protective orders are activated on-demand via UI + // Note: No monitoring interval needed - scale out orders are activated on-demand via UI logWithTimestamp('ProtectiveOrderService: Started (on-demand mode)'); } @@ -84,6 +89,11 @@ export class ProtectiveOrderService extends EventEmitter { enableBreakeven: boolean; breakevenTrimPercent?: number; trimLevels: Array<{ profitPercent: number; trimPercent: number }>; + enableTrailingTakeProfit: boolean; + trailingTakeProfitPercent?: number; + trailingActivationPercent?: number; + enableDCAOnDrop?: boolean; + disableDefaultTPSL?: boolean; } ): Promise { if (currentQuantity <= 0) { @@ -114,8 +124,17 @@ export class ProtectiveOrderService extends EventEmitter { // Clear any existing protective orders for this position this.clearProtectiveOrders(symbol, positionSide); + // Cancel default TP/SL if requested and mark position to skip future recreations + if (settings.disableDefaultTPSL) { + this.disabledDefaultTPSL.add(key); + await this.cancelDefaultTPSL(symbol, positionSide); + } else { + // Ensure we remove the flag if user re-enables default TP/SL + this.disabledDefaultTPSL.delete(key); + } + logWithTimestamp( - `ProtectiveOrderService: Activating protection for ${symbol} ${side} - Breakeven: ${settings.enableBreakeven}, Trim levels: ${settings.trimLevels.length}` + `ProtectiveOrderService: Activating scale out for ${symbol} ${side} - Breakeven: ${settings.enableBreakeven}, Trim levels: ${settings.trimLevels.length}, Trailing TP: ${settings.enableTrailingTakeProfit}, DCA: ${settings.enableDCAOnDrop || false}, Disable Default TP/SL: ${settings.disableDefaultTPSL || false}` ); // Place breakeven order if enabled @@ -135,8 +154,16 @@ export class ProtectiveOrderService extends EventEmitter { ); } + // Place trailing take profit if enabled + if (settings.enableTrailingTakeProfit) { + const trailPercent = settings.trailingTakeProfitPercent || 2; // Default 2% + const activationPercent = settings.trailingActivationPercent || 0; // Default 0% (immediate) + const enableDCA = settings.enableDCAOnDrop || false; + await this.placeTrailingTakeProfit(mockPosition, entryPrice, trailPercent, activationPercent, enableDCA, key); + } + logWithTimestamp( - `ProtectiveOrderService: Protection activated for ${symbol} ${side}` + `ProtectiveOrderService: Scale out activated for ${symbol} ${side}` ); } @@ -172,12 +199,26 @@ export class ProtectiveOrderService extends EventEmitter { price: symbolPrecision.formatPrice(symbol, triggerPrice), timeInForce: 'GTC', positionSide: position.positionSide, - reduceOnly: true, newClientOrderId: clientOrderId, }; + // In one-way mode, use reduceOnly. In hedge mode, don't use it. + if (this.config.global.positionMode !== 'HEDGE') { + orderParams.reduceOnly = true; + } + + logWithTimestamp( + `ProtectiveOrderService: Placing breakeven order for ${symbol}:`, + JSON.stringify({ ...orderParams, apiKey: '***' }, null, 2) + ); + const order = await placeOrder(orderParams, this.config.api); + logWithTimestamp( + `ProtectiveOrderService: โœ… Binance response for breakeven order:`, + JSON.stringify(order, null, 2) + ); + const protectiveOrder: ProtectiveOrder = { orderId: order.orderId, symbol, @@ -196,7 +237,7 @@ export class ProtectiveOrderService extends EventEmitter { this.activeOrders.get(key)!.push(protectiveOrder); logWithTimestamp( - `ProtectiveOrderService: Placed breakeven order for ${symbol} at ${triggerPrice.toFixed(2)} (${trimPercent}% of position)` + `ProtectiveOrderService: โœ… Placed breakeven order #${order.orderId} for ${symbol} at ${triggerPrice.toFixed(2)} (${trimPercent}% of position)` ); this.emit('protectiveOrderPlaced', protectiveOrder); @@ -245,10 +286,19 @@ export class ProtectiveOrderService extends EventEmitter { price: symbolPrecision.formatPrice(symbol, triggerPrice), timeInForce: 'GTC', positionSide: position.positionSide, - reduceOnly: true, newClientOrderId: clientOrderId, }; + // In one-way mode, use reduceOnly. In hedge mode, don't use it. + if (this.config.global.positionMode !== 'HEDGE') { + orderParams.reduceOnly = true; + } + + logWithTimestamp( + `ProtectiveOrderService: Placing trim order for ${symbol} (+${profitPercent}%):`, + JSON.stringify({ ...orderParams, apiKey: '***' }, null, 2) + ); + const order = await placeOrder(orderParams, this.config.api); const protectiveOrder: ProtectiveOrder = { @@ -269,7 +319,7 @@ export class ProtectiveOrderService extends EventEmitter { this.activeOrders.get(key)!.push(protectiveOrder); logWithTimestamp( - `ProtectiveOrderService: Placed trim order for ${symbol} at ${triggerPrice.toFixed(2)} (+${profitPercent}%, ${trimPercent}% of position)` + `ProtectiveOrderService: โœ… Placed trim order #${order.orderId} for ${symbol} at ${triggerPrice.toFixed(2)} (+${profitPercent}%, ${trimPercent}% of position)` ); this.emit('protectiveOrderPlaced', protectiveOrder); @@ -282,6 +332,119 @@ export class ProtectiveOrderService extends EventEmitter { } } + /** + * Place a trailing take profit order (never goes below entry - profit protection only) + */ + private async placeTrailingTakeProfit( + position: ExchangePosition, + entryPrice: number, + trailPercent: number, + activationPercent: number, + enableDCA: boolean, + key: string + ): Promise { + const symbol = position.symbol; + const posAmt = parseFloat(position.positionAmt); + const isLong = posAmt > 0; + + // Calculate activation price (when trailing starts) + const stopDistance = trailPercent / 100; + const activationDistance = activationPercent / 100; + + const activationPrice = activationPercent > 0 + ? (isLong ? entryPrice * (1 + activationDistance) : entryPrice * (1 - activationDistance)) + : entryPrice; + + // Calculate initial TP price - NEVER below entry for profit protection + const idealTpPrice = isLong + ? activationPrice * (1 - stopDistance) + : activationPrice * (1 + stopDistance); + + // Enforce minimum TP at entry price (break-even) to prevent losses + const initialTpPrice = isLong + ? Math.max(idealTpPrice, entryPrice) + : Math.min(idealTpPrice, entryPrice); + + // Full position quantity for the stop + const stopQuantity = Math.abs(posAmt); + const formattedQty = symbolPrecision.formatQuantity(symbol, stopQuantity); + + try { + const side = isLong ? 'SELL' : 'BUY'; + const clientOrderId = `po_trail_${symbol}_${Date.now()}`; + + const orderParams: any = { + symbol, + side, + type: 'STOP_MARKET', + quantity: formattedQty, + stopPrice: symbolPrecision.formatPrice(symbol, initialTpPrice), + positionSide: position.positionSide, + newClientOrderId: clientOrderId, + workingType: 'MARK_PRICE', // Use mark price to avoid liquidation wick manipulation + }; + + // In one-way mode, use reduceOnly. In hedge mode, don't use it. + if (this.config.global.positionMode !== 'HEDGE') { + orderParams.reduceOnly = true; + } + + logWithTimestamp( + `ProtectiveOrderService: Placing trailing TP for ${symbol}:`, + JSON.stringify({ ...orderParams, apiKey: '***' }, null, 2) + ); + + const order = await placeOrder(orderParams, this.config.api); + + logWithTimestamp( + `ProtectiveOrderService: โœ… Binance response for trailing TP:`, + JSON.stringify(order, null, 2) + ); + + const protectiveOrder: ProtectiveOrder = { + orderId: order.orderId, + symbol, + side, + positionSide: position.positionSide, + triggerType: 'trailing_tp', + triggerPercent: 0, + quantity: stopQuantity, + price: initialTpPrice, + createdAt: Date.now(), + trailPercent, + }; + + if (!this.activeOrders.has(key)) { + this.activeOrders.set(key, []); + } + this.activeOrders.get(key)!.push(protectiveOrder); + + // Track trailing TP state + this.trailingStops.set(key, { + trailPercent, + highestPrice: activationPrice, // Start tracking from activation price + orderId: order.orderId, + entryPrice, + enableDCA, + }); + + logWithTimestamp( + `ProtectiveOrderService: โœ… Placed trailing TP #${order.orderId} for ${symbol} at ${initialTpPrice.toFixed(2)} (activates at ${activationPercent}% profit, trails ${trailPercent}%, break-even protected)` + ); + + // Start monitoring if not already running + this.startTrailingTakeProfitMonitoring(); + + this.emit('protectiveOrderPlaced', protectiveOrder); + } catch (error: any) { + logErrorWithTimestamp( + `ProtectiveOrderService: Failed to place trailing stop for ${symbol}:`, + error?.response?.data || error?.message + ); + throw error; + } + } + // DEPRECATED: Old config-based methods (not used - protective orders are now on-demand via UI) /* public async checkPositionForProtectiveOrders( @@ -531,7 +694,96 @@ export class ProtectiveOrderService extends EventEmitter { public clearProtectiveOrders(symbol: string, positionSide: string): void { const key = this.getPositionKey(symbol, positionSide); this.activeOrders.delete(key); - logWithTimestamp(`ProtectiveOrderService: Cleared protective orders for ${key}`); + logWithTimestamp(`ProtectiveOrderService: Cleared scale out orders for ${key}`); + } + + /** + * Check if a position has active scale out + */ + public isProtectionActive(symbol: string, side: string): boolean { + const key = this.getPositionKey(symbol, side); + const orders = this.activeOrders.get(key); + return orders !== undefined && orders.length > 0; + } + + /** + * Cancel default TP/SL orders for a position + */ + private async cancelDefaultTPSL(symbol: string, positionSide: string): Promise { + try { + const { getOpenOrders } = await import('../api/market'); + const { cancelOrder } = await import('../api/orders'); + const openOrders = await getOpenOrders(symbol, this.config.api); + + // Filter for TP/SL orders (TAKE_PROFIT_MARKET, STOP_MARKET, STOP, TAKE_PROFIT) + // Exclude protective orders (po_ prefix) + const tpslOrders = openOrders.filter((order: any) => { + const isTPSL = ['TAKE_PROFIT_MARKET', 'STOP_MARKET', 'STOP', 'TAKE_PROFIT'].includes(order.type); + const isProtective = order.clientOrderId?.startsWith('po_'); + const matchesPositionSide = order.positionSide === positionSide; + return isTPSL && !isProtective && matchesPositionSide; + }); + + if (tpslOrders.length === 0) { + logWithTimestamp(`ProtectiveOrderService: No default TP/SL orders found for ${symbol} ${positionSide}`); + return; + } + + logWithTimestamp(`ProtectiveOrderService: Cancelling ${tpslOrders.length} default TP/SL orders for ${symbol} ${positionSide}`); + + for (const order of tpslOrders) { + try { + await cancelOrder({ symbol, orderId: order.orderId }, this.config.api); + logWithTimestamp(`ProtectiveOrderService: Cancelled default ${order.type} order #${order.orderId}`); + } catch (error: any) { + logErrorWithTimestamp(`ProtectiveOrderService: Failed to cancel order ${order.orderId}:`, error.message); + } + } + } catch (error: any) { + logErrorWithTimestamp(`ProtectiveOrderService: Failed to cancel default TP/SL for ${symbol}:`, error.message); + } + } + + /** + * Deactivate scale out for a position (cancel all scale out orders) + */ + public async deactivateProtection(symbol: string, side: string): Promise { + const key = this.getPositionKey(symbol, side); + const orders = this.activeOrders.get(key); + + if (!orders || orders.length === 0) { + logWithTimestamp(`ProtectiveOrderService: No active scale out for ${key}`); + return; + } + + // Mark this position as being manually deactivated + this.deactivatingKeys.add(key); + + logWithTimestamp(`ProtectiveOrderService: Deactivating scale out for ${key} - Cancelling ${orders.length} orders`); + + // Cancel all protective orders + const cancelOrder = (await import('../api/orders')).cancelOrder; + + for (const order of orders) { + try { + await cancelOrder({ symbol, orderId: Number(order.orderId) }, this.config.api); + logWithTimestamp(`ProtectiveOrderService: Cancelled ${order.triggerType} order for ${symbol}`); + } catch (error: any) { + logErrorWithTimestamp(`ProtectiveOrderService: Failed to cancel order ${order.orderId}:`, error.message); + } + } + + // Clear from tracking + this.activeOrders.delete(key); + this.trailingStops.delete(key); // Also clear trailing stop tracking + this.disabledDefaultTPSL.delete(key); // Allow default TP/SL to resume + + // Remove from deactivating set after a delay (to handle race conditions with ORDER_TRADE_UPDATE) + setTimeout(() => { + this.deactivatingKeys.delete(key); + }, 2000); + + logWithTimestamp(`ProtectiveOrderService: Scale out deactivated for ${key}`); } // Handle order fill events to remove from tracking @@ -546,10 +798,184 @@ export class ProtectiveOrderService extends EventEmitter { `ProtectiveOrderService: Protective order filled - ${order.symbol} ${order.triggerType} at ${order.price.toFixed(2)}` ); this.emit('protectiveOrderFilled', order); + + // If it was a trailing TP, clean up tracking + if (order.triggerType === 'trailing_tp') { + this.trailingStops.delete(key); + } + + // If no orders left, clean up + if (orders.length === 0) { + this.activeOrders.delete(key); + this.trailingStops.delete(key); + } break; } } } + + // Check if a position is being manually deactivated + public isDeactivating(symbol: string, side: string): boolean { + const key = this.getPositionKey(symbol, side); + return this.deactivatingKeys.has(key); + } + + /** + * Start monitoring trailing TPs to adjust them as price moves + */ + private startTrailingTakeProfitMonitoring(): void { + if (this.trailingStopMonitor) { + return; // Already monitoring + } + + logWithTimestamp('ProtectiveOrderService: Starting trailing TP monitoring'); + + this.trailingStopMonitor = setInterval(async () => { + await this.checkAndAdjustTrailingTakeProfits(); + }, 5000); // Check every 5 seconds + } + + /** + * Stop monitoring trailing TPs + */ + private stopTrailingTakeProfitMonitoring(): void { + if (this.trailingStopMonitor) { + clearInterval(this.trailingStopMonitor); + this.trailingStopMonitor = undefined; + logWithTimestamp('ProtectiveOrderService: Stopped trailing TP monitoring'); + } + } + + /** + * Check all active trailing TPs and adjust if needed + */ + private async checkAndAdjustTrailingTakeProfits(): Promise { + if (this.trailingStops.size === 0) { + this.stopTrailingTakeProfitMonitoring(); + return; + } + + try { + // Get current mark prices for all symbols + const { getMarkPrice } = await import('../api/market'); + const markPrices = (await getMarkPrice()) as any[]; + const priceMap = new Map(markPrices.map((p: any) => [p.symbol, parseFloat(p.markPrice)])); + + for (const [key, trailData] of this.trailingStops.entries()) { + const [symbol, positionSide] = key.split('_'); + const currentPrice = priceMap.get(symbol); + + if (!currentPrice) continue; + + const isLong = positionSide === 'LONG'; + + // Check if we have a new high (for LONG) or new low (for SHORT) + let needsAdjustment = false; + let newHighestPrice = trailData.highestPrice; + + if (isLong && currentPrice > trailData.highestPrice) { + newHighestPrice = currentPrice; + needsAdjustment = true; + } else if (!isLong && currentPrice < trailData.highestPrice) { + newHighestPrice = currentPrice; + needsAdjustment = true; + } + + if (needsAdjustment) { + // Calculate new TP price + const stopDistance = trailData.trailPercent / 100; + const idealTpPrice = isLong + ? newHighestPrice * (1 - stopDistance) + : newHighestPrice * (1 + stopDistance); + + // Enforce break-even: never let TP go below entry price + const newTpPrice = isLong + ? Math.max(idealTpPrice, trailData.entryPrice) + : Math.min(idealTpPrice, trailData.entryPrice); + + // Update the TP order + await this.adjustTrailingTakeProfit(symbol, positionSide, trailData.orderId, newTpPrice); + + // Update tracking + trailData.highestPrice = newHighestPrice; + } + } + } catch (error: any) { + logErrorWithTimestamp('ProtectiveOrderService: Error checking trailing TPs:', error.message); + } + } + + /** + * Adjust a trailing TP order to a new price + */ + private async adjustTrailingTakeProfit( + symbol: string, + positionSide: string, + oldOrderId: string, + newStopPrice: number + ): Promise { + try { + const { cancelOrder } = await import('../api/orders'); + const key = this.getPositionKey(symbol, positionSide); + + // Find the order in tracking + const orders = this.activeOrders.get(key); + const orderIndex = orders?.findIndex(o => o.orderId === oldOrderId); + + if (orderIndex === undefined || orderIndex === -1 || !orders) { + logWarnWithTimestamp(`ProtectiveOrderService: Trailing TP order ${oldOrderId} not found in tracking`); + return; + } + + const oldOrder = orders[orderIndex]; + const quantity = oldOrder.quantity; + const side = oldOrder.side; + + // Cancel old order + await cancelOrder({ symbol, orderId: Number(oldOrderId) }, this.config.api); + + // Place new order at adjusted price + const clientOrderId = `po_trail_${symbol}_${Date.now()}`; + const orderParams: any = { + symbol, + side, + type: 'STOP_MARKET', + quantity: symbolPrecision.formatQuantity(symbol, quantity), + stopPrice: symbolPrecision.formatPrice(symbol, newStopPrice), + positionSide, + newClientOrderId: clientOrderId, + workingType: 'MARK_PRICE', + }; + + if (this.config.global.positionMode !== 'HEDGE') { + orderParams.reduceOnly = true; + } + + const newOrder = await placeOrder(orderParams, this.config.api); + + // Update tracking + orders[orderIndex] = { + ...oldOrder, + orderId: newOrder.orderId, + price: newStopPrice, + }; + + // Update trail data + const trailData = this.trailingStops.get(key); + if (trailData) { + trailData.orderId = newOrder.orderId; + } + + logWithTimestamp( + `ProtectiveOrderService: โœ… Adjusted trailing TP for ${symbol} from ${oldOrder.price.toFixed(2)} to ${newStopPrice.toFixed(2)}` + ); + } catch (error: any) { + logErrorWithTimestamp( + `ProtectiveOrderService: Failed to adjust trailing TP for ${symbol}:`, + error?.response?.data || error?.message + ); + } + } // DEPRECATED: Placeholder for old automatic monitoring // private async checkAndPlaceProtectiveOrders(): Promise { @@ -566,6 +992,12 @@ export class ProtectiveOrderService extends EventEmitter { const key = this.getPositionKey(symbol, positionSide); return this.activeOrders.get(key) || []; } + + // Check if default TP/SL is disabled for this position + public isDefaultTPSLDisabled(symbol: string, positionSide: string): boolean { + const key = this.getPositionKey(symbol, positionSide); + return this.disabledDefaultTPSL.has(key); + } } // Singleton instance diff --git a/src/lib/services/websocketService.ts b/src/lib/services/websocketService.ts index 6a5b353..bcca37b 100644 --- a/src/lib/services/websocketService.ts +++ b/src/lib/services/websocketService.ts @@ -246,7 +246,6 @@ class WebSocketService { addMessageHandler(handler: MessageHandler): () => void { this.handlers.add(handler); - console.log(`[WebSocketService] Handler added. Total handlers: ${this.handlers.size}`); // Check if we should auto-connect (skip on excluded pages) if (typeof window !== 'undefined') { @@ -280,7 +279,6 @@ class WebSocketService { // Return cleanup function return () => { this.handlers.delete(handler); - console.log(`[WebSocketService] Handler removed. Total handlers: ${this.handlers.size}`); // If no more handlers, disconnect if (this.handlers.size === 0) { From 3d6cf0f7e2c6c9e583a2c32a1123baa7e9bc41a6 Mon Sep 17 00:00:00 2001 From: birdbathd Date: Mon, 24 Nov 2025 10:48:36 +1000 Subject: [PATCH 30/30] fix: trailing take profit activation and minimum profit logic - Fix bug where trailing TP placed order immediately at breakeven - Implement two-phase activation: wait for profit threshold, then trail - Add 0.5% minimum profit buffer to prevent loss from spread/fees - Remove duplicate variable declarations causing bot crash - Update monitoring loop to handle activation threshold properly - Add activationPercent and activated state to tracking interface --- src/lib/services/protectiveOrderService.ts | 277 ++++++++++++++------- 1 file changed, 183 insertions(+), 94 deletions(-) diff --git a/src/lib/services/protectiveOrderService.ts b/src/lib/services/protectiveOrderService.ts index b324caf..8f88f90 100644 --- a/src/lib/services/protectiveOrderService.ts +++ b/src/lib/services/protectiveOrderService.ts @@ -40,7 +40,7 @@ export class ProtectiveOrderService extends EventEmitter { private isRunning = false; private monitorInterval?: NodeJS.Timeout; private deactivatingKeys: Set = new Set(); // Track positions being manually deactivated - private trailingStops: Map = new Map(); + private trailingStops: Map = new Map(); private trailingStopMonitor?: NodeJS.Timeout; private disabledDefaultTPSL: Set = new Set(); // Track positions with disabled default TP/SL (key: "BTCUSDT_LONG") @@ -347,102 +347,43 @@ export class ProtectiveOrderService extends EventEmitter { const posAmt = parseFloat(position.positionAmt); const isLong = posAmt > 0; - // Calculate activation price (when trailing starts) - const stopDistance = trailPercent / 100; - const activationDistance = activationPercent / 100; + // NOTE: We DON'T place the TP order immediately! + // The monitoring loop will place it once activation threshold is reached + // This prevents placing a TP at breakeven when activation > trail distance + // Just track the trailing TP configuration - no order placed yet + const activationDistance = activationPercent / 100; const activationPrice = activationPercent > 0 ? (isLong ? entryPrice * (1 + activationDistance) : entryPrice * (1 - activationDistance)) : entryPrice; - - // Calculate initial TP price - NEVER below entry for profit protection - const idealTpPrice = isLong - ? activationPrice * (1 - stopDistance) - : activationPrice * (1 + stopDistance); - - // Enforce minimum TP at entry price (break-even) to prevent losses - const initialTpPrice = isLong - ? Math.max(idealTpPrice, entryPrice) - : Math.min(idealTpPrice, entryPrice); - // Full position quantity for the stop - const stopQuantity = Math.abs(posAmt); - const formattedQty = symbolPrecision.formatQuantity(symbol, stopQuantity); - - try { - const side = isLong ? 'SELL' : 'BUY'; - const clientOrderId = `po_trail_${symbol}_${Date.now()}`; + // Track trailing TP state WITHOUT placing order yet + // The monitoring loop will create the order once activation is reached + this.trailingStops.set(key, { + trailPercent, + highestPrice: entryPrice, // Track from entry, not activation + orderId: '', // No order yet - will be created on activation + entryPrice, + enableDCA, + activationPercent, + activated: false, // Will be set to true once activation threshold is reached + }); - const orderParams: any = { - symbol, - side, - type: 'STOP_MARKET', - quantity: formattedQty, - stopPrice: symbolPrecision.formatPrice(symbol, initialTpPrice), - positionSide: position.positionSide, - newClientOrderId: clientOrderId, - workingType: 'MARK_PRICE', // Use mark price to avoid liquidation wick manipulation - }; - - // In one-way mode, use reduceOnly. In hedge mode, don't use it. - if (this.config.global.positionMode !== 'HEDGE') { - orderParams.reduceOnly = true; - } - - logWithTimestamp( - `ProtectiveOrderService: Placing trailing TP for ${symbol}:`, - JSON.stringify({ ...orderParams, apiKey: '***' }, null, 2) - ); - - const order = await placeOrder(orderParams, this.config.api); - - logWithTimestamp( - `ProtectiveOrderService: โœ… Binance response for trailing TP:`, - JSON.stringify(order, null, 2) - ); - - const protectiveOrder: ProtectiveOrder = { - orderId: order.orderId, - symbol, - side, - positionSide: position.positionSide, - triggerType: 'trailing_tp', - triggerPercent: 0, - quantity: stopQuantity, - price: initialTpPrice, - createdAt: Date.now(), - trailPercent, - }; - - if (!this.activeOrders.has(key)) { - this.activeOrders.set(key, []); - } - this.activeOrders.get(key)!.push(protectiveOrder); - - // Track trailing TP state - this.trailingStops.set(key, { - trailPercent, - highestPrice: activationPrice, // Start tracking from activation price - orderId: order.orderId, - entryPrice, - enableDCA, - }); - - logWithTimestamp( - `ProtectiveOrderService: โœ… Placed trailing TP #${order.orderId} for ${symbol} at ${initialTpPrice.toFixed(2)} (activates at ${activationPercent}% profit, trails ${trailPercent}%, break-even protected)` - ); + logWithTimestamp( + `ProtectiveOrderService: โœ… Trailing TP configured for ${symbol}: activates at ${activationPercent}% profit (${activationPrice.toFixed(2)}), then trails ${trailPercent}% with min 0.5% profit buffer` + ); - // Start monitoring if not already running - this.startTrailingTakeProfitMonitoring(); + // Start monitoring - will place order when activation threshold is reached + this.startTrailingTakeProfitMonitoring(); - this.emit('protectiveOrderPlaced', protectiveOrder); - } catch (error: any) { - logErrorWithTimestamp( - `ProtectiveOrderService: Failed to place trailing stop for ${symbol}:`, - error?.response?.data || error?.message - ); - throw error; - } + // Emit event for UI feedback (no order ID yet) + this.emit('protectiveOrderConfigured', { + symbol, + positionSide: position.positionSide, + triggerType: 'trailing_tp', + activationPercent, + trailPercent, + }); } // DEPRECATED: Old config-based methods (not used - protective orders are now on-demand via UI) @@ -861,6 +802,10 @@ export class ProtectiveOrderService extends EventEmitter { const markPrices = (await getMarkPrice()) as any[]; const priceMap = new Map(markPrices.map((p: any) => [p.symbol, parseFloat(p.markPrice)])); + // Also get current positions to fetch quantity + const { getPositions } = await import('../api/market'); + const positions = await getPositions(this.config.api); + for (const [key, trailData] of this.trailingStops.entries()) { const [symbol, positionSide] = key.split('_'); const currentPrice = priceMap.get(symbol); @@ -869,7 +814,74 @@ export class ProtectiveOrderService extends EventEmitter { const isLong = positionSide === 'LONG'; - // Check if we have a new high (for LONG) or new low (for SHORT) + // STEP 1: Check if activation threshold has been reached + if (!trailData.activated) { + const activationDistance = trailData.activationPercent / 100; + const activationPrice = trailData.activationPercent > 0 + ? (isLong ? trailData.entryPrice * (1 + activationDistance) : trailData.entryPrice * (1 - activationDistance)) + : trailData.entryPrice; + + const activationReached = isLong + ? currentPrice >= activationPrice + : currentPrice <= activationPrice; + + if (activationReached) { + // Activation threshold reached! Place the initial TP order + logWithTimestamp( + `ProtectiveOrderService: ๐ŸŽฏ Activation threshold reached for ${symbol} at ${currentPrice.toFixed(2)} (target: ${activationPrice.toFixed(2)})` + ); + + // Find position to get quantity + const position = positions.find(p => p.symbol === symbol); + if (!position) { + logWarnWithTimestamp(`ProtectiveOrderService: Position ${symbol} not found, skipping TP placement`); + continue; + } + + const posAmt = parseFloat(position.positionAmt); + const stopQuantity = Math.abs(posAmt); + + // Calculate initial TP with MINIMUM 0.5% profit buffer + const minProfitBuffer = 0.005; // 0.5% minimum profit + const stopDistance = trailData.trailPercent / 100; + + const idealTpPrice = isLong + ? currentPrice * (1 - stopDistance) + : currentPrice * (1 + stopDistance); + + // Enforce MINIMUM 0.5% profit (not just breakeven) + const minProfitPrice = isLong + ? trailData.entryPrice * (1 + minProfitBuffer) + : trailData.entryPrice * (1 - minProfitBuffer); + + const initialTpPrice = isLong + ? Math.max(idealTpPrice, minProfitPrice) + : Math.min(idealTpPrice, minProfitPrice); + + // Place the TP order + const orderId = await this.placeTrailingTPOrder( + symbol, + positionSide, + position.positionSide, + stopQuantity, + initialTpPrice, + isLong + ); + + if (orderId) { + trailData.orderId = orderId; + trailData.activated = true; + trailData.highestPrice = currentPrice; // Start tracking from activation point + + logWithTimestamp( + `ProtectiveOrderService: โœ… Placed initial trailing TP #${orderId} at ${initialTpPrice.toFixed(2)} (min 0.5% profit: ${minProfitPrice.toFixed(2)})` + ); + } + } + continue; // Don't trail until activated + } + + // STEP 2: Trail the TP if price moves in favorable direction let needsAdjustment = false; let newHighestPrice = trailData.highestPrice; @@ -882,22 +894,31 @@ export class ProtectiveOrderService extends EventEmitter { } if (needsAdjustment) { - // Calculate new TP price + // Calculate new TP price with minimum 0.5% profit + const minProfitBuffer = 0.005; // 0.5% minimum profit const stopDistance = trailData.trailPercent / 100; + const idealTpPrice = isLong ? newHighestPrice * (1 - stopDistance) : newHighestPrice * (1 + stopDistance); - // Enforce break-even: never let TP go below entry price + const minProfitPrice = isLong + ? trailData.entryPrice * (1 + minProfitBuffer) + : trailData.entryPrice * (1 - minProfitBuffer); + const newTpPrice = isLong - ? Math.max(idealTpPrice, trailData.entryPrice) - : Math.min(idealTpPrice, trailData.entryPrice); + ? Math.max(idealTpPrice, minProfitPrice) + : Math.min(idealTpPrice, minProfitPrice); // Update the TP order await this.adjustTrailingTakeProfit(symbol, positionSide, trailData.orderId, newTpPrice); // Update tracking trailData.highestPrice = newHighestPrice; + + logWithTimestamp( + `ProtectiveOrderService: ๐Ÿ“ˆ Trailed TP for ${symbol} to ${newTpPrice.toFixed(2)} (high: ${newHighestPrice.toFixed(2)}, min profit: ${minProfitPrice.toFixed(2)})` + ); } } } catch (error: any) { @@ -905,6 +926,74 @@ export class ProtectiveOrderService extends EventEmitter { } } + /** + * Place the initial trailing TP order when activation threshold is reached + */ + private async placeTrailingTPOrder( + symbol: string, + positionSide: string, + positionSideAPI: string, + quantity: number, + stopPrice: number, + isLong: boolean + ): Promise { + try { + const side = isLong ? 'SELL' : 'BUY'; + const clientOrderId = `po_trail_${symbol}_${Date.now()}`; + + const orderParams: any = { + symbol, + side, + type: 'STOP_MARKET', + quantity: symbolPrecision.formatQuantity(symbol, quantity), + stopPrice: symbolPrecision.formatPrice(symbol, stopPrice), + positionSide: positionSideAPI, + newClientOrderId: clientOrderId, + workingType: 'MARK_PRICE', + }; + + if (this.config.global.positionMode !== 'HEDGE') { + orderParams.reduceOnly = true; + } + + logWithTimestamp( + `ProtectiveOrderService: Placing trailing TP order for ${symbol}:`, + JSON.stringify({ ...orderParams, apiKey: '***' }, null, 2) + ); + + const order = await placeOrder(orderParams, this.config.api); + + // Track the order + const key = this.getPositionKey(symbol, positionSide); + const protectiveOrder: ProtectiveOrder = { + orderId: order.orderId, + symbol, + side, + positionSide: positionSideAPI, + triggerType: 'trailing_tp', + triggerPercent: 0, // Activation already reached + quantity, + price: stopPrice, + createdAt: Date.now(), + }; + + if (!this.activeOrders.has(key)) { + this.activeOrders.set(key, []); + } + this.activeOrders.get(key)!.push(protectiveOrder); + + this.emit('protectiveOrderPlaced', protectiveOrder); + + return order.orderId; + } catch (error: any) { + logErrorWithTimestamp( + `ProtectiveOrderService: Failed to place trailing TP order for ${symbol}:`, + error?.response?.data || error?.message + ); + return ''; + } + } + /** * Adjust a trailing TP order to a new price */