#!/usr/bin/env python3 """ DOLPHIN 1-Hour Paper Trading Session with Full Logging ====================================================== Extended paper trading with comprehensive logging of trades and system state. Usage: python paper_trade_1h.py --duration 3600 --output /mnt/dolphinng5_predict/logs/paper_trade_1h.json """ import sys import json import time import asyncio import logging import argparse from datetime import datetime, timezone from pathlib import Path from typing import Dict, List, Any, Optional from dataclasses import dataclass, asdict sys.path.insert(0, '/mnt/dolphinng5_predict/prod/clean_arch') sys.path.insert(0, '/mnt/dolphinng5_predict') from adapters.hazelcast_feed import HazelcastDataFeed, MarketSnapshot # Configure logging logging.basicConfig( level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s' ) logger = logging.getLogger("PaperTrade1H") @dataclass class TradeRecord: """Record of a single trade.""" timestamp: str side: str # BUY or SELL symbol: str size: float price: float signal: float pnl: Optional[float] = None pnl_pct: Optional[float] = None def to_dict(self) -> Dict[str, Any]: return asdict(self) @dataclass class SystemState: """Complete system state snapshot including Hazelcast algorithm state.""" timestamp: str iteration: int # Market data symbol: str price: float bid: Optional[float] ask: Optional[float] # Eigenvalue signals eigenvalues: List[float] velocity_divergence: Optional[float] instability_composite: Optional[float] scan_number: int # Portfolio position: float entry_price: Optional[float] unrealized_pnl: float realized_pnl: float total_pnl: float # Trading signals signal_raw: float signal_threshold_buy: float signal_threshold_sell: float signal_triggered: bool action_taken: Optional[str] # System health data_age_sec: float hz_connected: bool # HAZELCAST ALGORITHM STATE (NEW) hz_safety_posture: Optional[str] # APEX, STALKER, HIBERNATE hz_safety_rm: Optional[float] # Risk metric hz_acb_boost: Optional[float] # Adaptive circuit breaker boost hz_acb_beta: Optional[float] # ACB beta hz_portfolio_capital: Optional[float] # Portfolio capital from Hz hz_portfolio_pnl: Optional[float] # Portfolio PnL from Hz def to_dict(self) -> Dict[str, Any]: d = asdict(self) # Limit eigenvalues to first 5 for readability d['eigenvalues'] = self.eigenvalues[:5] if self.eigenvalues else [] d['eigenvalues_count'] = len(self.eigenvalues) if self.eigenvalues else 0 return d def read_hz_algorithm_state() -> Dict[str, Any]: """Read algorithm state from Hazelcast.""" state = { 'safety_posture': None, 'safety_rm': None, 'acb_boost': None, 'acb_beta': None, 'portfolio_capital': None, 'portfolio_pnl': None, } try: import hazelcast client = hazelcast.HazelcastClient( cluster_name="dolphin", cluster_members=["127.0.0.1:5701"], ) # Read DOLPHIN_SAFETY try: safety_map = client.get_map('DOLPHIN_SAFETY').blocking() safety_data = safety_map.get('latest') if safety_data: if isinstance(safety_data, str): safety = json.loads(safety_data) else: safety = safety_data state['safety_posture'] = safety.get('posture') state['safety_rm'] = safety.get('Rm') except Exception as e: pass # Read DOLPHIN_FEATURES (ACB boost) try: features_map = client.get_map('DOLPHIN_FEATURES').blocking() acb_data = features_map.get('acb_boost') if acb_data: if isinstance(acb_data, str): acb = json.loads(acb_data) else: acb = acb_data state['acb_boost'] = acb.get('boost') state['acb_beta'] = acb.get('beta') except Exception as e: pass # Read DOLPHIN_STATE_BLUE try: state_map = client.get_map('DOLPHIN_STATE_BLUE').blocking() portfolio_data = state_map.get('latest') if portfolio_data: if isinstance(portfolio_data, str): portfolio = json.loads(portfolio_data) else: portfolio = portfolio_data state['portfolio_capital'] = portfolio.get('capital') state['portfolio_pnl'] = portfolio.get('pnl') except Exception as e: pass client.shutdown() except Exception as e: pass return state class ComprehensivePaperTrader: """Paper trader with full logging including Hazelcast algorithm state.""" def __init__(self, capital: float = 10000.0, buy_threshold: float = -0.01, sell_threshold: float = 0.01, trade_size: float = 0.001): self.capital = capital self.buy_threshold = buy_threshold self.sell_threshold = sell_threshold self.trade_size = trade_size self.position = 0.0 self.entry_price = 0.0 self.realized_pnl = 0.0 self.trades: List[TradeRecord] = [] self.states: List[SystemState] = [] self.start_time = datetime.now(timezone.utc) self.iteration = 0 def on_snapshot(self, snapshot: MarketSnapshot, data_age_sec: float = 0.0) -> SystemState: """Process market snapshot and log everything including Hz algorithm state.""" self.iteration += 1 # Read Hazelcast algorithm state every 10 iterations (to avoid overhead) hz_state = {} if self.iteration % 10 == 1: hz_state = read_hz_algorithm_state() elif self.states: # Carry over from previous state prev = self.states[-1] hz_state = { 'safety_posture': prev.hz_safety_posture, 'safety_rm': prev.hz_safety_rm, 'acb_boost': prev.hz_acb_boost, 'acb_beta': prev.hz_acb_beta, 'portfolio_capital': prev.hz_portfolio_capital, 'portfolio_pnl': prev.hz_portfolio_pnl, } # Extract signal signal = snapshot.velocity_divergence or 0.0 # Calculate unrealized PnL unrealized = 0.0 if self.position > 0 and self.entry_price > 0: unrealized = self.position * (snapshot.price - self.entry_price) # Determine action action_taken = None signal_triggered = False # Buy signal if signal < self.buy_threshold and self.position == 0: signal_triggered = True action_taken = "BUY" self.position = self.trade_size self.entry_price = snapshot.price trade = TradeRecord( timestamp=datetime.now(timezone.utc).isoformat(), side="BUY", symbol=snapshot.symbol, size=self.trade_size, price=snapshot.price, signal=signal ) self.trades.append(trade) logger.info(f"🟢 BUY {self.trade_size} {snapshot.symbol} @ ${snapshot.price:,.2f} " f"(signal: {signal:.6f})") # Sell signal elif signal > self.sell_threshold and self.position > 0: signal_triggered = True action_taken = "SELL" pnl = self.position * (snapshot.price - self.entry_price) pnl_pct = (pnl / (self.position * self.entry_price)) * 100 if self.entry_price > 0 else 0 self.realized_pnl += pnl trade = TradeRecord( timestamp=datetime.now(timezone.utc).isoformat(), side="SELL", symbol=snapshot.symbol, size=self.position, price=snapshot.price, signal=signal, pnl=pnl, pnl_pct=pnl_pct ) self.trades.append(trade) logger.info(f"šŸ”“ SELL {self.position} {snapshot.symbol} @ ${snapshot.price:,.2f} " f"(signal: {signal:.6f}, PnL: ${pnl:+.2f} / {pnl_pct:+.3f}%)") self.position = 0.0 self.entry_price = 0.0 # Create state record total_pnl = self.realized_pnl + unrealized state = SystemState( timestamp=datetime.now(timezone.utc).isoformat(), iteration=self.iteration, symbol=snapshot.symbol, price=snapshot.price, bid=None, # Could add order book data ask=None, eigenvalues=snapshot.eigenvalues or [], velocity_divergence=snapshot.velocity_divergence, instability_composite=getattr(snapshot, 'instability_composite', None), scan_number=getattr(snapshot, 'scan_number', 0), position=self.position, entry_price=self.entry_price if self.position > 0 else None, unrealized_pnl=unrealized, realized_pnl=self.realized_pnl, total_pnl=total_pnl, signal_raw=signal, signal_threshold_buy=self.buy_threshold, signal_threshold_sell=self.sell_threshold, signal_triggered=signal_triggered, action_taken=action_taken, data_age_sec=data_age_sec, hz_connected=True, # HAZELCAST ALGORITHM STATE hz_safety_posture=hz_state.get('safety_posture'), hz_safety_rm=hz_state.get('safety_rm'), hz_acb_boost=hz_state.get('acb_boost'), hz_acb_beta=hz_state.get('acb_beta'), hz_portfolio_capital=hz_state.get('portfolio_capital'), hz_portfolio_pnl=hz_state.get('portfolio_pnl'), ) self.states.append(state) # Log summary every 10 iterations with Hz state if self.iteration % 10 == 0: pos_str = f"POS:{self.position:.4f}" if self.position > 0 else "FLAT" posture = hz_state.get('safety_posture', 'N/A') boost = hz_state.get('acb_boost') boost_str = f"Boost:{boost:.2f}" if boost else "Boost:N/A" logger.info(f"[{self.iteration:4d}] {pos_str} | PnL:${total_pnl:+.2f} | " f"Price:${snapshot.price:,.2f} | Signal:{signal:.6f} | " f"Posture:{posture} | {boost_str}") return state def get_summary(self) -> Dict[str, Any]: """Get session summary.""" duration = datetime.now(timezone.utc) - self.start_time # Calculate statistics buy_trades = [t for t in self.trades if t.side == "BUY"] sell_trades = [t for t in self.trades if t.side == "SELL"] winning_trades = [t for t in sell_trades if (t.pnl or 0) > 0] losing_trades = [t for t in sell_trades if (t.pnl or 0) <= 0] avg_win = sum(t.pnl for t in winning_trades) / len(winning_trades) if winning_trades else 0 avg_loss = sum(t.pnl for t in losing_trades) / len(losing_trades) if losing_trades else 0 return { "session_info": { "start_time": self.start_time.isoformat(), "end_time": datetime.now(timezone.utc).isoformat(), "duration_sec": duration.total_seconds(), "iterations": self.iteration, }, "trading_config": { "capital": self.capital, "buy_threshold": self.buy_threshold, "sell_threshold": self.sell_threshold, "trade_size": self.trade_size, }, "results": { "total_trades": len(self.trades), "buy_trades": len(buy_trades), "sell_trades": len(sell_trades), "round_trips": len(sell_trades), "winning_trades": len(winning_trades), "losing_trades": len(losing_trades), "win_rate": len(winning_trades) / len(sell_trades) * 100 if sell_trades else 0, "avg_win": avg_win, "avg_loss": avg_loss, "realized_pnl": self.realized_pnl, "final_position": self.position, "final_unrealized_pnl": self.states[-1].unrealized_pnl if self.states else 0, "total_pnl": self.realized_pnl + (self.states[-1].unrealized_pnl if self.states else 0), }, "state_count": len(self.states), } def save_results(self, output_path: str): """Save complete results to JSON.""" output_file = Path(output_path) output_file.parent.mkdir(parents=True, exist_ok=True) results = { "summary": self.get_summary(), "trades": [t.to_dict() for t in self.trades], "states": [s.to_dict() for s in self.states], } with open(output_file, 'w') as f: json.dump(results, f, indent=2, default=str) logger.info(f"šŸ’¾ Results saved to: {output_file}") logger.info(f" Trades: {len(self.trades)}") logger.info(f" States: {len(self.states)}") async def paper_trade_1h(duration_seconds: int = 3600, output_path: Optional[str] = None): """Run 1-hour paper trading session with full logging.""" logger.info("=" * 80) logger.info("🐬 DOLPHIN 1-HOUR PAPER TRADING SESSION") logger.info("=" * 80) logger.info(f"Duration: {duration_seconds}s ({duration_seconds/60:.1f} minutes)") logger.info(f"Output: {output_path or 'console only'}") logger.info("") # Setup feed = HazelcastDataFeed({ 'hazelcast': {'cluster': 'dolphin', 'host': 'localhost:5701'} }) trader = ComprehensivePaperTrader( capital=10000.0, buy_threshold=-0.01, sell_threshold=0.01, trade_size=0.001 ) # Connect logger.info("Connecting to Hazelcast...") if not await feed.connect(): logger.error("Failed to connect!") return logger.info("āœ… Connected. Starting trading loop...") logger.info("") start_time = time.time() last_data_check = 0 try: while (time.time() - start_time) < duration_seconds: iteration_start = time.time() # Get latest snapshot snapshot = await feed.get_latest_snapshot("BTCUSDT") if snapshot: # Estimate data age (Hz doesn't give mtime directly in adapter) data_age = iteration_start - last_data_check if last_data_check > 0 else 0 last_data_check = iteration_start # Process and log trader.on_snapshot(snapshot, data_age_sec=data_age) else: logger.warning("āš ļø No snapshot available") # Calculate sleep to maintain 1s interval elapsed = time.time() - iteration_start sleep_time = max(0, 1.0 - elapsed) await asyncio.sleep(sleep_time) except KeyboardInterrupt: logger.info("\nšŸ›‘ Interrupted by user") except Exception as e: logger.error(f"āŒ Error: {e}") # Cleanup await feed.disconnect() # Final report logger.info("") logger.info("=" * 80) logger.info("šŸ“Š FINAL REPORT") logger.info("=" * 80) summary = trader.get_summary() logger.info(f"Duration: {summary['session_info']['duration_sec']:.1f}s") logger.info(f"Iterations: {summary['session_info']['iterations']}") logger.info(f"Total Trades: {summary['results']['total_trades']}") logger.info(f"Round Trips: {summary['results']['round_trips']}") logger.info(f"Win Rate: {summary['results']['win_rate']:.1f}%") logger.info(f"Realized PnL: ${summary['results']['realized_pnl']:+.2f}") logger.info(f"Final Position: {summary['results']['final_position']:.6f} BTC") logger.info(f"Unrealized PnL: ${summary['results']['final_unrealized_pnl']:+.2f}") logger.info(f"TOTAL PnL: ${summary['results']['total_pnl']:+.2f}") # Save results if output_path: trader.save_results(output_path) logger.info("") logger.info("=" * 80) logger.info("āœ… 1-Hour Paper Trading Session Complete") logger.info("=" * 80) return summary if __name__ == "__main__": parser = argparse.ArgumentParser(description="1-Hour Paper Trading with Full Logging") parser.add_argument('--duration', type=int, default=3600, help='Trading duration in seconds (default: 3600 = 1 hour)') parser.add_argument('--output', type=str, default='/mnt/dolphinng5_predict/logs/paper_trade_1h.json', help='Output JSON file path') args = parser.parse_args() asyncio.run(paper_trade_1h(args.duration, args.output))