You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
131 lines
4.9 KiB
Python
131 lines
4.9 KiB
Python
from __future__ import annotations
|
|
|
|
import datetime as dt
|
|
|
|
from libs.backtest.daily_gainers import (
|
|
DailyGainerCriteria,
|
|
collect_daily_gainer_signals,
|
|
evaluate_daily_gainer_signals,
|
|
)
|
|
|
|
|
|
def _bar(
|
|
open_price: float,
|
|
high: float,
|
|
low: float,
|
|
close: float,
|
|
volume: int,
|
|
) -> dict[str, float | int]:
|
|
return {
|
|
"open": open_price,
|
|
"high": high,
|
|
"low": low,
|
|
"close": close,
|
|
"volume": volume,
|
|
}
|
|
|
|
|
|
def test_collect_daily_gainer_signals_ranks_by_return_then_liquidity() -> None:
|
|
start = dt.date(2024, 1, 1)
|
|
dates = [start + dt.timedelta(days=i) for i in range(24)]
|
|
|
|
bars_by_symbol: dict[str, dict[dt.date, dict[str, float | int]]] = {
|
|
"AAA": {},
|
|
"BBB": {},
|
|
"CCC": {},
|
|
}
|
|
for idx, date in enumerate(dates[:-1]):
|
|
base_price = 100.0 + idx
|
|
for symbol in bars_by_symbol:
|
|
bars_by_symbol[symbol][date] = _bar(base_price, base_price + 1, base_price - 1, base_price, 1_000_000)
|
|
|
|
signal_date = dates[-1]
|
|
bars_by_symbol["AAA"][signal_date] = _bar(120.0, 128.0, 118.0, 126.0, 4_000_000)
|
|
bars_by_symbol["BBB"][signal_date] = _bar(121.0, 130.0, 119.0, 129.0, 3_000_000)
|
|
bars_by_symbol["CCC"][signal_date] = _bar(119.0, 125.0, 112.0, 116.0, 4_000_000)
|
|
|
|
criteria = DailyGainerCriteria(
|
|
min_change=0.03,
|
|
max_change=0.35,
|
|
min_volume_ratio=1.2,
|
|
min_close_location=0.55,
|
|
min_avg_dollar_volume=50_000_000.0,
|
|
top_n=2,
|
|
)
|
|
signals_by_date = collect_daily_gainer_signals(
|
|
bars_by_symbol=bars_by_symbol,
|
|
start_date=signal_date,
|
|
end_date=signal_date,
|
|
criteria=criteria,
|
|
)
|
|
|
|
signals = signals_by_date[signal_date]
|
|
assert [signal.symbol for signal in signals] == ["BBB", "AAA"]
|
|
assert [signal.rank for signal in signals] == [1, 2]
|
|
|
|
|
|
def test_evaluate_daily_gainer_signals_uses_next_open_entry() -> None:
|
|
signal_date = dt.date(2024, 2, 1)
|
|
entry_date = dt.date(2024, 2, 2)
|
|
day3 = dt.date(2024, 2, 5)
|
|
day5 = dt.date(2024, 2, 7)
|
|
|
|
bars_by_symbol = {
|
|
"AAA": {
|
|
dt.date(2024, 1, 2): _bar(100.0, 101.0, 99.0, 100.0, 1_000_000),
|
|
dt.date(2024, 1, 3): _bar(101.0, 102.0, 100.0, 101.0, 1_000_000),
|
|
dt.date(2024, 1, 4): _bar(102.0, 103.0, 101.0, 102.0, 1_000_000),
|
|
dt.date(2024, 1, 5): _bar(103.0, 104.0, 102.0, 103.0, 1_000_000),
|
|
dt.date(2024, 1, 8): _bar(104.0, 105.0, 103.0, 104.0, 1_000_000),
|
|
dt.date(2024, 1, 9): _bar(105.0, 106.0, 104.0, 105.0, 1_000_000),
|
|
dt.date(2024, 1, 10): _bar(106.0, 107.0, 105.0, 106.0, 1_000_000),
|
|
dt.date(2024, 1, 11): _bar(107.0, 108.0, 106.0, 107.0, 1_000_000),
|
|
dt.date(2024, 1, 12): _bar(108.0, 109.0, 107.0, 108.0, 1_000_000),
|
|
dt.date(2024, 1, 16): _bar(109.0, 110.0, 108.0, 109.0, 1_000_000),
|
|
dt.date(2024, 1, 17): _bar(110.0, 111.0, 109.0, 110.0, 1_000_000),
|
|
dt.date(2024, 1, 18): _bar(111.0, 112.0, 110.0, 111.0, 1_000_000),
|
|
dt.date(2024, 1, 19): _bar(112.0, 113.0, 111.0, 112.0, 1_000_000),
|
|
dt.date(2024, 1, 22): _bar(113.0, 114.0, 112.0, 113.0, 1_000_000),
|
|
dt.date(2024, 1, 23): _bar(114.0, 115.0, 113.0, 114.0, 1_000_000),
|
|
dt.date(2024, 1, 24): _bar(115.0, 116.0, 114.0, 115.0, 1_000_000),
|
|
dt.date(2024, 1, 25): _bar(116.0, 117.0, 115.0, 116.0, 1_000_000),
|
|
dt.date(2024, 1, 26): _bar(117.0, 118.0, 116.0, 117.0, 1_000_000),
|
|
dt.date(2024, 1, 29): _bar(118.0, 119.0, 117.0, 118.0, 1_000_000),
|
|
dt.date(2024, 1, 30): _bar(119.0, 120.0, 118.0, 119.0, 1_000_000),
|
|
dt.date(2024, 1, 31): _bar(120.0, 121.0, 119.0, 120.0, 1_000_000),
|
|
signal_date: _bar(123.0, 130.0, 122.0, 128.0, 4_000_000),
|
|
entry_date: _bar(129.0, 132.0, 128.0, 131.0, 3_000_000),
|
|
day3: _bar(130.0, 136.0, 129.0, 135.0, 3_000_000),
|
|
dt.date(2024, 2, 6): _bar(134.0, 138.0, 133.0, 137.0, 3_000_000),
|
|
day5: _bar(137.0, 140.0, 136.0, 139.0, 3_000_000),
|
|
}
|
|
}
|
|
|
|
signals_by_date = collect_daily_gainer_signals(
|
|
bars_by_symbol=bars_by_symbol,
|
|
start_date=signal_date,
|
|
end_date=signal_date,
|
|
criteria=DailyGainerCriteria(
|
|
min_change=0.05,
|
|
max_change=0.30,
|
|
min_volume_ratio=1.2,
|
|
min_close_location=0.55,
|
|
min_avg_dollar_volume=50_000_000.0,
|
|
top_n=1,
|
|
),
|
|
)
|
|
|
|
trades = evaluate_daily_gainer_signals(
|
|
signals_by_date=signals_by_date,
|
|
bars_by_symbol=bars_by_symbol,
|
|
)
|
|
|
|
assert len(trades) == 1
|
|
trade = trades[0]
|
|
assert trade.entry_date == entry_date
|
|
assert trade.entry_price == 129.0
|
|
assert trade.entry_gap_pct == 129.0 / 128.0 - 1.0
|
|
assert trade.hold_1d_return == 131.0 / 129.0 - 1.0
|
|
assert trade.hold_3d_return == 137.0 / 129.0 - 1.0
|
|
assert trade.hold_5d_return is None
|