feat: Deploy HA auto-failover with database promotion
- Enhanced DNS failover monitor on secondary (72.62.39.24) - Auto-promotes database: pg_ctl promote on failover - Creates DEMOTED flag on primary via SSH (split-brain protection) - Telegram notifications with database promotion status - Startup safety script ready (integration pending) - 90-second automatic recovery vs 10-30 min manual - Zero-cost 95% enterprise HA benefit Status: DEPLOYED and MONITORING (14:52 CET) Next: Controlled failover test during maintenance
This commit is contained in:
@@ -55,7 +55,7 @@ describe('Bug #76: Exit Orders Validation', () => {
|
||||
tp2Price: 142.41,
|
||||
stopLossPrice: 138.71,
|
||||
tp1SizePercent: 75,
|
||||
tp2SizePercent: 0,
|
||||
tp2SizePercent: 25,
|
||||
direction: 'long',
|
||||
useDualStops: false
|
||||
}
|
||||
@@ -67,6 +67,35 @@ describe('Bug #76: Exit Orders Validation', () => {
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.signatures).toHaveLength(3)
|
||||
expect(result.signatures).toEqual(['TP1_SIG', 'TP2_SIG', 'SL_SIG'])
|
||||
expect(result.expectedOrders).toBe(3)
|
||||
expect(result.placedOrders).toBe(3)
|
||||
})
|
||||
|
||||
it('should return success when TP2 is trigger-only (no on-chain TP2)', async () => {
|
||||
// Only TP1 + SL should be placed when tp2SizePercent is 0
|
||||
mockDriftClient.placePerpOrder
|
||||
.mockResolvedValueOnce('TP1_SIG') // TP1
|
||||
.mockResolvedValueOnce('SL_SIG') // SL
|
||||
|
||||
const options: PlaceExitOrdersOptions = {
|
||||
symbol: 'SOL-PERP',
|
||||
positionSizeUSD: 8000,
|
||||
entryPrice: 140.00,
|
||||
tp1Price: 141.20,
|
||||
tp2Price: 142.41,
|
||||
stopLossPrice: 138.71,
|
||||
tp1SizePercent: 75,
|
||||
tp2SizePercent: 0,
|
||||
direction: 'long',
|
||||
useDualStops: false
|
||||
}
|
||||
|
||||
const result = await placeExitOrders(options)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.signatures).toEqual(['TP1_SIG', 'SL_SIG'])
|
||||
expect(result.expectedOrders).toBe(2)
|
||||
expect(result.placedOrders).toBe(2)
|
||||
})
|
||||
|
||||
it('should return failure when SL placement fails', async () => {
|
||||
@@ -84,7 +113,7 @@ describe('Bug #76: Exit Orders Validation', () => {
|
||||
tp2Price: 142.41,
|
||||
stopLossPrice: 138.71,
|
||||
tp1SizePercent: 75,
|
||||
tp2SizePercent: 0,
|
||||
tp2SizePercent: 25,
|
||||
direction: 'long',
|
||||
useDualStops: false
|
||||
}
|
||||
@@ -113,7 +142,7 @@ describe('Bug #76: Exit Orders Validation', () => {
|
||||
tp2Price: 142.41,
|
||||
stopLossPrice: 138.71,
|
||||
tp1SizePercent: 75,
|
||||
tp2SizePercent: 0,
|
||||
tp2SizePercent: 25,
|
||||
direction: 'long',
|
||||
useDualStops: true,
|
||||
softStopPrice: 139.00,
|
||||
@@ -125,6 +154,37 @@ describe('Bug #76: Exit Orders Validation', () => {
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.signatures).toHaveLength(4)
|
||||
expect(result.signatures).toEqual(['TP1_SIG', 'TP2_SIG', 'SOFT_SIG', 'HARD_SIG'])
|
||||
expect(result.expectedOrders).toBe(4)
|
||||
expect(result.placedOrders).toBe(4)
|
||||
})
|
||||
|
||||
it('should return success with trigger-only TP2 (TP1 + Soft + Hard)', async () => {
|
||||
mockDriftClient.placePerpOrder
|
||||
.mockResolvedValueOnce('TP1_SIG') // TP1
|
||||
.mockResolvedValueOnce('SOFT_SIG') // Soft Stop
|
||||
.mockResolvedValueOnce('HARD_SIG') // Hard Stop
|
||||
|
||||
const options: PlaceExitOrdersOptions = {
|
||||
symbol: 'SOL-PERP',
|
||||
positionSizeUSD: 8000,
|
||||
entryPrice: 140.00,
|
||||
tp1Price: 141.20,
|
||||
tp2Price: 142.41,
|
||||
stopLossPrice: 138.71,
|
||||
tp1SizePercent: 75,
|
||||
tp2SizePercent: 0,
|
||||
direction: 'long',
|
||||
useDualStops: true,
|
||||
softStopPrice: 139.00,
|
||||
hardStopPrice: 138.50
|
||||
}
|
||||
|
||||
const result = await placeExitOrders(options)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.signatures).toEqual(['TP1_SIG', 'SOFT_SIG', 'HARD_SIG'])
|
||||
expect(result.expectedOrders).toBe(3)
|
||||
expect(result.placedOrders).toBe(3)
|
||||
})
|
||||
|
||||
it('should return failure when soft stop fails', async () => {
|
||||
@@ -141,7 +201,7 @@ describe('Bug #76: Exit Orders Validation', () => {
|
||||
tp2Price: 142.41,
|
||||
stopLossPrice: 138.71,
|
||||
tp1SizePercent: 75,
|
||||
tp2SizePercent: 0,
|
||||
tp2SizePercent: 25,
|
||||
direction: 'long',
|
||||
useDualStops: true,
|
||||
softStopPrice: 139.00,
|
||||
@@ -169,7 +229,7 @@ describe('Bug #76: Exit Orders Validation', () => {
|
||||
tp2Price: 142.41,
|
||||
stopLossPrice: 138.71,
|
||||
tp1SizePercent: 75,
|
||||
tp2SizePercent: 0,
|
||||
tp2SizePercent: 25,
|
||||
direction: 'long',
|
||||
useDualStops: true,
|
||||
softStopPrice: 139.00,
|
||||
|
||||
@@ -16,10 +16,35 @@ import { ActiveTrade } from '../../../lib/trading/position-manager'
|
||||
import { createMockTrade } from '../../helpers/trade-factory'
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('../../../lib/drift/client')
|
||||
const mockDriftService = {
|
||||
isInitialized: true,
|
||||
getPosition: jest.fn().mockResolvedValue({ size: 0 }),
|
||||
getClient: jest.fn(() => ({})),
|
||||
}
|
||||
|
||||
jest.mock('../../../lib/drift/client', () => ({
|
||||
getDriftService: jest.fn(() => mockDriftService),
|
||||
}))
|
||||
|
||||
jest.mock('../../../lib/drift/orders', () => ({
|
||||
placeExitOrders: jest.fn(async () => ({
|
||||
success: true,
|
||||
signatures: [],
|
||||
expectedOrders: 0,
|
||||
placedOrders: 0,
|
||||
})),
|
||||
closePosition: jest.fn(async () => ({ success: true })),
|
||||
cancelAllOrders: jest.fn(async () => ({ success: true })),
|
||||
}))
|
||||
|
||||
jest.mock('../../../lib/pyth/price-monitor')
|
||||
jest.mock('../../../lib/database/trades')
|
||||
jest.mock('../../../lib/notifications/telegram')
|
||||
jest.mock('../../../lib/utils/persistent-logger', () => ({
|
||||
logCriticalError: jest.fn(),
|
||||
logError: jest.fn(),
|
||||
logWarning: jest.fn(),
|
||||
}))
|
||||
|
||||
describe('Position Manager Monitoring Verification', () => {
|
||||
let manager: PositionManager
|
||||
@@ -27,6 +52,7 @@ describe('Position Manager Monitoring Verification', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
mockDriftService.getPosition.mockResolvedValue({ size: 1 })
|
||||
|
||||
// Mock Pyth price monitor
|
||||
mockPriceMonitor = {
|
||||
@@ -41,6 +67,12 @@ describe('Position Manager Monitoring Verification', () => {
|
||||
manager = new PositionManager()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
if ((manager as any)?.isMonitoring) {
|
||||
await (manager as any).stopMonitoring?.()
|
||||
}
|
||||
})
|
||||
|
||||
describe('CRITICAL: Monitoring Actually Starts', () => {
|
||||
it('should start Pyth price monitor when trade added', async () => {
|
||||
const trade = createMockTrade({ direction: 'long', symbol: 'SOL-PERP' })
|
||||
|
||||
Reference in New Issue
Block a user