- Add an SVG Bitcoin favicon and link it in index.html. - New indicators: macd() and roc(). - Five new strategies: macd_trend, bollinger_reversion, donchian_breakout, ema_cross, momentum (plus the existing sma_cross, rsi_reversion). - Default TIMEFRAME is now 5m (config, .env.example, compose, workflow, UI).
100 lines
3.3 KiB
Python
100 lines
3.3 KiB
Python
from __future__ import annotations
|
|
|
|
from collections.abc import Sequence
|
|
|
|
from app.engine.strategies.base import StrategyContext
|
|
from app.schemas import Candle
|
|
|
|
|
|
def make_candles(closes: Sequence[float]) -> list[Candle]:
|
|
return [Candle(ts=i, open=c, high=c, low=c, close=c, volume=1.0) for i, c in enumerate(closes)]
|
|
|
|
|
|
def context(closes: Sequence[float], position=None) -> StrategyContext:
|
|
candles = make_candles(closes)
|
|
return StrategyContext(candles=candles, position=position, equity=1000.0, cash=1000.0)
|
|
|
|
|
|
def test_all_strategies_discovered() -> None:
|
|
from app.engine.strategies import list_strategies
|
|
|
|
keys = {cls.key for cls in list_strategies()}
|
|
assert {
|
|
"sma_cross",
|
|
"rsi_reversion",
|
|
"macd_trend",
|
|
"bollinger_reversion",
|
|
"donchian_breakout",
|
|
"ema_cross",
|
|
"momentum",
|
|
} <= keys
|
|
|
|
|
|
def test_every_strategy_has_spec_and_params() -> None:
|
|
from app.engine.strategies import list_strategies, strategy_spec
|
|
|
|
for cls in list_strategies():
|
|
spec = strategy_spec(cls)
|
|
assert spec.key and spec.name and spec.description
|
|
# Defaults must round-trip through the constructor and be constructible.
|
|
instance = cls({})
|
|
assert instance.key == cls.key
|
|
# A short synthetic series must never raise for a registered strategy.
|
|
ctx = context([100, 101, 100, 102, 99, 103, 98, 104, 97, 105])
|
|
instance.on_candle(ctx)
|
|
|
|
|
|
def test_ema_cross_enters_on_cross_up() -> None:
|
|
from app.engine.strategies import get_strategy
|
|
|
|
strategy = get_strategy("ema_cross")({"fast": 2, "slow": 4, "trend_filter": 0, "size": 0.5})
|
|
# Fast EMA dips below the slow EMA, then crosses back above.
|
|
ctx = context([10, 10, 10, 10, 10, 10, 10, 10, 7, 9, 12])
|
|
strategy.on_candle(ctx)
|
|
intents = ctx.take_intents()
|
|
assert intents and intents[0].action == "enter_long"
|
|
|
|
|
|
def test_momentum_enters_on_positive_roc() -> None:
|
|
from app.engine.strategies import get_strategy
|
|
|
|
strategy = get_strategy("momentum")({"period": 3, "entry_threshold": 0.01, "atr_stop_mult": 0})
|
|
ctx = context([100, 100, 100, 105])
|
|
strategy.on_candle(ctx)
|
|
intents = ctx.take_intents()
|
|
assert intents and intents[0].action == "enter_long"
|
|
|
|
|
|
def test_macd_trend_no_intent_when_flat_short_history() -> None:
|
|
from app.engine.strategies import get_strategy
|
|
|
|
strategy = get_strategy("macd_trend")({})
|
|
ctx = context([100, 101, 102])
|
|
strategy.on_candle(ctx)
|
|
assert ctx.take_intents() == []
|
|
|
|
|
|
def test_bollinger_enters_below_lower_band() -> None:
|
|
from app.engine.strategies import get_strategy
|
|
|
|
strategy = get_strategy("bollinger_reversion")(
|
|
{"period": 5, "num_std": 1.0, "atr_stop_mult": 0}
|
|
)
|
|
# Flat then a sharp drop below the lower band.
|
|
ctx = context([100, 100, 100, 100, 80])
|
|
strategy.on_candle(ctx)
|
|
intents = ctx.take_intents()
|
|
assert intents and intents[0].action == "enter_long"
|
|
|
|
|
|
def test_donchian_enters_on_breakout() -> None:
|
|
from app.engine.strategies import get_strategy
|
|
|
|
strategy = get_strategy("donchian_breakout")(
|
|
{"entry_period": 5, "exit_period": 3, "atr_stop_mult": 0}
|
|
)
|
|
ctx = context([100, 100, 100, 100, 100, 110])
|
|
strategy.on_candle(ctx)
|
|
intents = ctx.take_intents()
|
|
assert intents and intents[0].action == "enter_long"
|