Prompt Chaining and Routing in Trading Systems

Trading systems process diverse request types - market orders, limit orders, options, compliance checks - each requiring specialized handling. Two fundamental patterns enable this: prompt chaining for sequential multi-step processing, and routing for intelligent task distribution. Mastering these patterns is essential for building robust financial workflows.

Prompt Chaining: The Assembly Line

Prompt chaining decomposes complex tasks into a sequence of simpler steps, where each step’s output becomes the next step’s input. Like an assembly line where specialized stations perform focused tasks, prompt chains break down problems that would overwhelm a single LLM call.

flowchart LR
    I[Trade Request] --> S1[Parse Order]
    S1 --> V1{Valid?}
    V1 -->|Yes| S2[Risk Check]
    V1 -->|No| E1[Error Handler]
    S2 --> V2{Approved?}
    V2 -->|Yes| S3[Execute Trade]
    V2 -->|No| E2[Rejection Handler]
    S3 --> S4[Confirm & Log]
    S4 --> O[Confirmation]

    classDef orangeClass fill:#F39C12,stroke:#333,stroke-width:2px,color:#fff

    class V1,V2 orangeClass

Why Chain Works Better Than Single Prompts

Consider processing a trading order with a single prompt:

1
2
3
4
5
6
7
8
# Single prompt approach - problematic
single_prompt = """
Parse this trade request, validate the order details, check risk limits,
determine if margin requirements are met, execute the trade if approved,
generate confirmation, and log the transaction.

Request: {trade_request}
"""

Problems with this approach:

  • Cognitive overload: Too many tasks reduce accuracy on each
  • Debugging difficulty: Can’t tell where failures occur
  • No intermediate validation: Errors cascade undetected
  • No recovery points: Must restart entire process on failure

With chaining:

1
2
3
4
5
# Chained approach - each step focused
parse_prompt = "Extract order type, symbol, quantity, and price from: {request}"
validate_prompt = "Validate this order against market rules: {parsed_order}"
risk_prompt = "Check position limits and margin for: {validated_order}"
execute_prompt = "Generate execution instruction for: {approved_order}"

Each step can be validated, retried, or handled separately.

The Error Propagation Problem

The critical challenge with chaining: errors compound. A small mistake in step 1 becomes a disaster by step 5.

flowchart LR
    S1[Step 1
Small Error] --> S2[Step 2
Amplified] S2 --> S3[Step 3
Worse] S3 --> S4[Step 4
Disaster] classDef orangeClass fill:#F39C12,stroke:#333,stroke-width:2px,color:#fff classDef pinkClass fill:#E74C3C,stroke:#333,stroke-width:2px,color:#fff class S1 orangeClass class S4 pinkClass

Example: A parsing error extracts quantity as 10,000 instead of 1,000. Without validation:

  • Risk check approves (incorrect quantity within limits)
  • Execution proceeds with 10x intended size
  • Customer loses significant money

Solution: Validation gates between every step.

Validation Strategies

Four primary approaches to validating intermediate outputs:

1. Programmatic Checks

Direct code verification of specific conditions:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pydantic import BaseModel, Field, validator

class TradeOrder(BaseModel):
symbol: str = Field(min_length=1, max_length=10)
quantity: int = Field(gt=0, le=1000000)
order_type: Literal["market", "limit", "stop"]
price: Optional[float] = Field(gt=0)

@validator('price')
def limit_orders_need_price(cls, v, values):
if values.get('order_type') == 'limit' and v is None:
raise ValueError('Limit orders require a price')
return v

def validate_parsed_order(output: str) -> TradeOrder:
"""Programmatic validation of parsed output"""
try:
data = json.loads(output)
return TradeOrder(**data)
except (json.JSONDecodeError, ValidationError) as e:
raise ValidationError(f"Invalid order format: {e}")

Best for: Format validation, range checks, required fields

2. LLM-Based Validation

Use another LLM call to assess quality:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
validation_prompt = """
Review this risk assessment for a trade order.

Assessment: {risk_assessment}
Order details: {order}

Check for:
1. All risk factors considered (market risk, credit risk, liquidity risk)
2. Calculations appear reasonable
3. Recommendation aligns with risk level
4. No obvious contradictions

Rate quality 1-10 and explain any issues found.
"""

Best for: Subjective quality, completeness, logical consistency

3. Rule-Based Validation

Explicit rules for expected patterns:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class TradingRules:
MAX_ORDER_VALUE = 1_000_000
ALLOWED_EXCHANGES = ["NYSE", "NASDAQ", "CBOE"]
TRADING_HOURS = (9, 30, 16, 0) # 9:30 AM - 4:00 PM

@classmethod
def validate(cls, order: TradeOrder) -> list[str]:
violations = []

# Value limit
order_value = order.quantity * (order.price or 0)
if order_value > cls.MAX_ORDER_VALUE:
violations.append(f"Order value ${order_value:,.0f} exceeds limit")

# Exchange check
if order.exchange not in cls.ALLOWED_EXCHANGES:
violations.append(f"Exchange {order.exchange} not allowed")

# Trading hours
now = datetime.now()
if not cls._is_trading_hours(now):
violations.append("Outside trading hours")

return violations

Best for: Business rules, compliance requirements, operational constraints

4. Confidence Scoring

Use LLM-provided confidence metrics:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
parse_prompt = """
Parse this trade request and provide confidence score.

Request: {request}

Response format:
{
"parsed": {...},
"confidence": 0.0-1.0,
"uncertain_fields": ["field names where confidence is low"]
}
"""

def validate_with_confidence(response: dict, threshold: float = 0.8):
if response["confidence"] < threshold:
uncertain = response.get("uncertain_fields", [])
raise LowConfidenceError(
f"Confidence {response['confidence']:.2f} below threshold. "
f"Uncertain fields: {uncertain}"
)
return response["parsed"]

Best for: Ambiguous inputs, gauging uncertainty, deciding when to escalate

Handling Validation Failures

When validation fails, several recovery strategies are available:

Simple Retry

For transient issues (LLM hiccups, timeouts):

1
2
3
4
5
from tenacity import retry, stop_after_attempt, wait_exponential

@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1))
def call_step_with_retry(prompt: str, input_data: dict):
return llm.complete(prompt.format(**input_data))

Re-prompt with Feedback

When validation provides specific failure reasons:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def step_with_correction(prompt: str, input_data: dict, max_attempts: int = 3):
for attempt in range(max_attempts):
output = llm.complete(prompt.format(**input_data))

try:
validated = validate(output)
return validated
except ValidationError as e:
if attempt == max_attempts - 1:
raise

# Add error feedback to next attempt
input_data["previous_attempt"] = output
input_data["error_feedback"] = str(e)
prompt = prompt + """

Your previous response had issues:
Previous attempt: {previous_attempt}
Error: {error_feedback}

Please correct and try again.
"""

Fallback Mechanisms

When retries fail:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def step_with_fallback(input_data: dict):
try:
return primary_agent.run(input_data)
except Exception:
try:
# Try simpler backup approach
return backup_agent.run(input_data)
except Exception:
# Return safe default or escalate
return {
"status": "MANUAL_REVIEW_REQUIRED",
"reason": "Automated processing failed",
"original_input": input_data
}

Self-Critique and Refinement

LLM reviews its own output:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
critique_prompt = """
Review this trade analysis for quality:
{analysis}

Criteria:
- All required fields present
- Numbers are reasonable for the market
- Conclusions follow from data

Identify any issues.
"""

refine_prompt = """
Original analysis: {analysis}
Critique: {critique}

Produce an improved version addressing the critique.
"""

def self_refining_step(input_data: dict):
analysis = analyst_agent.run(input_data)
critique = critic_agent.run(analysis)

if "no issues" in critique.lower():
return analysis

refined = refiner_agent.run(analysis=analysis, critique=critique)
return refined

The Routing Pattern

While chaining handles sequential processing, routing handles task distribution - directing different inputs to appropriate specialized handlers.

flowchart TD
    I[Incoming Request] --> C[Classifier]
    C --> R{Router}
    R -->|Equity| EQ[Equity Handler]
    R -->|Options| OPT[Options Handler]
    R -->|Forex| FX[Forex Handler]
    R -->|Crypto| CR[Crypto Handler]
    EQ --> O[Output]
    OPT --> O
    FX --> O
    CR --> O

    classDef blueClass fill:#4A90E2,stroke:#333,stroke-width:2px,color:#fff
    classDef orangeClass fill:#F39C12,stroke:#333,stroke-width:2px,color:#fff

    class C blueClass
    class R orangeClass

Two Stages: Classification + Dispatch

Stage 1: Classification

Determine the input’s nature, category, or intent:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
classification_prompt = """
Classify this trading request into one category:

Request: {request}

Categories:
- EQUITY_ORDER: Stock buy/sell orders
- OPTIONS_ORDER: Options contracts
- FOREX_ORDER: Currency exchange
- CRYPTO_ORDER: Cryptocurrency trades
- ACCOUNT_QUERY: Balance, position inquiries
- COMPLIANCE_CHECK: Regulatory verification

Respond with category name only.
"""

Stage 2: Task Dispatch

Route to appropriate handler based on classification:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class TradingRouter:
def __init__(self):
self.handlers = {
"EQUITY_ORDER": EquityOrderHandler(),
"OPTIONS_ORDER": OptionsOrderHandler(),
"FOREX_ORDER": ForexOrderHandler(),
"CRYPTO_ORDER": CryptoOrderHandler(),
"ACCOUNT_QUERY": AccountQueryHandler(),
"COMPLIANCE_CHECK": ComplianceHandler(),
}
self.classifier = ClassifierAgent()

def route(self, request: str) -> dict:
# Classification
category = self.classifier.classify(request)

# Dispatch
handler = self.handlers.get(category)
if handler is None:
return self.unknown_handler(request, category)

return handler.process(request)

Classification Methods

Rule-Based Classification

Fast and predictable for clear patterns:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def rule_based_classify(request: str) -> str:
request_lower = request.lower()

# Keyword matching
if any(word in request_lower for word in ["option", "call", "put", "strike"]):
return "OPTIONS_ORDER"
if any(word in request_lower for word in ["btc", "eth", "crypto", "bitcoin"]):
return "CRYPTO_ORDER"
if any(word in request_lower for word in ["eur", "usd", "forex", "fx"]):
return "FOREX_ORDER"
if any(word in request_lower for word in ["buy", "sell", "stock", "shares"]):
return "EQUITY_ORDER"
if any(word in request_lower for word in ["balance", "position", "holdings"]):
return "ACCOUNT_QUERY"

return "UNKNOWN"

LLM-Based Classification

Better for nuanced or ambiguous requests:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
llm_classification_prompt = """
Analyze this trading request and classify it.

Request: "{request}"

Consider:
- What action is being requested?
- What asset type is involved?
- Is this a trade or an inquiry?

Return JSON:
{
"category": "CATEGORY_NAME",
"confidence": 0.0-1.0,
"reasoning": "brief explanation"
}
"""

Hybrid Classification

Best of both worlds:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def hybrid_classify(request: str) -> str:
# Try rule-based first (fast, cheap)
category = rule_based_classify(request)

if category != "UNKNOWN":
return category

# Fall back to LLM for unclear cases
result = llm_classify(request)

if result["confidence"] < 0.7:
return "MANUAL_REVIEW"

return result["category"]

Applied Example: Trade Order Pipeline

Let’s build a complete pipeline combining chaining and routing:

flowchart TD
    REQ[Trade Request] --> PARSE[1. Parse Request]
    PARSE --> CLASSIFY[2. Classify Order Type]
    CLASSIFY --> ROUTE{Route by Type}

    ROUTE -->|Equity| EQ_CHAIN[Equity Chain]
    ROUTE -->|Options| OPT_CHAIN[Options Chain]
    ROUTE -->|Complex| HUMAN[Human Review]

    subgraph EQ_CHAIN[Equity Processing Chain]
        E1[Validate Order] --> E2[Check Risk]
        E2 --> E3[Execute]
        E3 --> E4[Confirm]
    end

    subgraph OPT_CHAIN[Options Processing Chain]
        O1[Validate Greeks] --> O2[Check Margin]
        O2 --> O3[Execute]
        O3 --> O4[Confirm]
    end

    EQ_CHAIN --> OUT[Output]
    OPT_CHAIN --> OUT

    classDef orangeClass fill:#F39C12,stroke:#333,stroke-width:2px,color:#fff

    class ROUTE orangeClass

Implementation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class TradePipeline:
def __init__(self):
self.parser = OrderParser()
self.classifier = OrderClassifier()
self.equity_chain = EquityOrderChain()
self.options_chain = OptionsOrderChain()

def process(self, request: str) -> dict:
# Step 1: Parse with validation
parsed = self.parser.parse(request)
if not self.parser.validate(parsed):
return {"status": "PARSE_ERROR", "details": parsed.errors}

# Step 2: Classify
order_type = self.classifier.classify(parsed)

# Step 3: Route to appropriate chain
if order_type == "EQUITY":
return self.equity_chain.process(parsed)
elif order_type == "OPTIONS":
return self.options_chain.process(parsed)
else:
return {"status": "MANUAL_REVIEW", "order": parsed}


class EquityOrderChain:
def process(self, order: dict) -> dict:
# Chain step 1: Validate
validated = self.validate_step(order)
if not validated["is_valid"]:
return {"status": "VALIDATION_FAILED", **validated}

# Chain step 2: Risk check
risk_result = self.risk_step(validated)
if risk_result["risk_level"] == "HIGH":
return {"status": "RISK_REJECTED", **risk_result}

# Chain step 3: Execute
execution = self.execute_step(risk_result)
if execution["status"] != "FILLED":
return {"status": "EXECUTION_FAILED", **execution}

# Chain step 4: Confirm
confirmation = self.confirm_step(execution)

return {
"status": "SUCCESS",
"confirmation": confirmation,
"audit_trail": {
"validation": validated,
"risk": risk_result,
"execution": execution
}
}

Context Management in Chains

As chains grow, context management becomes critical. LLMs have finite context windows, and overloading them degrades performance.

Selective Context Passing

Only pass what the next step needs:

1
2
3
4
5
6
7
8
9
10
11
12
def build_context_for_step(step_name: str, full_context: dict) -> dict:
"""Extract only relevant context for each step"""

context_requirements = {
"validate": ["parsed_order", "market_rules"],
"risk_check": ["validated_order", "position_limits", "account_balance"],
"execute": ["approved_order", "exchange_connection"],
"confirm": ["execution_result", "customer_email"]
}

required = context_requirements.get(step_name, [])
return {k: full_context[k] for k in required if k in full_context}

Contextual Reiteration

Restate critical information in each prompt to prevent “forgetting”:

1
2
3
4
5
6
7
8
9
10
11
12
13
execution_prompt = """
Execute this trade order.

CRITICAL CONTEXT (do not modify):
- Account: {account_id}
- Risk approval: {risk_approval_id}
- Maximum slippage: {max_slippage}%

Order to execute:
{approved_order}

Generate execution instruction.
"""

Context Summarization

For long chains, summarize earlier steps:

1
2
3
4
5
6
7
8
def summarize_previous_steps(chain_history: list) -> str:
summary_prompt = """
Summarize these processing steps in 2-3 sentences,
preserving key decisions and values:

{steps}
"""
return llm.complete(summary_prompt.format(steps=chain_history))

Takeaways

  1. Prompt chaining breaks complex tasks into focused steps, enabling validation and error recovery at each stage

  2. Validation gates between steps prevent error propagation - use programmatic checks, LLM validation, rules, or confidence scoring as appropriate

  3. Recovery strategies include retry, re-prompt with feedback, fallback mechanisms, and self-critique refinement

  4. Routing combines classification and dispatch to direct diverse inputs to specialized handlers

  5. Hybrid classification uses fast rule-based methods first, falling back to LLM for ambiguous cases

  6. Context management requires selective passing, critical reiteration, and summarization to work within LLM constraints


This is the fifth post in my Applied Agentic AI for Finance series. Next: Parallel Processing and Quality Control in Finance where we’ll explore concurrent execution and evaluator-optimizer patterns.

Modeling Agentic Workflows for Finance

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×