432 lines
13 KiB
Python
432 lines
13 KiB
Python
|
|
"""
|
||
|
|
Monte Carlo System Test Suite
|
||
|
|
=============================
|
||
|
|
|
||
|
|
Comprehensive tests for the MC envelope mapping system.
|
||
|
|
|
||
|
|
Run with:
|
||
|
|
python test_mc_system.py
|
||
|
|
|
||
|
|
Tests:
|
||
|
|
1. Sampler functionality
|
||
|
|
2. Validator constraint groups
|
||
|
|
3. Metrics computation
|
||
|
|
4. Store persistence
|
||
|
|
5. Executor (simulated mode)
|
||
|
|
6. Runner orchestration
|
||
|
|
7. ML training (optional)
|
||
|
|
"""
|
||
|
|
|
||
|
|
import sys
|
||
|
|
import os
|
||
|
|
import json
|
||
|
|
import tempfile
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
# Add parent to path for imports
|
||
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||
|
|
sys.path.insert(0, str(Path(__file__).parent))
|
||
|
|
|
||
|
|
import numpy as np
|
||
|
|
|
||
|
|
|
||
|
|
def test_sampler():
|
||
|
|
"""Test MCSampler."""
|
||
|
|
print("\n" + "="*70)
|
||
|
|
print("TEST 1: MCSampler")
|
||
|
|
print("="*70)
|
||
|
|
|
||
|
|
from nautilus_dolphin.mc import MCSampler, MCTrialConfig
|
||
|
|
|
||
|
|
sampler = MCSampler(base_seed=42)
|
||
|
|
|
||
|
|
# Test switch vector generation
|
||
|
|
switches = sampler.generate_switch_vectors()
|
||
|
|
print(f" Switch vectors: {len(switches)}")
|
||
|
|
assert 50 < len(switches) < 150, "Unexpected number of switch vectors"
|
||
|
|
|
||
|
|
# Test trial generation (small)
|
||
|
|
trials = sampler.generate_trials(n_samples_per_switch=5, max_trials=50)
|
||
|
|
print(f" Generated trials: {len(trials)}")
|
||
|
|
assert len(trials) <= 50, "Too many trials generated"
|
||
|
|
|
||
|
|
# Test champion generation
|
||
|
|
champion = sampler.generate_champion_trial()
|
||
|
|
assert champion.trial_id == -1, "Champion should have trial_id=-1"
|
||
|
|
assert champion.vel_div_threshold == -0.02, "Champion threshold mismatch"
|
||
|
|
|
||
|
|
# Test parameter ranges
|
||
|
|
for trial in trials[:10]:
|
||
|
|
assert -0.040 <= trial.vel_div_threshold <= -0.008, "vel_div_threshold out of range"
|
||
|
|
assert 0.10 <= trial.min_leverage <= 1.50, "min_leverage out of range"
|
||
|
|
assert 1.50 <= trial.max_leverage <= 12.00, "max_leverage out of range"
|
||
|
|
assert 0.05 <= trial.fraction <= 0.40, "fraction out of range"
|
||
|
|
|
||
|
|
print(" [PASS] Sampler tests passed")
|
||
|
|
return True
|
||
|
|
|
||
|
|
|
||
|
|
def test_validator():
|
||
|
|
"""Test MCValidator."""
|
||
|
|
print("\n" + "="*70)
|
||
|
|
print("TEST 2: MCValidator")
|
||
|
|
print("="*70)
|
||
|
|
|
||
|
|
from nautilus_dolphin.mc import MCSampler, MCValidator, ValidationStatus
|
||
|
|
|
||
|
|
sampler = MCSampler()
|
||
|
|
validator = MCValidator(verbose=False)
|
||
|
|
|
||
|
|
# Generate some trials
|
||
|
|
trials = sampler.generate_trials(n_samples_per_switch=3, max_trials=30)
|
||
|
|
|
||
|
|
# Validate
|
||
|
|
results = validator.validate_batch(trials)
|
||
|
|
|
||
|
|
# Stats
|
||
|
|
stats = validator.get_validity_stats(results)
|
||
|
|
print(f" Total: {stats['total']}")
|
||
|
|
print(f" Valid: {stats['valid']} ({stats['validity_rate']*100:.1f}%)")
|
||
|
|
|
||
|
|
# Most should be valid or rejected for specific reasons
|
||
|
|
assert stats['total'] == len(trials), "Stats total mismatch"
|
||
|
|
|
||
|
|
# Test specific constraints
|
||
|
|
# Create an invalid config
|
||
|
|
invalid_config = trials[0]._replace(
|
||
|
|
vel_div_extreme=-0.01, # Not extreme enough
|
||
|
|
vel_div_threshold=-0.015 # Should fail: extreme >= threshold
|
||
|
|
)
|
||
|
|
result = validator.validate(invalid_config)
|
||
|
|
assert not result.is_valid(), "Should reject invalid VD config"
|
||
|
|
print(f" Invalid config rejected: {result.reject_reason}")
|
||
|
|
|
||
|
|
# Test leverage constraint
|
||
|
|
invalid_config2 = trials[0]._replace(
|
||
|
|
min_leverage=5.0,
|
||
|
|
max_leverage=3.0 # Should fail: min >= max
|
||
|
|
)
|
||
|
|
result2 = validator.validate(invalid_config2)
|
||
|
|
assert not result2.is_valid(), "Should reject invalid leverage config"
|
||
|
|
|
||
|
|
print(" [PASS] Validator tests passed")
|
||
|
|
return True
|
||
|
|
|
||
|
|
|
||
|
|
def test_metrics():
|
||
|
|
"""Test MCMetrics."""
|
||
|
|
print("\n" + "="*70)
|
||
|
|
print("TEST 3: MCMetrics")
|
||
|
|
print("="*70)
|
||
|
|
|
||
|
|
from nautilus_dolphin.mc import MCSampler, MCMetrics, MCTrialResult
|
||
|
|
|
||
|
|
sampler = MCSampler()
|
||
|
|
config = sampler.generate_champion_trial()
|
||
|
|
|
||
|
|
metrics = MCMetrics(initial_capital=25000.0)
|
||
|
|
|
||
|
|
# Create dummy data
|
||
|
|
trades = [
|
||
|
|
{'pnl': 100, 'pnl_pct': 0.004, 'exit_type': 'tp', 'bars_held': 50},
|
||
|
|
{'pnl': -50, 'pnl_pct': -0.002, 'exit_type': 'stop', 'bars_held': 20},
|
||
|
|
{'pnl': 150, 'pnl_pct': 0.006, 'exit_type': 'tp', 'bars_held': 80},
|
||
|
|
] * 20
|
||
|
|
|
||
|
|
daily_pnls = [50, -20, 80, -10, 100, -30, 60, 40, -15, 90] * 5
|
||
|
|
|
||
|
|
date_stats = [{'date': f'2026-01-{i%31+1:02d}', 'pnl': daily_pnls[i]}
|
||
|
|
for i in range(len(daily_pnls))]
|
||
|
|
|
||
|
|
signal_stats = {
|
||
|
|
'dc_skip_rate': 0.1,
|
||
|
|
'ob_skip_rate': 0.05,
|
||
|
|
'dc_confirm_rate': 0.7,
|
||
|
|
'irp_match_rate': 0.6,
|
||
|
|
'entry_attempt_rate': 0.3,
|
||
|
|
'signal_to_trade_rate': 0.15,
|
||
|
|
}
|
||
|
|
|
||
|
|
# Compute metrics
|
||
|
|
result = metrics.compute(config, trades, daily_pnls, date_stats, signal_stats)
|
||
|
|
|
||
|
|
print(f" ROI: {result.roi_pct:.2f}%")
|
||
|
|
print(f" Profit Factor: {result.profit_factor:.2f}")
|
||
|
|
print(f" Win Rate: {result.win_rate:.2%}")
|
||
|
|
print(f" Sharpe: {result.sharpe_ratio:.2f}")
|
||
|
|
print(f" Max DD: {result.max_drawdown_pct:.2f}%")
|
||
|
|
print(f" N Trades: {result.n_trades}")
|
||
|
|
|
||
|
|
# Verify labels
|
||
|
|
print(f" Profitable: {result.profitable}")
|
||
|
|
print(f" Champion Region: {result.champion_region}")
|
||
|
|
print(f" Catastrophic: {result.catastrophic}")
|
||
|
|
|
||
|
|
# Test serialization
|
||
|
|
result_dict = result.to_dict()
|
||
|
|
assert 'P_vel_div_threshold' in result_dict, "Config params not in dict"
|
||
|
|
assert 'M_roi_pct' in result_dict, "Metrics not in dict"
|
||
|
|
assert 'L_profitable' in result_dict, "Labels not in dict"
|
||
|
|
|
||
|
|
print(" [PASS] Metrics tests passed")
|
||
|
|
return True
|
||
|
|
|
||
|
|
|
||
|
|
def test_store():
|
||
|
|
"""Test MCStore."""
|
||
|
|
print("\n" + "="*70)
|
||
|
|
print("TEST 4: MCStore")
|
||
|
|
print("="*70)
|
||
|
|
|
||
|
|
from nautilus_dolphin.mc import MCStore, MCSampler, MCMetrics, MCTrialResult
|
||
|
|
from nautilus_dolphin.mc.mc_validator import ValidationResult, ValidationStatus
|
||
|
|
|
||
|
|
# Use temp directory
|
||
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||
|
|
store = MCStore(output_dir=tmpdir, batch_size=10)
|
||
|
|
|
||
|
|
# Test validation result storage
|
||
|
|
val_results = [
|
||
|
|
ValidationResult(ValidationStatus.VALID, i)
|
||
|
|
for i in range(5)
|
||
|
|
]
|
||
|
|
val_results.append(ValidationResult(
|
||
|
|
ValidationStatus.REJECTED_V2, 5, "Test rejection"
|
||
|
|
))
|
||
|
|
|
||
|
|
store.save_validation_results(val_results, batch_id=0)
|
||
|
|
|
||
|
|
# Test trial result storage
|
||
|
|
sampler = MCSampler()
|
||
|
|
config = sampler.generate_champion_trial()
|
||
|
|
|
||
|
|
trial_results = []
|
||
|
|
for i in range(5):
|
||
|
|
result = MCTrialResult(
|
||
|
|
trial_id=i,
|
||
|
|
config=config._replace(trial_id=i),
|
||
|
|
roi_pct=np.random.uniform(-10, 50),
|
||
|
|
n_trades=np.random.randint(20, 200),
|
||
|
|
status='completed'
|
||
|
|
)
|
||
|
|
result.compute_labels()
|
||
|
|
trial_results.append(result)
|
||
|
|
|
||
|
|
store.save_trial_results(trial_results, batch_id=1)
|
||
|
|
|
||
|
|
# Test query
|
||
|
|
entries = store.query_index(limit=10)
|
||
|
|
print(f" Index entries: {len(entries)}")
|
||
|
|
assert len(entries) == 5, "Index query returned wrong count"
|
||
|
|
|
||
|
|
# Test stats
|
||
|
|
stats = store.get_corpus_stats()
|
||
|
|
print(f" Corpus stats: {stats}")
|
||
|
|
assert stats['total_trials'] == 5, "Corpus stats wrong"
|
||
|
|
|
||
|
|
print(" [PASS] Store tests passed")
|
||
|
|
|
||
|
|
return True
|
||
|
|
|
||
|
|
|
||
|
|
def test_executor():
|
||
|
|
"""Test MCExecutor (simulated mode)."""
|
||
|
|
print("\n" + "="*70)
|
||
|
|
print("TEST 5: MCExecutor (Simulated)")
|
||
|
|
print("="*70)
|
||
|
|
|
||
|
|
from nautilus_dolphin.mc import MCSampler, MCExecutor
|
||
|
|
|
||
|
|
sampler = MCSampler()
|
||
|
|
executor = MCExecutor(initial_capital=25000.0, verbose=False)
|
||
|
|
|
||
|
|
# Generate a few trials
|
||
|
|
trials = sampler.generate_trials(n_samples_per_switch=2, max_trials=5)
|
||
|
|
|
||
|
|
# Execute
|
||
|
|
results = executor.execute_batch(trials, progress_interval=2)
|
||
|
|
|
||
|
|
print(f" Executed {len(results)} trials")
|
||
|
|
|
||
|
|
for r in results:
|
||
|
|
print(f" Trial {r.trial_id}: ROI={r.roi_pct:.2f}%, Trades={r.n_trades}, Status={r.status}")
|
||
|
|
|
||
|
|
# All should complete
|
||
|
|
completed = sum(1 for r in results if r.status == 'completed')
|
||
|
|
print(f" Completed: {completed}/{len(results)}")
|
||
|
|
|
||
|
|
assert completed > 0, "No trials completed"
|
||
|
|
|
||
|
|
print(" [PASS] Executor tests passed")
|
||
|
|
return True
|
||
|
|
|
||
|
|
|
||
|
|
def test_runner():
|
||
|
|
"""Test MCRunner (small scale)."""
|
||
|
|
print("\n" + "="*70)
|
||
|
|
print("TEST 6: MCRunner (Small Scale)")
|
||
|
|
print("="*70)
|
||
|
|
|
||
|
|
from nautilus_dolphin.mc import MCRunner
|
||
|
|
|
||
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||
|
|
runner = MCRunner(
|
||
|
|
output_dir=tmpdir,
|
||
|
|
n_workers=1,
|
||
|
|
batch_size=5,
|
||
|
|
base_seed=42,
|
||
|
|
verbose=False
|
||
|
|
)
|
||
|
|
|
||
|
|
# Run small envelope mapping
|
||
|
|
stats = runner.run_envelope_mapping(
|
||
|
|
n_samples_per_switch=2,
|
||
|
|
max_trials=10,
|
||
|
|
resume=False
|
||
|
|
)
|
||
|
|
|
||
|
|
print(f" Total trials: {stats['total_trials']}")
|
||
|
|
print(f" Champion region: {stats['champion_count']}")
|
||
|
|
print(f" Catastrophic: {stats['catastrophic_count']}")
|
||
|
|
|
||
|
|
assert stats['total_trials'] > 0, "No trials completed"
|
||
|
|
|
||
|
|
# Test report generation
|
||
|
|
report = runner.generate_report()
|
||
|
|
assert 'Corpus Statistics' in report, "Report missing content"
|
||
|
|
|
||
|
|
print(" [PASS] Runner tests passed")
|
||
|
|
|
||
|
|
return True
|
||
|
|
|
||
|
|
|
||
|
|
def test_cli():
|
||
|
|
"""Test CLI entry point."""
|
||
|
|
print("\n" + "="*70)
|
||
|
|
print("TEST 7: CLI Entry Point")
|
||
|
|
print("="*70)
|
||
|
|
|
||
|
|
from nautilus_dolphin.run_mc_envelope import create_parser, cmd_sample, cmd_report
|
||
|
|
|
||
|
|
# Test parser
|
||
|
|
parser = create_parser()
|
||
|
|
|
||
|
|
# Test sample command args
|
||
|
|
args = parser.parse_args(['--mode', 'sample', '--n-samples', '5', '--max-trials', '10'])
|
||
|
|
assert args.mode == 'sample'
|
||
|
|
assert args.n_samples == 5
|
||
|
|
|
||
|
|
print(" Parser works")
|
||
|
|
|
||
|
|
# Test actual sample command (small)
|
||
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||
|
|
args.output_dir = tmpdir
|
||
|
|
args.seed = 42
|
||
|
|
args.quiet = True
|
||
|
|
|
||
|
|
result = cmd_sample(args)
|
||
|
|
assert result == 0, "Sample command failed"
|
||
|
|
|
||
|
|
# Check output
|
||
|
|
config_path = Path(tmpdir) / "manifests" / "all_configs.json"
|
||
|
|
assert config_path.exists(), "Config file not created"
|
||
|
|
|
||
|
|
with open(config_path) as f:
|
||
|
|
data = json.load(f)
|
||
|
|
|
||
|
|
print(f" Generated {len(data)} configs")
|
||
|
|
assert len(data) <= 10, "Too many configs generated"
|
||
|
|
|
||
|
|
print(" [PASS] CLI tests passed")
|
||
|
|
return True
|
||
|
|
|
||
|
|
|
||
|
|
def test_integration():
|
||
|
|
"""Integration test: full pipeline."""
|
||
|
|
print("\n" + "="*70)
|
||
|
|
print("TEST 8: Full Integration")
|
||
|
|
print("="*70)
|
||
|
|
|
||
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||
|
|
# Step 1: Generate
|
||
|
|
from nautilus_dolphin.mc import MCSampler
|
||
|
|
sampler = MCSampler(base_seed=42)
|
||
|
|
trials = sampler.generate_trials(n_samples_per_switch=2, max_trials=10)
|
||
|
|
|
||
|
|
# Step 2: Validate
|
||
|
|
from nautilus_dolphin.mc import MCValidator
|
||
|
|
validator = MCValidator()
|
||
|
|
valid_trials = [t for t in trials if validator.validate(t).is_valid()]
|
||
|
|
print(f" Valid trials: {len(valid_trials)}/{len(trials)}")
|
||
|
|
|
||
|
|
# Step 3: Execute
|
||
|
|
from nautilus_dolphin.mc import MCExecutor
|
||
|
|
executor = MCExecutor(verbose=False)
|
||
|
|
results = executor.execute_batch(valid_trials[:5])
|
||
|
|
|
||
|
|
# Step 4: Store
|
||
|
|
from nautilus_dolphin.mc import MCStore
|
||
|
|
store = MCStore(output_dir=tmpdir)
|
||
|
|
store.save_trial_results(results, batch_id=1)
|
||
|
|
|
||
|
|
# Step 5: Query
|
||
|
|
stats = store.get_corpus_stats()
|
||
|
|
print(f" Stored trials: {stats['total_trials']}")
|
||
|
|
|
||
|
|
assert stats['total_trials'] > 0, "No trials stored"
|
||
|
|
|
||
|
|
print(" [PASS] Integration test passed")
|
||
|
|
|
||
|
|
return True
|
||
|
|
|
||
|
|
|
||
|
|
def run_all_tests():
|
||
|
|
"""Run all tests."""
|
||
|
|
print("\n" + "="*70)
|
||
|
|
print("MONTE CARLO SYSTEM TEST SUITE")
|
||
|
|
print("="*70)
|
||
|
|
|
||
|
|
tests = [
|
||
|
|
("Sampler", test_sampler),
|
||
|
|
("Validator", test_validator),
|
||
|
|
("Metrics", test_metrics),
|
||
|
|
("Store", test_store),
|
||
|
|
("Executor", test_executor),
|
||
|
|
("Runner", test_runner),
|
||
|
|
("CLI", test_cli),
|
||
|
|
("Integration", test_integration),
|
||
|
|
]
|
||
|
|
|
||
|
|
passed = 0
|
||
|
|
failed = 0
|
||
|
|
|
||
|
|
for name, test_func in tests:
|
||
|
|
try:
|
||
|
|
if test_func():
|
||
|
|
passed += 1
|
||
|
|
else:
|
||
|
|
failed += 1
|
||
|
|
print(f" [FAIL] {name} test failed")
|
||
|
|
except Exception as e:
|
||
|
|
failed += 1
|
||
|
|
print(f" [FAIL] {name} test failed with exception: {e}")
|
||
|
|
import traceback
|
||
|
|
traceback.print_exc()
|
||
|
|
|
||
|
|
print("\n" + "="*70)
|
||
|
|
print("TEST SUMMARY")
|
||
|
|
print("="*70)
|
||
|
|
print(f"Passed: {passed}/{len(tests)}")
|
||
|
|
print(f"Failed: {failed}/{len(tests)}")
|
||
|
|
|
||
|
|
if failed == 0:
|
||
|
|
print("\n[OK] ALL TESTS PASSED!")
|
||
|
|
return 0
|
||
|
|
else:
|
||
|
|
print("\n[FAIL] SOME TESTS FAILED")
|
||
|
|
return 1
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
sys.exit(run_all_tests())
|