39875112a0
Python bot (bot/alpaclaudia): alpaca-py client, wheel strategy (CSP + covered calls) plus equity trailing stops, risk gates (cash buffer, cost-basis guard, per-symbol concentration cap), SQLite state log, Typer CLI (tick/loop/status/ report/dump-state), Discord daily report, pytest suite. Next.js 14 dashboard (dashboard/): read-only — reads the bot's SQLite directly and pulls live account/positions/orders from Alpaca. KPIs, equity chart, positions, bot-intents audit table, and orders table. Dark UI with Tailwind. systemd/: user-unit templates for the polling loop and the post-close report timer. docs/STRATEGY.md: wheel mechanics, risk invariants, later candidates. Defaults to BOT_MODE=dry — nothing is submitted to Alpaca until explicitly enabled in .env. ALPACA_ENV=paper by default; flipping to live requires an explicit second guard.
112 lines
2.4 KiB
Python
112 lines
2.4 KiB
Python
from alpaclaudia.config import RiskConfig
|
|
from alpaclaudia.risk import (
|
|
AccountSnapshot,
|
|
check_covered_call,
|
|
check_csp,
|
|
check_trailing_stop,
|
|
)
|
|
|
|
RISK = RiskConfig(
|
|
max_position_pct=0.25,
|
|
min_cash_buffer_pct=0.05,
|
|
trailing_stop_enabled=True,
|
|
trailing_stop_pct=0.08,
|
|
)
|
|
|
|
ACCT = AccountSnapshot(equity=100_000, cash=50_000, buying_power=50_000)
|
|
|
|
|
|
def test_csp_ok():
|
|
r = check_csp(
|
|
underlying="TSLA",
|
|
strike=200.0,
|
|
qty=1,
|
|
account=ACCT,
|
|
existing_short_puts=0,
|
|
max_short_puts_per_symbol=1,
|
|
risk_cfg=RISK,
|
|
)
|
|
assert r.ok, r.reason
|
|
|
|
|
|
def test_csp_blocked_by_cash_buffer():
|
|
r = check_csp(
|
|
underlying="TSLA",
|
|
strike=600.0, # 60k collateral, only 50k cash
|
|
qty=1,
|
|
account=ACCT,
|
|
existing_short_puts=0,
|
|
max_short_puts_per_symbol=1,
|
|
risk_cfg=RISK,
|
|
)
|
|
assert not r.ok
|
|
|
|
|
|
def test_csp_blocked_by_position_cap():
|
|
# 25% of 100k equity = 25k; strike 300 * 100 = 30k collateral
|
|
r = check_csp(
|
|
underlying="TSLA",
|
|
strike=300.0,
|
|
qty=1,
|
|
account=ACCT,
|
|
existing_short_puts=0,
|
|
max_short_puts_per_symbol=1,
|
|
risk_cfg=RISK,
|
|
)
|
|
assert not r.ok
|
|
assert "cap" in r.reason or "exceeds" in r.reason
|
|
|
|
|
|
def test_csp_blocked_by_concurrency():
|
|
r = check_csp(
|
|
underlying="TSLA",
|
|
strike=200.0,
|
|
qty=1,
|
|
account=ACCT,
|
|
existing_short_puts=1,
|
|
max_short_puts_per_symbol=1,
|
|
risk_cfg=RISK,
|
|
)
|
|
assert not r.ok
|
|
|
|
|
|
def test_covered_call_blocks_below_cost_basis():
|
|
positions = [
|
|
{"symbol": "TSLA", "qty": 200, "avg_entry_price": 250.0, "is_option": False}
|
|
]
|
|
r = check_covered_call(
|
|
underlying="TSLA",
|
|
strike=240.0,
|
|
qty=1,
|
|
positions=positions,
|
|
)
|
|
assert not r.ok
|
|
|
|
|
|
def test_covered_call_requires_shares():
|
|
r = check_covered_call(
|
|
underlying="TSLA",
|
|
strike=300.0,
|
|
qty=1,
|
|
positions=[],
|
|
)
|
|
assert not r.ok
|
|
|
|
|
|
def test_covered_call_ok():
|
|
positions = [
|
|
{"symbol": "TSLA", "qty": 200, "avg_entry_price": 250.0, "is_option": False}
|
|
]
|
|
r = check_covered_call(
|
|
underlying="TSLA",
|
|
strike=275.0,
|
|
qty=2,
|
|
positions=positions,
|
|
)
|
|
assert r.ok
|
|
|
|
|
|
def test_trailing_stop_needs_long_position():
|
|
r = check_trailing_stop(symbol="TSLA", qty=100, positions=[])
|
|
assert not r.ok
|