Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ out/
.eslintcache
.nyc_output/

# Desktop app
desktop/node_modules/
desktop/out/
desktop/release/

# Editor and OS files
.DS_Store
Thumbs.db
Expand Down
Empty file.
7 changes: 7 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ Run the CLI locally with:
pnpm azoth
```

## Desktop UI design

Desktop renderer features should follow the shared design-system note in
[docs/desktop-design-system.md](docs/desktop-design-system.md). Prefer the
tokens and primitives in `desktop/src/renderer/styles/globals.css` over one-off
utility class styling in JSX.

## Pull requests

- Keep changes focused and describe the behavior change clearly.
Expand Down
33 changes: 15 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ HNX, and UPCOM: every recommendation should be grounded in tool output and
constrained by explicit autonomy and risk settings.

> Azoth is investment software, not financial advice. Live trading can place
> real orders against a real account. Use advisory or paper mode until you have
> real orders against a real account. Use manual or paper mode until you have
> verified configuration, data quality, account state, and risk limits.

Latest release: [v0.1.2](docs/releases/v0.1.2.md)
Expand Down Expand Up @@ -70,8 +70,8 @@ Check market, portfolio, and backtest state:
- **Multi-agent desk**: structured analyst workflow with technical,
fundamentals, news, sentiment, bull, bear, research manager, trader, risk,
and portfolio roles.
- **Broker-aware execution**: advisory, confirm, and auto autonomy modes with
explicit user approval before broker calls, paper broker support, and DNSE
- **Broker-aware execution**: manual and auto autonomy modes with explicit user
approval before every tool call in manual mode, paper broker support, and DNSE
Entrade X integration for live accounts.
- **Risk controls**: position sizing limits, order notional limits, optional
ticker whitelist checks, market-session checks, margin-disabled enforcement,
Expand Down Expand Up @@ -140,7 +140,7 @@ checks.
| Team desk | Technical, fundamentals, news, sentiment, bull, bear, research manager, trader, risk, and portfolio roles. |
| Market data | Quotes, OHLCV, technical indicators, fundamentals, CafeF news, macro indices, foreign flow, and ticker discovery. |
| Portfolio | Broker state, sub-accounts, positions, cash, market value, and unrealized P&L. |
| Execution | Paper broker, optional DNSE broker, advisory/confirm/auto autonomy, human confirmation gate. |
| Execution | Paper broker, optional DNSE broker, manual/auto autonomy, human confirmation gate. |
| Risk | Notional cap, concentration cap, whitelist, market session, no-margin cash check, daily-loss halt, drawdown buy freeze. |
| Backtesting | Weekly team-driven replay, paper fills, fees, rejected guardrail orders, benchmark comparison, running-peak max drawdown. |
| Runtime | `~/.azoth` config, SQLite state, project session logs, build-safe schema fallback. |
Expand Down Expand Up @@ -239,7 +239,7 @@ still stream the full local team flow.
| `/positions` | Summarize current portfolio positions and exposures. |
| `/setup-llm` | Change LLM provider, API key, endpoint, and model after first-time setup. |
| `/setup-fhsc` | Configure FHSC broker access and switch `broker` to `fhsc`. |
| `/autonomy <advisory\|confirm\|auto>` | Persist the autonomy mode and rebuild tool access for new turns. |
| `/autonomy <manual\|auto>` | Persist the autonomy mode and rebuild tool access for new turns. |
| `/health [--probe]` | Check API key, config, DB, broker state, live-trading arm flag, market session, and optionally data providers. |
| `/about` | Show version, runtime paths, broker, provider, and release references. |
| `/new` | Start a new resumable session. |
Expand Down Expand Up @@ -272,7 +272,7 @@ Useful environment variables:
Default config:

```yaml
autonomy: advisory
autonomy: manual
model: glm-5.1

llm:
Expand Down Expand Up @@ -309,12 +309,10 @@ risk:

Autonomy modes:

- `advisory`: no order tools are exposed. Azoth recommends; the user executes.
- `confirm`: broker tools are available, but each broker read/write requires
CLI approval before Azoth contacts the broker.
- `auto`: broker tools still require CLI approval before Azoth contacts the
broker; approved orders then run through configured guardrails before
submission.
- `manual`: all tools are available, but each tool call requires user approval
before it runs.
- `auto`: all tools run without approval prompts. Broker orders still run
through configured guardrails before submission.

Broker modes:

Expand Down Expand Up @@ -387,7 +385,7 @@ and creates the matching GitHub release.

## Live Trading With DNSE

Live mode places real orders. Keep `autonomy: advisory` or `broker: paper`
Live mode places real orders. Keep `autonomy: manual` or `broker: paper`
until the checklist below is complete.

1. Open a DNSE account and enable Entrade X / LightSpeed API access.
Expand All @@ -397,8 +395,8 @@ until the checklist below is complete.
`GET https://api.dnse.com.vn/margin-service/loan-products` with the JWT and
choose the correct loan product id for your equity sub-account.
4. Set `broker: dnse` in `~/.azoth/config.yaml`.
5. Set `autonomy: confirm` first while testing. All broker calls prompt for
approval in both `confirm` and `auto`.
5. Set `autonomy: manual` first while testing. Manual mode prompts before every
tool call; auto mode bypasses approval prompts.
6. Run `pnpm test` and then `DNSE_TEST_LIVE=1 pnpm test` for read-only live
probes.
7. Verify `broker_state` and `list_orders` return the expected cash, positions,
Expand Down Expand Up @@ -487,6 +485,5 @@ Azoth is built around a few explicit constraints:
- Buy, sell, or hold recommendations should include technicals, fundamentals,
news, and macro context.
- News citations should include source URL and publish date.
- Order placement is disabled in advisory mode. In confirm/auto modes, every
broker call requires user approval before Azoth contacts the broker, and
approved orders still run through guardrails.
- Manual mode requires user approval before each tool call. Auto mode bypasses
approval prompts. Broker orders still run through guardrails in both modes.
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ Azoth v0.1.0, released on May 4, 2026, provides the public baseline:
- Add comparison workflows for pairs, sectors, and portfolio candidates.
- Improve drawdown, realized P&L, turnover, exposure, and concentration
reporting.
- Add pre-trade impact previews before confirm or auto execution.
- Add pre-trade impact previews before manual approval or auto execution.
- Add configurable risk presets for conservative, balanced, and aggressive
operation.
- Improve team synthesis with structured evidence tables and source timestamps.
Expand Down
25 changes: 25 additions & 0 deletions desktop/electron-builder.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
appId: com.toreleon.azoth
productName: Azoth
directories:
output: release
buildResources: resources
files:
- "out/**"
- "package.json"
- "!**/node_modules/*/{CHANGELOG.md,README.md,*.d.ts,*.map}"
asarUnpack:
- "**/node_modules/better-sqlite3/**"
mac:
category: public.app-category.finance
target:
- target: dmg
arch: [arm64, x64]
- target: zip
arch: [arm64, x64]
hardenedRuntime: true
gatekeeperAssess: false
entitlements: resources/entitlements.mac.plist
entitlementsInherit: resources/entitlements.mac.plist
notarize: false
dmg:
sign: false
45 changes: 45 additions & 0 deletions desktop/electron.vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { defineConfig, externalizeDepsPlugin } from "electron-vite";
import react from "@vitejs/plugin-react";
import { resolve } from "node:path";

const coreAlias = {
"@azoth/core": resolve(__dirname, "../src"),
"@shared": resolve(__dirname, "src/shared"),
};

const nativeExternals = ["better-sqlite3"];

export default defineConfig({
main: {
plugins: [externalizeDepsPlugin()],
resolve: { alias: coreAlias },
build: {
outDir: "out/main",
rollupOptions: {
external: nativeExternals,
input: resolve(__dirname, "src/main/index.ts"),
},
},
},
preload: {
plugins: [externalizeDepsPlugin()],
resolve: { alias: coreAlias },
build: {
outDir: "out/preload",
rollupOptions: {
input: resolve(__dirname, "src/preload/index.ts"),
},
},
},
renderer: {
root: resolve(__dirname, "src/renderer"),
plugins: [react()],
resolve: { alias: coreAlias },
build: {
outDir: resolve(__dirname, "out/renderer"),
rollupOptions: {
input: resolve(__dirname, "src/renderer/index.html"),
},
},
},
});
35 changes: 35 additions & 0 deletions desktop/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"name": "azoth-desktop",
"version": "0.1.0",
"private": true,
"main": "out/main/index.js",
"scripts": {
"dev": "electron-vite dev",
"build": "electron-vite build && electron-builder --mac",
"build:unpack": "electron-vite build && electron-builder --mac --dir",
"preview": "electron-vite preview",
"postinstall": "electron-builder install-app-deps",
"typecheck": "tsc -p tsconfig.node.json --noEmit && tsc -p tsconfig.web.json --noEmit",
"rebuild": "electron-rebuild -f -w better-sqlite3"
},
"dependencies": {
"better-sqlite3": "^11.10.0",
"zustand": "^4.5.5"
},
"devDependencies": {
"@electron/rebuild": "^3.6.0",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.0",
"autoprefixer": "^10.4.0",
"electron": "^33.0.0",
"electron-builder": "^25.0.0",
"electron-vite": "^2.3.0",
"postcss": "^8.4.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"tailwindcss": "^3.4.0",
"typescript": "^5.6.3",
"vite": "^5.4.0"
}
}
6 changes: 6 additions & 0 deletions desktop/postcss.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
16 changes: 16 additions & 0 deletions desktop/resources/entitlements.mac.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
</dict>
</plist>
70 changes: 70 additions & 0 deletions desktop/src/main/appSettings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
import { app, nativeTheme } from "electron";
import { azothHome } from "@azoth/core/runtime/paths.js";
import type { DesktopSettings } from "../shared/ipc.js";

const DEFAULT_SETTINGS: DesktopSettings = {
launchAtLogin: false,
hideOnClose: true,
showNotifications: true,
notifyOnOrderFill: true,
appearance: "light",
};

function settingsPath(): string {
return resolve(azothHome(), "desktop-settings.json");
}

function normalizeSettings(raw: Partial<DesktopSettings> | null | undefined): DesktopSettings {
const appearance = ["light", "dark", "system"].includes(String(raw?.appearance))
? raw!.appearance!
: DEFAULT_SETTINGS.appearance;
return {
launchAtLogin: Boolean(raw?.launchAtLogin ?? DEFAULT_SETTINGS.launchAtLogin),
hideOnClose: Boolean(raw?.hideOnClose ?? DEFAULT_SETTINGS.hideOnClose),
showNotifications: Boolean(raw?.showNotifications ?? DEFAULT_SETTINGS.showNotifications),
notifyOnOrderFill: Boolean(raw?.notifyOnOrderFill ?? DEFAULT_SETTINGS.notifyOnOrderFill),
appearance,
};
}

function readStoredSettings(): DesktopSettings {
const path = settingsPath();
if (!existsSync(path)) return DEFAULT_SETTINGS;
try {
return normalizeSettings(JSON.parse(readFileSync(path, "utf8")) as Partial<DesktopSettings>);
} catch {
return DEFAULT_SETTINGS;
}
}

function writeStoredSettings(settings: DesktopSettings): void {
mkdirSync(azothHome(), { recursive: true });
writeFileSync(settingsPath(), `${JSON.stringify(settings, null, 2)}\n`, {
encoding: "utf8",
mode: 0o600,
});
}

export function getDesktopSettings(): DesktopSettings {
const settings = readStoredSettings();
if (app.isReady()) {
settings.launchAtLogin = app.getLoginItemSettings().openAtLogin;
}
return settings;
}

export function applyDesktopSettings(settings = getDesktopSettings()): void {
nativeTheme.themeSource = settings.appearance;
if (app.isReady()) {
app.setLoginItemSettings({ openAtLogin: settings.launchAtLogin });
}
}

export function saveDesktopSettings(patch: Partial<DesktopSettings>): DesktopSettings {
const next = normalizeSettings({ ...getDesktopSettings(), ...patch });
writeStoredSettings(next);
applyDesktopSettings(next);
return getDesktopSettings();
}
34 changes: 34 additions & 0 deletions desktop/src/main/consent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { randomUUID } from "node:crypto";
import { setBrokerConsentHandler, type BrokerConsentRequest } from "@azoth/core/tools/brokerConsent.js";
import { sendStream } from "./streamBus.js";

const pending = new Map<string, (approved: boolean) => void>();

export function installConsentBridge(): void {
setBrokerConsentHandler(async (req: BrokerConsentRequest) => {
return new Promise<boolean>((resolve) => {
const id = randomUUID();
pending.set(id, resolve);
sendStream({
kind: "consent:request",
id,
action: req.action,
detail: req.detail,
broker: req.broker,
autonomy: req.autonomy,
});
});
});
}

export function respondConsent(id: string, approved: boolean): void {
const resolve = pending.get(id);
if (!resolve) return;
pending.delete(id);
resolve(approved);
}

export function clearConsent(): void {
for (const resolve of pending.values()) resolve(false);
pending.clear();
}
Loading
Loading