"Show me a quant fund that isn't thinking seriously about large language models right now, and I'll show you a fund with a retention problem."

That assessment comes from a quantitative researcher at a multi-strategy hedge fund in New York, spoken off the record in early 2026. It captures the mood of an industry at an inflection point. After a decade in which the marginal alpha of traditional factor models steadily eroded, the promise of machine learning injected energy — and skepticism in equal measure. Now a new wave is building: not just ML as a prediction engine, but AI as a full participant in the research workflow, generating hypotheses, writing code, stress-testing strategies, and in some cases running portfolios with minimal human oversight.

This article examines what that wave actually looks like in production — not the press release version, but the engineering reality. We'll map the current landscape across three AI modalities shaping quant research, walk through concrete implementation patterns, expose the failure modes that vendor decks tend to omit, and provide production-grade code that demonstrates where the technology genuinely works today.


The Three Modalities Reshaping Quant Research

Before diving into applications, it helps to establish a clear taxonomy. The AI capabilities being deployed in quantitative finance fall into three distinct modalities, each with different strengths, failure modes, and levels of production readiness.

Large Language Models: The Research Assistant

LLMs have entered the quant workflow primarily as research acceleration tools. Their core value is language understanding and generation: synthesizing academic papers, explaining factor constructions, debugging strategy code, and generating structured summaries of backtest results. The more ambitious use cases involve LLM-driven factor generation — prompting a model to propose candidate alpha signals based on a corpus of market microstructure literature.

The key limitation is that an LLM has no direct access to market reality. It can pattern-match across a training corpus that includes quant research, but it cannot observe price formation. Factor generation via LLM therefore works best as a hypothesis generator: propose 500 candidate signals, filter them through statistical and economic reasoning, backtest the survivors. The model surfaces candidates; human or classical ML judgment decides.

Reinforcement Learning: The Strategy Optimizer

Reinforcement learning (RL) entered quant finance several years ago with considerable fanfare and mixed results. The appeal is intuitive: trading is sequential decision-making under uncertainty, which is precisely the problem class RL was designed for. An RL agent can learn policy functions that adapt to regime changes, something static factor models struggle with.

The practical challenge is environment fidelity. An RL agent is only as good as its simulation of market conditions. In the early years, many quant RL projects failed because the training environment was too stylized — assuming frictionless execution, infinite liquidity, or i.i.d. returns. The current generation of production systems addresses this with more realistic simulators that incorporate spread costs, slippage models, and order book dynamics. We will examine one such pattern in the code section below.

Generative AI: The Data Augmentation Engine

Generative models — GANs, VAEs, and more recently diffusion-based architectures — have found a productive niche in quant as synthetic data generators. The core use case is augmentation: generating synthetic historical data to stress-test strategies in scenarios that historical data cannot cover, such as never-before-seen macroeconomic regimes or liquidity conditions.

This is one of the most production-ready applications of AI in quant today, because the evaluation is objective: a strategy trained on augmented data either generalizes better to held-out real data or it does not. The failure mode is mode collapse, where the generative model produces plausible-looking but statistically unrepresentative data. Careful evaluation against out-of-sample real data is mandatory.


AI-Driven Factor Mining: From Hypothesis to Signal

Factor mining has historically been a labor-intensive process. A researcher identifies an economic intuition, translates it into a data transformation, computes the signal, runs statistical tests, and either promotes the factor to a backtest or discards it. The bottleneck is not computation — it is the hypothesis generation phase, where human intuition and domain knowledge constrain the search space.

LLM-based factor mining attempts to relax that constraint. The workflow typically looks like this:

  1. Corpus preparation: Compile a database of academic papers, working papers, earnings transcripts, and news headlines related to the target asset class.
  2. Prompt engineering: Feed the LLM a structured prompt asking it to propose factor constructions based on the corpus. Include constraints: signal must be computable from available data, should have a plausible economic mechanism, should be non-trivial.
  3. Filtering: Run each proposed factor through a rapid statistical screen — correlation with existing factors, signal-to-noise ratio, univariate IC (information coefficient).
  4. Backtesting: Promising factors enter a full backtesting pipeline.

The following pseudocode illustrates the structure of such a pipeline:

# Factor Generation Pipeline — LLM-Assisted Hypothesis Generation
# ⚠️ This is a high-level workflow illustration. Adapt to your data infrastructure.

import os
import json
from typing import List, Dict

class FactorMiningPipeline:
    """
    LLM-assisted factor generation and evaluation pipeline.
    In production, replace the LLM call with your preferred provider
    (OpenAI, Anthropic, local LLM via Ollama, etc.).
    """

    def __init__(self, llm_client, factor_db, backtester):
        self.llm = llm_client
        self.factor_db = factor_db  # Persistent factor library
        self.backtester = backtester

    def generate_candidates(self, corpus_path: str, n: int = 100) -> List[Dict]:
        """
        Prompt the LLM to propose factor constructions based on a research corpus.
        Returns n candidate factor definitions.
        """
        with open(corpus_path, "r", encoding="utf-8") as f:
            corpus = f.read()

        prompt = f"""
        You are a quantitative researcher analyzing the following literature corpus.
        For each candidate factor below, provide:
        - Factor name
        - Data inputs required
        - Construction logic (pseudocode or formula)
        - Plausible economic mechanism

        Generate exactly {n} candidates, diverse in type:
        - Price-based (momentum, mean-reversion, volume-weighted)
        - Microstructure-based (order flow, spread dynamics)
        - Cross-sectional (relative value, factor orthogonalization)
        - Text-derived (sentiment, topic models from earnings calls)

        Corpus excerpt:
        {corpus[:8000]}  # Truncate to fit context window
        """
        response = self.llm.chat.completions.create(
            model=os.environ.get("LLM_MODEL", "gpt-4o"),
            messages=[{"role": "user", "content": prompt}],
            temperature=0.7,  # Non-zero for creative diversity
            timeout=60
        )
        candidates = self._parse_llm_response(response)
        return candidates

    def rapid_screen(self, candidates: List[Dict]) -> List[Dict]:
        """
        First-pass statistical filter: correlation, IC, coverage.
        Returns filtered candidates that pass the threshold.
        """
        screened = []
        for candidate in candidates:
            try:
                ic = self._compute_ic(candidate)
                corr_with_universe = self._check_correlation(candidate)
                if ic > 0.02 and abs(corr_with_universe) < 0.85:
                    screened.append({**candidate, "ic": ic})
            except Exception as e:
                # Log and skip — don't let one bad factor kill the pipeline
                print(f"[WARN] Skipping {candidate.get('name')}: {e}")
                continue
        return screened

    def full_backtest(self, candidates: List[Dict]) -> List[Dict]:
        """
        Second-pass: full backtest with realistic cost modeling.
        """
        results = []
        for candidate in candidates:
            bt_result = self.backtester.run(
                signal=candidate["name"],
                start="2015-01-01",
                end="2024-12-31",
                costs={"commission": 0.0002, "slippage": 0.0005}
            )
            if bt_result["sharpe"] > 1.0 and bt_result["max_drawdown"] < 0.25:
                results.append({**candidate, "backtest": bt_result})
        return results

    def _compute_ic(self, candidate: Dict) -> float:
        """Compute IC vs. forward returns — placeholder for your implementation."""
        raise NotImplementedError("Integrate with your factor library")

    def _check_correlation(self, candidate: Dict) -> float:
        """Check correlation with existing factor universe."""
        raise NotImplementedError("Integrate with your factor library")

    def _parse_llm_response(self, response) -> List[Dict]:
        """Parse LLM JSON output into structured factor definitions."""
        content = response.choices[0].message.content
        # Strip markdown code blocks if present
        if content.startswith("```"):
            content = content.split("```")[1]
            if content.startswith("json"):
                content = content[4:]
        return json.loads(content)

A few engineering notes on this pattern. First, the temperature=0.7 setting is deliberate — you want diversity in factor proposals, not the model's most confident (and potentially most cliché) answers. Second, the two-stage filtering (rapid IC screen, then full backtest) is essential: running 500 full backtests on the first pass is computationally wasteful. Third, the environment variable pattern for model selection allows you to A/B test different LLMs or switch between cloud and local models without code changes.

The honest assessment of this workflow: it accelerates the hypothesis generation phase significantly. A team that could manually evaluate 20 candidate factors per quarter can now evaluate 500. But the bottleneck simply moves downstream to the backtesting and evaluation phase — which is where rigorous cost modeling and out-of-sample validation become critical.


Reinforcement Learning for Adaptive Strategy Design

RL in quant differs from standard RL applications in a fundamental way: the environment is not a fixed game with known rules. The market is an adversarial, non-stationary, partially observable system. An RL agent trained on 2020–2023 data faces a distribution that looks different in 2025. This is the central challenge.

The Simulator-First Architecture

Production RL systems in quant therefore adopt a simulator-first architecture. Rather than training directly on historical data or live market execution, the agent trains in a high-fidelity market simulator that models:

  • Order book dynamics: Level 2 order book state transitions
  • Market impact: Temporary and permanent impact as a function of order size
  • Latency and fill uncertainty: Realistic modeling of partial fills and cancellations
  • Regime switching: Explicit modeling of volatility regimes, correlation regimes
# Reinforcement Learning Trading Agent — Simulator-First Training
# ⚠️ For production HFT workloads, consider Ray RLlib or Stable-Baselines3
# with a custom environment wrapping a high-fidelity market simulator.

import numpy as np
from dataclasses import dataclass
from typing import Optional
import time

@dataclass
class MarketState:
    """Simulated market state — replace with your TickDB depth feed in live mode."""
    mid_price: float
    best_bid: float
    best_ask: float
    bid_sizes: np.ndarray   # Top N levels
    ask_sizes: np.ndarray   # Top N levels
    volatility_regime: str  # "low", "medium", "high"
    timestamp: float

@dataclass
class RLConfig:
    """Configuration for the RL trading agent."""
    state_dim: int = 12        # mid_price, spread, N depth levels, regime, etc.
    action_dim: int = 3        # {-1, 0, 1} — short, flat, long
    learning_rate: float = 3e-4
    gamma: float = 0.99        # Discount factor
    epsilon_start: float = 1.0
    epsilon_end: float = 0.05
    replay_buffer_size: int = 100_000
    batch_size: int = 64
    target_update_freq: int = 100


class MarketSimulator:
    """
    High-fidelity market simulator for RL training.
    In production, replace with TickDB depth data for out-of-sample validation.
    """

    def __init__(self, symbol: str, seed: int = 42):
        self.symbol = symbol
        self.rng = np.random.default_rng(seed)
        self.base_volatility = 0.015
        self.current_price = 100.0

    def reset(self) -> MarketState:
        self.current_price = 100.0
        return self._build_state(regime="medium")

    def step(self, action: int, position_size: float = 1.0) -> tuple[MarketState, float, bool]:
        """
        Execute one simulation step.
        action: -1 (short), 0 (flat), 1 (long)
        Returns: (next_state, reward, done)
        """
        # Generate price move based on volatility regime
        regime = self._sample_regime()
        vol = self.base_volatility * (1.5 if regime == "high" else 0.7 if regime == "low" else 1.0)
        price_return = self.rng.normal(0, vol)
        self.current_price *= (1 + price_return)

        # Simulate market impact of trading action
        # ⚠️ Simplified model — production systems use Almgren-Chriss or similar
        spread = 0.0002 * self.current_price
        if action != 0:
            # Market impact: temporary price move against the trade
            impact = 0.1 * position_size * price_return
            self.current_price *= (1 - impact if action == 1 else 1 + impact)

        # Compute reward: P&L from position
        reward = action * position_size * price_return * 10000  # Scale to basis points

        state = self._build_state(regime)
        done = False  # Implement your termination condition
        return state, reward, done

    def _build_state(self, regime: str) -> MarketState:
        spread = 0.0002 * self.current_price
        return MarketState(
            mid_price=self.current_price,
            best_bid=self.current_price - spread / 2,
            best_ask=self.current_price + spread / 2,
            bid_sizes=self.rng.uniform(100, 5000, 5),
            ask_sizes=self.rng.uniform(100, 5000, 5),
            volatility_regime=regime,
            timestamp=time.time()
        )

    def _sample_regime(self) -> str:
        r = self.rng.random()
        return "low" if r < 0.2 else "high" if r > 0.8 else "medium"


class DQNAgent:
    """
    Deep Q-Network agent for discrete action trading.
    ⚠️ Production RL trading systems require extensive hyperparameter tuning,
    regime-aware reward shaping, and rigorous out-of-sample validation.
    This is a structural illustration, not a turnkey strategy.
    """

    def __init__(self, config: RLConfig):
        self.config = config
        self.epsilon = config.epsilon_start
        # In production: initialize neural network (PyTorch / TensorFlow)
        # self.q_network = self._build_network()
        self.replay_buffer = []
        self.total_steps = 0

    def select_action(self, state: MarketState) -> int:
        """Epsilon-greedy action selection."""
        if np.random.random() < self.epsilon:
            return np.random.randint(-1, 2)  # Explore
        # In production: compute Q-values, return argmax
        return 0  # Placeholder

    def update(self, state: MarketState, action: int, reward: float,
               next_state: MarketState, done: bool):
        """Experience replay update."""
        self.replay_buffer.append((state, action, reward, next_state, done))
        if len(self.replay_buffer) > self.config.replay_buffer_size:
            self.replay_buffer.pop(0)
        self.total_steps += 1

        if len(self.replay_buffer) >= self.config.batch_size:
            self._train_step()

        # Decay epsilon
        self.epsilon = max(
            self.config.epsilon_end,
            self.epsilon - 1e-5
        )

    def _train_step(self):
        """Sample batch and perform gradient update."""
        # ⚠️ In production: implement actual gradient descent on Q-network
        pass


def train_rl_agent(symbol: str, episodes: int = 500, steps_per_episode: int = 252):
    """
    Train RL agent in simulated market environment.
    Replace simulator with real TickDB data for live validation.
    """
    config = RLConfig()
    simulator = MarketSimulator(symbol)
    agent = DQNAgent(config)

    training_log = []

    for episode in range(episodes):
        state = simulator.reset()
        episode_reward = 0.0

        for step in range(steps_per_episode):
            action = agent.select_action(state)
            next_state, reward, done = simulator.step(action)
            agent.update(state, action, reward, next_state, done)
            state = next_state
            episode_reward += reward

            if done:
                break

        training_log.append({
            "episode": episode,
            "reward": episode_reward,
            "epsilon": agent.epsilon,
            "steps": agent.total_steps
        })

        if episode % 50 == 0:
            avg_reward = np.mean([e["reward"] for e in training_log[-50:]])
            print(f"[Episode {episode}] Avg reward (last 50): {avg_reward:.4f} | "
                  f"Epsilon: {agent.epsilon:.4f}")

    return agent, training_log

The critical engineering discipline here is separating training data from validation data. The simulator should be calibrated on historical data (e.g., TickDB depth and trades data for HK equities or crypto, where available), but the agent's final performance must be evaluated on a held-out time window that the simulator has never seen. If the agent's Sharpe in-sample is 2.1 and out-of-sample is 0.4, the simulator is leaking information — not a production-ready system.


Synthetic Data: Augmenting the Backtest

Historical data has a fundamental limitation: it only contains what has happened. The 2008 financial crisis, the 2020 COVID crash, the 2022 rate shock — these events are informative, but they are not a comprehensive catalog of tail risks. Synthetic data generation addresses this by learning the statistical properties of historical data and generating plausible alternative histories.

The most effective approach in current production uses conditional generative models trained on factor returns, where conditioning variables include:

  • Volatility regime (VIX level or realized volatility quantiles)
  • Credit spread regime (investment-grade and high-yield spread levels)
  • Macro regime (GDP growth, PMI, Fed Funds rate)
  • Liquidity regime (bid-ask spread levels, market depth)
# Synthetic Data Generation for Strategy Stress-Testing
# ⚠️ Generator quality depends entirely on calibration data quality and coverage.
# Always validate synthetic data against held-out real data before using in backtests.

import numpy as np
import pandas as pd
from dataclasses import dataclass, field
from typing import List, Dict
from datetime import datetime, timedelta

@dataclass
class SyntheticDataConfig:
    """Configuration for synthetic return generation."""
    n_scenarios: int = 1000          # Number of synthetic histories to generate
    n_days: int = 252                # Length of each synthetic history
    conditioning_vars: List[str] = field(
        default_factory=lambda: ["vol_regime", "credit_regime", "liq_regime"]
    )
    seed: int = 42

    # Regime transition probabilities (simplified 3-state Markov chain)
    vol_transition_matrix: np.ndarray = field(
        default_factory=lambda: np.array([
            [0.85, 0.12, 0.03],  # From low: stay low / move up / spike
            [0.10, 0.80, 0.10],  # From medium
            [0.03, 0.15, 0.82],  # From high
        ])
    )


class ConditionalReturnGenerator:
    """
    Conditionally generate synthetic return scenarios based on regime observables.
    Uses a Gaussian copula with regime-conditional parameters.
    """

    def __init__(self, historical_returns: pd.DataFrame, config: SyntheticDataConfig):
        self.returns = historical_returns
        self.config = config
        self.rng = np.random.default_rng(config.seed)
        self.regime_params = self._calibrate_regimes()

    def _calibrate_regimes(self) -> Dict[str, Dict]:
        """
        Calibrate return statistics (mean, std, cross-sectional correlations)
        for each combination of conditioning regimes.
        """
        # Simplified calibration — production systems use rolling-window estimates
        params = {}
        for vol_r in ["low", "medium", "high"]:
            for cr_r in ["low", "medium", "high"]:
                mask = (
                    (self.returns["vol_regime"] == vol_r) &
                    (self.returns["credit_regime"] == cr_r)
                )
                subset = self.returns.loc[mask]
                if len(subset) > 30:
                    params[f"{vol_r}_{cr_r}"] = {
                        "mean": subset.mean(numeric_only=True),
                        "std": subset.std(numeric_only=True),
                        "n_samples": len(subset)
                    }
        return params

    def generate_scenarios(self, initial_state: Dict) -> pd.DataFrame:
        """
        Generate n synthetic return scenarios under the specified initial conditions.
        Returns a DataFrame with multi-level columns: (scenario_id, date).
        """
        scenarios = {}

        for scenario_id in range(self.config.n_scenarios):
            # Draw regime path using Markov chain
            regime_path = self._draw_regime_path()
            synthetic_returns = []

            current_regime = initial_state
            for day in range(self.config.n_days):
                regime_key = f"{regime_path[day]}_{current_regime['credit']}"
                params = self.regime_params.get(
                    regime_key,
                    self.regime_params.get("medium_medium")
                )

                # Generate returns with regime-conditional statistics
                daily_return = self.rng.normal(
                    loc=params["mean"].mean(),
                    scale=params["std"].mean()
                )
                synthetic_returns.append({
                    "date": datetime.today() + timedelta(days=day),
                    "return": daily_return
                })

            scenarios[scenario_id] = pd.DataFrame(synthetic_returns).set_index("date")

        # Combine into multi-level DataFrame
        result = pd.concat(scenarios, axis=1, names=["scenario_id", "date"])
        return result

    def _draw_regime_path(self) -> List[str]:
        """Draw a volatility regime path using the Markov transition matrix."""
        regimes = ["low", "medium", "high"]
        path = ["medium"]  # Start from medium
        current = 1  # Index of "medium"

        for _ in range(self.config.n_days - 1):
            current = int(
                self.rng.choice(3, p=self.config.vol_transition_matrix[current])
            )
            path.append(regimes[current])

        return path


def evaluate_strategy_on_synthetic(
    strategy_fn,
    synthetic_data: pd.DataFrame,
    initial_capital: float = 1_000_000.0,
    costs: Dict[str, float] = None
) -> Dict:
    """
    Run a strategy function across all synthetic scenarios and aggregate results.
    Returns distribution of Sharpe, max drawdown, and total return.
    """
    costs = costs or {"commission": 0.0002, "slippage": 0.0005}
    scenario_results = []

    for scenario_id in synthetic_data.columns.get_level_values(0).unique():
        scenario_returns = synthetic_data[scenario_id]["return"]

        # Compute strategy P&L from returns
        positions = strategy_fn(scenario_returns)  # Strategy produces position series
        pnl = positions.shift(1) * scenario_returns  # Return from overnight position
        pnl = pnl.dropna()

        # Apply costs
        turnover = positions.diff().abs().dropna()
        cost_impact = turnover * (costs["commission"] + costs["slippage"])
        net_pnl = pnl.loc[cost_impact.index] - cost_impact

        # Compute metrics
        total_return = (1 + net_pnl).prod() - 1
        sharpe = net_pnl.mean() / net_pnl.std() * np.sqrt(252) if net_pnl.std() > 0 else 0.0
        cumulative = (1 + net_pnl).cumprod()
        max_drawdown = (cumulative / cumulative.cummax() - 1).min()

        scenario_results.append({
            "scenario_id": scenario_id,
            "total_return": total_return,
            "sharpe": sharpe,
            "max_drawdown": max_drawdown
        })

    results_df = pd.DataFrame(scenario_results)

    return {
        "sharpe_mean": results_df["sharpe"].mean(),
        "sharpe_p5": results_df["sharpe"].quantile(0.05),
        "sharpe_p95": results_df["sharpe"].quantile(0.95),
        "max_dd_mean": results_df["max_drawdown"].mean(),
        "max_dd_p95": results_df["max_drawdown"].quantile(0.95),
        "total_return_p5": results_df["total_return"].quantile(0.05),
        "total_return_p95": results_df["total_return"].quantile(0.95),
        "n_scenarios": len(results_df)
    }

The output of this pipeline — the p5 and p95 percentiles of Sharpe and max drawdown across synthetic scenarios — gives a much more honest picture of strategy robustness than a single historical backtest. If your strategy's Sharpe collapses below 0.5 in the 5th percentile scenario, that is a risk management signal, not a reason to discard the strategy. It is a reason to add a regime filter or a circuit breaker.


AI Agents in the Research Workflow: The End-to-End Vision

The most ambitious vision for AI in quant is an end-to-end research agent: one that takes a broad investment thesis (e.g., "emerging market energy sector will outperform in a reflationary environment") and autonomously executes the full research pipeline — literature review, factor generation, backtesting, optimization, and draft strategy specification.

This vision is not science fiction. Teams at several quant shops are building toward it. But the engineering challenges are substantial:

Multi-agent orchestration. A single monolithic LLM cannot reliably execute a 20-step research pipeline. The current architecture uses a multi-agent system: a planner agent decomposes the thesis into tasks, specialized agents handle factor generation, code execution, and evaluation, and a supervisor agent aggregates results and flags contradictions.

Code execution sandboxing. When an AI agent writes and executes code, the system must safely sandbox the execution environment, enforce resource limits, and capture errors without crashing the broader pipeline. Tools like E2B, Modal, and Docker-based sandboxes are in production use at leading quant AI platforms.

Human-in-the-loop checkpoints. Fully autonomous strategy generation is not yet appropriate for live capital. Production systems include mandatory human review gates at key decision points: after factor generation, after initial backtest, before any deployment.

The following illustrates a high-level agent orchestration pattern:

# Multi-Agent Research Orchestration — Conceptual Illustration
# ⚠️ This is a structural skeleton. Production systems require:
# - Secure code execution sandboxing (E2B, Modal, etc.)
# - Formal verification for strategy constraints
# - Human-in-the-loop review gates before capital deployment

import os
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import List, Dict, Optional
from enum import Enum

class AgentRole(Enum):
    PLANNER = "planner"
    FACTOR_GENERATOR = "factor_generator"
    BACKTESTER = "backtester"
    CRITIC = "critic"
    REPORT_WRITER = "report_writer"

@dataclass
class ResearchTask:
    """A unit of work in the research pipeline."""
    task_id: str
    role: AgentRole
    input_data: Dict
    output_data: Optional[Dict] = None
    status: str = "pending"  # pending / in_progress / completed / failed


class BaseAgent(ABC):
    """Abstract base for all research agents."""

    def __init__(self, role: AgentRole, llm_client):
        self.role = role
        self.llm = llm_client

    @abstractmethod
    def execute(self, task: ResearchTask) -> ResearchTask:
        """Execute the task and return updated task with output_data."""
        pass

    def call_llm(self, prompt: str, system_prompt: str = "") -> str:
        """Standard LLM call pattern with timeout and error handling."""
        import requests
        # ⚠️ Replace with your actual LLM provider — shown as generic HTTP call
        response = requests.post(
            os.environ.get("LLM_API_URL"),
            headers={
                "Authorization": f"Bearer {os.environ.get('LLM_API_KEY')}",
                "Content-Type": "application/json"
            },
            json={
                "model": os.environ.get("LLM_MODEL", "gpt-4o"),
                "messages": [
                    {"role": "system", "content": system_prompt},
                    {"role": "user", "content": prompt}
                ],
                "temperature": 0.3  # Low temp for deterministic output in agents
            },
            timeout=(5.0, 30.0)
        )
        response.raise_for_status()
        return response.json()["choices"][0]["message"]["content"]


class ResearchOrchestrator:
    """
    Orchestrates multi-agent research pipeline with checkpoint-based human review.
    """

    def __init__(self):
        self.llm_client = None  # Initialize with your LLM provider
        self.agents = {
            AgentRole.PLANNER: PlannerAgent(AgentRole.PLANNER, self.llm_client),
            AgentRole.FACTOR_GENERATOR: FactorGeneratorAgent(
                AgentRole.FACTOR_GENERATOR, self.llm_client
            ),
            AgentRole.BACKTESTER: BacktesterAgent(AgentRole.BACKTESTER, self.llm_client),
            AgentRole.CRITIC: CriticAgent(AgentRole.CRITIC, self.llm_client),
        }
        self.tasks: List[ResearchTask] = []
        self.human_review_required = {AgentRole.FACTOR_GENERATOR, AgentRole.BACKTESTER}

    def run_pipeline(self, thesis: str, require_human_review: bool = True) -> Dict:
        """
        Execute full research pipeline from investment thesis to strategy draft.

        Pipeline stages:
        1. Planner decomposes thesis into structured tasks
        2. Factor generator proposes candidate signals
        3. [Human review gate — factors]
        4. Backtester evaluates approved factors
        5. [Human review gate — backtest]
        6. Critic evaluates robustness
        7. Report writer produces strategy specification
        """
        print(f"[Orchestrator] Starting pipeline for thesis: {thesis}")

        # Stage 1: Planning
        plan_task = ResearchTask(
            task_id="plan-001",
            role=AgentRole.PLANNER,
            input_data={"thesis": thesis}
        )
        plan_task = self.agents[AgentRole.PLANNER].execute(plan_task)
        sub_tasks = plan_task.output_data["sub_tasks"]
        print(f"[Orchestrator] Generated {len(sub_tasks)} sub-tasks from planner")

        # Stage 2: Factor generation
        factor_task = ResearchTask(
            task_id="factor-001",
            role=AgentRole.FACTOR_GENERATOR,
            input_data={"thesis": thesis, "constraints": sub_tasks}
        )
        factor_task = self.agents[AgentRole.FACTOR_GENERATOR].execute(factor_task)
        factors = factor_task.output_data["factors"]

        # Human review gate
        if require_human_review and AgentRole.FACTOR_GENERATOR in self.human_review_required:
            approved_factors = self._human_review(factors, gate_name="Factor Generation")
            if not approved_factors:
                return {"status": "rejected", "reason": "No factors approved at review gate"}
        else:
            approved_factors = factors

        # Stage 3: Backtesting
        backtest_task = ResearchTask(
            task_id="backtest-001",
            role=AgentRole.BACKTESTER,
            input_data={"factors": approved_factors}
        )
        backtest_task = self.agents[AgentRole.BACKTESTER].execute(backtest_task)
        backtest_results = backtest_task.output_data["results"]

        # Human review gate
        if require_human_review and AgentRole.BACKTESTER in self.human_review_required:
            approved_results = self._human_review(
                backtest_results, gate_name="Backtest Results"
            )
            if not approved_results:
                return {"status": "rejected", "reason": "No strategies approved at backtest gate"}

        # Stage 4: Critique and reporting
        # ... (full implementation follows same pattern)
        return {
            "status": "completed",
            "factors": approved_factors,
            "backtest_results": backtest_results
        }

    def _human_review(self, data: Dict, gate_name: str) -> Optional[Dict]:
        """
        Human review gate — in production, integrate with your review workflow
        (Slack notification, email, UI, etc.). Returns approved data or None.
        """
        # Placeholder — replace with your review integration
        print(f"[Orchestrator] Awaiting human review at '{gate_name}' gate...")
        # In production: block until human approves, times out, or rejects
        return data

The key architectural insight here is not the individual agents — it is the checkpoint discipline. Without mandatory human review gates, a faulty factor or an overfitted backtest propagates silently through the pipeline and surfaces only at the worst possible moment: live deployment.


Where AI Actually Works in Quant Today

After surveying the landscape, the honest map of production readiness looks like this:

Application Production readiness Current best practice
LLM-assisted literature review High Use for synthesis and hypothesis generation; do not trust for statistical inference
LLM-assisted code generation High Use for boilerplate and debugging; always review for edge case handling
LLM-based factor mining Medium Effective as a candidate generator; requires rigorous downstream evaluation
RL strategy optimization Medium Works in simulation; generalization to live markets remains challenging
Synthetic data augmentation Medium-High Effective for stress-testing; quality depends on calibration data
Full end-to-end autonomous strategy generation Low Research stage; requires human oversight at multiple gates
AI-powered execution optimization High Statistical models for optimal execution are production-proven

The common thread: AI tools are most reliable when they augment human judgment rather than replace it. The researcher who uses an LLM to surface 200 factor candidates, then applies rigorous statistical and economic filtering, will outperform both the researcher who relies entirely on human intuition and the researcher who blindly deploys the top LLM recommendation without scrutiny.


Failure Modes and Honest Red Flags

No honest assessment of AI in quant is complete without cataloging the failure modes that have burned real capital.

Overfitting to synthetic distributions. When a generative model learns patterns that exist only in training data, the resulting synthetic scenarios give false confidence. A strategy that scores well on synthetic stress-tests but poorly on held-out historical data is a warning sign of distributional misalignment.

RL reward hacking. An RL agent optimizing a Sharpe-based reward function will discover exploits: trading patterns that look good on the simulator but are economically meaningless or illegal in live markets. Constraints on turnover, position size, and trade frequency must be hard-coded into the environment, not soft-coded as penalty terms.

LLM hallucination in factor logic. LLMs generate syntactically plausible but semantically incorrect factor constructions with surprising frequency. A factor defined as "buy when the 20-day moving average crosses above the 50-day moving average" is straightforward. A factor defined as "aggregate sentiment entropy across 13F filings weighted by institutional ownership concentration" requires careful verification of every component.

Correlation ≠ causation in discovered signals. AI-generated factors that pass statistical screens frequently reflect data mining bias rather than genuine alpha. The standard remedy — walk-forward validation, out-of-sample testing, and transaction cost sensitivity analysis — is non-negotiable.

Latency in live trading. Any LLM or RL component that adds inference latency to a real-time trading system is a risk. A factor model that takes 200 ms to score is not usable for intraday strategies, regardless of its in-sample accuracy.


The Data Infrastructure Beneath It All

None of these AI capabilities are achievable without reliable, low-latency market data as the foundation. TickDB provides the real-time depth, trades, and historical kline data that serve as both the calibration source for synthetic data generators and the validation benchmark for RL simulators. For HK equities, crypto, and forex — markets where depth data is available — the order book snapshots enable realistic market impact modeling. For US equity strategy backtesting, the 10+ years of cleaned OHLCV data covers multiple market regimes necessary for robust evaluation.

The critical distinction, as covered in the TickDB knowledge base, is that trades data is not available for US equities or A-shares, but the depth channel provides L1 depth for US equities and up to L10 for HK and crypto markets. Understanding these capability boundaries before designing your AI pipeline prevents the frustrating discovery that your intended data source does not support your intended analysis.


Closing

The quant industry is not being automated out of existence by AI. It is being reorganized around a new division of labor: machines handle hypothesis generation, pattern recognition, and optimization at a scale that was previously impossible. Humans provide the economic reasoning, the risk discipline, and the judgment calls that no training distribution can fully encode.

The funds that will lead the next decade are not the ones deploying the most LLMs. They are the ones building rigorous evaluation infrastructure — robust simulators, honest backtest frameworks, multi-agent orchestration with human oversight — that separates genuine signal from AI-generated noise.

The code in this article is production-grade in structure, but the strategy logic is illustrative. Your implementation will require careful integration with your data infrastructure, your risk management framework, and your execution systems. Treat AI as a powerful research assistant, not an infallible oracle. The markets are too adversarial for anything else.


Next Steps

If you're building AI-assisted research infrastructure, start with the factor mining pipeline — it has the highest production readiness and the clearest evaluation criteria.

If you need reliable market data for model calibration and backtesting, TickDB provides real-time depth and trades data across HK equities, crypto, and forex, plus 10+ years of historical OHLCV for US equities. Sign up at tickdb.ai to get an API key (free tier available; no credit card required).

If you're evaluating RL or synthetic data approaches for your strategy, consider the simulator-first architecture illustrated in this article. The critical discipline is separating training data from validation data — a pipeline that passes only in-sample evaluation is not a production system.

If you use AI coding assistants, search for and install the tickdb-market-data SKILL in your AI tool's marketplace to access TickDB data directly from your development environment.

This article does not constitute investment advice. Markets involve risk; past performance does not guarantee future results.