| |
| """ |
| AirMicroDrip Perpetual Futures Liquidation System |
| Monitors positions and executes liquidations when needed |
| No mocks - real position monitoring and liquidation execution |
| """ |
|
|
| import json |
| import sqlite3 |
| import asyncio |
| from typing import Dict, List, Optional |
| from datetime import datetime, timedelta |
| from perp_trading_engine import PerpTradingEngine, Position, Side |
|
|
| |
| LIQUIDATION_CONFIG = { |
| "maintenance_margin_rate": 0.05, |
| "liquidation_threshold": 0.01, |
| "liquidation_bonus": 0.05, |
| "insurance_fund_rate": 0.02, |
| "check_interval_seconds": 10, |
| "max_liquidation_per_check": 5, |
| } |
|
|
|
|
| class LiquidationSystem: |
| """Manages position liquidations""" |
| |
| def __init__( |
| self, |
| trading_engine: PerpTradingEngine, |
| db_path: str = "perp_trading.db", |
| ): |
| self.trading_engine = trading_engine |
| self.db_path = db_path |
| self.liquidation_log = [] |
| self.insurance_fund = 0.0 |
| |
| async def start_monitoring(self): |
| """Start liquidation monitoring loop""" |
| print("Starting liquidation monitoring...") |
| |
| while True: |
| await self._check_liquidations() |
| await asyncio.sleep(LIQUIDATION_CONFIG["check_interval_seconds"]) |
| |
| async def _check_liquidations(self): |
| """Check for liquidatable positions""" |
| |
| positions = self._get_all_positions() |
| |
| liquidatable = [] |
| |
| for position in positions: |
| |
| self.trading_engine.update_unrealized_pnl() |
| |
| |
| updated_position = self.trading_engine.get_position( |
| position["trader"], |
| position["market"] |
| ) |
| |
| if not updated_position: |
| continue |
| |
| |
| if self._is_liquidatable(updated_position): |
| liquidatable.append(updated_position) |
| |
| |
| for position in liquidatable[:LIQUIDATION_CONFIG["max_liquidation_per_check"]]: |
| await self._execute_liquidation(position) |
| |
| def _get_all_positions(self) -> List[Dict]: |
| """Get all positions from database""" |
| conn = sqlite3.connect(self.db_path) |
| cursor = conn.cursor() |
| |
| cursor.execute(""" |
| SELECT position_id, trader, market, side, size, entry_price, leverage, margin, |
| unrealized_pnl, realized_pnl, liquidation_price, opened_at, updated_at |
| FROM positions |
| """) |
| |
| results = cursor.fetchall() |
| conn.close() |
| |
| return [ |
| { |
| "position_id": r[0], |
| "trader": r[1], |
| "market": r[2], |
| "side": r[3], |
| "size": r[4], |
| "entry_price": r[5], |
| "leverage": r[6], |
| "margin": r[7], |
| "unrealized_pnl": r[8], |
| "realized_pnl": r[9], |
| "liquidation_price": r[10], |
| "opened_at": r[11], |
| "updated_at": r[12], |
| } |
| for r in results |
| ] |
| |
| def _is_liquidatable(self, position: Position) -> bool: |
| """Check if position is liquidatable""" |
| market_state = self.trading_engine.market_states[position.market] |
| current_price = market_state.mark_price |
| |
| |
| position_value = position.size * current_price |
| if position_value == 0: |
| return False |
| margin_ratio = position.margin / position_value |
| |
| |
| if margin_ratio < LIQUIDATION_CONFIG["maintenance_margin_rate"]: |
| return True |
| |
| |
| if position.side == Side.LONG: |
| if current_price <= position.liquidation_price: |
| return True |
| else: |
| if current_price >= position.liquidation_price: |
| return True |
| |
| return False |
| |
| async def _execute_liquidation(self, position: Position): |
| """Execute position liquidation""" |
| print(f"Liquidating position {position.position_id}...") |
| |
| market_state = self.trading_engine.market_states[position.market] |
| current_price = market_state.mark_price |
| |
| |
| liquidation_value = position.size * current_price |
| |
| |
| bonus = liquidation_value * LIQUIDATION_CONFIG["liquidation_bonus"] |
| |
| |
| insurance_contribution = liquidation_value * LIQUIDATION_CONFIG["insurance_fund_rate"] |
| |
| |
| self._close_position(position, current_price) |
| |
| |
| self.insurance_fund += insurance_contribution |
| |
| |
| liquidation_record = { |
| "timestamp": datetime.utcnow().isoformat(), |
| "position_id": position.position_id, |
| "trader": position.trader, |
| "market": position.market, |
| "side": position.side.value, |
| "size": position.size, |
| "liquidation_price": current_price, |
| "liquidation_value": liquidation_value, |
| "liquidation_bonus": bonus, |
| "insurance_contribution": insurance_contribution, |
| "remaining_margin": max(0, position.margin - liquidation_value), |
| } |
| |
| self.liquidation_log.append(liquidation_record) |
| |
| print(f"Liquidation executed: {liquidation_record}") |
| |
| def _close_position(self, position: Position, close_price: float): |
| """Close position in database""" |
| conn = sqlite3.connect(self.db_path) |
| cursor = conn.cursor() |
| |
| |
| if position.side == Side.LONG: |
| realized_pnl = position.size * (close_price - position.entry_price) |
| else: |
| realized_pnl = position.size * (position.entry_price - close_price) |
| |
| |
| cursor.execute(""" |
| UPDATE positions |
| SET size = 0, unrealized_pnl = 0, realized_pnl = realized_pnl, updated_at = ? |
| WHERE position_id = ? |
| """, (datetime.utcnow().isoformat(), position.position_id)) |
| |
| conn.commit() |
| conn.close() |
| |
| def get_liquidation_stats(self) -> Dict: |
| """Get liquidation statistics""" |
| if not self.liquidation_log: |
| return { |
| "total_liquidations": 0, |
| "total_value": 0.0, |
| "insurance_fund": self.insurance_fund, |
| } |
| |
| total_liquidations = len(self.liquidation_log) |
| total_value = sum(l["liquidation_value"] for l in self.liquidation_log) |
| total_bonuses = sum(l["liquidation_bonus"] for l in self.liquidation_log) |
| |
| return { |
| "total_liquidations": total_liquidations, |
| "total_value": total_value, |
| "total_bonuses": total_bonuses, |
| "insurance_fund": self.insurance_fund, |
| "recent_liquidations": self.liquidation_log[-10:], |
| } |
| |
| def get_at_risk_positions(self) -> List[Dict]: |
| """Get positions at risk of liquidation""" |
| positions = self._get_all_positions() |
| at_risk = [] |
| |
| for pos_data in positions: |
| position = Position( |
| position_id=pos_data["position_id"], |
| trader=pos_data["trader"], |
| market=pos_data["market"], |
| side=Side(pos_data["side"]), |
| size=pos_data["size"], |
| entry_price=pos_data["entry_price"], |
| leverage=pos_data["leverage"], |
| margin=pos_data["margin"], |
| liquidation_price=pos_data["liquidation_price"], |
| opened_at=datetime.fromisoformat(pos_data["opened_at"]), |
| updated_at=datetime.fromisoformat(pos_data["updated_at"]), |
| ) |
| |
| |
| market_state = self.trading_engine.market_states[position.market] |
| current_price = market_state.mark_price |
| position_value = position.size * current_price |
| if position_value == 0 or current_price == 0: |
| continue |
| margin_ratio = position.margin / position_value |
| |
| |
| if margin_ratio < LIQUIDATION_CONFIG["maintenance_margin_rate"] * 1.2: |
| at_risk.append({ |
| "position_id": position.position_id, |
| "trader": position.trader, |
| "market": position.market, |
| "margin_ratio": margin_ratio, |
| "liquidation_price": position.liquidation_price, |
| "current_price": current_price, |
| "distance_to_liquidation": abs(current_price - position.liquidation_price) / current_price, |
| }) |
| |
| return sorted(at_risk, key=lambda x: x["margin_ratio"]) |
| |
| def manual_liquidation(self, position_id: str, liquidator: str) -> Dict: |
| """Manually trigger liquidation (for liquidators)""" |
| |
| conn = sqlite3.connect(self.db_path) |
| cursor = conn.cursor() |
| |
| cursor.execute(""" |
| SELECT position_id, trader, market, side, size, entry_price, leverage, margin, liquidation_price |
| FROM positions |
| WHERE position_id = ? |
| """, (position_id,)) |
| |
| result = cursor.fetchone() |
| conn.close() |
| |
| if not result: |
| return {"status": "error", "message": "Position not found"} |
| |
| position = Position( |
| position_id=result[0], |
| trader=result[1], |
| market=result[2], |
| side=Side(result[3]), |
| size=result[4], |
| entry_price=result[5], |
| leverage=result[6], |
| margin=result[7], |
| liquidation_price=result[8], |
| ) |
| |
| |
| asyncio.run(self._execute_liquidation(position)) |
| |
| return { |
| "status": "success", |
| "position_id": position_id, |
| "liquidator": liquidator, |
| } |
|
|
|
|
| if __name__ == "__main__": |
| |
| trading_engine = PerpTradingEngine() |
| liquidation_system = LiquidationSystem(trading_engine) |
| |
| |
| at_risk = liquidation_system.get_at_risk_positions() |
| |
| print("\n" + "="*50) |
| print("At-Risk Positions") |
| print("="*50) |
| for pos in at_risk: |
| print(f"Position: {pos['position_id']}") |
| print(f"Trader: {pos['trader']}") |
| print(f"Market: {pos['market']}") |
| print(f"Margin Ratio: {pos['margin_ratio']:.2%}") |
| print(f"Distance to Liquidation: {pos['distance_to_liquidation']:.2%}") |
| print() |
| |
| |
| stats = liquidation_system.get_liquidation_stats() |
| print("="*50) |
| print("Liquidation Statistics") |
| print("="*50) |
| print(json.dumps(stats, indent=2)) |
|
|