Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions VWAPBB_README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# VWAPBB Strategy – XAUUSD

A full-stack trading strategy application implementing the VWAP + Bollinger Bands (VWAPBB) strategy for XAUUSD on the 5-minute timeframe.

## Strategy Overview

### Indicators
- **Bollinger Bands (BB)** – 20-period, 2 standard deviations on the 5-minute chart
- **EMA 200** – Calculated on the 15-minute timeframe for trend filtering
- **VWAP** – Volume Weighted Average Price, resetting daily

### Trend Filter
The EMA 200 on the 15-minute timeframe determines the overall market trend:
- **Bullish**: Price above EMA 200 → only BUY signals
- **Bearish**: Price below EMA 200 → only SELL signals

### Buy Conditions (Bullish Trend)

**Scenario 1**: Price above EMA 200 AND above VWAP
- Price touches/approaches VWAP or Lower Bollinger Band
- Bullish engulfing pattern appears → Enter at candle close

**Scenario 2**: Price above EMA 200 but below VWAP
- Price touches/approaches Lower Bollinger Band
- Bullish engulfing pattern appears → Enter at candle close

### Sell Conditions (Bearish Trend)
- Price below EMA 200
- Price touches/approaches VWAP or Upper Bollinger Band
- Bearish engulfing pattern appears → Enter at candle close

## Tech Stack

### Backend
- **Python 3.12** + **FastAPI**
- **pandas** / **numpy** for indicator calculations
- Simulated XAUUSD data generation

### Frontend
- **React 18** + **TypeScript** + **Vite**
- **Lightweight Charts** (TradingView) for interactive charting
- Dark theme with responsive design

## Getting Started

### Prerequisites
- Python 3.10+
- Node.js 18+

### Backend Setup
```bash
cd backend
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
python main.py
```
The API will be available at `http://localhost:8000`.

### Frontend Setup
```bash
cd frontend
npm install
npm run dev
```
The dashboard will be available at `http://localhost:5173`.

## API Endpoints

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/strategy?num_candles=500` | Get candles, indicators, and signals |
| GET | `/api/health` | Health check |
| GET | `/docs` | Interactive API documentation (Swagger) |
73 changes: 73 additions & 0 deletions backend/data_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import time
import numpy as np
import pandas as pd


def generate_simulated_xauusd(
num_candles: int = 500,
timeframe_seconds: int = 300,
base_price: float = 2650.0,
) -> pd.DataFrame:
"""
Generate simulated XAUUSD OHLCV data for demonstration.
Produces realistic gold price movements with trends and volatility.
"""
np.random.seed(42)

end_time = int(time.time())
end_time = end_time - (end_time % timeframe_seconds)
timestamps = [end_time - (num_candles - 1 - i) * timeframe_seconds for i in range(num_candles)]

prices = [base_price]
volatility = 0.0008 # ~0.08% per 5-min candle for gold

# Add a slow trend component
trend = np.sin(np.linspace(0, 4 * np.pi, num_candles)) * 15

for i in range(1, num_candles):
change = np.random.normal(0, volatility) * prices[-1]
trend_component = (trend[i] - trend[i - 1])
new_price = prices[-1] + change + trend_component
prices.append(max(new_price, base_price * 0.95))

candles = []
for i in range(num_candles):
p = prices[i]
spread = p * np.random.uniform(0.0002, 0.001)

o = p + np.random.normal(0, spread * 0.3)
c = p + np.random.normal(0, spread * 0.3)

h = max(o, c) + abs(np.random.normal(0, spread * 0.5))
l = min(o, c) - abs(np.random.normal(0, spread * 0.5))

vol = np.random.uniform(100, 5000)

candles.append({
"timestamp": timestamps[i],
"open": round(o, 2),
"high": round(h, 2),
"low": round(l, 2),
"close": round(c, 2),
"volume": round(vol, 2),
})

return pd.DataFrame(candles)


def resample_to_15m(df_5m: pd.DataFrame) -> pd.DataFrame:
"""Resample 5-minute candles to 15-minute candles."""
df = df_5m.copy()
df["datetime"] = pd.to_datetime(df["timestamp"], unit="s")
df = df.set_index("datetime")

resampled = df.resample("15min").agg({
"timestamp": "first",
"open": "first",
"high": "max",
"low": "min",
"close": "last",
"volume": "sum",
}).dropna()

return resampled.reset_index(drop=True)
74 changes: 74 additions & 0 deletions backend/indicators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import numpy as np
import pandas as pd


def calculate_ema(series: pd.Series, period: int) -> pd.Series:
"""Calculate Exponential Moving Average."""
return series.ewm(span=period, adjust=False).mean()


def calculate_bollinger_bands(
series: pd.Series, period: int = 20, std_dev: float = 2.0
) -> tuple[pd.Series, pd.Series, pd.Series]:
"""Calculate Bollinger Bands (upper, middle, lower)."""
middle = series.rolling(window=period).mean()
rolling_std = series.rolling(window=period).std()
upper = middle + (rolling_std * std_dev)
lower = middle - (rolling_std * std_dev)
return upper, middle, lower


def calculate_vwap(df: pd.DataFrame) -> pd.Series:
"""
Calculate Volume Weighted Average Price.
Resets at the start of each trading day.
"""
df = df.copy()
df["typical_price"] = (df["high"] + df["low"] + df["close"]) / 3.0
df["tp_volume"] = df["typical_price"] * df["volume"]

df["date"] = pd.to_datetime(df["timestamp"], unit="s").dt.date

vwap_values = []
for _, group in df.groupby("date"):
cum_tp_vol = group["tp_volume"].cumsum()
cum_vol = group["volume"].cumsum()
vwap = cum_tp_vol / cum_vol.replace(0, np.nan)
vwap_values.append(vwap)

if vwap_values:
return pd.concat(vwap_values).sort_index()
return pd.Series(dtype=float)


def is_bullish_engulfing(
prev_open: float,
prev_close: float,
curr_open: float,
curr_close: float,
) -> bool:
"""Detect bullish engulfing candlestick pattern."""
prev_bearish = prev_close < prev_open
curr_bullish = curr_close > curr_open
engulfs = curr_open <= prev_close and curr_close >= prev_open
return prev_bearish and curr_bullish and engulfs


def is_bearish_engulfing(
prev_open: float,
prev_close: float,
curr_open: float,
curr_close: float,
) -> bool:
"""Detect bearish engulfing candlestick pattern."""
prev_bullish = prev_close > prev_open
curr_bearish = curr_close < curr_open
engulfs = curr_open >= prev_close and curr_close <= prev_open
return prev_bullish and curr_bearish and engulfs


def is_near(price: float, level: float, threshold_pct: float = 0.001) -> bool:
"""Check if price is near a given level within a percentage threshold."""
if level == 0:
return False
return abs(price - level) / level <= threshold_pct
92 changes: 92 additions & 0 deletions backend/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

from models import Candle, IndicatorValues, StrategyResponse, Trend
from indicators import calculate_ema, calculate_bollinger_bands, calculate_vwap
from strategy import determine_trend, generate_signals
from data_provider import generate_simulated_xauusd, resample_to_15m

app = FastAPI(
title="VWAPBB Strategy – XAUUSD",
description="Full-stack VWAP + Bollinger Bands trading strategy for XAUUSD",
version="1.0.0",
)

app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)


@app.get("/api/strategy", response_model=StrategyResponse)
def get_strategy_data(num_candles: int = 500):
"""
Generate XAUUSD data with all indicators and trading signals.
Returns candles, indicator overlays, and buy/sell signals.
"""
# Generate 5-minute candle data
df_5m = generate_simulated_xauusd(num_candles=num_candles)

# Resample to 15-minute for EMA 200 trend filter
df_15m = resample_to_15m(df_5m)
ema_200_15m_series = calculate_ema(df_15m["close"], 200)
ema_200_15m_value = float(ema_200_15m_series.iloc[-1]) if len(ema_200_15m_series) > 0 else None

# Calculate indicators on 5-minute data
bb_upper, bb_middle, bb_lower = calculate_bollinger_bands(df_5m["close"])
vwap = calculate_vwap(df_5m)

# Determine current trend
current_price = float(df_5m["close"].iloc[-1])
trend = determine_trend(ema_200_15m_value or 0, current_price)

# Generate signals
signals = generate_signals(df_5m, ema_200_15m_value or 0)

# Build response
candles = [
Candle(
timestamp=int(row["timestamp"]),
open=row["open"],
high=row["high"],
low=row["low"],
close=row["close"],
volume=row["volume"],
)
for _, row in df_5m.iterrows()
]

indicators = []
for i, row in df_5m.iterrows():
idx = int(i) if not isinstance(i, int) else i
indicators.append(
IndicatorValues(
timestamp=int(row["timestamp"]),
ema_200=ema_200_15m_value,
vwap=float(vwap.iloc[idx]) if idx < len(vwap) and not vwap.isna().iloc[idx] else None,
bb_upper=float(bb_upper.iloc[idx]) if idx < len(bb_upper) and not bb_upper.isna().iloc[idx] else None,
bb_middle=float(bb_middle.iloc[idx]) if idx < len(bb_middle) and not bb_middle.isna().iloc[idx] else None,
bb_lower=float(bb_lower.iloc[idx]) if idx < len(bb_lower) and not bb_lower.isna().iloc[idx] else None,
)
)

return StrategyResponse(
candles=candles,
indicators=indicators,
signals=signals,
trend=trend,
ema_200_15m=ema_200_15m_value,
)


@app.get("/api/health")
def health_check():
return {"status": "ok", "strategy": "VWAPBB", "pair": "XAUUSD"}


if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
48 changes: 48 additions & 0 deletions backend/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from pydantic import BaseModel
from typing import Optional
from enum import Enum


class Trend(str, Enum):
BULLISH = "bullish"
BEARISH = "bearish"
NEUTRAL = "neutral"


class SignalType(str, Enum):
BUY = "buy"
SELL = "sell"


class Candle(BaseModel):
timestamp: int
open: float
high: float
low: float
close: float
volume: float


class IndicatorValues(BaseModel):
timestamp: int
ema_200: Optional[float] = None
vwap: Optional[float] = None
bb_upper: Optional[float] = None
bb_middle: Optional[float] = None
bb_lower: Optional[float] = None


class Signal(BaseModel):
timestamp: int
signal_type: SignalType
price: float
scenario: str
reason: str


class StrategyResponse(BaseModel):
candles: list[Candle]
indicators: list[IndicatorValues]
signals: list[Signal]
trend: Trend
ema_200_15m: Optional[float] = None
8 changes: 8 additions & 0 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
fastapi==0.115.6
uvicorn==0.34.0
pandas==2.2.3
numpy==2.2.1
pydantic==2.10.4
httpx==0.28.1
python-dotenv==1.0.1
websockets==14.1
Loading