Skip to content

Commit 3d3a29b

Browse files
authored
Merge pull request #123 from TraderAlice/dev
docs: rewrite README positioning and structure
2 parents 83f6035 + a7dc107 commit 3d3a29b

6 files changed

Lines changed: 438 additions & 463 deletions

File tree

README.md

Lines changed: 69 additions & 177 deletions
Large diffs are not rendered by default.

docs/project-structure.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Project Structure
2+
3+
Open Alice is a pnpm monorepo with Turborepo build orchestration.
4+
5+
```
6+
packages/
7+
├── ibkr/ # @traderalice/ibkr — IBKR TWS API TypeScript port
8+
└── opentypebb/ # @traderalice/opentypebb — OpenBB platform TS port
9+
ui/ # React frontend (Vite, 13 pages)
10+
src/
11+
├── main.ts # Composition root — wires everything together
12+
├── core/
13+
│ ├── agent-center.ts # Top-level AI orchestration, owns ProviderRouter
14+
│ ├── ai-provider-manager.ts # GenerateRouter + StreamableResult + AskOptions
15+
│ ├── tool-center.ts # Centralized tool registry (Vercel + MCP export)
16+
│ ├── mcp-export.ts # Shared MCP export layer with type coercion
17+
│ ├── session.ts # JSONL session store + format converters
18+
│ ├── compaction.ts # Auto-summarize long context windows
19+
│ ├── config.ts # Zod-validated config loader
20+
│ ├── event-log.ts # Append-only JSONL event log
21+
│ ├── connector-center.ts # ConnectorCenter — push delivery + last-interacted tracking
22+
│ ├── async-channel.ts # AsyncChannel for streaming provider events to SSE
23+
│ ├── tool-call-log.ts # Tool invocation logging
24+
│ ├── media.ts # MediaAttachment extraction
25+
│ ├── media-store.ts # Media file persistence
26+
│ └── types.ts # Plugin, EngineContext interfaces
27+
├── ai-providers/
28+
│ ├── vercel-ai-sdk/ # Vercel AI SDK ToolLoopAgent wrapper
29+
│ ├── agent-sdk/ # Claude backend (@anthropic-ai/claude-agent-sdk, OAuth + API key)
30+
│ └── mock/ # Mock provider (testing)
31+
├── domain/
32+
│ ├── trading/ # Unified multi-account trading, guard pipeline, git-like commits
33+
│ │ ├── account-manager.ts # UTA lifecycle (init, reconnect, enable/disable) + registry
34+
│ │ ├── git-persistence.ts # Git state load/save
35+
│ │ ├── brokers/
36+
│ │ │ ├── registry.ts # Broker self-registration (configSchema + configFields + fromConfig)
37+
│ │ │ ├── alpaca/ # Alpaca (US equities)
38+
│ │ │ ├── ccxt/ # CCXT (100+ crypto exchanges)
39+
│ │ │ ├── ibkr/ # Interactive Brokers (TWS/Gateway)
40+
│ │ │ └── mock/ # In-memory test broker
41+
│ │ ├── git/ # Trading-as-Git engine (stage → commit → push)
42+
│ │ ├── guards/ # Pre-execution safety checks (position size, cooldown, whitelist)
43+
│ │ └── snapshot/ # Periodic + event-driven account state capture, equity curve
44+
│ ├── market-data/ # Structured data layer (opentypebb in-process + OpenBB API remote)
45+
│ │ ├── equity/ # Equity data + SymbolIndex (SEC/TMX local cache)
46+
│ │ ├── crypto/ # Crypto data layer
47+
│ │ ├── currency/ # Currency/forex data layer
48+
│ │ ├── commodity/ # Commodity data layer (EIA, spot prices)
49+
│ │ ├── economy/ # Macro economy data layer
50+
│ │ └── client/ # Data backend clients (opentypebb SDK, openbb-api)
51+
│ ├── analysis/ # Indicators, technical analysis
52+
│ ├── news/ # RSS collector + archive search
53+
│ ├── brain/ # Cognitive state (memory, emotion)
54+
│ └── thinking/ # Safe expression evaluator
55+
├── tool/ # AI tool definitions — thin bridge from domain to ToolCenter
56+
│ ├── trading.ts # Trading tools (delegates to domain/trading)
57+
│ ├── equity.ts # Equity fundamental tools
58+
│ ├── market.ts # Symbol search tools
59+
│ ├── analysis.ts # Indicator calculation tools
60+
│ ├── news.ts # News archive tools
61+
│ ├── brain.ts # Cognition tools
62+
│ ├── thinking.ts # Reasoning tools
63+
│ ├── browser.ts # Browser automation tools (wraps openclaw)
64+
│ └── session.ts # Session awareness tools
65+
├── server/
66+
│ ├── mcp.ts # MCP protocol server
67+
│ └── opentypebb.ts # Embedded OpenBB-compatible HTTP API (optional)
68+
├── connectors/
69+
│ ├── web/ # Web UI (Hono, SSE streaming, sub-channels)
70+
│ ├── telegram/ # Telegram bot (grammY, magic link auth, /trading panel)
71+
│ ├── mcp-ask/ # MCP Ask connector (external agent conversation)
72+
│ └── mock/ # Mock connector (testing)
73+
├── task/
74+
│ ├── cron/ # Cron scheduling (engine, listener, AI tools)
75+
│ └── heartbeat/ # Periodic heartbeat with structured response protocol
76+
└── openclaw/ # ⚠️ Frozen — DO NOT MODIFY
77+
data/
78+
├── config/ # JSON configuration files
79+
├── sessions/ # JSONL conversation histories (web/, telegram/, cron/)
80+
├── brain/ # Agent memory and emotion logs
81+
├── cache/ # API response caches
82+
├── trading/ # Trading commit history + snapshots (per-account)
83+
├── news-collector/ # Persistent news archive (JSONL)
84+
├── cron/ # Cron job definitions (jobs.json)
85+
├── event-log/ # Persistent event log (events.jsonl)
86+
├── tool-calls/ # Tool invocation logs
87+
└── media/ # Uploaded attachments
88+
default/ # Factory defaults (persona, heartbeat, skills)
89+
docs/ # Documentation
90+
```

ui/src/App.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@ import { SettingsPage } from './pages/SettingsPage'
99
import { AIProviderPage } from './pages/AIProviderPage'
1010
import { MarketDataPage } from './pages/MarketDataPage'
1111
import { NewsPage } from './pages/NewsPage'
12+
import { NewsCollectorPage } from './pages/NewsCollectorPage'
1213
import { TradingPage } from './pages/TradingPage'
1314
import { ConnectorsPage } from './pages/ConnectorsPage'
1415
import { DevPage } from './pages/DevPage'
1516

1617
export type Page =
17-
| 'chat' | 'portfolio' | 'automation' | 'logs' | 'market-data' | 'news' | 'connectors'
18+
| 'chat' | 'portfolio' | 'news' | 'automation' | 'logs' | 'market-data' | 'news-collector' | 'connectors'
1819
| 'trading'
1920
| 'ai-provider' | 'settings' | 'dev'
2021

@@ -25,6 +26,7 @@ export const ROUTES: Record<Page, string> = {
2526
'automation': '/automation',
2627
'logs': '/logs',
2728
'market-data': '/market-data',
29+
'news-collector': '/news-collector',
2830
'news': '/news',
2931
'connectors': '/connectors',
3032
'trading': '/trading',
@@ -66,6 +68,7 @@ export function App() {
6668
<Route path="/automation" element={<AutomationPage />} />
6769
<Route path="/logs" element={<LogsPage />} />
6870
<Route path="/market-data" element={<MarketDataPage />} />
71+
<Route path="/news-collector" element={<NewsCollectorPage />} />
6972
<Route path="/news" element={<NewsPage />} />
7073
{/* Redirects for old URLs */}
7174
<Route path="/events" element={<Navigate to="/logs" replace />} />

ui/src/components/Sidebar.tsx

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,18 @@ const NAV_SECTIONS: NavSection[] = [
4646
</svg>
4747
),
4848
},
49+
{
50+
page: 'news',
51+
label: 'News',
52+
icon: (active) => (
53+
<svg width="18" height="18" viewBox="0 0 24 24" fill={active ? 'currentColor' : 'none'} stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
54+
<path d="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-2 2zm0 0a2 2 0 0 1-2-2v-9h4" />
55+
<path d="M10 7h8" />
56+
<path d="M10 11h8" />
57+
<path d="M10 15h4" />
58+
</svg>
59+
),
60+
},
4961
],
5062
},
5163
{
@@ -77,14 +89,13 @@ const NAV_SECTIONS: NavSection[] = [
7789
),
7890
},
7991
{
80-
page: 'news',
81-
label: 'News',
92+
page: 'news-collector',
93+
label: 'News Collector',
8294
icon: (active) => (
8395
<svg width="18" height="18" viewBox="0 0 24 24" fill={active ? 'currentColor' : 'none'} stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
84-
<path d="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-2 2zm0 0a2 2 0 0 1-2-2v-9h4" />
85-
<path d="M10 7h8" />
86-
<path d="M10 11h8" />
87-
<path d="M10 15h4" />
96+
<path d="M4 11a9 9 0 0 1 9 9" />
97+
<path d="M4 4a16 16 0 0 1 16 16" />
98+
<circle cx="5" cy="19" r="1" />
8899
</svg>
89100
),
90101
},

ui/src/pages/NewsCollectorPage.tsx

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import { useState } from 'react'
2+
import { type AppConfig, type NewsCollectorConfig, type NewsCollectorFeed } from '../api'
3+
import { SaveIndicator } from '../components/SaveIndicator'
4+
import { ConfigSection, Field, inputClass } from '../components/form'
5+
import { Toggle } from '../components/Toggle'
6+
import { useConfigPage } from '../hooks/useConfigPage'
7+
import { PageHeader } from '../components/PageHeader'
8+
9+
// ==================== Config Section ====================
10+
11+
const DEFAULT_NEWS_CONFIG: NewsCollectorConfig = {
12+
enabled: true,
13+
intervalMinutes: 10,
14+
maxInMemory: 2000,
15+
retentionDays: 7,
16+
feeds: [],
17+
}
18+
19+
function CollectorSettings() {
20+
const { config, status, loadError, updateConfig, updateConfigImmediate, retry } = useConfigPage<NewsCollectorConfig>({
21+
section: 'news',
22+
extract: (full: AppConfig) => (full as Record<string, unknown>).news as NewsCollectorConfig,
23+
})
24+
25+
const cfg = config ?? DEFAULT_NEWS_CONFIG
26+
const enabled = cfg.enabled !== false
27+
28+
return (
29+
<div className="flex-1 overflow-y-auto">
30+
<div className="max-w-[880px] mx-auto">
31+
<div className="flex items-center justify-end gap-3 mb-4">
32+
<SaveIndicator status={status} onRetry={retry} />
33+
<Toggle size="sm" checked={enabled} onChange={(v) => updateConfigImmediate({ enabled: v })} />
34+
</div>
35+
36+
<div className={`${!enabled ? 'opacity-40 pointer-events-none' : ''}`}>
37+
{/* Collection Settings */}
38+
<ConfigSection
39+
title="Collection Settings"
40+
description="Control how often articles are fetched and how long they are retained in the archive."
41+
>
42+
<div className="grid grid-cols-2 gap-4">
43+
<Field label="Fetch interval (min)">
44+
<input
45+
className={inputClass}
46+
type="number"
47+
min={1}
48+
value={cfg.intervalMinutes}
49+
onChange={(e) => updateConfig({ intervalMinutes: Number(e.target.value) || 10 })}
50+
/>
51+
</Field>
52+
<Field label="Retention (days)">
53+
<input
54+
className={inputClass}
55+
type="number"
56+
min={1}
57+
value={cfg.retentionDays}
58+
onChange={(e) => updateConfig({ retentionDays: Number(e.target.value) || 7 })}
59+
/>
60+
</Field>
61+
</div>
62+
</ConfigSection>
63+
64+
{/* RSS Feeds */}
65+
<FeedsSection
66+
feeds={cfg.feeds}
67+
onChange={(feeds) => updateConfigImmediate({ feeds })}
68+
/>
69+
</div>
70+
{loadError && <p className="text-[13px] text-red mt-4">Failed to load configuration.</p>}
71+
</div>
72+
</div>
73+
)
74+
}
75+
76+
// ==================== Feeds Section ====================
77+
78+
function FeedsSection({
79+
feeds,
80+
onChange,
81+
}: {
82+
feeds: NewsCollectorFeed[]
83+
onChange: (feeds: NewsCollectorFeed[]) => void
84+
}) {
85+
const [newName, setNewName] = useState('')
86+
const [newUrl, setNewUrl] = useState('')
87+
const [newSource, setNewSource] = useState('')
88+
89+
const removeFeed = (index: number) => onChange(feeds.filter((_, i) => i !== index))
90+
91+
const addFeed = () => {
92+
if (!newName.trim() || !newUrl.trim() || !newSource.trim()) return
93+
onChange([...feeds, { name: newName.trim(), url: newUrl.trim(), source: newSource.trim() }])
94+
setNewName('')
95+
setNewUrl('')
96+
setNewSource('')
97+
}
98+
99+
return (
100+
<ConfigSection
101+
title="RSS Feeds"
102+
description={
103+
feeds.length > 0
104+
? `${feeds.length} feed${feeds.length > 1 ? 's' : ''} configured. Articles are searchable via globNews, grepNews, and readNews tools.`
105+
: 'No feeds configured yet. Add feeds to start collecting articles.'
106+
}
107+
>
108+
{/* Existing feeds */}
109+
{feeds.length > 0 && (
110+
<div className="space-y-2 mb-4">
111+
{feeds.map((feed, i) => (
112+
<div
113+
key={`${feed.source}-${i}`}
114+
className="flex items-center gap-3 border border-border/60 rounded-lg px-3 py-2.5"
115+
>
116+
<div className="flex-1 min-w-0">
117+
<p className="text-[13px] font-medium text-text truncate">{feed.name}</p>
118+
<p className="text-[12px] text-text-muted truncate">{feed.url}</p>
119+
<p className="text-[11px] text-text-muted/50 mt-0.5">source: {feed.source}</p>
120+
</div>
121+
<button
122+
onClick={() => removeFeed(i)}
123+
className="shrink-0 text-text-muted hover:text-red transition-colors p-1"
124+
title="Remove feed"
125+
>
126+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
127+
<line x1="18" y1="6" x2="6" y2="18" />
128+
<line x1="6" y1="6" x2="18" y2="18" />
129+
</svg>
130+
</button>
131+
</div>
132+
))}
133+
</div>
134+
)}
135+
136+
{/* Add feed form */}
137+
<div className="border border-border/40 rounded-lg p-4 space-y-3">
138+
<p className="text-[13px] font-medium text-text-muted">Add Feed</p>
139+
<div className="grid grid-cols-2 gap-3">
140+
<Field label="Name">
141+
<input className={inputClass} value={newName} onChange={(e) => setNewName(e.target.value)} placeholder="e.g. CoinDesk" />
142+
</Field>
143+
<Field label="Source Tag">
144+
<input className={inputClass} value={newSource} onChange={(e) => setNewSource(e.target.value)} placeholder="e.g. coindesk" />
145+
</Field>
146+
</div>
147+
<Field label="Feed URL">
148+
<input className={inputClass} value={newUrl} onChange={(e) => setNewUrl(e.target.value)} placeholder="https://example.com/rss.xml" />
149+
</Field>
150+
<button
151+
onClick={addFeed}
152+
disabled={!newName.trim() || !newUrl.trim() || !newSource.trim()}
153+
className="btn-secondary"
154+
>
155+
Add Feed
156+
</button>
157+
</div>
158+
</ConfigSection>
159+
)
160+
}
161+
162+
// ==================== Page ====================
163+
164+
export function NewsCollectorPage() {
165+
return (
166+
<div className="flex flex-col flex-1 min-h-0">
167+
<PageHeader
168+
title="News Collector"
169+
description="Configure RSS feeds and collection settings."
170+
/>
171+
172+
<div className="flex-1 flex flex-col min-h-0 px-4 md:px-8 py-5">
173+
<CollectorSettings />
174+
</div>
175+
</div>
176+
)
177+
}

0 commit comments

Comments
 (0)