From 18e3e73e8323a9a94acc54e350cb1de542f8100d Mon Sep 17 00:00:00 2001 From: mindesbunister Date: Wed, 5 Nov 2025 10:00:39 +0100 Subject: [PATCH] feat: refresh exit orders after TP1 and add dry-run harness --- lib/trading/position-manager.ts | 172 ++++++++++++++++++++------------ package-lock.json | 171 +++++++++++++++++++++++++++++++ package.json | 1 + scripts/dry-run-tp1-test.js | 103 +++++++++++++++++++ 4 files changed, 383 insertions(+), 64 deletions(-) create mode 100644 scripts/dry-run-tp1-test.js diff --git a/lib/trading/position-manager.ts b/lib/trading/position-manager.ts index d806b75..26ba375 100644 --- a/lib/trading/position-manager.ts +++ b/lib/trading/position-manager.ts @@ -333,12 +333,7 @@ export class PositionManager { trade.tp1Hit = true trade.currentSize = positionSizeUSD - // Move SL to breakeven after TP1 - trade.stopLossPrice = trade.entryPrice - trade.slMovedToBreakeven = true - console.log(`๐Ÿ›ก๏ธ Stop loss moved to breakeven: $${trade.stopLossPrice.toFixed(4)}`) - - await this.saveTradeState(trade) + await this.handlePostTp1Adjustments(trade, 'on-chain TP1 detection') } else if (trade.tp1Hit && !trade.tp2Hit && reductionPercent >= 85) { // TP2 fired (total should be ~95% closed, 5% runner left) @@ -350,15 +345,15 @@ export class PositionManager { await this.saveTradeState(trade) - // CRITICAL: Don't return early! Continue monitoring the runner position - // The trailing stop logic at line 732 needs to run - } else { // Partial fill detected but unclear which TP - just update size console.log(`โš ๏ธ Unknown partial fill detected - updating tracked size to $${positionSizeUSD.toFixed(2)}`) trade.currentSize = positionSizeUSD await this.saveTradeState(trade) } + + // Continue monitoring the remaining position + return } // CRITICAL: Check for entry price mismatch (NEW position opened) @@ -380,10 +375,10 @@ export class PositionManager { trade.lastPrice, trade.direction ) - const accountPnLPercent = profitPercent * trade.leverage - const estimatedPnL = (trade.currentSize * profitPercent) / 100 + const accountPnL = profitPercent * trade.leverage + const estimatedPnL = (trade.currentSize * accountPnL) / 100 - console.log(`๐Ÿ’ฐ Estimated P&L for lost trade: ${profitPercent.toFixed(2)}% price โ†’ ${accountPnLPercent.toFixed(2)}% account โ†’ $${estimatedPnL.toFixed(2)} realized`) + console.log(`๐Ÿ’ฐ Estimated P&L for lost trade: ${profitPercent.toFixed(2)}% price โ†’ ${accountPnL.toFixed(2)}% account โ†’ $${estimatedPnL.toFixed(2)} realized`) try { await updateTradeExit({ @@ -450,7 +445,8 @@ export class PositionManager { currentPrice, trade.direction ) - realizedPnL = (sizeForPnL * profitPercent) / 100 + const accountPnL = profitPercent * trade.leverage + realizedPnL = (sizeForPnL * accountPnL) / 100 } // Determine exit reason from trade state and P&L @@ -632,56 +628,7 @@ export class PositionManager { // Move SL based on breakEvenTriggerPercent setting trade.tp1Hit = true trade.currentSize = trade.positionSize * ((100 - this.config.takeProfit1SizePercent) / 100) - const newStopLossPrice = this.calculatePrice( - trade.entryPrice, - this.config.breakEvenTriggerPercent, // Use configured breakeven level - trade.direction - ) - trade.stopLossPrice = newStopLossPrice - trade.slMovedToBreakeven = true - - console.log(`๐Ÿ”’ SL moved to +${this.config.breakEvenTriggerPercent}% (${this.config.takeProfit1SizePercent}% closed, ${100 - this.config.takeProfit1SizePercent}% remaining): ${newStopLossPrice.toFixed(4)}`) - - // CRITICAL: Cancel old on-chain SL orders and place new ones at updated price - try { - console.log('๐Ÿ—‘๏ธ Cancelling old stop loss orders...') - const { cancelAllOrders, placeExitOrders } = await import('../drift/orders') - const cancelResult = await cancelAllOrders(trade.symbol) - if (cancelResult.success) { - console.log(`โœ… Cancelled ${cancelResult.cancelledCount || 0} old orders`) - - // Place new SL orders at breakeven/profit level for remaining position - console.log(`๐Ÿ›ก๏ธ Placing new SL orders at $${newStopLossPrice.toFixed(4)} for remaining position...`) - const exitOrdersResult = await placeExitOrders({ - symbol: trade.symbol, - positionSizeUSD: trade.currentSize, - entryPrice: trade.entryPrice, - tp1Price: trade.tp2Price, // Only TP2 remains - tp2Price: trade.tp2Price, // Dummy, won't be used - stopLossPrice: newStopLossPrice, - tp1SizePercent: 100, // Close remaining 25% at TP2 - tp2SizePercent: 0, - direction: trade.direction, - useDualStops: this.config.useDualStops, - softStopPrice: trade.direction === 'long' - ? newStopLossPrice * 1.005 // 0.5% above for long - : newStopLossPrice * 0.995, // 0.5% below for short - hardStopPrice: newStopLossPrice, - }) - - if (exitOrdersResult.success) { - console.log('โœ… New SL orders placed on-chain at updated price') - } else { - console.error('โŒ Failed to place new SL orders:', exitOrdersResult.error) - } - } - } catch (error) { - console.error('โŒ Failed to update on-chain SL orders:', error) - // Don't fail the TP1 exit if SL update fails - software monitoring will handle it - } - - // Save state after TP1 - await this.saveTradeState(trade) + await this.handlePostTp1Adjustments(trade, 'software TP1 execution') return } @@ -707,7 +654,7 @@ export class PositionManager { } // 5. Take profit 2 (remaining position) - if (trade.tp1Hit && !trade.tp2Hit && this.shouldTakeProfit2(currentPrice, trade)) { + if (trade.tp1Hit && this.shouldTakeProfit2(currentPrice, trade)) { console.log(`๐ŸŽŠ TP2 HIT: ${trade.symbol} at ${profitPercent.toFixed(2)}%`) // Calculate how much to close based on TP2 size percent @@ -926,6 +873,103 @@ export class PositionManager { console.log('โœ… All positions closed') } + private async handlePostTp1Adjustments(trade: ActiveTrade, context: string): Promise { + if (trade.currentSize <= 0) { + console.log(`โš ๏ธ Skipping TP1 adjustments for ${trade.symbol} (${context}) because current size is $${trade.currentSize.toFixed(2)}`) + await this.saveTradeState(trade) + return + } + + const newStopLossPrice = this.calculatePrice( + trade.entryPrice, + this.config.breakEvenTriggerPercent, + trade.direction + ) + + trade.stopLossPrice = newStopLossPrice + trade.slMovedToBreakeven = true + + console.log(`๐Ÿ”’ (${context}) SL moved to +${this.config.breakEvenTriggerPercent}% (${this.config.takeProfit1SizePercent}% closed, ${100 - this.config.takeProfit1SizePercent}% remaining): ${newStopLossPrice.toFixed(4)}`) + + await this.refreshExitOrders(trade, { + stopLossPrice: newStopLossPrice, + tp1Price: trade.tp2Price, + tp1SizePercent: 100, + tp2Price: trade.tp2Price, + tp2SizePercent: 0, + context, + }) + + await this.saveTradeState(trade) + } + + private async refreshExitOrders( + trade: ActiveTrade, + options: { + stopLossPrice: number + tp1Price: number + tp1SizePercent: number + tp2Price?: number + tp2SizePercent?: number + context: string + } + ): Promise { + if (trade.currentSize <= 0) { + console.log(`โš ๏ธ Skipping exit order refresh for ${trade.symbol} (${options.context}) because tracked size is zero`) + return + } + + try { + console.log(`๐Ÿ—‘๏ธ (${options.context}) Cancelling existing exit orders before refresh...`) + const { cancelAllOrders, placeExitOrders } = await import('../drift/orders') + const cancelResult = await cancelAllOrders(trade.symbol) + if (cancelResult.success) { + console.log(`โœ… (${options.context}) Cancelled ${cancelResult.cancelledCount || 0} old orders`) + } else { + console.warn(`โš ๏ธ (${options.context}) Failed to cancel old orders: ${cancelResult.error}`) + } + + const tp2Price = options.tp2Price ?? options.tp1Price + const tp2SizePercent = options.tp2SizePercent ?? 0 + + const refreshParams: any = { + symbol: trade.symbol, + positionSizeUSD: trade.currentSize, + entryPrice: trade.entryPrice, + tp1Price: options.tp1Price, + tp2Price, + stopLossPrice: options.stopLossPrice, + tp1SizePercent: options.tp1SizePercent, + tp2SizePercent, + direction: trade.direction, + useDualStops: this.config.useDualStops, + } + + if (this.config.useDualStops) { + const softStopBuffer = this.config.softStopBuffer ?? 0.4 + const softStopPrice = trade.direction === 'long' + ? options.stopLossPrice * (1 + softStopBuffer / 100) + : options.stopLossPrice * (1 - softStopBuffer / 100) + + refreshParams.softStopPrice = softStopPrice + refreshParams.softStopBuffer = softStopBuffer + refreshParams.hardStopPrice = options.stopLossPrice + } + + console.log(`๐Ÿ›ก๏ธ (${options.context}) Placing refreshed exit orders: size=$${trade.currentSize.toFixed(2)} SL=${options.stopLossPrice.toFixed(4)} TP=${options.tp1Price.toFixed(4)}`) + const exitOrdersResult = await placeExitOrders(refreshParams) + + if (exitOrdersResult.success) { + console.log(`โœ… (${options.context}) Exit orders refreshed on-chain`) + } else { + console.error(`โŒ (${options.context}) Failed to place refreshed exit orders: ${exitOrdersResult.error}`) + } + } catch (error) { + console.error(`โŒ (${options.context}) Error refreshing exit orders:`, error) + // Monitoring loop will still enforce SL logic even if on-chain refresh fails + } + } + /** * Save trade state to database (for persistence across restarts) */ diff --git a/package-lock.json b/package-lock.json index 66954a4..b2cf0b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "@types/node": "^20.11.0", "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", + "ts-node": "^10.9.2", "typescript": "^5.3.0" }, "engines": { @@ -188,6 +189,30 @@ "@solana/web3.js": "^1.68.0" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@drift-labs/sdk": { "version": "2.143.0", "resolved": "https://registry.npmjs.org/@drift-labs/sdk/-/sdk-2.143.0.tgz", @@ -4208,6 +4233,34 @@ "node": ">=20.18.0" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/bn.js": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.2.0.tgz", @@ -4289,6 +4342,32 @@ "@types/node": "*" } }, + "node_modules/acorn": { + "version": "8.15.0", + "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" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/agentkeepalive": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", @@ -4947,6 +5026,13 @@ "node": "^14.18.0 || >=16.10.0" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-fetch": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", @@ -5116,6 +5202,16 @@ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "license": "Apache-2.0" }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", @@ -6462,6 +6558,13 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "license": "ISC" }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -8003,6 +8106,57 @@ "integrity": "sha512-320x5Ggei84AxzlXp91QkIGSw5wgaLT6GeAH0KsqDmRZdVWW2OiSeVvElVoatk3f7nicwXlElXsoFkARiGE2yg==", "license": "MIT" }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -8112,6 +8266,13 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -8268,6 +8429,16 @@ "node": ">=12" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/zod": { "version": "4.0.17", "resolved": "https://registry.npmjs.org/zod/-/zod-4.0.17.tgz", diff --git a/package.json b/package.json index 3c0f9ca..d845a29 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@types/node": "^20.11.0", "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", + "ts-node": "^10.9.2", "typescript": "^5.3.0" }, "engines": { diff --git a/scripts/dry-run-tp1-test.js b/scripts/dry-run-tp1-test.js new file mode 100644 index 0000000..3a071b8 --- /dev/null +++ b/scripts/dry-run-tp1-test.js @@ -0,0 +1,103 @@ +process.env.TS_NODE_COMPILER_OPTIONS = JSON.stringify({ + module: 'CommonJS', + moduleResolution: 'node', + esModuleInterop: true, + allowSyntheticDefaultImports: true, +}) + +const Module = require('module') +const originalRequire = Module.prototype.require +Module.prototype.require = function mockHelius(moduleId, ...args) { + if (moduleId === 'helius-laserstream' || moduleId.startsWith('helius-laserstream/')) { + console.log('โš ๏ธ Mocking helius-laserstream module for dry run') + return {} + } + return originalRequire.apply(this, [moduleId, ...args]) +} + +require('ts-node/register') + +const { Keypair } = require('@solana/web3.js') +const tradesModule = require('../lib/database/trades') + +// Stub Prisma interactions for dry run so we don't need a database +tradesModule.getOpenTrades = async () => { + console.log('โ„น๏ธ Mock getOpenTrades returning empty list for dry run') + return [] +} +tradesModule.updateTradeState = async () => { + console.log('โ„น๏ธ Mock updateTradeState no-op') +} + +const { getInitializedPositionManager } = require('../lib/trading/position-manager') +const driftOrders = require('../lib/drift/orders') + +// Stub Drift order helpers so we avoid needing an on-chain connection +driftOrders.cancelAllOrders = async (symbol) => { + console.log(`โ„น๏ธ Mock cancelAllOrders invoked for ${symbol}`) + return { success: true, cancelledCount: 0 } +} + +driftOrders.placeExitOrders = async (options) => { + console.log('โ„น๏ธ Mock placeExitOrders called with:', options) + return { success: true, signatures: ['MOCK_TP', 'MOCK_SL'] } +} + +async function main() { + process.env.DRY_RUN = 'true' + process.env.DRIFT_ENV = 'devnet' + process.env.SOLANA_RPC_URL = process.env.SOLANA_RPC_URL || 'https://api.devnet.solana.com' + + const kp = Keypair.generate() + process.env.DRIFT_WALLET_PRIVATE_KEY = JSON.stringify(Array.from(kp.secretKey)) + + console.log('๐Ÿงช Starting TP1 refresh dry-run test') + + const manager = await getInitializedPositionManager() + + const entryPrice = 150 + const trade = { + id: `dry-run-${Date.now()}`, + positionId: 'DRY_RUN_POSITION', + symbol: 'SOL-PERP', + direction: 'long', + entryPrice, + entryTime: Date.now(), + positionSize: 200, + leverage: 10, + stopLossPrice: entryPrice * 0.985, + tp1Price: entryPrice * 1.004, + tp2Price: entryPrice * 1.007, + emergencyStopPrice: entryPrice * 0.98, + currentSize: 200 * 0.25, + tp1Hit: true, + tp2Hit: false, + slMovedToBreakeven: false, + slMovedToProfit: false, + trailingStopActive: false, + realizedPnL: 10, + unrealizedPnL: 0, + peakPnL: 12, + peakPrice: entryPrice * 1.01, + maxFavorableExcursion: 1.2, + maxAdverseExcursion: -0.6, + maxFavorablePrice: entryPrice * 1.01, + maxAdversePrice: entryPrice * 0.99, + priceCheckCount: 0, + lastPrice: entryPrice, + lastUpdateTime: Date.now(), + } + + await manager.addTrade(trade) + + console.log('โžก๏ธ Invoking handlePostTp1Adjustments...') + await manager.handlePostTp1Adjustments(trade, 'dry-run script') + + console.log('โœ… Dry run complete. Review logs above for refreshed exit order placement.') + process.exit(0) +} + +main().catch((err) => { + console.error('โŒ Dry run test failed:', err) + process.exit(1) +})