rqfc-dev 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
rqfc/__init__.py ADDED
@@ -0,0 +1,110 @@
1
+ # rqfc — RQFC fund trading client
2
+ #
3
+ # A thin, authenticated client for the RQFC trading backend. Traders log in with
4
+ # credentials or an API key issued by an admin; the backend holds each pod's
5
+ # Alpaca keys and submits trades on their behalf. No Alpaca keys ever touch this
6
+ # client.
7
+ #
8
+ # Trader usage:
9
+ # import rqfc
10
+ # rqfc.login("alice@example.com", "password")
11
+ # # or: rqfc.login(api_key="rqfc_...")
12
+ # acct = rqfc.pod("Alpha Equities") # a pod you're assigned to
13
+ # acct.buy("AAPL", 10)
14
+ # acct.positions()
15
+ # acct.sync() # refresh the dashboard
16
+ #
17
+ # Admin usage:
18
+ # rqfc.login("admin@example.com", "password")
19
+ # admin = rqfc.admin()
20
+ # pod = admin.create_pod("Vol Arb", "options", capital=100000,
21
+ # alpaca_api_key="PK...", alpaca_api_secret="...")
22
+ # admin.add_trader(pod["id"], trader_id, role="trader")
23
+ # admin.allocate_capital(pod["id"], 150000)
24
+ from __future__ import annotations
25
+
26
+ import os
27
+
28
+ from ._session import Session
29
+ from .client import Account
30
+ from .admin import Admin
31
+
32
+ __version__ = "1.0.0"
33
+ DEFAULT_BACKEND_URL = "https://fund-tkb1.onrender.com"
34
+
35
+ _session: Session | None = None
36
+
37
+
38
+ def configure(backend_url: str = None) -> Session:
39
+ """Set backend connection details.
40
+
41
+ Defaults to RQFC_BACKEND_URL, then the production RQFC API. Local dev can
42
+ pass backend_url="http://localhost:8000".
43
+ """
44
+ global _session
45
+ backend_url = backend_url or os.environ.get("RQFC_BACKEND_URL", DEFAULT_BACKEND_URL)
46
+ _session = Session(backend_url)
47
+ return _session
48
+
49
+
50
+ def login(email: str = None, password: str = None, *, api_key: str = None,
51
+ backend_url: str = None) -> dict:
52
+ """Authenticate and start a session. Returns your profile."""
53
+ sess = configure(backend_url)
54
+ if api_key:
55
+ if email or password:
56
+ raise ValueError("Use either email/password or api_key, not both.")
57
+ sess.use_api_key(api_key)
58
+ else:
59
+ if not email or not password:
60
+ raise ValueError("Call rqfc.login(email, password) or rqfc.login(api_key='rqfc_...').")
61
+ profile = sess.login(email, password)
62
+ if profile:
63
+ me = profile
64
+ tag = " (admin)" if me.get("is_admin") else ""
65
+ print(
66
+ f"Logged in as {me['display_name']}{tag}. "
67
+ f"Pods: {[p['pods']['name'] for p in me.get('pods', [])] or 'none assigned'}"
68
+ )
69
+ return me
70
+
71
+ me = sess.get("/me")
72
+ tag = " (admin)" if me.get("is_admin") else ""
73
+ print(f"Logged in as {me['display_name']}{tag}. Pods: {[p['pods']['name'] for p in me['pods']] or 'none assigned'}")
74
+ return me
75
+
76
+
77
+ def _require_session() -> Session:
78
+ if _session is None or not _session.access_token:
79
+ raise RuntimeError(
80
+ "Not logged in. Call rqfc.login(email, password) or "
81
+ "rqfc.login(api_key='rqfc_...') first."
82
+ )
83
+ return _session
84
+
85
+
86
+ def pod(name_or_id: str) -> Account:
87
+ """Select a pod to trade, by name or id."""
88
+ return Account(_require_session(), name_or_id)
89
+
90
+
91
+ def admin() -> Admin:
92
+ """Get the admin interface (requires an admin account)."""
93
+ return Admin(_require_session())
94
+
95
+
96
+ def whoami() -> dict:
97
+ """Your profile and pod assignments."""
98
+ return _require_session().get("/me")
99
+
100
+
101
+ __all__ = [
102
+ "DEFAULT_BACKEND_URL",
103
+ "configure",
104
+ "login",
105
+ "pod",
106
+ "admin",
107
+ "whoami",
108
+ "Account",
109
+ "Admin",
110
+ ]
rqfc/_session.py ADDED
@@ -0,0 +1,72 @@
1
+ """Authenticated HTTP session against the RQFC backend.
2
+
3
+ The backend owns auth, authorization, and Alpaca credentials. This client stores
4
+ only a backend-issued token or trader API key and sends it as bearer auth.
5
+ """
6
+ import re
7
+
8
+ import requests
9
+
10
+ _UUID_RE = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", re.I)
11
+
12
+
13
+ def looks_like_uuid(value: str) -> bool:
14
+ return bool(value and _UUID_RE.match(str(value)))
15
+
16
+
17
+ class Session:
18
+ def __init__(self, backend_url: str):
19
+ self.backend_url = backend_url.rstrip("/")
20
+ self.access_token = None
21
+ self.profile = None
22
+
23
+ def login(self, email: str, password: str) -> dict:
24
+ r = requests.post(
25
+ f"{self.backend_url}/auth/login",
26
+ json={"email": email, "password": password},
27
+ timeout=30,
28
+ )
29
+ if r.status_code != 200:
30
+ raise RuntimeError(f"Login failed [{r.status_code}]: {r.text}")
31
+ data = r.json()
32
+ self.access_token = data["token"]
33
+ self.profile = data.get("profile")
34
+ return self.profile or {}
35
+
36
+ def use_api_key(self, api_key: str) -> None:
37
+ if not api_key:
38
+ raise ValueError("api_key is required.")
39
+ self.access_token = api_key
40
+ self.profile = None
41
+
42
+ # ── HTTP helpers ─────────────────────────────────────────────────────────
43
+
44
+ def _headers(self) -> dict:
45
+ if not self.access_token:
46
+ raise RuntimeError(
47
+ "Not logged in. Call rqfc.login(email, password) or "
48
+ "rqfc.login(api_key='rqfc_...') first."
49
+ )
50
+ return {"Authorization": f"Bearer {self.access_token}"}
51
+
52
+ def get(self, path: str, params: dict = None):
53
+ return self._handle(requests.get(
54
+ f"{self.backend_url}{path}", headers=self._headers(), params=params, timeout=60))
55
+
56
+ def post(self, path: str, json: dict = None):
57
+ return self._handle(requests.post(
58
+ f"{self.backend_url}{path}", headers=self._headers(), json=json, timeout=60))
59
+
60
+ def delete(self, path: str, json: dict = None):
61
+ return self._handle(requests.delete(
62
+ f"{self.backend_url}{path}", headers=self._headers(), json=json, timeout=60))
63
+
64
+ @staticmethod
65
+ def _handle(r: requests.Response):
66
+ if r.status_code >= 400:
67
+ try:
68
+ detail = r.json().get("detail", r.text)
69
+ except Exception:
70
+ detail = r.text
71
+ raise RuntimeError(f"[{r.status_code}] {detail}")
72
+ return r.json() if r.content else None
rqfc/admin.py ADDED
@@ -0,0 +1,58 @@
1
+ """Admin operations — manage pods, traders, capital. Admins only.
2
+
3
+ All calls hit the backend's /admin endpoints, which re-check is_admin server-side.
4
+ """
5
+ from ._session import Session
6
+
7
+
8
+ class Admin:
9
+ def __init__(self, session: Session):
10
+ self._s = session
11
+ me = self._s.get("/me")
12
+ if not me.get("is_admin"):
13
+ raise PermissionError("This account does not have admin access.")
14
+
15
+ # ── Pods ─────────────────────────────────────────────────────────────────
16
+
17
+ def create_pod(self, name, asset_class, *, benchmark_symbol="SPY", description=None,
18
+ capital=0, alpaca_api_key=None, alpaca_api_secret=None,
19
+ alpaca_account_id=None) -> dict:
20
+ """Create a pod, optionally attaching its Alpaca paper-account keys."""
21
+ return self._s.post("/admin/pods", {
22
+ "name": name,
23
+ "asset_class": asset_class,
24
+ "benchmark_symbol": benchmark_symbol,
25
+ "description": description,
26
+ "allocated_capital": capital,
27
+ "alpaca_api_key": alpaca_api_key,
28
+ "alpaca_api_secret": alpaca_api_secret,
29
+ "alpaca_account_id": alpaca_account_id,
30
+ })
31
+
32
+ def set_alpaca(self, pod_id, *, api_key=None, api_secret=None, account_id=None) -> dict:
33
+ """Attach or update a pod's Alpaca credentials."""
34
+ return self._s.post(f"/admin/pods/{pod_id}/alpaca", {
35
+ "alpaca_api_key": api_key,
36
+ "alpaca_api_secret": api_secret,
37
+ "alpaca_account_id": account_id,
38
+ })
39
+
40
+ def allocate_capital(self, pod_id, amount, note=None) -> dict:
41
+ """Set a pod's allocated capital (logged to the audit trail)."""
42
+ return self._s.post(f"/admin/pods/{pod_id}/capital", {"amount": amount, "note": note})
43
+
44
+ # ── Memberships ──────────────────────────────────────────────────────────
45
+
46
+ def list_traders(self) -> list:
47
+ """All registered traders (id, display_name, is_admin)."""
48
+ return self._s.get("/admin/traders")
49
+
50
+ def add_trader(self, pod_id, trader_id, role="trader") -> dict:
51
+ """Assign a trader to a pod. role: 'trader' or 'pm'."""
52
+ return self._s.post("/admin/memberships",
53
+ {"pod_id": pod_id, "trader_id": trader_id, "role": role})
54
+
55
+ def remove_trader(self, pod_id, trader_id) -> dict:
56
+ """Remove a trader from a pod."""
57
+ return self._s.delete("/admin/memberships",
58
+ {"pod_id": pod_id, "trader_id": trader_id, "role": "trader"})
rqfc/client.py ADDED
@@ -0,0 +1,86 @@
1
+ """The trader-facing Account object.
2
+
3
+ Every method is an authenticated call to the backend, which holds the pod's
4
+ Alpaca keys and submits on your behalf. You can only act on pods you're assigned
5
+ to (admins can act on any pod).
6
+ """
7
+ from ._session import Session, looks_like_uuid
8
+
9
+
10
+ class Account:
11
+ """A pod you can trade. Obtain one via rqfc.pod(name_or_id)."""
12
+
13
+ def __init__(self, session: Session, pod_ref: str):
14
+ self._s = session
15
+ self.pod_id = self._resolve_pod(pod_ref)
16
+
17
+ def _resolve_pod(self, ref: str) -> str:
18
+ if looks_like_uuid(ref):
19
+ return ref
20
+ for p in self._s.get("/pods"):
21
+ if p["name"].lower() == str(ref).lower():
22
+ return p["id"]
23
+ raise ValueError(f"No pod named '{ref}'. Run rqfc.whoami() to see your pods.")
24
+
25
+ # ── Orders ───────────────────────────────────────────────────────────────
26
+
27
+ def _order(self, **kw):
28
+ return self._s.post("/orders", {"pod_id": self.pod_id, **kw})
29
+
30
+ def buy(self, symbol, qty, order_type="market", limit_price=None, time_in_force="day"):
31
+ """Buy shares. order_type: 'market' (default) or 'limit'."""
32
+ return self._order(symbol=symbol, side="buy", qty=qty, order_type=order_type,
33
+ order_label=order_type, limit_price=limit_price,
34
+ time_in_force=time_in_force)
35
+
36
+ def sell(self, symbol, qty, order_type="market", limit_price=None, time_in_force="day"):
37
+ """Sell shares you hold in the pod."""
38
+ return self._order(symbol=symbol, side="sell", qty=qty, order_type=order_type,
39
+ order_label=order_type, limit_price=limit_price,
40
+ time_in_force=time_in_force)
41
+
42
+ def short(self, symbol, qty, time_in_force="day"):
43
+ """Short sell. Close with cover()."""
44
+ return self._order(symbol=symbol, side="sell", qty=qty, order_label="short",
45
+ time_in_force=time_in_force)
46
+
47
+ def cover(self, symbol, qty, time_in_force="day"):
48
+ """Buy back a short position."""
49
+ return self._order(symbol=symbol, side="buy", qty=qty, order_label="cover",
50
+ time_in_force=time_in_force)
51
+
52
+ def dollar_buy(self, symbol, amount, time_in_force="day"):
53
+ """Buy by dollar amount, e.g. dollar_buy('AAPL', 5000)."""
54
+ return self._order(symbol=symbol, side="buy", notional=amount, order_label="dollar_buy",
55
+ time_in_force=time_in_force)
56
+
57
+ def dollar_sell(self, symbol, amount, time_in_force="day"):
58
+ """Sell by dollar amount."""
59
+ return self._order(symbol=symbol, side="sell", notional=amount, order_label="dollar_sell",
60
+ time_in_force=time_in_force)
61
+
62
+ def cancel(self, order_id):
63
+ """Cancel an open order by its Alpaca order id."""
64
+ return self._s.post("/orders/cancel", {"pod_id": self.pod_id, "order_id": order_id})
65
+
66
+ # ── Read ─────────────────────────────────────────────────────────────────
67
+
68
+ def account(self):
69
+ """Live equity, cash, and buying power for the pod."""
70
+ return self._s.get(f"/pods/{self.pod_id}/account")
71
+
72
+ def positions(self):
73
+ """Live open positions for the pod."""
74
+ return self._s.get(f"/pods/{self.pod_id}/positions")
75
+
76
+ def price(self, symbol):
77
+ """Latest trade price."""
78
+ return self._s.get("/market/price", {"symbol": symbol, "pod_id": self.pod_id})
79
+
80
+ def bars(self, symbol, days=30):
81
+ """Daily OHLCV bars."""
82
+ return self._s.get("/market/bars", {"symbol": symbol, "pod_id": self.pod_id, "days": days})
83
+
84
+ def sync(self):
85
+ """Pull the pod's positions + NAV from Alpaca into the dashboard."""
86
+ return self._s.post(f"/sync/{self.pod_id}")
@@ -0,0 +1,114 @@
1
+ Metadata-Version: 2.4
2
+ Name: rqfc-dev
3
+ Version: 1.0.0
4
+ Summary: Thin client for the RQFC fund trading backend
5
+ Author: RQFC
6
+ Requires-Python: >=3.9
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: requests>=2.28.0
9
+
10
+ # rqfc
11
+
12
+ Thin Python client for the RQFC fund trading backend.
13
+
14
+ Traders log in with credentials or an API key issued by an admin. The backend
15
+ owns authentication, permissions, and each pod's Alpaca credentials, then
16
+ submits trades on the trader's behalf. **No Alpaca keys or Supabase settings
17
+ ever touch this client**. A pod is one Alpaca account; several traders share a
18
+ pod; admins can trade any pod and manage capital/membership.
19
+
20
+ See `../docs/ARCHITECTURE.md` and `../docs/RUNBOOK.md` for the full system.
21
+
22
+ ## Install
23
+ From GitHub:
24
+
25
+ ```bash
26
+ pip install git+https://github.com/Rutgers-Quant-Finance-Club/fund.git
27
+ ```
28
+
29
+ After publishing to PyPI, this becomes:
30
+
31
+ ```bash
32
+ pip install rqfc
33
+ ```
34
+
35
+ For local package development from this repo:
36
+ ```bash
37
+ pip install -e .
38
+ ```
39
+
40
+ ## Trader Login
41
+ By default, `rqfc` talks to the deployed RQFC API:
42
+
43
+ ```text
44
+ https://fund-tkb1.onrender.com
45
+ ```
46
+
47
+ Admins issue either email/password credentials or a trader API key. Traders do
48
+ not need to configure Supabase or Alpaca.
49
+
50
+ Email/password login:
51
+ ```python
52
+ import rqfc
53
+
54
+ rqfc.login("alice@example.com", "password")
55
+ ```
56
+
57
+ API key login, useful for scripts and strategy runners:
58
+ ```python
59
+ import rqfc
60
+
61
+ rqfc.login(api_key="rqfc_...")
62
+ ```
63
+
64
+ ## Local Dev Override
65
+ To test against a local or staging backend, use `RQFC_BACKEND_URL`:
66
+ ```bash
67
+ export RQFC_BACKEND_URL=http://localhost:8000
68
+ ```
69
+
70
+ Or pass it directly:
71
+ ```python
72
+ import rqfc
73
+
74
+ rqfc.login("alice@example.com", "password", backend_url="http://localhost:8000")
75
+ ```
76
+
77
+ ## Trader Usage
78
+ ```python
79
+ import rqfc
80
+
81
+ rqfc.login(api_key="rqfc_...")
82
+
83
+ rqfc.whoami() # your profile + assigned pods
84
+
85
+ acct = rqfc.pod("Alpha Equities") # by name or id; must be assigned
86
+ acct.buy("AAPL", 10)
87
+ acct.sell("AAPL", 5)
88
+ acct.short("TSLA", 3)
89
+ acct.dollar_buy("NVDA", 5000)
90
+ acct.account() # live equity / cash / buying power
91
+ acct.positions() # live positions
92
+ acct.price("AAPL")
93
+ acct.sync() # refresh the dashboard (positions, NAV, metrics)
94
+ ```
95
+
96
+ ## Admin Usage
97
+ ```python
98
+ rqfc.login("admin@example.com", "password")
99
+ admin = rqfc.admin()
100
+
101
+ pod = admin.create_pod("Vol Arb", "options", capital=100000,
102
+ alpaca_api_key="PK...", alpaca_api_secret="...")
103
+ admin.list_traders()
104
+ admin.add_trader(pod["id"], trader_id, role="trader")
105
+ admin.allocate_capital(pod["id"], 150000)
106
+ admin.remove_trader(pod["id"], trader_id)
107
+ ```
108
+
109
+ ## Method Reference
110
+ **Account** (`rqfc.pod(...)`): `buy`, `sell`, `short`, `cover`, `dollar_buy`,
111
+ `dollar_sell`, `cancel`, `account`, `positions`, `price`, `bars`, `sync`.
112
+
113
+ **Admin** (`rqfc.admin()`): `create_pod`, `set_alpaca`, `allocate_capital`,
114
+ `list_traders`, `add_trader`, `remove_trader`.
@@ -0,0 +1,8 @@
1
+ rqfc/__init__.py,sha256=itistNFeXksxp70wqc_iJz6DaeiElyPYU2glimVGA6Y,3431
2
+ rqfc/_session.py,sha256=gbDGGYg2zuUjCcpW6TTlnxI3IKt1l1kRHecJrowM4fs,2669
3
+ rqfc/admin.py,sha256=575p0b0TH21avTEnHvUKY3ZsNKkIFv6uYa0KBh1UjgU,2782
4
+ rqfc/client.py,sha256=VmE9EpufTrUI4CD8Ep8uXwRHcS6OEiF0cU4E32EiN3k,4030
5
+ rqfc_dev-1.0.0.dist-info/METADATA,sha256=DxrNSwXaJ4R3ANtNYmyBixU88MHqF47FTcDi8iVQSRw,2902
6
+ rqfc_dev-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
7
+ rqfc_dev-1.0.0.dist-info/top_level.txt,sha256=5H5kAXL053EVccroS7Hy14k4NBfiie_H14ZoeIb4dVo,5
8
+ rqfc_dev-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ rqfc