v1.1 autonomous upgrade: continuous curation, risk engine, scheduler hardening, systemd service
This commit is contained in:
@@ -6,7 +6,11 @@ ALPACA_BASE_URL=https://paper-api.alpaca.markets
|
|||||||
# Runtime
|
# Runtime
|
||||||
PAPER_MODE=true
|
PAPER_MODE=true
|
||||||
MAX_ORDER_USD=5
|
MAX_ORDER_USD=5
|
||||||
|
MAX_DAILY_NOTIONAL=50
|
||||||
|
MAX_OPEN_POSITIONS=6
|
||||||
|
MIN_CONFIDENCE=0.60
|
||||||
TRADE_INTERVAL_HOURS=2
|
TRADE_INTERVAL_HOURS=2
|
||||||
|
CURATE_INTERVAL_MINUTES=30
|
||||||
TIMEZONE=America/Los_Angeles
|
TIMEZONE=America/Los_Angeles
|
||||||
|
|
||||||
# Ollama
|
# Ollama
|
||||||
|
|||||||
43
README.md
43
README.md
@@ -1,33 +1,50 @@
|
|||||||
# alpaca-llm-bot-v1
|
# alpaca-llm-bot-v1
|
||||||
|
|
||||||
Autonomous 2-hour trading loop powered by:
|
Autonomous LLM trading system (paper-first) powered by:
|
||||||
- Alpaca trading API
|
- Alpaca trading API
|
||||||
- Ollama local model inference
|
- Ollama local model inference
|
||||||
- Searx web data ingestion
|
- Searx web data ingestion
|
||||||
- FastAPI dark dashboard
|
- FastAPI dark dashboard
|
||||||
|
- APScheduler autonomous loops
|
||||||
|
|
||||||
## Safety defaults
|
## Autonomous behavior
|
||||||
- `PAPER_MODE=true`
|
- Curates market/news context every `CURATE_INTERVAL_MINUTES` (default 30)
|
||||||
- max order notional `$5`
|
- Runs trade decision cycle every `TRADE_INTERVAL_HOURS` (default 2)
|
||||||
- confidence gate `>= 0.55` before order placement
|
- LLM decides buy/sell/hold per symbol
|
||||||
|
- Executes only if confidence >= `MIN_CONFIDENCE`
|
||||||
|
- Hard risk caps:
|
||||||
|
- max `$5` order notional (default)
|
||||||
|
- max daily notional cap
|
||||||
|
- max open positions cap
|
||||||
|
- market-open gate
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
```bash
|
```bash
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
# fill ALPACA_API_KEY / ALPACA_API_SECRET
|
# fill ALPACA_API_KEY / ALPACA_API_SECRET
|
||||||
chmod +x run.sh
|
python3 -m venv .venv
|
||||||
./run.sh
|
source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
uvicorn app:app --host 0.0.0.0 --port 8089
|
||||||
```
|
```
|
||||||
|
|
||||||
Open dashboard:
|
Dashboard:
|
||||||
- `http://<host>:8089/`
|
- `http://<host>:8089/`
|
||||||
|
|
||||||
Trigger immediate cycle:
|
Manual triggers:
|
||||||
```bash
|
```bash
|
||||||
|
curl -X POST http://127.0.0.1:8089/curate-now
|
||||||
curl -X POST http://127.0.0.1:8089/run-now
|
curl -X POST http://127.0.0.1:8089/run-now
|
||||||
```
|
```
|
||||||
|
|
||||||
## Production notes
|
## Production service (systemd)
|
||||||
- Put behind reverse proxy + auth
|
```bash
|
||||||
- Keep paper mode until behavior validated
|
sudo cp systemd/alpaca-llm-bot-v1.service /etc/systemd/system/
|
||||||
- Add hard stop-loss and max daily drawdown before enabling live mode
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable --now alpaca-llm-bot-v1
|
||||||
|
sudo systemctl status alpaca-llm-bot-v1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Important
|
||||||
|
- Keep `PAPER_MODE=true` until you observe stable behavior.
|
||||||
|
- This is experimental and not financial advice.
|
||||||
|
|||||||
BIN
__pycache__/app.cpython-312.pyc
Normal file
BIN
__pycache__/app.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/bot.cpython-312.pyc
Normal file
BIN
__pycache__/bot.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/config.cpython-312.pyc
Normal file
BIN
__pycache__/config.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/db.cpython-312.pyc
Normal file
BIN
__pycache__/db.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/services.cpython-312.pyc
Normal file
BIN
__pycache__/services.cpython-312.pyc
Normal file
Binary file not shown.
13
app.py
13
app.py
@@ -3,8 +3,8 @@ from fastapi.responses import JSONResponse
|
|||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from db import init_db, SessionLocal, BotDecision, TradeExecution
|
from db import init_db, SessionLocal, BotDecision, TradeExecution, CuratedInsight
|
||||||
from bot import start_scheduler, run_cycle
|
from bot import start_scheduler, run_cycle, curate_cycle
|
||||||
from services import account_snapshot
|
from services import account_snapshot
|
||||||
|
|
||||||
app = FastAPI(title="alpaca-llm-bot-v1")
|
app = FastAPI(title="alpaca-llm-bot-v1")
|
||||||
@@ -24,6 +24,11 @@ def run_now():
|
|||||||
run_cycle()
|
run_cycle()
|
||||||
return {"ok": True, "ran": True}
|
return {"ok": True, "ran": True}
|
||||||
|
|
||||||
|
@app.post("/curate-now")
|
||||||
|
def curate_now():
|
||||||
|
curate_cycle()
|
||||||
|
return {"ok": True, "curated": True}
|
||||||
|
|
||||||
@app.get("/api/account")
|
@app.get("/api/account")
|
||||||
def api_account():
|
def api_account():
|
||||||
return JSONResponse(account_snapshot())
|
return JSONResponse(account_snapshot())
|
||||||
@@ -35,12 +40,14 @@ def home(request: Request):
|
|||||||
since = datetime.utcnow() - timedelta(hours=24)
|
since = datetime.utcnow() - timedelta(hours=24)
|
||||||
decisions = db.query(BotDecision).order_by(BotDecision.ts.desc()).limit(120).all()
|
decisions = db.query(BotDecision).order_by(BotDecision.ts.desc()).limit(120).all()
|
||||||
trades = db.query(TradeExecution).order_by(TradeExecution.ts.desc()).limit(120).all()
|
trades = db.query(TradeExecution).order_by(TradeExecution.ts.desc()).limit(120).all()
|
||||||
|
insights = db.query(CuratedInsight).order_by(CuratedInsight.ts.desc()).limit(30).all()
|
||||||
stats = {
|
stats = {
|
||||||
"decisions": db.query(func.count(BotDecision.id)).filter(BotDecision.ts >= since).scalar() or 0,
|
"decisions": db.query(func.count(BotDecision.id)).filter(BotDecision.ts >= since).scalar() or 0,
|
||||||
"trades": db.query(func.count(TradeExecution.id)).filter(TradeExecution.ts >= since).scalar() or 0,
|
"trades": db.query(func.count(TradeExecution.id)).filter(TradeExecution.ts >= since).scalar() or 0,
|
||||||
"executed": db.query(func.count(BotDecision.id)).filter(BotDecision.ts >= since, BotDecision.status == "executed").scalar() or 0,
|
"executed": db.query(func.count(BotDecision.id)).filter(BotDecision.ts >= since, BotDecision.status == "executed").scalar() or 0,
|
||||||
"failed": db.query(func.count(BotDecision.id)).filter(BotDecision.ts >= since, BotDecision.status == "failed").scalar() or 0,
|
"failed": db.query(func.count(BotDecision.id)).filter(BotDecision.ts >= since, BotDecision.status == "failed").scalar() or 0,
|
||||||
|
"insights": db.query(func.count(CuratedInsight.id)).filter(CuratedInsight.ts >= since).scalar() or 0,
|
||||||
}
|
}
|
||||||
return templates.TemplateResponse("index.html", {"request": request, "decisions": decisions, "trades": trades, "stats": stats})
|
return templates.TemplateResponse("index.html", {"request": request, "decisions": decisions, "trades": trades, "insights": insights, "stats": stats})
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|||||||
59
bot.py
59
bot.py
@@ -1,17 +1,49 @@
|
|||||||
from apscheduler.schedulers.background import BackgroundScheduler
|
from apscheduler.schedulers.background import BackgroundScheduler
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
import json
|
import json
|
||||||
|
from sqlalchemy import func
|
||||||
from config import settings
|
from config import settings
|
||||||
from db import SessionLocal, BotDecision, TradeExecution
|
from db import SessionLocal, BotDecision, TradeExecution, CuratedInsight
|
||||||
from services import searx_news, ollama_decide, place_order
|
from services import searx_news, ollama_decide, place_order, market_open, positions_snapshot, summarize_news_with_ollama
|
||||||
|
|
||||||
scheduler = BackgroundScheduler(timezone=settings.timezone)
|
scheduler = BackgroundScheduler(timezone=settings.timezone)
|
||||||
|
|
||||||
|
|
||||||
|
def _daily_spent(db):
|
||||||
|
since = datetime.utcnow() - timedelta(hours=24)
|
||||||
|
return float(db.query(func.coalesce(func.sum(TradeExecution.notional), 0)).filter(TradeExecution.ts >= since).scalar() or 0)
|
||||||
|
|
||||||
|
|
||||||
|
def curate_cycle():
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
for symbol in settings.symbols:
|
||||||
|
news = searx_news(symbol)
|
||||||
|
summary = summarize_news_with_ollama(symbol, news)
|
||||||
|
row = CuratedInsight(symbol=symbol, summary=summary, sources=json.dumps(news)[:60000])
|
||||||
|
db.add(row)
|
||||||
|
db.commit()
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
def run_cycle():
|
def run_cycle():
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
try:
|
try:
|
||||||
|
if not market_open():
|
||||||
|
return
|
||||||
|
spent = _daily_spent(db)
|
||||||
|
if spent >= settings.max_daily_notional:
|
||||||
|
return
|
||||||
|
|
||||||
|
pos = positions_snapshot()
|
||||||
|
if len(pos) >= settings.max_open_positions:
|
||||||
|
return
|
||||||
|
|
||||||
for symbol in settings.symbols:
|
for symbol in settings.symbols:
|
||||||
|
if spent >= settings.max_daily_notional:
|
||||||
|
break
|
||||||
|
|
||||||
news = searx_news(symbol)
|
news = searx_news(symbol)
|
||||||
decision = ollama_decide(symbol, news)
|
decision = ollama_decide(symbol, news)
|
||||||
|
|
||||||
@@ -28,8 +60,20 @@ def run_cycle():
|
|||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(drow)
|
db.refresh(drow)
|
||||||
|
|
||||||
if decision["action"] in {"buy", "sell"} and decision["confidence"] >= 0.55:
|
should_trade = (
|
||||||
res = place_order(symbol, decision["action"], min(settings.max_order_usd, decision["order_usd"]))
|
decision["action"] in {"buy", "sell"}
|
||||||
|
and decision["confidence"] >= settings.min_confidence
|
||||||
|
)
|
||||||
|
|
||||||
|
if should_trade:
|
||||||
|
notional = min(settings.max_order_usd, decision["order_usd"], settings.max_daily_notional - spent)
|
||||||
|
if notional <= 0:
|
||||||
|
drow.status = "risk_blocked"
|
||||||
|
db.add(drow)
|
||||||
|
db.commit()
|
||||||
|
continue
|
||||||
|
|
||||||
|
res = place_order(symbol, decision["action"], notional)
|
||||||
ok = bool(res and res.get("ok"))
|
ok = bool(res and res.get("ok"))
|
||||||
drow.status = "executed" if ok else "failed"
|
drow.status = "executed" if ok else "failed"
|
||||||
db.add(drow)
|
db.add(drow)
|
||||||
@@ -37,11 +81,13 @@ def run_cycle():
|
|||||||
symbol=symbol,
|
symbol=symbol,
|
||||||
side=decision["action"],
|
side=decision["action"],
|
||||||
qty=float((res or {}).get("json", {}).get("qty", 0) or 0),
|
qty=float((res or {}).get("json", {}).get("qty", 0) or 0),
|
||||||
notional=min(settings.max_order_usd, decision["order_usd"]),
|
notional=notional,
|
||||||
alpaca_order_id=(res or {}).get("json", {}).get("id", ""),
|
alpaca_order_id=(res or {}).get("json", {}).get("id", ""),
|
||||||
raw=json.dumps(res)[:60000],
|
raw=json.dumps(res)[:60000],
|
||||||
))
|
))
|
||||||
db.commit()
|
db.commit()
|
||||||
|
if ok:
|
||||||
|
spent += notional
|
||||||
else:
|
else:
|
||||||
drow.status = "skipped"
|
drow.status = "skipped"
|
||||||
db.add(drow)
|
db.add(drow)
|
||||||
@@ -51,5 +97,6 @@ def run_cycle():
|
|||||||
|
|
||||||
|
|
||||||
def start_scheduler():
|
def start_scheduler():
|
||||||
|
scheduler.add_job(curate_cycle, "interval", minutes=settings.curate_interval_minutes, id="curate_cycle", replace_existing=True)
|
||||||
scheduler.add_job(run_cycle, "interval", hours=settings.trade_interval_hours, id="trade_cycle", replace_existing=True)
|
scheduler.add_job(run_cycle, "interval", hours=settings.trade_interval_hours, id="trade_cycle", replace_existing=True)
|
||||||
scheduler.start()
|
scheduler.start()
|
||||||
|
|||||||
@@ -10,7 +10,12 @@ class Settings:
|
|||||||
paper_mode = os.getenv("PAPER_MODE", "true").lower() == "true"
|
paper_mode = os.getenv("PAPER_MODE", "true").lower() == "true"
|
||||||
|
|
||||||
max_order_usd = float(os.getenv("MAX_ORDER_USD", "5"))
|
max_order_usd = float(os.getenv("MAX_ORDER_USD", "5"))
|
||||||
|
max_daily_notional = float(os.getenv("MAX_DAILY_NOTIONAL", "50"))
|
||||||
|
max_open_positions = int(os.getenv("MAX_OPEN_POSITIONS", "6"))
|
||||||
|
min_confidence = float(os.getenv("MIN_CONFIDENCE", "0.60"))
|
||||||
|
|
||||||
trade_interval_hours = int(os.getenv("TRADE_INTERVAL_HOURS", "2"))
|
trade_interval_hours = int(os.getenv("TRADE_INTERVAL_HOURS", "2"))
|
||||||
|
curate_interval_minutes = int(os.getenv("CURATE_INTERVAL_MINUTES", "30"))
|
||||||
timezone = os.getenv("TIMEZONE", "America/Los_Angeles")
|
timezone = os.getenv("TIMEZONE", "America/Los_Angeles")
|
||||||
|
|
||||||
ollama_url = os.getenv("OLLAMA_URL", "http://10.30.20.110:11434")
|
ollama_url = os.getenv("OLLAMA_URL", "http://10.30.20.110:11434")
|
||||||
|
|||||||
8
db.py
8
db.py
@@ -30,6 +30,14 @@ class TradeExecution(Base):
|
|||||||
alpaca_order_id = Column(String(128))
|
alpaca_order_id = Column(String(128))
|
||||||
raw = Column(Text)
|
raw = Column(Text)
|
||||||
|
|
||||||
|
class CuratedInsight(Base):
|
||||||
|
__tablename__ = "insights"
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
ts = Column(DateTime, default=datetime.utcnow)
|
||||||
|
symbol = Column(String(16), index=True)
|
||||||
|
summary = Column(Text)
|
||||||
|
sources = Column(Text)
|
||||||
|
|
||||||
|
|
||||||
def init_db():
|
def init_db():
|
||||||
Base.metadata.create_all(bind=engine)
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|||||||
70
services.py
70
services.py
@@ -1,12 +1,11 @@
|
|||||||
import json
|
import json
|
||||||
import random
|
import random
|
||||||
import requests
|
import requests
|
||||||
from datetime import datetime
|
|
||||||
from config import settings
|
from config import settings
|
||||||
|
|
||||||
|
|
||||||
def searx_news(symbol: str, limit: int = 8):
|
def searx_news(symbol: str, limit: int = 10):
|
||||||
q = f"{symbol} stock news earnings guidance analyst"
|
q = f"{symbol} stock news earnings guidance analyst macro risk"
|
||||||
params = {"q": q, "format": "json", "language": "en"}
|
params = {"q": q, "format": "json", "language": "en"}
|
||||||
try:
|
try:
|
||||||
r = requests.get(settings.searx_url, params=params, timeout=20)
|
r = requests.get(settings.searx_url, params=params, timeout=20)
|
||||||
@@ -14,19 +13,41 @@ def searx_news(symbol: str, limit: int = 8):
|
|||||||
data = r.json()
|
data = r.json()
|
||||||
out = []
|
out = []
|
||||||
for it in data.get("results", [])[:limit]:
|
for it in data.get("results", [])[:limit]:
|
||||||
out.append({"title": it.get("title", ""), "url": it.get("url", ""), "content": it.get("content", "")[:400]})
|
out.append({"title": it.get("title", ""), "url": it.get("url", ""), "content": it.get("content", "")[:500]})
|
||||||
return out
|
return out
|
||||||
except Exception:
|
except Exception:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def summarize_news_with_ollama(symbol: str, context_items: list):
|
||||||
|
payload = {
|
||||||
|
"model": settings.ollama_model,
|
||||||
|
"stream": False,
|
||||||
|
"prompt": json.dumps({
|
||||||
|
"task": "Summarize market-moving info into a concise, neutral brief.",
|
||||||
|
"symbol": symbol,
|
||||||
|
"news": context_items,
|
||||||
|
"format": {"summary": "<=140 words", "bullish_points": ["..."], "bearish_points": ["..."]}
|
||||||
|
}),
|
||||||
|
"format": "json",
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
r = requests.post(f"{settings.ollama_url}/api/generate", json=payload, timeout=45)
|
||||||
|
r.raise_for_status()
|
||||||
|
resp = r.json().get("response", "{}")
|
||||||
|
parsed = json.loads(resp)
|
||||||
|
return parsed.get("summary", "no-summary")
|
||||||
|
except Exception:
|
||||||
|
return "summary-unavailable"
|
||||||
|
|
||||||
|
|
||||||
def ollama_decide(symbol: str, context_items: list):
|
def ollama_decide(symbol: str, context_items: list):
|
||||||
prompt = {
|
prompt = {
|
||||||
"task": "You are a strict trading policy engine. Return JSON only.",
|
"task": "You are a conservative autonomous trading policy engine. Return strict JSON only.",
|
||||||
"constraints": {
|
"constraints": {
|
||||||
"actions": ["buy", "sell", "hold"],
|
"actions": ["buy", "sell", "hold"],
|
||||||
"max_order_usd": settings.max_order_usd,
|
"max_order_usd": settings.max_order_usd,
|
||||||
"style": "conservative intraday swing",
|
"risk": "do not overtrade; prefer hold on weak signal",
|
||||||
},
|
},
|
||||||
"symbol": symbol,
|
"symbol": symbol,
|
||||||
"news": context_items,
|
"news": context_items,
|
||||||
@@ -44,19 +65,18 @@ def ollama_decide(symbol: str, context_items: list):
|
|||||||
"format": "json",
|
"format": "json",
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
r = requests.post(f"{settings.ollama_url}/api/generate", json=payload, timeout=40)
|
r = requests.post(f"{settings.ollama_url}/api/generate", json=payload, timeout=45)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
resp = r.json().get("response", "{}")
|
resp = r.json().get("response", "{}")
|
||||||
d = json.loads(resp)
|
d = json.loads(resp)
|
||||||
action = d.get("action", "hold").lower()
|
action = str(d.get("action", "hold")).lower()
|
||||||
if action not in {"buy", "sell", "hold"}:
|
if action not in {"buy", "sell", "hold"}:
|
||||||
action = "hold"
|
action = "hold"
|
||||||
confidence = float(d.get("confidence", 0.5))
|
confidence = max(0.0, min(1.0, float(d.get("confidence", 0.5))))
|
||||||
order_usd = min(float(d.get("order_usd", settings.max_order_usd)), settings.max_order_usd)
|
order_usd = min(float(d.get("order_usd", settings.max_order_usd)), settings.max_order_usd)
|
||||||
reason = d.get("reason", "fallback")
|
reason = d.get("reason", "fallback")
|
||||||
return {"action": action, "confidence": confidence, "order_usd": order_usd, "reason": reason}
|
return {"action": action, "confidence": confidence, "order_usd": order_usd, "reason": reason}
|
||||||
except Exception:
|
except Exception:
|
||||||
# resilient fallback to hold or tiny buy
|
|
||||||
return {
|
return {
|
||||||
"action": random.choice(["hold", "hold", "buy"]),
|
"action": random.choice(["hold", "hold", "buy"]),
|
||||||
"confidence": 0.3,
|
"confidence": 0.3,
|
||||||
@@ -73,16 +93,6 @@ def alpaca_headers():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def alpaca_last_price(symbol: str):
|
|
||||||
url = f"https://data.alpaca.markets/v2/stocks/{symbol}/trades/latest"
|
|
||||||
try:
|
|
||||||
r = requests.get(url, headers=alpaca_headers(), timeout=20)
|
|
||||||
r.raise_for_status()
|
|
||||||
return float(r.json()["trade"]["p"])
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def place_order(symbol: str, action: str, order_usd: float):
|
def place_order(symbol: str, action: str, order_usd: float):
|
||||||
if action not in {"buy", "sell"}:
|
if action not in {"buy", "sell"}:
|
||||||
return None
|
return None
|
||||||
@@ -107,3 +117,23 @@ def account_snapshot():
|
|||||||
return r.json()
|
return r.json()
|
||||||
except Exception:
|
except Exception:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def positions_snapshot():
|
||||||
|
try:
|
||||||
|
r = requests.get(f"{settings.alpaca_base}/v2/positions", headers=alpaca_headers(), timeout=20)
|
||||||
|
if r.ok:
|
||||||
|
return r.json()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def market_open():
|
||||||
|
try:
|
||||||
|
r = requests.get(f"{settings.alpaca_base}/v2/clock", headers=alpaca_headers(), timeout=20)
|
||||||
|
if r.ok:
|
||||||
|
return bool(r.json().get("is_open", False))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|||||||
14
systemd/alpaca-llm-bot-v1.service
Normal file
14
systemd/alpaca-llm-bot-v1.service
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=alpaca-llm-bot-v1 autonomous trading bot
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
WorkingDirectory=/home/drjones/.openclaw/workspace/alpaca-llm-bot-v1
|
||||||
|
EnvironmentFile=/home/drjones/.openclaw/workspace/alpaca-llm-bot-v1/.env
|
||||||
|
ExecStart=/home/drjones/.openclaw/workspace/alpaca-llm-bot-v1/.venv/bin/uvicorn app:app --host 0.0.0.0 --port 8089
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -7,11 +7,12 @@
|
|||||||
<style>
|
<style>
|
||||||
:root { --bg:#0b0f17; --card:#111827; --text:#e5e7eb; --muted:#9ca3af; --ok:#10b981; --bad:#ef4444; --acc:#60a5fa; }
|
:root { --bg:#0b0f17; --card:#111827; --text:#e5e7eb; --muted:#9ca3af; --ok:#10b981; --bad:#ef4444; --acc:#60a5fa; }
|
||||||
body{background:var(--bg);color:var(--text);font-family:Inter,system-ui,sans-serif;margin:0;padding:24px}
|
body{background:var(--bg);color:var(--text);font-family:Inter,system-ui,sans-serif;margin:0;padding:24px}
|
||||||
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px}
|
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px}
|
||||||
.card{background:var(--card);border:1px solid #1f2937;border-radius:12px;padding:14px}
|
.card{background:var(--card);border:1px solid #1f2937;border-radius:12px;padding:14px}
|
||||||
.h{font-size:13px;color:var(--muted)} .v{font-size:24px;font-weight:700}
|
.h{font-size:13px;color:var(--muted)} .v{font-size:24px;font-weight:700}
|
||||||
table{width:100%;border-collapse:collapse} th,td{padding:8px;border-bottom:1px solid #1f2937;text-align:left;font-size:13px}
|
table{width:100%;border-collapse:collapse} th,td{padding:8px;border-bottom:1px solid #1f2937;text-align:left;font-size:13px}
|
||||||
.buy{color:var(--ok)} .sell{color:var(--bad)} .hold{color:var(--muted)}
|
.buy{color:var(--ok)} .sell{color:var(--bad)} .hold{color:var(--muted)}
|
||||||
|
.row{display:grid;grid-template-columns:2fr 1fr;gap:12px}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -21,24 +22,41 @@
|
|||||||
<div class="card"><div class="h">Trades (24h)</div><div class="v">{{ stats.trades }}</div></div>
|
<div class="card"><div class="h">Trades (24h)</div><div class="v">{{ stats.trades }}</div></div>
|
||||||
<div class="card"><div class="h">Executed</div><div class="v">{{ stats.executed }}</div></div>
|
<div class="card"><div class="h">Executed</div><div class="v">{{ stats.executed }}</div></div>
|
||||||
<div class="card"><div class="h">Failed</div><div class="v">{{ stats.failed }}</div></div>
|
<div class="card"><div class="h">Failed</div><div class="v">{{ stats.failed }}</div></div>
|
||||||
|
<div class="card"><div class="h">Curated Insights</div><div class="v">{{ stats.insights }}</div></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3>Recent Decisions</h3>
|
<div class="row">
|
||||||
<div class="card">
|
<div>
|
||||||
<table>
|
<h3>Recent Decisions</h3>
|
||||||
<thead><tr><th>Time</th><th>Symbol</th><th>Action</th><th>Confidence</th><th>Status</th><th>Reason</th></tr></thead>
|
<div class="card">
|
||||||
<tbody>
|
<table>
|
||||||
{% for d in decisions %}
|
<thead><tr><th>Time</th><th>Symbol</th><th>Action</th><th>Confidence</th><th>Status</th><th>Reason</th></tr></thead>
|
||||||
<tr>
|
<tbody>
|
||||||
<td>{{ d.ts }}</td><td>{{ d.symbol }}</td>
|
{% for d in decisions %}
|
||||||
<td class="{{ d.action }}">{{ d.action }}</td>
|
<tr>
|
||||||
<td>{{ '%.2f'|format(d.confidence or 0) }}</td>
|
<td>{{ d.ts }}</td><td>{{ d.symbol }}</td>
|
||||||
<td>{{ d.status }}</td>
|
<td class="{{ d.action }}">{{ d.action }}</td>
|
||||||
<td>{{ d.reason }}</td>
|
<td>{{ '%.2f'|format(d.confidence or 0) }}</td>
|
||||||
</tr>
|
<td>{{ d.status }}</td>
|
||||||
{% endfor %}
|
<td>{{ d.reason }}</td>
|
||||||
</tbody>
|
</tr>
|
||||||
</table>
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3>Curated Market Briefs</h3>
|
||||||
|
<div class="card" style="max-height:520px;overflow:auto">
|
||||||
|
{% for i in insights %}
|
||||||
|
<div style="margin-bottom:10px;padding-bottom:10px;border-bottom:1px solid #1f2937">
|
||||||
|
<div><b>{{ i.symbol }}</b> · <span class="h">{{ i.ts }}</span></div>
|
||||||
|
<div style="font-size:13px;line-height:1.4">{{ i.summary }}</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3>Recent Trades</h3>
|
<h3>Recent Trades</h3>
|
||||||
|
|||||||
Reference in New Issue
Block a user