AI-Only VWAP¶
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 )