systemr-cli 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.
- neo/__init__.py +3 -0
- neo/__main__.py +5 -0
- neo/auth.py +192 -0
- neo/cli.py +205 -0
- neo/client.py +64 -0
- neo/commands/__init__.py +0 -0
- neo/commands/auth_commands.py +303 -0
- neo/commands/chat_commands.py +937 -0
- neo/commands/cron_commands.py +179 -0
- neo/commands/doctor_command.py +178 -0
- neo/commands/eval_commands.py +73 -0
- neo/commands/journal_commands.py +197 -0
- neo/commands/plan_commands.py +77 -0
- neo/commands/risk_commands.py +68 -0
- neo/commands/scan_commands.py +62 -0
- neo/commands/size_commands.py +60 -0
- neo/config.py +70 -0
- neo/confirmation.py +311 -0
- neo/credits.py +98 -0
- neo/cron.py +365 -0
- neo/display/__init__.py +0 -0
- neo/display/chat_renderer.py +127 -0
- neo/display/formatters.py +112 -0
- neo/display/tables.py +53 -0
- neo/display/theme.py +154 -0
- neo/hooks.py +170 -0
- neo/logging.py +56 -0
- neo/model_failover.py +193 -0
- neo/orchestrator.py +288 -0
- neo/profile.py +505 -0
- neo/store.py +405 -0
- neo/streaming.py +315 -0
- neo/types.py +109 -0
- systemr_cli-1.0.0.dist-info/METADATA +191 -0
- systemr_cli-1.0.0.dist-info/RECORD +37 -0
- systemr_cli-1.0.0.dist-info/WHEEL +4 -0
- systemr_cli-1.0.0.dist-info/entry_points.txt +3 -0
neo/streaming.py
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"""SSE streaming client for System R chat endpoint.
|
|
2
|
+
|
|
3
|
+
Implements the Server-Sent Events specification:
|
|
4
|
+
https://html.spec.whatwg.org/multipage/server-sent-events.html
|
|
5
|
+
|
|
6
|
+
Features:
|
|
7
|
+
- Multi-line data: field concatenation (spec-compliant)
|
|
8
|
+
- Comment filtering (lines starting with :)
|
|
9
|
+
- id: field tracking for reconnection via Last-Event-Id
|
|
10
|
+
- retry: field acknowledgment
|
|
11
|
+
- Reconnection with configurable max retries
|
|
12
|
+
- HTTPS validation on API URL
|
|
13
|
+
- Configurable timeouts and endpoint paths
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from typing import Any, AsyncIterator
|
|
21
|
+
|
|
22
|
+
import httpx
|
|
23
|
+
import structlog
|
|
24
|
+
|
|
25
|
+
from neo.config import get_api_url
|
|
26
|
+
|
|
27
|
+
logger = structlog.get_logger(module="streaming")
|
|
28
|
+
|
|
29
|
+
# ── Configurable constants ──────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
MAX_HISTORY_MESSAGES: int = 20
|
|
32
|
+
STREAM_TIMEOUT_SECONDS: float = 180.0
|
|
33
|
+
CONNECT_TIMEOUT_SECONDS: float = 15.0
|
|
34
|
+
BLOCKING_TIMEOUT_SECONDS: float = 120.0
|
|
35
|
+
CHAT_STREAM_PATH: str = "/api/v1/agent/chat/stream"
|
|
36
|
+
CHAT_BLOCKING_PATH: str = "/api/v1/agent/chat"
|
|
37
|
+
CHAT_CONFIRM_PATH: str = "/api/v1/agent/chat/confirm"
|
|
38
|
+
MAX_RETRIES: int = 1
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class SSEEvent:
|
|
43
|
+
"""A single Server-Sent Event from the chat stream.
|
|
44
|
+
|
|
45
|
+
Attributes:
|
|
46
|
+
event: Event type (thinking, action, text_delta, confirmation_required, done, error).
|
|
47
|
+
data: Raw data string (may be multi-line per SSE spec).
|
|
48
|
+
id: Optional event ID for reconnection tracking.
|
|
49
|
+
parsed: Data parsed as JSON dict. Falls back to {"text": data} on parse failure.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
event: str
|
|
53
|
+
data: str
|
|
54
|
+
id: str | None = None
|
|
55
|
+
parsed: dict[str, Any] = field(default_factory=dict)
|
|
56
|
+
|
|
57
|
+
def __post_init__(self) -> None:
|
|
58
|
+
"""Parse data as JSON on construction."""
|
|
59
|
+
if self.data:
|
|
60
|
+
try:
|
|
61
|
+
self.parsed = json.loads(self.data)
|
|
62
|
+
except (json.JSONDecodeError, TypeError):
|
|
63
|
+
self.parsed = {"text": self.data}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class ChatRequest:
|
|
68
|
+
"""Request payload for the chat stream endpoint.
|
|
69
|
+
|
|
70
|
+
Attributes:
|
|
71
|
+
user_input: The user's message (required).
|
|
72
|
+
session_id: Optional session identifier.
|
|
73
|
+
model: Optional model override (e.g., "anthropic.claude-opus-4-6").
|
|
74
|
+
history: Conversation history (truncated to MAX_HISTORY_MESSAGES).
|
|
75
|
+
profile: PROFILE.md content for context injection.
|
|
76
|
+
rules: RULES.md content for rule enforcement.
|
|
77
|
+
research_mode: If True, enriches with news/sentiment data.
|
|
78
|
+
thinking_visible: If True, streams thinking events to the client.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
user_input: str
|
|
82
|
+
session_id: str | None = None
|
|
83
|
+
model: str | None = None
|
|
84
|
+
history: list[dict[str, str]] = field(default_factory=list)
|
|
85
|
+
profile: str | None = None
|
|
86
|
+
rules: str | None = None
|
|
87
|
+
daily_context: str | None = None
|
|
88
|
+
research_mode: bool = False
|
|
89
|
+
thinking_visible: bool = True
|
|
90
|
+
|
|
91
|
+
def to_dict(self) -> dict[str, Any]:
|
|
92
|
+
"""Serialize to API request payload.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Dict ready for JSON serialization. Empty optional fields are omitted.
|
|
96
|
+
"""
|
|
97
|
+
payload: dict[str, Any] = {"user_input": self.user_input}
|
|
98
|
+
if self.session_id:
|
|
99
|
+
payload["session_id"] = self.session_id
|
|
100
|
+
if self.model:
|
|
101
|
+
payload["model"] = self.model
|
|
102
|
+
if self.history:
|
|
103
|
+
payload["history"] = self.history[-MAX_HISTORY_MESSAGES:]
|
|
104
|
+
# Merge profile + daily context into a single context field
|
|
105
|
+
context_parts = []
|
|
106
|
+
if self.profile:
|
|
107
|
+
context_parts.append(self.profile)
|
|
108
|
+
if self.daily_context:
|
|
109
|
+
context_parts.append(self.daily_context)
|
|
110
|
+
if context_parts:
|
|
111
|
+
payload["context"] = "\n\n---\n\n".join(context_parts)
|
|
112
|
+
if self.rules:
|
|
113
|
+
payload["rules"] = self.rules
|
|
114
|
+
payload["research_mode"] = self.research_mode
|
|
115
|
+
return payload
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
async def stream_chat(
|
|
119
|
+
request: ChatRequest,
|
|
120
|
+
access_token: str,
|
|
121
|
+
) -> AsyncIterator[SSEEvent]:
|
|
122
|
+
"""Stream chat responses from the backend via SSE.
|
|
123
|
+
|
|
124
|
+
Implements the full SSE parsing specification:
|
|
125
|
+
- Multi-line data: fields are concatenated with newlines
|
|
126
|
+
- Comment lines (starting with :) are ignored
|
|
127
|
+
- id: field tracked for reconnection via Last-Event-Id header
|
|
128
|
+
- retry: field acknowledged
|
|
129
|
+
- Reconnection on stream drop (up to MAX_RETRIES)
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
request: The chat request payload.
|
|
133
|
+
access_token: Bearer token for authentication.
|
|
134
|
+
|
|
135
|
+
Yields:
|
|
136
|
+
SSEEvent objects: thinking, action, text_delta, confirmation_required, done, error.
|
|
137
|
+
"""
|
|
138
|
+
api_url = get_api_url()
|
|
139
|
+
url = f"{api_url}{CHAT_STREAM_PATH}"
|
|
140
|
+
|
|
141
|
+
headers = {
|
|
142
|
+
"Authorization": f"Bearer {access_token}",
|
|
143
|
+
"Accept": "text/event-stream",
|
|
144
|
+
"Cache-Control": "no-cache",
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
last_event_id: str | None = None
|
|
148
|
+
retries = 0
|
|
149
|
+
|
|
150
|
+
while retries <= MAX_RETRIES:
|
|
151
|
+
try:
|
|
152
|
+
if last_event_id:
|
|
153
|
+
headers["Last-Event-Id"] = last_event_id
|
|
154
|
+
|
|
155
|
+
timeout = httpx.Timeout(
|
|
156
|
+
STREAM_TIMEOUT_SECONDS, connect=CONNECT_TIMEOUT_SECONDS,
|
|
157
|
+
)
|
|
158
|
+
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
159
|
+
async with client.stream(
|
|
160
|
+
"POST", url, json=request.to_dict(), headers=headers,
|
|
161
|
+
) as response:
|
|
162
|
+
if response.status_code != 200:
|
|
163
|
+
body = await response.aread()
|
|
164
|
+
logger.error(
|
|
165
|
+
"stream_http_error",
|
|
166
|
+
status=response.status_code,
|
|
167
|
+
body=body.decode(errors="replace")[:200],
|
|
168
|
+
)
|
|
169
|
+
yield SSEEvent(
|
|
170
|
+
event="error",
|
|
171
|
+
data=json.dumps({
|
|
172
|
+
"message": (
|
|
173
|
+
f"HTTP {response.status_code}: "
|
|
174
|
+
f"{body.decode(errors='replace')[:200]}"
|
|
175
|
+
),
|
|
176
|
+
}),
|
|
177
|
+
)
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
# SSE spec-compliant parser
|
|
181
|
+
event_type = ""
|
|
182
|
+
data_lines: list[str] = []
|
|
183
|
+
event_id: str | None = None
|
|
184
|
+
|
|
185
|
+
async for line in response.aiter_lines():
|
|
186
|
+
# Comment lines — ignore (often keep-alives)
|
|
187
|
+
if line.startswith(":"):
|
|
188
|
+
continue
|
|
189
|
+
|
|
190
|
+
if line.startswith("event:"):
|
|
191
|
+
event_type = line[6:].strip()
|
|
192
|
+
|
|
193
|
+
elif line.startswith("data:"):
|
|
194
|
+
# SSE spec: consecutive data: lines concatenated with \n
|
|
195
|
+
data_lines.append(line[5:].strip())
|
|
196
|
+
|
|
197
|
+
elif line.startswith("id:"):
|
|
198
|
+
event_id = line[3:].strip()
|
|
199
|
+
last_event_id = event_id
|
|
200
|
+
|
|
201
|
+
elif line.startswith("retry:"):
|
|
202
|
+
pass # Acknowledged, not used for timing
|
|
203
|
+
|
|
204
|
+
elif line == "":
|
|
205
|
+
# Empty line = dispatch event
|
|
206
|
+
if data_lines:
|
|
207
|
+
data_str = "\n".join(data_lines)
|
|
208
|
+
evt = event_type or "text_delta"
|
|
209
|
+
yield SSEEvent(
|
|
210
|
+
event=evt, data=data_str, id=event_id,
|
|
211
|
+
)
|
|
212
|
+
# Reset for next event
|
|
213
|
+
event_type = ""
|
|
214
|
+
data_lines = []
|
|
215
|
+
event_id = None
|
|
216
|
+
|
|
217
|
+
# Flush any remaining event at stream end
|
|
218
|
+
if data_lines:
|
|
219
|
+
data_str = "\n".join(data_lines)
|
|
220
|
+
evt = event_type or "text_delta"
|
|
221
|
+
yield SSEEvent(
|
|
222
|
+
event=evt, data=data_str, id=event_id,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# Stream completed normally
|
|
226
|
+
return
|
|
227
|
+
|
|
228
|
+
except (httpx.ReadTimeout, httpx.RemoteProtocolError, httpx.ReadError):
|
|
229
|
+
retries += 1
|
|
230
|
+
logger.warning("stream_connection_lost", retry=retries)
|
|
231
|
+
if retries > MAX_RETRIES:
|
|
232
|
+
yield SSEEvent(
|
|
233
|
+
event="error",
|
|
234
|
+
data=json.dumps({
|
|
235
|
+
"message": "Stream connection lost. Falling back.",
|
|
236
|
+
}),
|
|
237
|
+
)
|
|
238
|
+
return
|
|
239
|
+
|
|
240
|
+
except httpx.ConnectError:
|
|
241
|
+
logger.error("stream_connect_failed")
|
|
242
|
+
yield SSEEvent(
|
|
243
|
+
event="error",
|
|
244
|
+
data=json.dumps({
|
|
245
|
+
"message": "Cannot connect to System R. Check your network.",
|
|
246
|
+
}),
|
|
247
|
+
)
|
|
248
|
+
return
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
async def chat_blocking(
|
|
252
|
+
request: ChatRequest,
|
|
253
|
+
access_token: str,
|
|
254
|
+
) -> dict[str, Any]:
|
|
255
|
+
"""Fallback blocking chat call (non-streaming).
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
request: The chat request payload.
|
|
259
|
+
access_token: Bearer token for authentication.
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
Response dict from the API.
|
|
263
|
+
|
|
264
|
+
Raises:
|
|
265
|
+
httpx.HTTPStatusError: On non-2xx responses.
|
|
266
|
+
"""
|
|
267
|
+
api_url = get_api_url()
|
|
268
|
+
url = f"{api_url}{CHAT_BLOCKING_PATH}"
|
|
269
|
+
headers = {"Authorization": f"Bearer {access_token}"}
|
|
270
|
+
|
|
271
|
+
timeout = httpx.Timeout(
|
|
272
|
+
BLOCKING_TIMEOUT_SECONDS, connect=CONNECT_TIMEOUT_SECONDS,
|
|
273
|
+
)
|
|
274
|
+
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
275
|
+
resp = await client.post(url, json=request.to_dict(), headers=headers)
|
|
276
|
+
resp.raise_for_status()
|
|
277
|
+
return resp.json()
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
async def send_confirmation(
|
|
281
|
+
confirmation_token: str,
|
|
282
|
+
approved: bool,
|
|
283
|
+
access_token: str,
|
|
284
|
+
) -> dict[str, Any]:
|
|
285
|
+
"""Send confirmation response for a pending trade action.
|
|
286
|
+
|
|
287
|
+
The backend pauses execution when it encounters a trade action
|
|
288
|
+
requiring confirmation. It sends a confirmation_required SSE event
|
|
289
|
+
with a token. The CLI prompts the user, then calls this endpoint
|
|
290
|
+
to approve or deny. The backend then resumes or cancels.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
confirmation_token: Token from the confirmation_required event.
|
|
294
|
+
approved: True to approve, False to deny.
|
|
295
|
+
access_token: Bearer token for authentication.
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
Response dict from the API.
|
|
299
|
+
"""
|
|
300
|
+
api_url = get_api_url()
|
|
301
|
+
url = f"{api_url}{CHAT_CONFIRM_PATH}"
|
|
302
|
+
headers = {"Authorization": f"Bearer {access_token}"}
|
|
303
|
+
|
|
304
|
+
payload = {
|
|
305
|
+
"confirmation_token": confirmation_token,
|
|
306
|
+
"approved": approved,
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
timeout = httpx.Timeout(
|
|
310
|
+
BLOCKING_TIMEOUT_SECONDS, connect=CONNECT_TIMEOUT_SECONDS,
|
|
311
|
+
)
|
|
312
|
+
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
313
|
+
resp = await client.post(url, json=payload, headers=headers)
|
|
314
|
+
resp.raise_for_status()
|
|
315
|
+
return resp.json()
|
neo/types.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Financial types for System R CLI.
|
|
2
|
+
|
|
3
|
+
Rule: Decimal for money, int for counts, str for IDs.
|
|
4
|
+
|
|
5
|
+
All financial values in System R use decimal.Decimal to prevent
|
|
6
|
+
floating-point rounding errors that can lose real money. These
|
|
7
|
+
types are the foundation — every module that handles prices,
|
|
8
|
+
risk amounts, or percentages imports from here.
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
from neo.types import Price, RiskAmount, Quantity, Percentage, to_decimal
|
|
12
|
+
|
|
13
|
+
entry = Price("198.50")
|
|
14
|
+
risk = RiskAmount("1046.80")
|
|
15
|
+
shares = Quantity(160)
|
|
16
|
+
risk_pct = Percentage("1.97")
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from decimal import Decimal, ROUND_HALF_UP
|
|
22
|
+
from typing import Union
|
|
23
|
+
|
|
24
|
+
# Type aliases for clarity and type checking.
|
|
25
|
+
# All are Decimal — the alias communicates intent.
|
|
26
|
+
Price = Decimal
|
|
27
|
+
RiskAmount = Decimal
|
|
28
|
+
Percentage = Decimal
|
|
29
|
+
|
|
30
|
+
# Quantity is int — shares, contracts, units. Never fractional.
|
|
31
|
+
Quantity = int
|
|
32
|
+
|
|
33
|
+
# Rounding contexts
|
|
34
|
+
PRICE_PLACES = Decimal("0.01") # $198.50
|
|
35
|
+
RISK_PLACES = Decimal("0.01") # $1046.80
|
|
36
|
+
PCT_PLACES = Decimal("0.01") # 1.97%
|
|
37
|
+
R_MULTIPLE_PLACES = Decimal("0.01") # +2.30R
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def to_decimal(value: Union[str, int, float, Decimal]) -> Decimal:
|
|
41
|
+
"""Convert any numeric value to Decimal safely.
|
|
42
|
+
|
|
43
|
+
Handles str, int, float, and Decimal inputs.
|
|
44
|
+
Float is converted via string to avoid float representation errors.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
value: The numeric value to convert.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
A Decimal representation of the value.
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
InvalidOperation: If the value cannot be converted.
|
|
54
|
+
"""
|
|
55
|
+
if isinstance(value, Decimal):
|
|
56
|
+
return value
|
|
57
|
+
if isinstance(value, float):
|
|
58
|
+
# Convert float via string to avoid repr issues
|
|
59
|
+
# e.g., float 0.1 -> "0.1" -> Decimal("0.1")
|
|
60
|
+
return Decimal(str(value))
|
|
61
|
+
return Decimal(value)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def round_price(value: Decimal) -> Decimal:
|
|
65
|
+
"""Round a price to 2 decimal places (standard for equities).
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
value: The price to round.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Price rounded to nearest cent.
|
|
72
|
+
"""
|
|
73
|
+
return value.quantize(PRICE_PLACES, rounding=ROUND_HALF_UP)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def round_risk(value: Decimal) -> Decimal:
|
|
77
|
+
"""Round a risk amount to 2 decimal places.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
value: The risk amount to round.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Risk amount rounded to nearest cent.
|
|
84
|
+
"""
|
|
85
|
+
return value.quantize(RISK_PLACES, rounding=ROUND_HALF_UP)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def round_pct(value: Decimal) -> Decimal:
|
|
89
|
+
"""Round a percentage to 2 decimal places.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
value: The percentage to round.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Percentage rounded to 2 decimals (e.g., 1.97).
|
|
96
|
+
"""
|
|
97
|
+
return value.quantize(PCT_PLACES, rounding=ROUND_HALF_UP)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def round_r(value: Decimal) -> Decimal:
|
|
101
|
+
"""Round an R-multiple to 2 decimal places.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
value: The R-multiple to round.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
R-multiple rounded to 2 decimals (e.g., 2.30).
|
|
108
|
+
"""
|
|
109
|
+
return value.quantize(R_MULTIPLE_PLACES, rounding=ROUND_HALF_UP)
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: systemr-cli
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: System R AI — trading operating system for agents
|
|
5
|
+
Author-email: System R AI <ashim@systemr.ai>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Environment :: Console
|
|
9
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
16
|
+
Requires-Python: >=3.11
|
|
17
|
+
Requires-Dist: click>=8.0.0
|
|
18
|
+
Requires-Dist: httpx>=0.24.0
|
|
19
|
+
Requires-Dist: prompt-toolkit>=3.0
|
|
20
|
+
Requires-Dist: rich>=13.0.0
|
|
21
|
+
Requires-Dist: structlog>=23.0.0
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# System R CLI
|
|
30
|
+
|
|
31
|
+
Trading operating system in your terminal. Like Claude Code, but for trading.
|
|
32
|
+
|
|
33
|
+
Type natural language. System R runs the full stack — 55 institutional-grade tools, 25 broker adapters, risk engine, G-Score — all from your terminal.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install systemr-cli
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Or from source:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
git clone https://github.com/System-R-AI/systemr-neo.git
|
|
45
|
+
cd systemr-neo
|
|
46
|
+
pip install -e .
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Requires Python 3.11+.
|
|
50
|
+
|
|
51
|
+
## Quick Start
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# Set up your profile
|
|
55
|
+
systemr setup
|
|
56
|
+
|
|
57
|
+
# Connect to System R
|
|
58
|
+
systemr login
|
|
59
|
+
|
|
60
|
+
# Start chatting
|
|
61
|
+
systemr chat
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
That's it. Type in plain English:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
you > buy TSLA 2% risk
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
System R will size the position, check risk, show you the trade card, and ask for confirmation before executing.
|
|
71
|
+
|
|
72
|
+
## Commands
|
|
73
|
+
|
|
74
|
+
### Trading
|
|
75
|
+
| Command | Description |
|
|
76
|
+
|---------|-------------|
|
|
77
|
+
| `systemr chat` | Interactive conversation with System R |
|
|
78
|
+
| `systemr size` | Position sizing (G-formula) |
|
|
79
|
+
| `systemr risk` | Pre-trade risk validation |
|
|
80
|
+
| `systemr eval` | System performance analysis (G-Score) |
|
|
81
|
+
| `systemr scan` | Market scanner |
|
|
82
|
+
| `systemr plan` | Trade planning |
|
|
83
|
+
|
|
84
|
+
### Local
|
|
85
|
+
| Command | Description |
|
|
86
|
+
|---------|-------------|
|
|
87
|
+
| `systemr journal` | Trade journal (add/list/show/edit/export) |
|
|
88
|
+
| `systemr cron` | Scheduled tasks (add/list/remove/run) |
|
|
89
|
+
| `systemr doctor` | Health check and diagnostics |
|
|
90
|
+
| `systemr setup` | Profile and rules wizard |
|
|
91
|
+
|
|
92
|
+
### Auth
|
|
93
|
+
| Command | Description |
|
|
94
|
+
|---------|-------------|
|
|
95
|
+
| `systemr login` | Connect to System R |
|
|
96
|
+
| `systemr logout` | Disconnect |
|
|
97
|
+
| `systemr whoami` | Show current session |
|
|
98
|
+
|
|
99
|
+
## Chat Commands
|
|
100
|
+
|
|
101
|
+
Inside `systemr chat`, use slash commands:
|
|
102
|
+
|
|
103
|
+
| Command | Action |
|
|
104
|
+
|---------|--------|
|
|
105
|
+
| `/morning` | Morning briefing (4 parallel agents) |
|
|
106
|
+
| `/eod` | End of day review + journal |
|
|
107
|
+
| `/plan` | Plan today's trades |
|
|
108
|
+
| `/portfolio` | Show open positions |
|
|
109
|
+
| `/risk` | Risk dashboard |
|
|
110
|
+
| `/memory <query>` | Search past memories |
|
|
111
|
+
| `/sessions` | List recent sessions |
|
|
112
|
+
| `/cron` | Manage scheduled tasks |
|
|
113
|
+
| `/permissions` | View/switch safety profile |
|
|
114
|
+
| `/remember <text>` | Save a memory |
|
|
115
|
+
| `/model <name>` | Switch LLM model |
|
|
116
|
+
| `/credits` | Session credit usage |
|
|
117
|
+
| `/help` | All commands |
|
|
118
|
+
|
|
119
|
+
## Safety
|
|
120
|
+
|
|
121
|
+
Three-tier confirmation protocol:
|
|
122
|
+
|
|
123
|
+
- **AUTO** — Read-only actions (quotes, analysis, calculations)
|
|
124
|
+
- **CONFIRM** — Trade actions (place/cancel/modify order) — show details, y/n
|
|
125
|
+
- **DOUBLE_CONFIRM** — Destructive actions (kill switch) — type "KILL"
|
|
126
|
+
|
|
127
|
+
Permission profiles: `paper` (max safety), `standard` (default), `experienced` (relaxed stops).
|
|
128
|
+
|
|
129
|
+
## Architecture
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
~/.systemr/
|
|
133
|
+
PROFILE.md # Trader identity, risk params
|
|
134
|
+
RULES.md # Hard/soft rules + standing orders
|
|
135
|
+
auth.json # API credentials (chmod 600)
|
|
136
|
+
config.json # Settings, model config
|
|
137
|
+
journal.db # SQLite trade journal
|
|
138
|
+
memory/
|
|
139
|
+
MEMORY.md # Index
|
|
140
|
+
YYYY-MM-DD.md # Daily trading logs
|
|
141
|
+
*.md # Lessons, notes, violations
|
|
142
|
+
sessions/
|
|
143
|
+
last_session.json # Resume state
|
|
144
|
+
cron/
|
|
145
|
+
jobs.json # Scheduled tasks
|
|
146
|
+
runs/ # Run history (JSONL)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
All data stays on your machine. The backend (`agents.systemr.ai`) processes requests — your profile, rules, and journal never leave your disk.
|
|
150
|
+
|
|
151
|
+
## Backend
|
|
152
|
+
|
|
153
|
+
System R CLI connects to `agents.systemr.ai`:
|
|
154
|
+
|
|
155
|
+
- 55 MCP tools (sizing, risk, intelligence, planning, behavioral)
|
|
156
|
+
- 25 broker/exchange adapters
|
|
157
|
+
- 187 domain services
|
|
158
|
+
- Per-agent encryption (AES-128-CBC)
|
|
159
|
+
- Multi-model LLM via AWS Bedrock (Claude, GPT, Nova)
|
|
160
|
+
- Compute credit billing (USDC, SOL, OSR)
|
|
161
|
+
|
|
162
|
+
## Development
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
git clone https://github.com/System-R-AI/systemr-neo.git
|
|
166
|
+
cd systemr-neo
|
|
167
|
+
pip install -e ".[dev]"
|
|
168
|
+
pytest
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Rules
|
|
172
|
+
|
|
173
|
+
- Decimal for all money — never float
|
|
174
|
+
- structlog only — zero `print()`
|
|
175
|
+
- Additive-only — never remove/rename
|
|
176
|
+
- Tests for every module
|
|
177
|
+
|
|
178
|
+
### Stats
|
|
179
|
+
|
|
180
|
+
- 33 source files, ~8,800 lines
|
|
181
|
+
- 206 tests passing
|
|
182
|
+
- Zero bare `print()` statements
|
|
183
|
+
- Python 3.11+
|
|
184
|
+
|
|
185
|
+
## Links
|
|
186
|
+
|
|
187
|
+
- [systemr.ai](https://systemr.ai) — Documentation
|
|
188
|
+
- [agents.systemr.ai](https://agents.systemr.ai) — API
|
|
189
|
+
- [app.systemr.ai](https://app.systemr.ai) — Portal
|
|
190
|
+
|
|
191
|
+
Built by [Ashim Nandi](https://ashimnandi.com) and Shannon at [System R AI](https://systemr.ai).
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
neo/__init__.py,sha256=tfzk1-_akK5JhODp5y_JU5oWNX0sBADJckoZy1aDJi8,89
|
|
2
|
+
neo/__main__.py,sha256=jgcHytKsLPyX3dO6FyuxJqV_NunRjsR0TbmURtwyEJs,72
|
|
3
|
+
neo/auth.py,sha256=hyfCHiusymLl_ZT-x1hdPLp-BWlIe0PjoxPSEBZRmcc,6056
|
|
4
|
+
neo/cli.py,sha256=c14ISxc9RJWolO-pAsImhKpvX8OIBo4_oqjEx9pdt_o,6379
|
|
5
|
+
neo/client.py,sha256=OcZgXBynuoaUU8B2MLaoPl_t0DgF1TquRDkC1r3_jzI,2102
|
|
6
|
+
neo/config.py,sha256=NBwWUg48JHhLqIOONpJOxwKI279TINSH-cVh4f3WJc0,2137
|
|
7
|
+
neo/confirmation.py,sha256=2elJIZu9pUTyabnrHywolFmXidMa78BNJDjWf2Y4vYw,10628
|
|
8
|
+
neo/credits.py,sha256=VINRtrMDvQLwnFofNarGfVuKkMrvIbfGW7fcbGcYRm0,3211
|
|
9
|
+
neo/cron.py,sha256=RRbvAhY2rng2RyuW5Qfqz9s63Pompq6VbxJIbLqyt2E,9806
|
|
10
|
+
neo/hooks.py,sha256=ifeyuwCrQhlu4mDodymu5fFEK9Cwd9LYEKX2IQOvwBQ,4903
|
|
11
|
+
neo/logging.py,sha256=vU_kWAZs6BQg8QfepRfau0Yeu8AALUdU-oOwH_P8nMs,1707
|
|
12
|
+
neo/model_failover.py,sha256=oxxG0xfoY_5s5plrd0fX9itfvk-avxuWYApPH_yZH1I,5957
|
|
13
|
+
neo/orchestrator.py,sha256=WmOkMtOS3Bqr1nChsPuVQ6KcfF9BZFRcr-n2FKq9JuM,9718
|
|
14
|
+
neo/profile.py,sha256=L9vSsELJwPmqEu3sNoBa3ALZbm9fVRtYlXf49c_nlrA,14985
|
|
15
|
+
neo/store.py,sha256=Ws8ZOa0A9QbKQFI87afgEvxcTkcXjv9FDAyV6W4H_1w,13189
|
|
16
|
+
neo/streaming.py,sha256=ot2jkfq0fj4KPpjkJbQCijCQFFZWIPh_tMWXyaszfVg,11009
|
|
17
|
+
neo/types.py,sha256=9hVIylz_yl9r8dnWg2OmXCpfZiggIR2evi6E067EQ7o,2922
|
|
18
|
+
neo/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
|
+
neo/commands/auth_commands.py,sha256=FxhLmYtMvQ8jrMu6VZh7IWYcArJ3pkJYCpDCDbX8ObA,11455
|
|
20
|
+
neo/commands/chat_commands.py,sha256=E0kSUDRoF3pT6qo5vPSy_Zqi-BpBZ_oDA3kf1zVImr0,35003
|
|
21
|
+
neo/commands/cron_commands.py,sha256=RhXRaj-MhYrZierQAzQ3Iv67x4cj1V_RVQwgb8FdYfg,5682
|
|
22
|
+
neo/commands/doctor_command.py,sha256=Ammu2R55fdLrtNMgrnE8W2eR4J2R8lY2YkJHsiIeSk4,6088
|
|
23
|
+
neo/commands/eval_commands.py,sha256=CPyy910ZEl3_eOLeGqesWtnuuwnWiatdilrPbpdhToQ,2385
|
|
24
|
+
neo/commands/journal_commands.py,sha256=rKowVMlkfWT3hU9gQshfnDcyeSikvSecyd5wB-CN7NA,6695
|
|
25
|
+
neo/commands/plan_commands.py,sha256=Qp7bCGnu_1Ti1M5TG6s2SAEy9sSHrXV0Q_TRqNcXV4g,2897
|
|
26
|
+
neo/commands/risk_commands.py,sha256=aU6bNHBbVaDreXAtLOYXBEWgHRIJrucAQuuxmaUJNNE,2416
|
|
27
|
+
neo/commands/scan_commands.py,sha256=nvwT_FpknmgZsnzmL_OcHYDuKaNmycP8Pd-w5EzDiVo,2014
|
|
28
|
+
neo/commands/size_commands.py,sha256=wbhWkDDwEkXvPTTRq-nYTdEwAI7XUQsPDM5N5y3a118,2268
|
|
29
|
+
neo/display/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
30
|
+
neo/display/chat_renderer.py,sha256=FlDDZIingiowYPDmaWkGoBOEV6KZNUGVhmIX7qfTa2k,3681
|
|
31
|
+
neo/display/formatters.py,sha256=lPUsYHVDORa-q0HwfuwOx83npQfPojO1ACsU3lDpIFo,2808
|
|
32
|
+
neo/display/tables.py,sha256=lPFYIeUnJSCNGeuOR3f2GFcEUOg0fKiSf4meOk2p18Q,1625
|
|
33
|
+
neo/display/theme.py,sha256=gBCXAAMZwYQV-Y-hClCsFcSwpVUEV3NQaWSxZOcC8k8,4941
|
|
34
|
+
systemr_cli-1.0.0.dist-info/METADATA,sha256=85BFL54avhpV4oWhX9Ejsm0YHBYJXEPFKXc1OvLeOec,5338
|
|
35
|
+
systemr_cli-1.0.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
36
|
+
systemr_cli-1.0.0.dist-info/entry_points.txt,sha256=KBhZic5uYphEuxbD5KCmJU2f770dxHaCFxPK_HGwFkA,58
|
|
37
|
+
systemr_cli-1.0.0.dist-info/RECORD,,
|