Bitcoin favicon, more strategies, default timeframe 5m
CI/CD / backend (pull_request) Successful in 5m21s
CI/CD / frontend (pull_request) Successful in 5m4s
CI/CD / build (pull_request) Skipped
CI/CD / deploy (pull_request) Skipped
CI/CD / cleanup (pull_request) Skipped

- 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).
This commit is contained in:
matitos committed 2026-09-18 20:03:30 +00:00
1 parent 186a6949ee
commit c500fb7572
18 files changed
+488 -11

No files matched your search

+23 -1
View File
@@ -2,7 +2,7 @@ from __future__ import annotations
import pytest
from app.engine.indicators import atr, ema, rolling_std, rsi, sma
from app.engine.indicators import atr, ema, macd, roc, rolling_std, rsi, sma
from app.schemas import Candle
@@ -38,3 +38,25 @@ def test_atr_flat_market_is_zero() -> None:
def test_rolling_std() -> None:
result = rolling_std([1, 1, 1, 1], 2)
assert result[-1] == pytest.approx(0.0)
def test_roc() -> None:
values = [100.0, 110.0, 121.0, 100.0]
out = roc(values, 2)
assert out[0] is None and out[1] is None
assert out[2] == pytest.approx(0.21)
assert out[3] == pytest.approx(-0.09090909, rel=1e-4)
def test_macd_shapes_and_crossover_sign() -> None:
rising = [float(i) for i in range(1, 80)]
macd_line, signal_line, histogram = macd(rising, 12, 26, 9)
assert len(macd_line) == len(rising)
assert macd_line[-1] is not None and signal_line[-1] is not None
# In a steady uptrend MACD is positive (but can be below its signal after lag).
assert macd_line[-1] > 0
assert histogram[-1] == pytest.approx(macd_line[-1] - signal_line[-1])
falling = [float(i) for i in range(80, 0, -1)]
macd_falling, _, _ = macd(falling, 12, 26, 9)
assert macd_falling[-1] < 0
+99
View File
@@ -0,0 +1,99 @@
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"