Skip to content

Commit ed0f0a0

Browse files
costajohntclaude
andcommitted
Add linting, coverage, dependabot; clean up dead code and README
- Add ruff linter + pytest-cov to dev deps and CI pipeline - CI now runs lint check before tests, uploads coverage report - Add dependabot for pip and github-actions weekly updates - Remove analyze_sentiment_with_claude alias and rename all callers - Update README architecture with 8 missing strategy modules, 2 scripts - Fix test count in README (698 tests across 34 files) - Fix all ruff lint issues (import sorting, unused vars, whitespace) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ad4a02f commit ed0f0a0

72 files changed

Lines changed: 529 additions & 450 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/dependabot.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
version: 2
2+
updates:
3+
- package-ecosystem: "pip"
4+
directory: "/"
5+
schedule:
6+
interval: "weekly"
7+
open-pull-requests-limit: 5
8+
labels:
9+
- "dependencies"
10+
11+
- package-ecosystem: "github-actions"
12+
directory: "/"
13+
schedule:
14+
interval: "weekly"
15+
open-pull-requests-limit: 5
16+
labels:
17+
- "dependencies"

.github/workflows/ci.yml

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,23 @@ concurrency:
1111
cancel-in-progress: true
1212

1313
jobs:
14+
lint:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- name: Install uv
20+
uses: astral-sh/setup-uv@v4
21+
22+
- name: Install Python and dependencies
23+
run: uv sync
24+
25+
- name: Lint with ruff
26+
run: uv run ruff check .
27+
1428
test:
1529
runs-on: ubuntu-latest
30+
needs: lint
1631
steps:
1732
- uses: actions/checkout@v4
1833

@@ -22,5 +37,12 @@ jobs:
2237
- name: Install Python and dependencies
2338
run: uv sync
2439

25-
- name: Run tests
26-
run: uv run python -m pytest tests/ -v --tb=short
40+
- name: Run tests with coverage
41+
run: uv run python -m pytest tests/ -v --tb=short --cov=strategies --cov=scripts --cov-report=term-missing --cov-report=html
42+
43+
- name: Upload coverage report
44+
if: always()
45+
uses: actions/upload-artifact@v4
46+
with:
47+
name: coverage-report
48+
path: htmlcov/

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,14 @@ strategies/
118118
signal_scorer.py Empirical signal scoring (bucket win rates)
119119
constants.py Tunable strategy parameters (agent-modifiable)
120120
enums.py Shared enums (Action, InsiderRecommendation)
121+
cooldown.py Re-entry cooldown after stop-loss exits
122+
equity_tracker.py Per-position high-water mark tracking
123+
regime.py Market regime detection (bull/bear/sideways)
124+
resilience.py API retry decorator with exponential backoff
125+
risk_guard.py Portfolio-level kill switch (daily loss + drawdown)
126+
state_db.py SQLite trading state persistence
127+
stop_orders.py Stop-loss order placement helpers
128+
weekly_trend.py Weekly SMA trend filter (blocks severe downtrends)
121129
122130
scripts/
123131
scan_with_sentiment.py 3-layer composite signal + sizing + sector guard
@@ -137,14 +145,16 @@ scripts/
137145
strategy_agent.py Autonomous strategy agent (weekly review + event check)
138146
agent_tools.py Agent tool definitions + safety-hardcoded dispatch
139147
shadow_evaluator.py Shadow experiment evaluation
148+
daily_metrics.py Daily performance metrics snapshot
149+
walkforward.py Walk-forward backtesting validation
140150
daily_run.sh Local pipeline automation
141151
account_status.py Quick account connection check
142152
run_strategy.py Single-ticker mean-reversion analysis
143153
momentum_screener.py Market-wide momentum screener
144154
145155
data/ Trading state (signals.csv, trades.csv, metrics.json)
146156
data/agent/ Agent state (changelog, experiments, overrides)
147-
tests/ 654 tests across 30 test files
157+
tests/ 698 tests across 34 test files
148158
```
149159

150160
## Testing

client.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@
33
import os
44
from pathlib import Path
55

6-
from dotenv import load_dotenv
7-
8-
from alpaca.trading.client import TradingClient
96
from alpaca.data.historical import StockHistoricalDataClient
7+
from alpaca.trading.client import TradingClient
8+
from dotenv import load_dotenv
109

1110

1211
def _load_env():

pyproject.toml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,21 @@ dependencies = [
1919
[dependency-groups]
2020
dev = [
2121
"pytest>=9.0.2",
22+
"pytest-cov>=6.0",
23+
"ruff>=0.11.0",
2224
]
25+
26+
[tool.ruff]
27+
target-version = "py314"
28+
line-length = 120
29+
30+
[tool.ruff.lint]
31+
select = ["E", "F", "W", "I"]
32+
ignore = ["E501"]
33+
34+
[tool.ruff.lint.per-file-ignores]
35+
"tests/*.py" = ["E402"]
36+
"scripts/*.py" = ["E402"]
37+
38+
[tool.pytest.ini_options]
39+
addopts = "--tb=short"

scripts/agent_tools.py

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import subprocess
1414
import sys
1515
import tempfile
16-
from datetime import datetime, date, timedelta
16+
from datetime import date, datetime, timedelta
1717
from pathlib import Path
1818

1919
sys.path.insert(0, str(Path(__file__).parent.parent))
@@ -209,7 +209,7 @@ def _files_in_cooling_period() -> set[str]:
209209
cooling.add(f)
210210
except (ValueError, TypeError):
211211
# Fail safe: treat unparseable as within cooling period
212-
print(f" WARNING: Could not parse timestamp for deployed change — treating as in cooling period")
212+
print(" WARNING: Could not parse timestamp for deployed change — treating as in cooling period")
213213
for f in change.get("files_modified", []):
214214
cooling.add(f)
215215
return cooling
@@ -335,7 +335,7 @@ def read_performance_summary() -> dict:
335335

336336
def read_positions() -> dict:
337337
"""Read current open positions from Alpaca API.
338-
338+
339339
Returns dict with positions list and account summary.
340340
"""
341341
try:
@@ -372,7 +372,7 @@ def read_positions() -> dict:
372372

373373
def read_code(file_path: str) -> dict:
374374
"""Read a file from the repo.
375-
375+
376376
Returns dict with content or error. Validates path is within project.
377377
"""
378378
try:
@@ -403,10 +403,10 @@ def read_experiment_results() -> dict:
403403
experiments.append(json.loads(f.read_text()))
404404
except Exception as e:
405405
experiments.append({"file": f.name, "error": str(e)})
406-
406+
407407
active = [e for e in experiments if e.get("status") == "active"]
408408
completed = [e for e in experiments if e.get("status") in ("promoted", "discarded", "expired")]
409-
409+
410410
return {
411411
"total": len(experiments),
412412
"active": len(active),
@@ -685,11 +685,9 @@ def compute_measured_impact(change_id: str) -> dict:
685685

686686
# Find the changelog entry
687687
entry = None
688-
entry_idx = None
689688
for i, change in enumerate(changelog["changes"]):
690689
if change.get("id") == change_id:
691690
entry = change
692-
entry_idx = i
693691
break
694692

695693
if entry is None:
@@ -881,7 +879,7 @@ def _compute_strategy_correlation() -> dict:
881879

882880
def edit_file(file_path: str, old_string: str, new_string: str, rationale: str) -> dict:
883881
"""Edit a file with no-touch enforcement.
884-
882+
885883
Returns success/failure dict. Rejects edits to protected files and files
886884
in cooling period.
887885
"""
@@ -931,7 +929,7 @@ def edit_file(file_path: str, old_string: str, new_string: str, rationale: str)
931929

932930
def run_backtest(params: dict | None = None) -> dict:
933931
"""Run pipeline_backtest.py and return structured results.
934-
932+
935933
Enforces max invocations per run. params is a dict of CLI arguments.
936934
"""
937935
global _backtests_this_run
@@ -1031,7 +1029,7 @@ def run_tests() -> dict:
10311029

10321030
def create_experiment(experiment_id: str, hypothesis: str, variant: dict) -> dict:
10331031
"""Create a shadow-mode experiment.
1034-
1032+
10351033
variant should contain: file, parameter, baseline_value, variant_value.
10361034
Enforces max active experiment limit.
10371035
"""
@@ -1095,7 +1093,7 @@ def create_experiment(experiment_id: str, hypothesis: str, variant: dict) -> dic
10951093

10961094
def deploy_change(commit_message: str, evidence: dict) -> dict:
10971095
"""Atomic deploy: branch, commit, test, backtest, merge.
1098-
1096+
10991097
Enforces: budget, test pass, backtest Sharpe floor, safety invariants.
11001098
evidence dict should contain: sample_size, change_type, diagnosis.
11011099
"""
@@ -1316,19 +1314,19 @@ def _update_pending_status(status: str, commit_sha: str | None = None, snapshot:
13161314

13171315
def rollback_change(commit_sha: str, reason: str) -> dict:
13181316
"""Revert a specific agent-authored commit.
1319-
1317+
13201318
Only reverts commits authored by the agent (checks changelog).
13211319
Runs tests after revert to confirm clean state.
13221320
"""
13231321
changelog = _load_changelog()
1324-
1322+
13251323
# Find the change in changelog
13261324
change_entry = None
13271325
for change in changelog["changes"]:
13281326
if change.get("commit_sha") == commit_sha:
13291327
change_entry = change
13301328
break
1331-
1329+
13321330
if change_entry is None:
13331331
return {"success": False, "error": f"Commit {commit_sha} not found in agent changelog — refusing to revert non-agent commits"}
13341332

@@ -1339,7 +1337,7 @@ def rollback_change(commit_sha: str, reason: str) -> dict:
13391337
dependent_shas = []
13401338
change_files = set(change_entry.get("files_modified", []))
13411339
change_ts = change_entry.get("timestamp", "")
1342-
1340+
13431341
for other in changelog["changes"]:
13441342
if other.get("commit_sha") == commit_sha:
13451343
continue
@@ -1415,7 +1413,7 @@ def rollback_change(commit_sha: str, reason: str) -> dict:
14151413

14161414
def pause_strategy(strategy_name: str, reason: str) -> dict:
14171415
"""Write a pause flag for a strategy. Auto-expires after 3 days.
1418-
1416+
14191417
Valid strategies: screener, momentum, sentiment.
14201418
"""
14211419
valid = {"screener", "momentum", "sentiment"}
@@ -1466,12 +1464,12 @@ def is_strategy_paused(strategy_name: str) -> bool:
14661464

14671465
def log_decision(summary: str, details: dict) -> dict:
14681466
"""Log a decision to the changelog. REQUIRED every agent run.
1469-
1467+
14701468
Use for both action decisions and "do nothing" decisions.
14711469
"""
14721470
_ensure_agent_dirs()
14731471
changelog = _load_changelog()
1474-
1472+
14751473
entry = {
14761474
"id": f"dec-{datetime.now().strftime('%Y%m%d-%H%M%S')}",
14771475
"timestamp": datetime.now().isoformat(),

scripts/backtest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def main():
6363
if trades:
6464
print(f" Completed trades: {len(trades)}")
6565
wins = [t for t in trades if t["pnl_pct"] > 0]
66-
losses = [t for t in trades if t["pnl_pct"] <= 0]
66+
[t for t in trades if t["pnl_pct"] <= 0]
6767
avg_pnl = sum(t["pnl_pct"] for t in trades) / len(trades)
6868
total_pnl = sum(t["pnl_pct"] for t in trades)
6969
print(f" Win rate: {len(wins)}/{len(trades)} ({len(wins)/len(trades)*100:.0f}%)")

scripts/daily_metrics.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
sys.path.insert(0, str(Path(__file__).parent.parent))
1414

1515
from client import get_trading_client
16-
from scripts.trade_logger import log_daily_metrics, SIGNAL_LOG, TRADE_LOG
16+
from scripts.trade_logger import SIGNAL_LOG, log_daily_metrics
1717

1818

1919
def _count_today_signals() -> tuple[int, int, int]:
@@ -36,7 +36,7 @@ def _count_today_signals() -> tuple[int, int, int]:
3636
print(f" NOTE: Signal log not found ({SIGNAL_LOG}) — signal counts will be zero.")
3737
except (csv.Error, OSError) as e:
3838
print(f" WARNING: Error reading signal log: {e}")
39-
print(f" Signal counts may be incomplete.")
39+
print(" Signal counts may be incomplete.")
4040
return signals, buys, vetoes
4141

4242

scripts/dashboard.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616

1717
sys.path.insert(0, str(Path(__file__).parent.parent))
1818

19-
from client import get_trading_client, get_data_client
20-
from strategies.sector import get_sector_exposure, get_sector, SECTOR_MAP
21-
from strategies.cooldown import load_cooldowns, is_on_cooldown, COOLDOWN_DAYS
19+
from client import get_trading_client
20+
from strategies.cooldown import COOLDOWN_DAYS, is_on_cooldown, load_cooldowns
21+
from strategies.sector import get_sector_exposure
2222

2323
app = Flask(__name__)
2424

@@ -92,8 +92,8 @@ def _read_log_tail(n: int = 50) -> list[str]:
9292
def _get_hold_days(trading_client, symbol: str) -> int | None:
9393
"""Look up when the most recent buy order for *symbol* was filled."""
9494
try:
95-
from alpaca.trading.requests import GetOrdersRequest
9695
from alpaca.trading.enums import QueryOrderStatus
96+
from alpaca.trading.requests import GetOrdersRequest
9797

9898
orders = trading_client.get_orders(GetOrdersRequest(
9999
status=QueryOrderStatus.CLOSED,
@@ -275,7 +275,7 @@ def _compute_performance_data(positions: list, trading_client) -> dict:
275275

276276
win_rate = (len(wins) / len(trades_with_pl) * 100) if trades_with_pl else 0
277277
avg_win_pct = (sum(w["pl_pct"] for w in wins) / len(wins)) if wins else 0
278-
avg_loss_pct = (sum(l["pl_pct"] for l in losses) / len(losses)) if losses else 0
278+
avg_loss_pct = (sum(t["pl_pct"] for t in losses) / len(losses)) if losses else 0
279279

280280
best_trade = max(trades_with_pl, key=lambda x: x["pl_pct"]) if trades_with_pl else None
281281
worst_trade = min(trades_with_pl, key=lambda x: x["pl_pct"]) if trades_with_pl else None
@@ -287,7 +287,7 @@ def _compute_performance_data(positions: list, trading_client) -> dict:
287287
vetoed_buy_signals = total_buy_signals - executed_buy_signals
288288

289289
# Of executed buys, how many are currently winning (open or closed)?
290-
winning_symbols = set(w["symbol"] for w in wins)
290+
set(w["symbol"] for w in wins)
291291
# Also count open positions that are currently profitable
292292
winning_open = set(ot["symbol"] for ot in open_trades if ot["pl_pct"] > 0)
293293
losing_open = set(ot["symbol"] for ot in open_trades if ot["pl_pct"] <= 0)

0 commit comments

Comments
 (0)