Files
DOLPHIN/prod/clean_arch/paper_trade_1h.py
hjnormey 01c19662cb initial: import DOLPHIN baseline 2026-04-21 from dolphinng5_predict working tree
Includes core prod + GREEN/BLUE subsystems:
- prod/ (BLUE harness, configs, scripts, docs)
- nautilus_dolphin/ (GREEN Nautilus-native impl + dvae/ preserved)
- adaptive_exit/ (AEM engine + models/bucket_assignments.pkl)
- Observability/ (EsoF advisor, TUI, dashboards)
- external_factors/ (EsoF producer)
- mc_forewarning_qlabs_fork/ (MC regime/envelope)

Excludes runtime caches, logs, backups, and reproducible artifacts per .gitignore.
2026-04-21 16:58:38 +02:00

489 lines
17 KiB
Python
Executable File

#!/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))