Skip to main content

Mercury Redesign: Clean Architecture Implementation

Β· 6 min read
Max Kaido
Architect

After extensive discussion and implementation, we've achieved a clean, minimal architecture that separates concerns properly and avoids over-engineering.

Core Architecture Principles​

  1. Clear Separation: Variants (experiment tracking) vs Instances (deployment)
  2. Single Responsibility: Config = strategy, Instance = deployment, Scheduler = timing
  3. Type Safety: Strong enums prevent runtime errors
  4. No Database Pollution: Infrastructure concerns stay out of business logic
  5. Config-Over-Env: ABH variants determined by time + TypeScript config, NEVER environment variables

Final Architecture​

Variants (Experiment Tracking)​

enum MercuryVariant {
MAIN = 'MAIN', // Production-ready strategy
A = 'A', // Experimental variant A
B = 'B', // Experimental variant B
H = 'H', // Experimental variant H (heuristic)
}

Instances (Hardware/Deployment)​

enum MercuryInstance {
R = 'R', // Readonly shadow trading
W = 'W', // Live trading
ABH = 'ABH', // Experimental (variants determined by time + config)
}

Tournament Configuration (Pure Strategy)​

interface TournamentConfig {
// ... existing strategy fields ...
topK: number; // Tournament parameter: how many positions to find

experimental?: {
experimentId: string; // Experiment identifier
variant: MercuryVariant; // For metrics/analysis
topL?: number; // Strategy recommendation (≀ topK)
};
}

Instance Service (Risk Management)​

class InstanceService {
getLiveRatio(): number; // From LIVE_RATIO env var (0.0-1.0)
calculateLiveCount(topL: number): number; // Math.floor(topL Γ— liveRatio)
}

Key Concepts Explained​

topK vs topL vs liveRatio​

  • topK: Tournament finds top K positions from all candidates
  • topL: Strategy recommends L positions as optimal (≀ topK)
  • liveRatio: Risk control - what percentage of topL to trade live
  • liveCount: Final result - Math.floor(topL Γ— liveRatio)

Instance Behavior​

# R Instance (Shadow Production)
MERCURY_INSTANCE=R # β†’ Instance: R, Variant: MAIN, liveCount: 0

# W Instance (Live Production)
MERCURY_INSTANCE=W # β†’ Instance: W, Variant: MAIN, liveCount: topL Γ— liveRatio
LIVE_RATIO=0.5 # β†’ 50% of recommended positions go live

# ABH Instance (Experimental)
MERCURY_INSTANCE=ABH # β†’ Instance: ABH, Variant: determined by time + experiment config, liveCount: 0

Configuration File Structure (Code-Based)​

// dike/tournaments/config/main.ts - Production configuration with EXPLICIT schedules
export const MAIN_TOURNAMENTS = [
{
type: TournamentType.MOMENTUM_STRENGTH_BUY,
cronPattern: '0 20 13 * * *', // 13:20 UTC β€” US ramp start
configVersion: 'v2',
description: 'US market opening momentum - strong upward bias',
config: { /* full tournament config with experimental fields */ }
},
// ... 7 more tournaments with exact timing and config versions
];

// config/experiments/ - Experiment lifecycle management
experiments/
β”œβ”€β”€ pending/ # Future experiments (planning phase)
β”‚ └── 1-price-normalization.ts
β”œβ”€β”€ in-progress/ # Currently running experiments
β”‚ └── 0-comparison-methods.ts
└── conducted/ # Completed experiments (historical record)
└── (empty for now)

Tournament Schedule Principles βš‘β€‹

🎯 EXPLICIT OVER IMPLICIT

  • All tournament schedules visible in config/main.ts
  • No hidden schedules in other modules
  • Exact config versions specified (v2, v1)
  • Human-readable descriptions for each tournament

πŸ“Έ BASELINE SNAPSHOT REQUIREMENT

  • Every experiment file contains EXACT copy of main schedule
  • Preserves historical baseline at experiment start time
  • Prevents "what was the baseline?" confusion
  • No lazy imports - explicit duplication required

🚫 NO "LATEST_VERSION" SHORTCUTS

  • CRITICAL INSIGHT: LATEST_VERSION was hiding experiment chaos
  • Previous mistake: See improvement β†’ Replace config β†’ Call it "latest"
  • Proper approach: Experiment with configV1 vs configV2 β†’ Conclude which is better
  • Result: Clear record of what changed and why
// ❌ BAD: LATEST_VERSION hides experimental changes
configVersion: LATEST_VERSION[TournamentType.MOMENTUM_STRENGTH_BUY], // What version? When changed? Why?

// βœ… GOOD: Explicit version with experimental intent
configVersion: 'v2', // Clear version, experiment will test v2 vs v3

πŸ’Ž GOLD PRINCIPLE: Config improvements must go through experiment framework, not direct "latest" replacement

Experiment Lifecycle Management​

πŸ”„ Experiment States:

  1. PENDING: Experiment planned but not implemented
  2. IN-PROGRESS: Currently running and collecting data
  3. CONDUCTED: Completed with results analyzed

πŸ“ File Movement Process:

# Start new experiment
mv experiments/pending/1-price-normalization.ts experiments/in-progress/

# Complete experiment
mv experiments/in-progress/0-comparison-methods.ts experiments/conducted/

Single-Variable Experiment Principle​

Each experiment MUST test only ONE variable:

// βœ… GOOD: Experiment 0 - Only comparison method varies
variants: {
A: { comparisonMethod: 'two-step', normalizePrice: false },
B: { comparisonMethod: 'structured', normalizePrice: false },
H: { comparisonMethod: 'heuristic', normalizePrice: false },
}

// ❌ BAD: Multiple variables (ruins analysis)
variants: {
A: { comparisonMethod: 'two-step', normalizePrice: false },
B: { comparisonMethod: 'structured', normalizePrice: true }, // 2 changes!
}

Experiment File Organization Principles​

  1. Experiment ID as Filename: 0-comparison-methods.ts where 0-comparison-methods is the experiment ID
  2. A Variant = MAIN Copy: Always include exact copy of MAIN config at experiment time (prevents "what was baseline?" confusion)
  3. B/H Variants = Concise Changes: Only specify differences from A to reduce duplication
  4. Time Shifting: A (+1 hour), B (A + 20 mins), H (A + 40 mins) for conflict prevention
  5. Historical Preservation: A variant shows exact baseline used, even if MAIN evolves later

πŸ† GOLDEN RULE: Config-First Experimentation​

ALL experiment parameters MUST be stored in TournamentConfig.experimental

experimental: {
experimentId: '0-comparison-methods',
variant: MercuryVariant.B,
topL: 5,

// EXPERIMENT OVERRIDES - Every parameter that varies between variants
comparisonMethod: 'structured',
normalizePrice: true,
// ... any other experiment-specific settings
}

Why This Prevents Experiment Chaos:

  • βœ… Reproducible: All parameters saved in database with results
  • βœ… Traceable: Can see exactly what configuration produced each result
  • βœ… No Memory Loss: Can't forget what parameters were being tested
  • βœ… Clean Comparisons: Variants differ only in explicitly tracked parameters
  • βœ… Historical Analysis: Past experiments remain analyzable even after code changes

Anti-Pattern (Causes Chaos):

# ❌ WRONG: Parameters scattered in environment variables
COMPARISON_METHOD=structured
NORMALIZE_PRICES=true
# Result: Forget what was being tested, can't reproduce results

Implementation Status βœ…β€‹

Phase 1: Code-Based Configuration System​

  • βœ… COMPLETED: Extended TournamentConfig with experimental fields
  • βœ… COMPLETED: Added topK to tournament configuration
  • βœ… COMPLETED: Created tournament config factory with variant handling
  • βœ… COMPLETED: Strong typing with MercuryVariant and MercuryInstance enums
  • πŸ“¦ TAGGED: v2025.06.20-mercury-experimental-framework

Phase 2: Simplified Variant Architecture​

  • βœ… COMPLETED: Clean separation of variants (MAIN/A/B/H) vs instances (R/W/ABH)
  • βœ… COMPLETED: R & W instances both run MAIN variant with different risk levels
  • βœ… COMPLETED: ABH instance determines variants from time + experiment configuration
  • βœ… COMPLETED: Removed confusing legacy enums and phantom variants

Infrastructure Services​

  • βœ… COMPLETED: InstanceService for live trading configuration
  • βœ… COMPLETED: InstanceSchedulerService for time slot management
  • βœ… COMPLETED: Clean metrics with both instance + variant labels
  • βœ… COMPLETED: Proper dependency injection in AtlasModule

Example Scenarios​

Conservative Live Trading Rollout​

# Start conservative
LIVE_RATIO=0.1 # topL=7 β†’ liveCount=0 (Math.floor(0.7))

# Gradual increase
LIVE_RATIO=0.3 # topL=7 β†’ liveCount=2 (Math.floor(2.1))

# Full confidence
LIVE_RATIO=1.0 # topL=7 β†’ liveCount=7 (Math.floor(7.0))

Experiment Analysis​

# After experiment shows only top 3 positions are profitable
topL=3, LIVE_RATIO=0.5 # β†’ liveCount=1 (Math.floor(1.5))

Hardware Isolation​

  • R Instance: Shadow trading, separate database, no live keys
  • W Instance: Live trading, separate database, has live keys
  • ABH Instance: Experimental, separate database, no live keys

ABH Variant Scheduling​

// ABH determines variant based on time within 2-hour blocks
const blockMinute = (currentHour % 2) * 60 + currentMinute;

if (blockMinute >= 60 && blockMinute < 80) {
variant = MercuryVariant.A; // Minutes 60-79
} else if (blockMinute >= 80 && blockMinute < 100) {
variant = MercuryVariant.B; // Minutes 80-99
} else if (blockMinute >= 100 && blockMinute < 120) {
variant = MercuryVariant.H; // Minutes 100-119
}

NO ENVIRONMENT VARIABLES for variant determination - pure time + config approach!

ABH Instance Tournament Scheduling πŸŽ―β€‹

CRITICAL ARCHITECTURE: ABH instance schedules 24 tournaments total:

  • 8 base tournaments (from main production schedule)
  • Γ— 3 variants (A, B, H) = 24 scheduled tournaments
// ABH Scheduling Pattern
class TournamentOrchestrator {
setupABHExperimentalSchedule(experiment: ExperimentConfig) {
experiment.baseSchedule.forEach((tournamentConfig) => {
// Schedule variant A with +1 hour offset
this.scheduleVariantTournament(tournamentConfig, MercuryVariant.A, 60);

// Schedule variant B with +80 minutes offset
this.scheduleVariantTournament(tournamentConfig, MercuryVariant.B, 80);

// Schedule variant H with +100 minutes offset
this.scheduleVariantTournament(tournamentConfig, MercuryVariant.H, 100);
});
}
}

ARCHITECTURAL VIOLATIONS PREVENTED:

  • ❌ NEVER use MERCURY_VARIANT=A/B/H environment variables
  • ❌ NEVER determine variants from environment at runtime
  • βœ… ALWAYS determine variants from time + TypeScript experiment configuration
  • βœ… ALWAYS schedule all 24 tournaments at ABH startup

SINGLE EXECUTION METHOD:

// Unified tournament execution - no variant-specific methods
runTournament(tournamentType: TournamentType, configOverride?: Partial<TournamentConfig>)

Benefits Achieved​

🎯 Clean Architecture​

  • Strategy configuration separated from deployment concerns
  • Time slots handled by scheduler, not stored in config
  • Live trading controlled by instance service, not config data

πŸ”’ Type Safety​

  • Strong enums prevent typos and runtime errors
  • IDE autocomplete and refactoring support
  • Clear architectural intent enforced by type system

πŸ“Š Proper Metrics​

  • Both instance and variant labels for complete tracking
  • Clear distinction between deployment and experiment data
  • No phantom variants or confusing legacy labels

πŸš€ Scalable Risk Management​

  • Gradual live trading rollout via liveRatio
  • Strategy recommendations captured in topL
  • Fine-grained control without touching strategy logic

πŸ›‘οΈ Safety by Design​

  • Only W instance has live trading keys (hardware isolation)
  • Conservative defaults (liveRatio=0, topL=topK)
  • Manual restart control for configuration changes

Implementation Questions & Answers​

These are the specific answers provided during 2 hours of architectural discussion:

Phase 2: Historical Analysis Scripts (Before ABH Consolidation)​

  1. What metrics should we extract from A/B/H variants before consolidating? Question-driven scripts for known pain points (to be provided)
  2. Which date range should we analyze for historical comparison data? Will answer later (based on available data)
  3. How do we identify which variant (A/B/H) each tournament belongs to in historical data? Separate DBs + API access while running

Phase 3: ABH Implementation​

  1. How exactly do we determine which comparison method to use for each tournament in ABH? Time-based scheduling within ABH slot + experiment config
  2. Where does the ABH variant read the current experiment config from? TypeScript experiment configuration files
  3. Do we keep all A/B/H environment files and modify them, or create new mercury-abh.env? Use single ABH environment with MERCURY_INSTANCE=ABH only

Phase 4: Data Tracking​

  1. Which specific entities need experimentId/portfolioAccount fields? Just tournaments (add experimentId + variant, portfolioAccount derived from tournamentType + experimentId + variant)

  2. How do we populate these fields? At tournament creation

Phase 5: Analysis​

  1. What specific metrics do agents need from the API? Question-driven metrics linked to experiment configs (e.g., model comparison speed, prompt performance impact, optimal topK for PnL)

  2. How do agents access Mercury backend? Direct API calls wrapped with handy scripts that provide exact data needed to answer specific questions

Phase 6: Live Trading​

  1. Where does liveCount get set? In config (code). Environment only specifies instance type (W/R/ABH), everything else from config β†’ naturally flows to DB

  2. How do we prevent accidental live trading? Only W instance has live trading keys. R = readonly mode. Start with liveCount: 1, manual review, then gradually increase

Phase 7: Agent Integration​

  1. How should agents change experiment configs? Code modification (since everything in code)

  2. What triggers ABH restart after config change? Manual by user

Phase 8: Dashboard​

  1. Which dashboard components are most broken/inconsistent? Will assess after previous phases (some bugs may naturally disappear)

Next Steps​

  1. Historical Analysis: SKIPPED - Would slow down main feature. Instead: tag current git state for future API image builds, preserve existing DBs (no action needed)
  2. ABH Implementation: Single instance running A, B, or H variants based on config
  3. Agent Integration: Scripts for config modification and analysis
  4. Dashboard Cleanup: Remove inconsistencies now that architecture is clean
  5. Live Trading: Gradual rollout with user review and manual scaling

Historical Analysis (Future Option)​

  • Git Tag: Current state tagged for building legacy API images if needed later
  • Database Preservation: Existing A/B/H DBs preserved automatically (no action required)
  • Future Access: Can analyze historical data later without blocking current development

The foundation is solid - clean separation of concerns with proper type safety and no over-engineering! 🎯

🎯 Production Schedule Now Visible:​