Skip to content
Merged
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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Changelog

All notable changes to Open Alice will be documented in this file.
All notable changes to OpenAlice will be documented in this file.

## [Unreleased]

Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Open Alice
# OpenAlice

File-driven AI trading agent. All state (sessions, config, logs) stored as files — no database.

Expand Down
6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Contributing to Open Alice
# Contributing to OpenAlice

Thanks for your interest in Open Alice!
Thanks for your interest in OpenAlice!

## Issues — Yes, Please

Expand All @@ -17,7 +17,7 @@ The more detail you provide, the faster we can act on it. Screenshots, logs, and

**We do not accept external pull requests.** This is not a reflection on the quality of contributions — it's a security decision.

Open Alice is a trading agent that executes real financial operations. Every line of code that runs has direct access to exchange accounts and API keys. Accepting external code — even well-intentioned code — introduces supply chain risk that we cannot afford. A single malicious dependency update, a subtle logic change in order execution, or a backdoor in a utility function could result in real financial loss.
OpenAlice is a trading agent that executes real financial operations. Every line of code that runs has direct access to exchange accounts and API keys. Accepting external code — even well-intentioned code — introduces supply chain risk that we cannot afford. A single malicious dependency update, a subtle logic change in order execution, or a backdoor in a utility function could result in real financial loss.

We review and implement all changes internally to maintain full control over the security surface.

Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<p align="center">
<img src="docs/images/alice-full.png" alt="Open Alice" width="128">
<img src="docs/images/alice-full.png" alt="OpenAlice" width="128">
</p>

<p align="center">
<a href="https://github.com/TraderAlice/OpenAlice/actions/workflows/ci.yml"><img src="https://github.com/TraderAlice/OpenAlice/actions/workflows/ci.yml/badge.svg" alt="CI"></a> · <a href="LICENSE"><img src="https://img.shields.io/badge/License-AGPL--3.0-blue.svg" alt="License: AGPL-3.0"></a> · <a href="https://openalice.ai"><img src="https://img.shields.io/badge/Website-openalice.ai-blue" alt="openalice.ai"></a> · <a href="https://openalice.ai/docs"><img src="https://img.shields.io/badge/Docs-Read-green" alt="Docs"></a> · <a href="https://deepwiki.com/TraderAlice/OpenAlice"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a>
</p>

# Open Alice
# OpenAlice

Your one-person Wall Street. Alice is an AI trading agent that covers equities, crypto, commodities, forex, and macro — from research and analysis through position entry, ongoing management, to exit.

Expand All @@ -17,11 +17,11 @@ Your one-person Wall Street. Alice is an AI trading agent that covers equities,
Alice runs on your own machine, because trading involves private keys and real money — that trust can't be outsourced.

<p align="center">
<img src="docs/images/preview.png" alt="Open Alice Preview" width="720">
<img src="docs/images/preview.png" alt="OpenAlice Preview" width="720">
</p>

> [!CAUTION]
> **Open Alice is experimental software in active development.** Many features and interfaces are incomplete and subject to breaking changes. Do not use this software for live trading with real funds unless you fully understand and accept the risks involved. The authors provide no guarantees of correctness, reliability, or profitability, and accept no liability for financial losses.
> **OpenAlice is experimental software in active development.** Many features and interfaces are incomplete and subject to breaking changes. Do not use this software for live trading with real funds unless you fully understand and accept the risks involved. The authors provide no guarantees of correctness, reliability, or profitability, and accept no liability for financial losses.

## Features

Expand Down Expand Up @@ -175,11 +175,11 @@ On first run, defaults are auto-copied to the user override path. Edit the user

## Project Structure

Open Alice is a pnpm monorepo with Turborepo build orchestration. See [docs/project-structure.md](docs/project-structure.md) for the full file tree.
OpenAlice is a pnpm monorepo with Turborepo build orchestration. See [docs/project-structure.md](docs/project-structure.md) for the full file tree.

## Roadmap to v1

Open Alice is in pre-release. All planned v1 milestones are now complete — remaining work is testing and stabilization.
OpenAlice is in pre-release. All planned v1 milestones are now complete — remaining work is testing and stabilization.

- [x] **Tool confirmation** — achieved through Trading-as-Git's push approval mechanism. Order execution requires explicit user approval at the push step, similar to merging a PR
- [x] **Trading-as-Git stable interface** — the core workflow (stage → commit → push → approval) is stable and running in production
Expand Down
2 changes: 1 addition & 1 deletion cliff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
header = """
# Changelog

All notable changes to Open Alice will be documented in this file.\n
All notable changes to OpenAlice will be documented in this file.\n
"""
body = """
{%- macro remote_url() -%}
Expand Down
2 changes: 1 addition & 1 deletion docs/project-structure.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Project Structure

Open Alice is a pnpm monorepo with Turborepo build orchestration.
OpenAlice is a pnpm monorepo with Turborepo build orchestration.

```
packages/
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"securities",
"file-driven"
],
"author": "Open Alice Contributors",
"author": "OpenAlice Contributors",
"license": "AGPL-3.0-only",
"repository": {
"type": "git",
Expand Down
4 changes: 2 additions & 2 deletions packages/ibkr/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This document captures the exploration, decisions, and trade-offs made during th

## Background

Open Alice needed IBKR trading support. The Unified Trading Account system was already designed in IBKR's style, so the data model was a natural fit. The question was how to connect.
OpenAlice needed IBKR trading support. The Unified Trading Account system was already designed in IBKR's style, so the data model was a natural fit. The question was how to connect.

## Evaluating Connection Options

Expand All @@ -13,7 +13,7 @@ Open Alice needed IBKR trading support. The Unified Trading Account system was a
A community TypeScript implementation of the TWS socket protocol.

- **Pros**: Ready to use, npm install and go.
- **Cons**: 340 GitHub stars at the time of evaluation. Open Alice itself had 1100+. Depending on a smaller project for a critical path (real-money trading) was deemed too risky. Supply chain concerns: single maintainer, infrequent updates, unclear maintenance commitment.
- **Cons**: 340 GitHub stars at the time of evaluation. OpenAlice itself had 1100+. Depending on a smaller project for a critical path (real-money trading) was deemed too risky. Supply chain concerns: single maintainer, infrequent updates, unclear maintenance commitment.

**Decision**: Rejected due to supply chain risk.

Expand Down
20 changes: 10 additions & 10 deletions packages/ibkr/src/decoder/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,11 @@ export function applyAccountHandlers(decoder: Decoder): void {
}

const position = decodeDecimal(fields)
const marketPrice = decodeFloat(fields)
const marketValue = decodeFloat(fields)
const averageCost = decodeFloat(fields) // ver 3
const unrealizedPNL = decodeFloat(fields) // ver 3
const realizedPNL = decodeFloat(fields) // ver 3
const marketPrice = decodeDecimal(fields).toString()
const marketValue = decodeDecimal(fields).toString()
const averageCost = decodeDecimal(fields).toString() // ver 3
const unrealizedPNL = decodeDecimal(fields).toString() // ver 3
const realizedPNL = decodeDecimal(fields).toString() // ver 3
const accountName = decodeStr(fields) // ver 4

if (version === 6 && d.serverVersion === 39) {
Expand All @@ -144,11 +144,11 @@ export function applyAccountHandlers(decoder: Decoder): void {
const contract = decodeContractProto(proto.contract)

const position = proto.position !== undefined ? new Decimal(proto.position) : UNSET_DECIMAL
const marketPrice = proto.marketPrice ?? 0
const marketValue = proto.marketValue ?? 0
const averageCost = proto.averageCost ?? 0
const unrealizedPNL = proto.unrealizedPNL ?? 0
const realizedPNL = proto.realizedPNL ?? 0
const marketPrice = String(proto.marketPrice ?? 0)
const marketValue = String(proto.marketValue ?? 0)
const averageCost = String(proto.averageCost ?? 0)
const unrealizedPNL = String(proto.unrealizedPNL ?? 0)
const realizedPNL = String(proto.realizedPNL ?? 0)
const accountName = proto.accountName ?? ''

d.wrapper.updatePortfolio(
Expand Down
20 changes: 10 additions & 10 deletions packages/ibkr/src/wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,11 +170,11 @@ export interface EWrapper {
updatePortfolio(
contract: Contract,
position: Decimal,
marketPrice: number,
marketValue: number,
averageCost: number,
unrealizedPNL: number,
realizedPNL: number,
marketPrice: string,
marketValue: string,
averageCost: string,
unrealizedPNL: string,
realizedPNL: string,
accountName: string,
): void;

Expand Down Expand Up @@ -713,11 +713,11 @@ export class DefaultEWrapper implements EWrapper {
updatePortfolio(
_contract: Contract,
_position: Decimal,
_marketPrice: number,
_marketValue: number,
_averageCost: number,
_unrealizedPNL: number,
_realizedPNL: number,
_marketPrice: string,
_marketValue: string,
_averageCost: string,
_unrealizedPNL: string,
_realizedPNL: string,
_accountName: string,
): void {}

Expand Down
7 changes: 4 additions & 3 deletions src/connectors/telegram/telegram-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,7 @@ export class TelegramPlugin implements Plugin {
let equityStr = ''
try {
const acc = await uta.getAccount()
equityStr = ` $${acc.netLiquidation.toFixed(0)}`
equityStr = ` $${Number(acc.netLiquidation).toFixed(0)}`
} catch { /* skip */ }
lines.push(`${healthIcon} ${uta.label}${equityStr}${pendingTag}`)
keyboard.text(uta.label, `trading:view:${uta.id}`)
Expand All @@ -497,8 +497,9 @@ export class TelegramPlugin implements Plugin {
// Account info
try {
const acc = await uta.getAccount()
const pnl = acc.unrealizedPnL >= 0 ? `+$${acc.unrealizedPnL.toFixed(0)}` : `-$${Math.abs(acc.unrealizedPnL).toFixed(0)}`
lines.push(`Equity $${acc.netLiquidation.toFixed(0)} Cash $${acc.totalCashValue.toFixed(0)} PnL ${pnl}`)
const pnlNum = Number(acc.unrealizedPnL)
const pnl = pnlNum >= 0 ? `+$${pnlNum.toFixed(0)}` : `-$${Math.abs(pnlNum).toFixed(0)}`
lines.push(`Equity $${Number(acc.netLiquidation).toFixed(0)} Cash $${Number(acc.totalCashValue).toFixed(0)} PnL ${pnl}`)
} catch {
lines.push('(account data unavailable)')
}
Expand Down
145 changes: 145 additions & 0 deletions src/core/agent-event.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { describe, it, expect } from 'vitest'
import { AgentEventSchemas, validateEventPayload } from './agent-event.js'
import type { AgentEventMap } from './agent-event.js'

// ==================== Schema Completeness ====================

describe('AgentEventSchemas', () => {
const expectedTypes: (keyof AgentEventMap)[] = [
'cron.fire', 'cron.done', 'cron.error',
'heartbeat.done', 'heartbeat.skip', 'heartbeat.error',
'message.received', 'message.sent',
'trigger',
]

it('should have a schema for every key in AgentEventMap', () => {
for (const type of expectedTypes) {
expect(AgentEventSchemas[type], `missing schema for "${type}"`).toBeDefined()
}
})

it('should not have extra schemas beyond AgentEventMap', () => {
const schemaKeys = Object.keys(AgentEventSchemas)
expect(schemaKeys.sort()).toEqual([...expectedTypes].sort())
})
})

// ==================== validateEventPayload ====================

describe('validateEventPayload', () => {
// -- cron.fire --
it('should accept valid cron.fire payload', () => {
expect(() => validateEventPayload('cron.fire', {
jobId: 'abc', jobName: 'test', payload: 'hello',
})).not.toThrow()
})

it('should reject cron.fire with missing jobId', () => {
expect(() => validateEventPayload('cron.fire', {
jobName: 'test', payload: 'hello',
})).toThrow(/Invalid payload.*cron\.fire/)
})

it('should reject cron.fire with wrong type (number instead of string)', () => {
expect(() => validateEventPayload('cron.fire', {
jobId: 123, jobName: 'test', payload: 'hello',
})).toThrow(/Invalid payload.*cron\.fire/)
})

// -- cron.done --
it('should accept valid cron.done payload', () => {
expect(() => validateEventPayload('cron.done', {
jobId: 'abc', jobName: 'test', reply: 'ok', durationMs: 100,
})).not.toThrow()
})

// -- cron.error --
it('should accept valid cron.error payload', () => {
expect(() => validateEventPayload('cron.error', {
jobId: 'abc', jobName: 'test', error: 'boom', durationMs: 50,
})).not.toThrow()
})

// -- heartbeat.done --
it('should accept valid heartbeat.done payload', () => {
expect(() => validateEventPayload('heartbeat.done', {
reply: 'all good', reason: 'CHAT_YES', durationMs: 200, delivered: true,
})).not.toThrow()
})

// -- heartbeat.skip --
it('should accept heartbeat.skip with optional parsedReason', () => {
expect(() => validateEventPayload('heartbeat.skip', {
reason: 'ack', parsedReason: 'All systems normal.',
})).not.toThrow()
})

it('should accept heartbeat.skip without parsedReason', () => {
expect(() => validateEventPayload('heartbeat.skip', {
reason: 'outside-active-hours',
})).not.toThrow()
})

it('should reject heartbeat.skip with missing reason', () => {
expect(() => validateEventPayload('heartbeat.skip', {
parsedReason: 'something',
})).toThrow(/Invalid payload.*heartbeat\.skip/)
})

// -- heartbeat.error --
it('should accept valid heartbeat.error payload', () => {
expect(() => validateEventPayload('heartbeat.error', {
error: 'timeout', durationMs: 5000,
})).not.toThrow()
})

// -- message.received --
it('should accept valid message.received payload', () => {
expect(() => validateEventPayload('message.received', {
channel: 'web', to: 'default', prompt: 'hello',
})).not.toThrow()
})

// -- message.sent --
it('should accept valid message.sent payload', () => {
expect(() => validateEventPayload('message.sent', {
channel: 'web', to: 'default', prompt: 'hello', reply: 'hi', durationMs: 300,
})).not.toThrow()
})

it('should reject message.sent with missing reply', () => {
expect(() => validateEventPayload('message.sent', {
channel: 'web', to: 'default', prompt: 'hello', durationMs: 300,
})).toThrow(/Invalid payload.*message\.sent/)
})

// -- trigger --
it('should accept valid trigger payload', () => {
expect(() => validateEventPayload('trigger', {
source: 'webhook', name: 'price-alert', data: { symbol: 'BTC', price: 100000 },
})).not.toThrow()
})

it('should accept trigger with empty data', () => {
expect(() => validateEventPayload('trigger', {
source: 'api', name: 'test', data: {},
})).not.toThrow()
})

it('should reject trigger with missing source', () => {
expect(() => validateEventPayload('trigger', {
name: 'test', data: {},
})).toThrow(/Invalid payload.*trigger/)
})

// -- unregistered types --
it('should pass for unregistered event types', () => {
expect(() => validateEventPayload('some.random.type', {
anything: 'goes', here: 42,
})).not.toThrow()
})

it('should pass for unregistered type with null payload', () => {
expect(() => validateEventPayload('unknown.type', null)).not.toThrow()
})
})
Loading
Loading