summaryrefslogtreecommitdiff
path: root/services/strategy-engine/tests/test_volume_profile_strategy.py
blob: be123b05a939f78d34ee80a0b238d2a25b3a1c83 (plain)
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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
"""Tests for the Volume Profile strategy."""
from datetime import datetime, timezone
from decimal import Decimal

import pytest

from shared.models import Candle, OrderSide
from strategies.volume_profile_strategy import VolumeProfileStrategy


def make_candle(close: float, volume: float = 1.0) -> Candle:
    return Candle(
        symbol="BTC/USDT",
        timeframe="1m",
        open_time=datetime(2024, 1, 1, tzinfo=timezone.utc),
        open=Decimal(str(close)),
        high=Decimal(str(close)),
        low=Decimal(str(close)),
        close=Decimal(str(close)),
        volume=Decimal(str(volume)),
    )


def test_volume_profile_warmup_period():
    strategy = VolumeProfileStrategy()
    strategy.configure({"lookback_period": 10, "num_bins": 5})
    assert strategy.warmup_period == 10


def test_volume_profile_no_signal_insufficient_data():
    strategy = VolumeProfileStrategy()
    strategy.configure({"lookback_period": 10, "num_bins": 5})
    # Feed fewer candles than lookback_period
    for _ in range(5):
        result = strategy.on_candle(make_candle(100.0, 10.0))
    assert result is None


def test_volume_profile_buy_at_value_area_low():
    """Concentrate volume around 95-105, price drops to 88, bounces back to 99."""
    strategy = VolumeProfileStrategy()
    strategy.configure({
        "lookback_period": 10,
        "num_bins": 5,
        "value_area_pct": 0.7,
        "quantity": "0.01",
    })

    # Build profile: 10 candles with volume concentrated around 95-105
    profile_data = [
        (95, 50), (97, 50), (99, 100), (100, 100), (101, 100),
        (103, 50), (105, 50), (100, 100), (99, 100), (101, 50),
    ]
    for price, vol in profile_data:
        strategy.on_candle(make_candle(price, vol))

    # Price drops below value area low
    strategy.on_candle(make_candle(88.0, 1.0))

    # Price bounces back into value area (between va_low and poc)
    signal = strategy.on_candle(make_candle(99.0, 1.0))

    assert signal is not None
    assert signal.side == OrderSide.BUY


def test_volume_profile_sell_at_value_area_high():
    """Concentrate volume around 95-105, price rises to 112, pulls back to 101."""
    strategy = VolumeProfileStrategy()
    strategy.configure({
        "lookback_period": 10,
        "num_bins": 5,
        "value_area_pct": 0.7,
        "quantity": "0.01",
    })

    # Build profile: 10 candles with volume concentrated around 95-105
    profile_data = [
        (95, 50), (97, 50), (99, 100), (100, 100), (101, 100),
        (103, 50), (105, 50), (100, 100), (99, 100), (101, 50),
    ]
    for price, vol in profile_data:
        strategy.on_candle(make_candle(price, vol))

    # Price rises above value area high
    strategy.on_candle(make_candle(112.0, 1.0))

    # Price pulls back into value area (between poc and va_high)
    signal = strategy.on_candle(make_candle(101.0, 1.0))

    assert signal is not None
    assert signal.side == OrderSide.SELL


def test_volume_profile_reset_clears_state():
    strategy = VolumeProfileStrategy()
    strategy.configure({"lookback_period": 10, "num_bins": 5})

    # Feed enough candles to establish profile
    for _ in range(10):
        strategy.on_candle(make_candle(100.0, 10.0))

    strategy.reset()

    # After reset, should not have enough data
    result = strategy.on_candle(make_candle(100.0, 10.0))
    assert result is None