489 lines
17 KiB
Python
489 lines
17 KiB
Python
|
|
#!/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))
|