Skip to content

Commit c57afef

Browse files
authored
Merge pull request #12 from mlsys-io/claude/auto-test-markdown-files-Mj0Bh
Add GitHub Actions CI workflow and unit tests for auto-testing
2 parents b18a7ca + 5e0d58b commit c57afef

5 files changed

Lines changed: 631 additions & 0 deletions

File tree

.github/workflows/test.yml

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
name: Auto Test
2+
3+
on:
4+
push:
5+
branches: [main, develop, "feature/**", "claude/**"]
6+
pull_request:
7+
branches: [main, develop]
8+
9+
jobs:
10+
# ---------------------------------------------------------------------------
11+
# Job 1: Lint & import checks (fast, no data needed)
12+
# ---------------------------------------------------------------------------
13+
lint:
14+
name: Lint & Syntax Check
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- uses: actions/setup-python@v5
20+
with:
21+
python-version: "3.11"
22+
23+
- name: Install minimal dependencies
24+
run: |
25+
pip install --upgrade pip
26+
pip install pyflakes
27+
28+
- name: Check Python syntax (all project modules)
29+
run: |
30+
pyflakes alpha/ strategy/ portfolio/ user_data/strategies/ dataset/
31+
32+
# ---------------------------------------------------------------------------
33+
# Job 2: Unit tests for pure-Python portfolio logic (no freqtrade needed)
34+
# ---------------------------------------------------------------------------
35+
unit-tests:
36+
name: Unit Tests
37+
runs-on: ubuntu-latest
38+
steps:
39+
- uses: actions/checkout@v4
40+
with:
41+
lfs: true
42+
43+
- uses: actions/setup-python@v5
44+
with:
45+
python-version: "3.11"
46+
47+
- name: Install dependencies
48+
run: |
49+
pip install --upgrade pip
50+
pip install -r requirements.txt
51+
pip install scipy pytest
52+
pip install -e .
53+
54+
- name: Run unit tests
55+
run: pytest tests/ -v --tb=short
56+
57+
# ---------------------------------------------------------------------------
58+
# Job 3: Strategy import validation (ensures all strategies load correctly)
59+
# ---------------------------------------------------------------------------
60+
strategy-import:
61+
name: Strategy Import Validation
62+
runs-on: ubuntu-latest
63+
steps:
64+
- uses: actions/checkout@v4
65+
with:
66+
lfs: true
67+
68+
- uses: actions/setup-python@v5
69+
with:
70+
python-version: "3.11"
71+
72+
- name: Install system dependencies (TA-Lib)
73+
run: |
74+
sudo apt-get update
75+
sudo apt-get install -y build-essential wget
76+
wget https://github.com/ta-lib/ta-lib/releases/download/v0.6.4/ta-lib-0.6.4-src.tar.gz
77+
tar -xzf ta-lib-0.6.4-src.tar.gz
78+
cd ta-lib-0.6.4
79+
./configure --prefix=/usr
80+
make -j$(nproc)
81+
sudo make install
82+
sudo ldconfig
83+
84+
- name: Install Python dependencies
85+
run: |
86+
pip install --upgrade pip
87+
pip install -r requirements.txt
88+
pip install scipy
89+
pip install -e .
90+
91+
- name: Validate all strategy imports
92+
run: |
93+
python -c "
94+
import sys
95+
errors = []
96+
97+
# Alpha modules
98+
try:
99+
from alpha.interface import IAlpha
100+
print('OK: alpha.interface.IAlpha')
101+
except Exception as e:
102+
errors.append(f'alpha.interface: {e}')
103+
104+
try:
105+
from alpha.SimpleEmaFactors import EmaAlpha
106+
print('OK: alpha.SimpleEmaFactors.EmaAlpha')
107+
except Exception as e:
108+
errors.append(f'alpha.SimpleEmaFactors: {e}')
109+
110+
# Trading strategies
111+
try:
112+
from strategy.EmaCrossStrategy import EmaCrossStrategy
113+
print('OK: strategy.EmaCrossStrategy')
114+
except Exception as e:
115+
errors.append(f'strategy.EmaCrossStrategy: {e}')
116+
117+
try:
118+
from strategy.MacdAdxStrategy import MacdAdxStrategy
119+
print('OK: strategy.MacdAdxStrategy')
120+
except Exception as e:
121+
errors.append(f'strategy.MacdAdxStrategy: {e}')
122+
123+
# Portfolio strategies
124+
try:
125+
from user_data.strategies.ONS import ONS_Portfolio
126+
print('OK: user_data.strategies.ONS.ONS_Portfolio')
127+
except Exception as e:
128+
errors.append(f'user_data.strategies.ONS: {e}')
129+
130+
try:
131+
from user_data.strategies.inv_vol import InverseVolatilityPortfolio
132+
print('OK: user_data.strategies.inv_vol.InverseVolatilityPortfolio')
133+
except Exception as e:
134+
errors.append(f'user_data.strategies.inv_vol: {e}')
135+
136+
try:
137+
from user_data.strategies.min_var import MinimumVariancePortfolio
138+
print('OK: user_data.strategies.min_var.MinimumVariancePortfolio')
139+
except Exception as e:
140+
errors.append(f'user_data.strategies.min_var: {e}')
141+
142+
try:
143+
from user_data.strategies.best_single_asset import BestSingleAssetPortfolio
144+
print('OK: user_data.strategies.best_single_asset.BestSingleAssetPortfolio')
145+
except Exception as e:
146+
errors.append(f'user_data.strategies.best_single_asset: {e}')
147+
148+
# Portfolio pipeline
149+
try:
150+
from portfolio.PortfolioManagement import run_portfolio
151+
print('OK: portfolio.PortfolioManagement.run_portfolio')
152+
except Exception as e:
153+
errors.append(f'portfolio.PortfolioManagement: {e}')
154+
155+
if errors:
156+
print(f'\nFAILED: {len(errors)} import error(s):')
157+
for err in errors:
158+
print(f' - {err}')
159+
sys.exit(1)
160+
else:
161+
print(f'\nAll imports passed.')
162+
"
163+
164+
# ---------------------------------------------------------------------------
165+
# Job 4: Portfolio pipeline smoke test (runs the standalone pipeline)
166+
# ---------------------------------------------------------------------------
167+
portfolio-pipeline:
168+
name: Portfolio Pipeline Smoke Test
169+
runs-on: ubuntu-latest
170+
steps:
171+
- uses: actions/checkout@v4
172+
with:
173+
lfs: true
174+
175+
- uses: actions/setup-python@v5
176+
with:
177+
python-version: "3.11"
178+
179+
- name: Install system dependencies (TA-Lib)
180+
run: |
181+
sudo apt-get update
182+
sudo apt-get install -y build-essential wget
183+
wget https://github.com/ta-lib/ta-lib/releases/download/v0.6.4/ta-lib-0.6.4-src.tar.gz
184+
tar -xzf ta-lib-0.6.4-src.tar.gz
185+
cd ta-lib-0.6.4
186+
./configure --prefix=/usr
187+
make -j$(nproc)
188+
sudo make install
189+
sudo ldconfig
190+
191+
- name: Install Python dependencies
192+
run: |
193+
pip install --upgrade pip
194+
pip install -r requirements.txt
195+
pip install scipy
196+
pip install -e .
197+
198+
- name: Run standalone portfolio pipeline
199+
run: python -m portfolio.PortfolioManagement
200+
201+
# ---------------------------------------------------------------------------
202+
# Job 5: Backtest smoke tests (crypto-only, fast timerange)
203+
# ---------------------------------------------------------------------------
204+
backtest-smoke:
205+
name: Backtest Smoke Tests
206+
runs-on: ubuntu-latest
207+
steps:
208+
- uses: actions/checkout@v4
209+
with:
210+
lfs: true
211+
212+
- uses: actions/setup-python@v5
213+
with:
214+
python-version: "3.11"
215+
216+
- name: Install system dependencies (TA-Lib)
217+
run: |
218+
sudo apt-get update
219+
sudo apt-get install -y build-essential wget
220+
wget https://github.com/ta-lib/ta-lib/releases/download/v0.6.4/ta-lib-0.6.4-src.tar.gz
221+
tar -xzf ta-lib-0.6.4-src.tar.gz
222+
cd ta-lib-0.6.4
223+
./configure --prefix=/usr
224+
make -j$(nproc)
225+
sudo make install
226+
sudo ldconfig
227+
228+
- name: Install Python dependencies
229+
run: |
230+
pip install --upgrade pip
231+
pip install -r requirements.txt
232+
pip install scipy
233+
pip install -e .
234+
235+
- name: Backtest EmaCrossStrategy (crypto, 1d)
236+
run: |
237+
freqtrade backtesting \
238+
--strategy EmaCrossStrategy \
239+
--strategy-path ./strategy \
240+
--timeframe 1d \
241+
--timerange 20250501-20250601 \
242+
--pairs BTC/USDT ETH/USDT
243+
244+
- name: Backtest MacdAdxStrategy (stocks, 1d)
245+
run: |
246+
freqtrade backtesting \
247+
--strategy MacdAdxStrategy \
248+
--strategy-path ./strategy \
249+
--timeframe 1d \
250+
--timerange 20250501-20250601 \
251+
--pairs AAPL/USDT MSFT/USDT
252+
253+
- name: Backtest ONS_Portfolio (mixed, 5m, short range)
254+
run: |
255+
freqtrade backtesting \
256+
--strategy ONS_Portfolio \
257+
--strategy-path ./user_data/strategies \
258+
--timeframe 5m \
259+
--timerange 20260101-20260103 \
260+
--pairs BTC/USDT ETH/USDT AAPL/USDT \
261+
--dry-run-wallet 1000000

tests/__init__.py

Whitespace-only changes.

tests/test_alpha.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"""Tests for the alpha factor interface and EMA alpha implementation."""
2+
3+
import numpy as np
4+
import pandas as pd
5+
import pytest
6+
7+
talib = pytest.importorskip("talib", reason="TA-Lib C library not installed")
8+
9+
from alpha.interface import IAlpha
10+
from alpha.SimpleEmaFactors import EmaAlpha
11+
12+
13+
def _make_ohlcv(n=100):
14+
"""Create a minimal synthetic OHLCV DataFrame."""
15+
np.random.seed(42)
16+
close = 100 + np.cumsum(np.random.randn(n) * 0.5)
17+
return pd.DataFrame({
18+
"date": pd.date_range("2025-01-01", periods=n, freq="D"),
19+
"open": close - 0.1,
20+
"high": close + 0.5,
21+
"low": close - 0.5,
22+
"close": close,
23+
"volume": np.random.randint(100, 10000, size=n).astype(float),
24+
})
25+
26+
27+
class TestIAlpha:
28+
def test_is_abstract(self):
29+
with pytest.raises(TypeError):
30+
IAlpha(pd.DataFrame(), {})
31+
32+
def test_subclass_must_implement_process(self):
33+
class Incomplete(IAlpha):
34+
pass
35+
36+
with pytest.raises(TypeError):
37+
Incomplete(pd.DataFrame(), {})
38+
39+
40+
class TestEmaAlpha:
41+
def test_process_adds_expected_columns(self):
42+
df = _make_ohlcv()
43+
result = EmaAlpha(df, {"pair": "BTC/USDT"}).process()
44+
45+
for col in ["ema_fast", "ema_slow", "ema_exit", "mean-volume"]:
46+
assert col in result.columns, f"Missing column: {col}"
47+
48+
def test_ema_values_are_numeric(self):
49+
df = _make_ohlcv()
50+
result = EmaAlpha(df, {"pair": "BTC/USDT"}).process()
51+
assert result["ema_fast"].dtype in [np.float64, np.float32]
52+
53+
def test_ema_fast_shorter_than_slow(self):
54+
df = _make_ohlcv(200)
55+
result = EmaAlpha(df, {"pair": "TEST"}).process()
56+
# EMA fast (period 12) should be closer to recent prices than EMA slow (period 26)
57+
# After warm-up, fast EMA std should be >= slow EMA std (reacts faster)
58+
fast_std = result["ema_fast"].iloc[50:].std()
59+
slow_std = result["ema_slow"].iloc[50:].std()
60+
assert fast_std >= slow_std * 0.5 # fast reacts more (wider variance)
61+
62+
def test_mean_volume_is_rolling_average(self):
63+
df = _make_ohlcv(50)
64+
result = EmaAlpha(df, {}).process()
65+
# First 19 rows should be NaN (rolling window = 20)
66+
assert result["mean-volume"].iloc[:19].isna().all()
67+
# Row 19 onward should have values
68+
assert result["mean-volume"].iloc[19:].notna().all()

tests/test_data_integrity.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""Tests for data file integrity — verifies feather files have correct schema."""
2+
3+
import os
4+
import glob
5+
import pandas as pd
6+
import pytest
7+
8+
DATA_DIR = os.path.join(os.path.dirname(__file__), "..", "user_data", "data", "binance")
9+
REQUIRED_COLUMNS = {"date", "open", "high", "low", "close", "volume"}
10+
11+
# Representative subset: one crypto, one stock, one index per timeframe
12+
SAMPLE_FILES = [
13+
"BTC_USDT-1d.feather",
14+
"BTC_USDT-4h.feather",
15+
"BTC_USDT-5m.feather",
16+
"AAPL_USDT-1d.feather",
17+
"DJI_USDT-1d.feather",
18+
]
19+
20+
21+
def _feather_available():
22+
path = os.path.join(DATA_DIR, "BTC_USDT-1d.feather")
23+
if not os.path.isfile(path):
24+
return False
25+
# Check it's not a Git LFS pointer (small text file starting with "version")
26+
with open(path, "rb") as f:
27+
header = f.read(20)
28+
return not header.startswith(b"version ")
29+
30+
31+
@pytest.mark.skipif(not _feather_available(), reason="Data files not available (LFS not pulled)")
32+
class TestDataIntegrity:
33+
@pytest.mark.parametrize("filename", SAMPLE_FILES)
34+
def test_required_columns_present(self, filename):
35+
path = os.path.join(DATA_DIR, filename)
36+
if not os.path.isfile(path):
37+
pytest.skip(f"{filename} not found")
38+
df = pd.read_feather(path)
39+
missing = REQUIRED_COLUMNS - set(df.columns)
40+
assert not missing, f"{filename} missing columns: {missing}"
41+
42+
@pytest.mark.parametrize("filename", SAMPLE_FILES)
43+
def test_no_empty_files(self, filename):
44+
path = os.path.join(DATA_DIR, filename)
45+
if not os.path.isfile(path):
46+
pytest.skip(f"{filename} not found")
47+
df = pd.read_feather(path)
48+
assert len(df) > 0, f"{filename} is empty"
49+
50+
@pytest.mark.parametrize("filename", SAMPLE_FILES)
51+
def test_close_prices_positive(self, filename):
52+
path = os.path.join(DATA_DIR, filename)
53+
if not os.path.isfile(path):
54+
pytest.skip(f"{filename} not found")
55+
df = pd.read_feather(path)
56+
assert (df["close"] > 0).all(), f"{filename} has non-positive close prices"
57+
58+
def test_naming_convention_consistent(self):
59+
files = glob.glob(os.path.join(DATA_DIR, "*.feather"))
60+
for f in files:
61+
basename = os.path.basename(f)
62+
# Expected: {TICKER}_USDT-{timeframe}.feather
63+
assert "_USDT-" in basename, f"Unexpected naming: {basename}"
64+
assert basename.endswith(".feather"), f"Wrong extension: {basename}"

0 commit comments

Comments
 (0)