From 4d17bf801894446df1e4a4a81ed77f9252efa622 Mon Sep 17 00:00:00 2001 From: drjones Date: Wed, 25 Feb 2026 19:09:05 -0800 Subject: [PATCH] v1.1 autonomous upgrade: continuous curation, risk engine, scheduler hardening, systemd service --- .env.example | 4 ++ README.md | 43 +++++++++++----- __pycache__/app.cpython-312.pyc | Bin 0 -> 4928 bytes __pycache__/bot.cpython-312.pyc | Bin 0 -> 5448 bytes __pycache__/config.cpython-312.pyc | Bin 0 -> 2704 bytes __pycache__/db.cpython-312.pyc | Bin 0 -> 2578 bytes __pycache__/services.cpython-312.pyc | Bin 0 -> 7268 bytes app.py | 13 +++-- bot.py | 59 +++++++++++++++++++--- config.py | 5 ++ db.py | 8 +++ services.py | 70 +++++++++++++++++++-------- systemd/alpaca-llm-bot-v1.service | 14 ++++++ templates/index.html | 52 +++++++++++++------- 14 files changed, 209 insertions(+), 59 deletions(-) create mode 100644 __pycache__/app.cpython-312.pyc create mode 100644 __pycache__/bot.cpython-312.pyc create mode 100644 __pycache__/config.cpython-312.pyc create mode 100644 __pycache__/db.cpython-312.pyc create mode 100644 __pycache__/services.cpython-312.pyc create mode 100644 systemd/alpaca-llm-bot-v1.service diff --git a/.env.example b/.env.example index 5ba7b55..fd5697c 100644 --- a/.env.example +++ b/.env.example @@ -6,7 +6,11 @@ ALPACA_BASE_URL=https://paper-api.alpaca.markets # Runtime PAPER_MODE=true MAX_ORDER_USD=5 +MAX_DAILY_NOTIONAL=50 +MAX_OPEN_POSITIONS=6 +MIN_CONFIDENCE=0.60 TRADE_INTERVAL_HOURS=2 +CURATE_INTERVAL_MINUTES=30 TIMEZONE=America/Los_Angeles # Ollama diff --git a/README.md b/README.md index 3bae18e..3d70963 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,50 @@ # alpaca-llm-bot-v1 -Autonomous 2-hour trading loop powered by: +Autonomous LLM trading system (paper-first) powered by: - Alpaca trading API - Ollama local model inference - Searx web data ingestion - FastAPI dark dashboard +- APScheduler autonomous loops -## Safety defaults -- `PAPER_MODE=true` -- max order notional `$5` -- confidence gate `>= 0.55` before order placement +## Autonomous behavior +- Curates market/news context every `CURATE_INTERVAL_MINUTES` (default 30) +- Runs trade decision cycle every `TRADE_INTERVAL_HOURS` (default 2) +- 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 ```bash cp .env.example .env # fill ALPACA_API_KEY / ALPACA_API_SECRET -chmod +x run.sh -./run.sh +python3 -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt +uvicorn app:app --host 0.0.0.0 --port 8089 ``` -Open dashboard: +Dashboard: - `http://:8089/` -Trigger immediate cycle: +Manual triggers: ```bash +curl -X POST http://127.0.0.1:8089/curate-now curl -X POST http://127.0.0.1:8089/run-now ``` -## Production notes -- Put behind reverse proxy + auth -- Keep paper mode until behavior validated -- Add hard stop-loss and max daily drawdown before enabling live mode +## Production service (systemd) +```bash +sudo cp systemd/alpaca-llm-bot-v1.service /etc/systemd/system/ +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. diff --git a/__pycache__/app.cpython-312.pyc b/__pycache__/app.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..34104a72f893d0305a9822a8d528a2ae892e7818 GIT binary patch literal 4928 zcmc&%YitzP6}~h3e%RjiuE(>6@<>eMwavP*p^);ZFpI%4ED3gb?E2ARXUxueW_OvH z1#H<7C`fVDMhU-MBugX`Qi`xdDAWYsq~Z*x(Xxg&sTWS@KqTXs$Kg zR~)UCY(p8rHeN-yOGEKCK<}!>73*wM1?{UrZv^z7HPHSFy153uX&rQ+f)3W8H?PB_ zrGlnv&|B6)w^qp;3UH(>7>9J-lT~xwt$e2NhY)T44^YiPEKW&89yWBm?X1Z5X9|g zv&!}hZv)Jx@CwX!DK&d>vAo13m8>*vILsCExn%9-lF-2R|GXQ-0#Zsghbq=XAf@ad zHGR|Q77+`R5_T%73B)b*93*;SXL7knZd#)702U4K$XHxPf3te74t`|~d}(cw+Ms8~ z(UamBK}oI62n z_W8X9Yin^mXAhk5_DuHTOAF%d;ph1l5NjU+kN_Hsq)B50_WeD88WxGkgbB$9^?E`M ze3`J2v=s*NG02z}K7xj;Lo3bHFBR3(Ua+H7fK0*{Qh0%z!HU8XxUx>NIX6|7w*!52i2;gE_>o~+m~z+Lc| z^nU1gnO^yeF-N#+EIf_3Q&-AJ1vC^3ODvJ)&CGzA&XKc38n0AW6!V{hJIm-a_Jq1p zMWq|-hM<|oo31NmMis4rIsi0TOJ%g>o1uE2!7N+`0~PY+B#W zyn^~pnOiHT^`Fk%whnXbg=R@_EZ3^<8Y{tHxw=wbxBO1y?@nE*(XfV{&5~22HFo-D z-DGH%ZRQdoc2TvMSKUQb!|U5c#jn$^x{IoYb$3y7U-%ncS6KTLd~t8%|2LHM z|LVS~JYU~V9sie|R=)_Zy3?wLb?>y|pZ~>uvCc)?oL#|xKeMF6I|l_ZW_@E_N(L0~UUOE};&oLPzGq=~cB2C2w~ z6&6hzHh~v;#jr4fAmLw_gzrYezjDK72Gs`1W8%D^KoD-1A(UtE8&=KBQV`>O<>a88 zHo#0F3)zFiWNeU-3)o~nljQ2iOE^c9@mgDor5LSRQ(2LdqR@@~TD}g42vLoz<>#9& z_pg$Tw!l|DdZFoBljhrcbMV*w%k8^V->!MvN|0JOa_z`sLJNlVV6PhNy}ReP`+u`v z?;BA22DH8-T5wPg9#@0M3-1kU!4W-pS`D7og7JCRia)SGU!xZznm?@jdsTn$U6>YC z`=WZ^pxQU6^$ltMqq_fu>OWB!KB@Un>HdW3PiX$JdHYJBWnuTV-HS0T(4_}dpau=a9N{NZWZ#3&ixmh#D9v{N$7tIIRap)xfA07@v1O32nGRU#D-jf6@7Q z=ThR%_^om6jlEiEpB{>;q3Hd@gYkRgg;S&Ip;0X~riWNH#A>0*hoPxLdTNED7gEoZFcy>pM+xkuZ)SEKgnR8*y+_s8`^C)GnI3-Jl{(1b?)RHx3V)H#jfA5xiu zm|3CQZaA+yZw9|;`@C&w=uYfbOxwCgqxb6cL6tsuf9OH%UaW93p+*zSV<~+stBz$0 z@|-$0r_mq$6QR%m`OK7%+=PyhUxPeGt|D}lJVw4uuIoWWCE)LJ%!d!xDBUrI{(%sI)9C%EPF z0wX~7D^8o(`L3CA785T&R_2sB=>ngGj|@&Gz}a)T zqU$=dnF;O!mr=}I)Ucf66f;RQthubLnAw?`0GT<4>5_Gs$P4FQF^d!Q{nBwzjZ+Kx z?;ut!1VKDT?T=9BBNTarHatchkJ0;IqW2%8-74Ds2yOd|y`d0%YuVm$+45)ahR>#! zy}g%RPYCNr8?S7f=QVd^Ogy-~q&8ep8?LI|WDXKUcWDr6MYVX%z=s=GNgomT^sTQE OyfCEk8SXIitA7IyfrVE9 literal 0 HcmV?d00001 diff --git a/__pycache__/bot.cpython-312.pyc b/__pycache__/bot.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6e6b9288dbf9fc1b234527814bbfacc1a41f9f66 GIT binary patch literal 5448 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 literal 0 HcmV?d00001 diff --git a/__pycache__/config.cpython-312.pyc b/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a64e7c3d9c4ad614f7509206cd7633ba889ee0cb GIT binary patch literal 2704 zcmaKuPfQzE8o=L-4Yn~hu`vNdLQ=YEIwUa$0*y)9YQPh4;(yqt0!!`F;5P-6vB#Mi zCpBtRSIPm@O27%{1U)rX4x1y#p0VM)>Z!L@QMJ1{?Hm6qyVTgyoANM(YQr#`7KICmuo|aTU8fa2IEvYl;Y?C_Ok{U$knpAH~ z>O6X@N%aw`bw!8J+YPeWHQttf|AHT_!C zz2g>*4p*qJJtq@uf!5YL^w;BNkTE@=9)R@O(!~r-)mQfc^Ded~1y#-FR}}1I!*O^}Gh9ZfnN$iY-qHrb_y%`E}(HU;t?DtLju_u*agB%P+Q(WRBHVo%t3kiac z;og~r1e-ddA?6DyE{W-JKenbq5iT8za_ik}PLQ)1ek3d@kj<|MqCj>Z3y0YVTg}yL zuL_EaZAp$zELL3%{=bXy ziK)rJWMD)Qcsa9*yOJ}B>gzJ9V7%Wyi8(Pfc6D@Y0$YO9FwUmt*4@foF{=t>XCuCm zWl8m+Wo%{Raqrxl2=5kov96&d1-GvV#<_w!Y6XXPgc5et=ZcV#1r^)t%Vk~> zaGNOI7i8Q~?G(c?5q(MyC>1+${AW;-ks!mOg0S0IB0ejYU|v$QQl1yFDVtYo3aKFE zVL?)|hK96lE}MrLDgS8}3HgkGJ*vzj0TNu0@9`pBm5Q>0dox9u6ypyuu_}_{V5^$V z3F}f`z#Wn(@;M$BWf6DQ=f);QY%_}USyf?IGP2R=kTg2Bpk-L#)m3a>l}MS*1xZ$M zn^GXfAP-f}7I0g!Kxk}HO1Wi8R2Ul5K0l@u#W=kE{x`Pj&R2Yel1#1!pgI^wv+{Rx zOg#jLuKsQJu6M_)4P1Jh&|UuWtwYb*?djdnPDmRZeLALlCd-jS_rUhhF0;dEXRc@? z^SV1xzTLE}+<1JN7(RDRK66d#u4@N3pSh;Xw_bU?6{a$@$2?;8V^5QMe?a$4l_M{_ zgOyT6-YY#SY0Ucvs(vo0dpXT~`i1vQWvPskH^nsh2x$ni`Tl>uZ(6`JtjCPs(Ue$-f`d~ye z4;*&&ZOB`t%~GXbyD+79T`$kQY~)|Lp)o<-#gY8G&uowF26h6KPaZ?P`~7nG%E1?Q=SHcqi~W!EuFJapN;!BqJpOp-ugrJM)6hXl9}Ybqjy)TWY4M~!oOC literal 0 HcmV?d00001 diff --git a/__pycache__/db.cpython-312.pyc b/__pycache__/db.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..85347084f586b91b0e848a5adc7ee6da77751c51 GIT binary patch literal 2578 zcmc&$%WE4)7@w6^`;fGf?8tJ0ok!b{Wb=fymp}>BcAN)^dx+>^8(3C56MLO~*t4s~ zIuzCr=)pZG_z;&K>O!Fj_8-vyp_c}`xEdPL(o20S*uCV`Z+0zNO??T4c3^)qzi%Fz z`M%$Lqwh5>kD&bd-B-?h4x!&T=@#Hg+4%*kCkP`fxX2R>!OIvKFKc8y(Gb0yk@F-& z@?=96_?V2Vc&ed#c_W|jtXuF3Mj>g7Zqd^WO+Xn$)U|YVWR!pjM4LUfXXvYNZe3c- zqz0n`pOJ3e93c;GfYpUF4|p;QX?tDljmz_K@D*^b>r_~0 z-{r-B_IJk_TJjsE9xo@n(&G>u*w;RXumIm54*}Wx?Os5!(zmy2OV#tNFcw4;#CEv* zmTWh=Iy}0?_lEFgsRCX5LgJuDZ%a^kjRAo2&BW*fMP<;W{8;-Mpo1e zpP?O?fL`TrFZ0I>Sf^C zp~sUemzQs?;!W*bJlW$L+Hqh5!z;Pvqwy^;w)X#nIy&PH^byioV5ZPgwMAPT%3c zWkpW{FfjxTrHabVM=hrK0pEhZC*iulTbH`PcN)$D zZ~=|Jp@0elk58=(EbAME(RiYhue67jF2v)V@=%*BeH!2B)JDJfXm$L{k2h+1{2}kJ zE}dT-Pq)E$aIk7Vho1jdHAL%0m%l+`A1}9LU16p1CZutKZ~CD#Jp;MkNgD^pdrGhT z>nNw8lf$ORGaHk`ph;~Kz8=Rb;48|3>n6Mgz~gxA4dYmEO8dTa2Oy)Ka0v|cNXt#iKEZO(Qi)uAddbh z>dDcj^cu^D^RDIEGsJ6AeyF}nnA|m`U=l#S@7E6XN_jthlCIJl(98emgc#tDtRM(4 zQGE;5pQHLqbmav)`T~voDi3eSM`E#~*4hhO!>6AQpMFX<)%RklQyy8ZZI;i(#a%Hk iyxTsxi=f&)tO~>J`YwWMw`>^ literal 0 HcmV?d00001 diff --git a/__pycache__/services.cpython-312.pyc b/__pycache__/services.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e12be40792cc6aaf0581b4c5ce937f1e4d24f7dd GIT binary patch literal 7268 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 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, "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: db.close() diff --git a/bot.py b/bot.py index 02aa83e..7c47798 100644 --- a/bot.py +++ b/bot.py @@ -1,17 +1,49 @@ from apscheduler.schedulers.background import BackgroundScheduler -from datetime import datetime +from datetime import datetime, timedelta import json +from sqlalchemy import func from config import settings -from db import SessionLocal, BotDecision, TradeExecution -from services import searx_news, ollama_decide, place_order +from db import SessionLocal, BotDecision, TradeExecution, CuratedInsight +from services import searx_news, ollama_decide, place_order, market_open, positions_snapshot, summarize_news_with_ollama 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(): db = SessionLocal() 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: + if spent >= settings.max_daily_notional: + break + news = searx_news(symbol) decision = ollama_decide(symbol, news) @@ -28,8 +60,20 @@ def run_cycle(): db.commit() db.refresh(drow) - if decision["action"] in {"buy", "sell"} and decision["confidence"] >= 0.55: - res = place_order(symbol, decision["action"], min(settings.max_order_usd, decision["order_usd"])) + should_trade = ( + 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")) drow.status = "executed" if ok else "failed" db.add(drow) @@ -37,11 +81,13 @@ def run_cycle(): symbol=symbol, side=decision["action"], 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", ""), raw=json.dumps(res)[:60000], )) db.commit() + if ok: + spent += notional else: drow.status = "skipped" db.add(drow) @@ -51,5 +97,6 @@ def run_cycle(): 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.start() diff --git a/config.py b/config.py index 693b832..61b0c87 100644 --- a/config.py +++ b/config.py @@ -10,7 +10,12 @@ class Settings: paper_mode = os.getenv("PAPER_MODE", "true").lower() == "true" 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")) + curate_interval_minutes = int(os.getenv("CURATE_INTERVAL_MINUTES", "30")) timezone = os.getenv("TIMEZONE", "America/Los_Angeles") ollama_url = os.getenv("OLLAMA_URL", "http://10.30.20.110:11434") diff --git a/db.py b/db.py index 084d4b8..167432a 100644 --- a/db.py +++ b/db.py @@ -30,6 +30,14 @@ class TradeExecution(Base): alpaca_order_id = Column(String(128)) 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(): Base.metadata.create_all(bind=engine) diff --git a/services.py b/services.py index 83ae10b..5c4c291 100644 --- a/services.py +++ b/services.py @@ -1,12 +1,11 @@ import json import random import requests -from datetime import datetime from config import settings -def searx_news(symbol: str, limit: int = 8): - q = f"{symbol} stock news earnings guidance analyst" +def searx_news(symbol: str, limit: int = 10): + q = f"{symbol} stock news earnings guidance analyst macro risk" params = {"q": q, "format": "json", "language": "en"} try: 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() out = [] 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 except Exception: 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): 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": { "actions": ["buy", "sell", "hold"], "max_order_usd": settings.max_order_usd, - "style": "conservative intraday swing", + "risk": "do not overtrade; prefer hold on weak signal", }, "symbol": symbol, "news": context_items, @@ -44,19 +65,18 @@ def ollama_decide(symbol: str, context_items: list): "format": "json", } 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() resp = r.json().get("response", "{}") d = json.loads(resp) - action = d.get("action", "hold").lower() + action = str(d.get("action", "hold")).lower() if action not in {"buy", "sell", "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) reason = d.get("reason", "fallback") return {"action": action, "confidence": confidence, "order_usd": order_usd, "reason": reason} except Exception: - # resilient fallback to hold or tiny buy return { "action": random.choice(["hold", "hold", "buy"]), "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): if action not in {"buy", "sell"}: return None @@ -107,3 +117,23 @@ def account_snapshot(): return r.json() except Exception: 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 diff --git a/systemd/alpaca-llm-bot-v1.service b/systemd/alpaca-llm-bot-v1.service new file mode 100644 index 0000000..e85cdd8 --- /dev/null +++ b/systemd/alpaca-llm-bot-v1.service @@ -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 diff --git a/templates/index.html b/templates/index.html index 571c9de..dbf006c 100644 --- a/templates/index.html +++ b/templates/index.html @@ -7,11 +7,12 @@ @@ -21,24 +22,41 @@
Trades (24h)
{{ stats.trades }}
Executed
{{ stats.executed }}
Failed
{{ stats.failed }}
+
Curated Insights
{{ stats.insights }}
-

Recent Decisions

-
- - - - {% for d in decisions %} - - - - - - - - {% endfor %} - -
TimeSymbolActionConfidenceStatusReason
{{ d.ts }}{{ d.symbol }}{{ d.action }}{{ '%.2f'|format(d.confidence or 0) }}{{ d.status }}{{ d.reason }}
+
+
+

Recent Decisions

+
+ + + + {% for d in decisions %} + + + + + + + + {% endfor %} + +
TimeSymbolActionConfidenceStatusReason
{{ d.ts }}{{ d.symbol }}{{ d.action }}{{ '%.2f'|format(d.confidence or 0) }}{{ d.status }}{{ d.reason }}
+
+
+ +
+

Curated Market Briefs

+
+ {% for i in insights %} +
+
{{ i.symbol }} ยท {{ i.ts }}
+
{{ i.summary }}
+
+ {% endfor %} +
+

Recent Trades