initial: alpaclaudia paper-trading bot + dashboard
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.
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
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
|
||||
Reference in New Issue
Block a user