AI-Only VWAP

AI VWAP workflow using LumiBot runtime skills, rules, market evidence, and execution

ai_vwap.py is a minimal AI-only equity strategy. Python creates one trading agent and runs it each iteration. The prompt owns the VWAP policy while the built-in stock-trading skill supplies reusable research, sizing, order, and verification mechanics. Active rules limit it to one position and one entry per day.

How it works

  • The agent computes VWAP only from bars visible at the simulated time.

  • It evaluates the configured dip or reclaim threshold and sizes from current risk.

  • It manages an existing position before considering another entry.

  • It reconciles the exact submitted order, open orders, and fresh positions. In backtests, a short bounded terminal wait lets the simulator process the agent’s own market order without creating an open-ended polling loop.

Verified backtest evidence

The final refactored strategy completed a bounded local backtest from 2026-08-04 through 2026-08-07 with hourly decisions over minute evidence. It bought 12 SPY shares at $771.23 and sold those same 12 shares at $769.37. There were no duplicate exit submissions and no residual position. The portfolio ended near $99,978 from a $100,000 start. The tear sheet rounded total return to -0.00%, annualized return to -2.02%, and maximum drawdown to -0.02%. This short result is mechanical evidence, not a performance claim.

export GEMINI_API_KEY="your-key"
export DATADOWNLOADER_BASE_URL="https://data.example.test"
export DATADOWNLOADER_API_KEY="your-data-key"
export BACKTESTING_DATA_SOURCE="ThetaData"
python -m lumibot.example_strategies.ai_vwap

Set BACKTESTING_START, BACKTESTING_END, and optional AI_VWAP_* variables to reproduce a specific policy and window.

  1"""AI-only VWAP mean-reversion / reclaim strategy.
  2
  3Python only creates and runs a LumiBot agent. All trading policy lives in the
  4system prompt. Prefer minute bars and the get_indicator('vwap') tool when available.
  5
  6Local backtest:
  7    GEMINI_API_KEY=... BACKTESTING_DATA_SOURCE=ThetaData \
  8        python -m lumibot.example_strategies.ai_vwap
  9
 10Optional env overrides (AI_VWAP_*):
 11    AI_VWAP_UNDERLYING=SPY
 12    AI_VWAP_DEVIATION_PCT=0.0015
 13    AI_VWAP_RISK_FRACTION=0.01
 14    AI_VWAP_MAX_SHARES=200
 15    AI_VWAP_HOLD_BARS=30
 16    AI_VWAP_SLEEPTIME=1H
 17"""
 18
 19import os
 20from datetime import datetime, timedelta
 21from pathlib import Path
 22
 23from lumibot.strategies.strategy import Strategy
 24
 25
 26def build_vwap_system_prompt(params: dict) -> str:
 27    underlying = str(params.get("underlying", "SPY")).upper()
 28    deviation_pct = float(params.get("deviation_pct", 0.0015))
 29    risk_fraction = float(params.get("risk_fraction", 0.01))
 30    max_shares = int(params.get("max_shares", 200))
 31    hold_bars = int(params.get("hold_bars", 30))
 32    return f"""
 33You are the complete decision-maker for an AI-only {underlying} VWAP strategy
 34inside LumiBot. There is no Python trading logic outside you.
 35
 36STRATEGY PARAMETERS:
 37- underlying: {underlying}
 38- deviation_pct: {deviation_pct}
 39- risk_fraction: {risk_fraction}
 40- max_shares: {max_shares}
 41- hold_bars: {hold_bars}
 42
 43Rules:
 441. Compute VWAP from completed minute bars and current tool evidence. Never invent it.
 452. Long entry (mean-reversion toward VWAP). Compute
 46   pct_below = (VWAP - last_price) / VWAP using the latest tool prices.
 47   When flat and pct_below >= {deviation_pct:.4f}, require reclaim evidence
 48   (last_price crossing back toward/above VWAP) before buying. A dip below the
 49   threshold without reclaim confirmation is a no-trade condition.
 503. Prefer market entries and exits. Size so
 51   approximate risk is at most {risk_fraction:.2%} of portfolio value, capped at
 52   {max_shares} shares. One position at a time.
 534. Exit when price returns to VWAP, reaches a modest extension above VWAP, or about
 54   {hold_bars} bars have passed since entry. Manage an open position before opening
 55   another.
 565. Open at most one new position per trading day and do not re-enter on the same
 57   day after an exit.
 58
 59Use only evidence available at the current runtime datetime. A no-trade decision
 60is valid only when VWAP cannot be computed or the reclaim rule is not met.
 61""".strip()
 62
 63
 64class AIVWAPStrategy(Strategy):
 65    parameters = {
 66        "underlying": "SPY",
 67        "deviation_pct": 0.0015,
 68        "risk_fraction": 0.01,
 69        "max_shares": 200,
 70        "hold_bars": 30,
 71        # The agent still analyzes minute bars, but hourly decisions avoid needless calls.
 72        "sleeptime": "1H",
 73    }
 74
 75    def initialize(self):
 76        self.sleeptime = str(self.parameters.get("sleeptime", "1H"))
 77        self.agents.create(
 78            name="vwap",
 79            model="gemini-3.5-flash-lite",
 80            allow_trading=True,
 81            system_prompt=build_vwap_system_prompt(self.parameters),
 82            rules_path=Path(__file__).with_name("agent_rules") / "ai_vwap.rules.json",
 83        )
 84
 85    def on_trading_iteration(self):
 86        params = dict(self.parameters)
 87        underlying = str(params.get("underlying", "SPY")).upper()
 88        self.agents["vwap"].run(
 89            task_prompt=f"Run the {underlying} VWAP workflow for this completed bar.",
 90            context={
 91                "current_datetime": self.get_datetime().isoformat(),
 92                "strategy_parameters": params,
 93            },
 94        )
 95
 96
 97def _parameters_from_env(defaults: dict) -> dict:
 98    """Override strategy parameters from AI_VWAP_* environment variables when set."""
 99    params = dict(defaults)
100    if os.environ.get("AI_VWAP_UNDERLYING"):
101        params["underlying"] = os.environ["AI_VWAP_UNDERLYING"].strip().upper()
102    if os.environ.get("AI_VWAP_DEVIATION_PCT"):
103        params["deviation_pct"] = float(os.environ["AI_VWAP_DEVIATION_PCT"])
104    if os.environ.get("AI_VWAP_RISK_FRACTION"):
105        params["risk_fraction"] = float(os.environ["AI_VWAP_RISK_FRACTION"])
106    if os.environ.get("AI_VWAP_MAX_SHARES"):
107        params["max_shares"] = int(os.environ["AI_VWAP_MAX_SHARES"])
108    if os.environ.get("AI_VWAP_HOLD_BARS"):
109        params["hold_bars"] = int(os.environ["AI_VWAP_HOLD_BARS"])
110    if os.environ.get("AI_VWAP_SLEEPTIME"):
111        params["sleeptime"] = os.environ["AI_VWAP_SLEEPTIME"].strip()
112    return params
113
114
115if __name__ == "__main__":
116    backtesting_end = datetime.fromisoformat(os.environ.get("BACKTESTING_END", datetime.now().date().isoformat()))
117    backtesting_start = datetime.fromisoformat(
118        os.environ.get("BACKTESTING_START", (backtesting_end - timedelta(days=5)).date().isoformat())
119    )
120    AIVWAPStrategy.backtest(
121        None,
122        backtesting_start=backtesting_start,
123        backtesting_end=backtesting_end,
124        benchmark_asset="SPY",
125        budget=100_000,
126        parameters=_parameters_from_env(AIVWAPStrategy.parameters),
127    )