Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
cd72e9d
Merge pull request #71 from TraderAlice/dev
luokerenx4 Mar 17, 2026
a9a0e34
Merge pull request #72 from TraderAlice/dev
luokerenx4 Mar 18, 2026
7f3096f
Merge pull request #73 from TraderAlice/dev
luokerenx4 Mar 18, 2026
e009f6f
Merge pull request #74 from TraderAlice/dev
luokerenx4 Mar 19, 2026
fcf2209
Merge pull request #80 from TraderAlice/dev
luokerenx4 Mar 22, 2026
680cf40
Merge pull request #81 from TraderAlice/dev
luokerenx4 Mar 22, 2026
b9a2662
Merge pull request #82 from TraderAlice/dev
luokerenx4 Mar 22, 2026
9a6d62c
Merge pull request #83 from TraderAlice/dev
luokerenx4 Mar 23, 2026
057eb79
Merge pull request #84 from TraderAlice/dev
luokerenx4 Mar 23, 2026
af39134
Merge pull request #86 from TraderAlice/dev
luokerenx4 Mar 24, 2026
9edb274
Merge pull request #87 from TraderAlice/dev
luokerenx4 Mar 25, 2026
06345e1
Merge pull request #88 from TraderAlice/dev
luokerenx4 Mar 26, 2026
c9241d6
Merge pull request #97 from TraderAlice/dev
luokerenx4 Mar 31, 2026
df303f9
Merge pull request #98 from TraderAlice/dev
luokerenx4 Apr 1, 2026
bbb679f
Merge pull request #100 from TraderAlice/dev
luokerenx4 Apr 3, 2026
97470b8
Merge pull request #101 from TraderAlice/dev
luokerenx4 Apr 4, 2026
c50f9fe
Merge pull request #102 from TraderAlice/dev
luokerenx4 Apr 5, 2026
c6625c2
Merge pull request #104 from TraderAlice/dev
luokerenx4 Apr 6, 2026
3da936d
Merge pull request #106 from TraderAlice/dev
luokerenx4 Apr 7, 2026
f44c878
Merge pull request #112 from TraderAlice/dev
luokerenx4 Apr 8, 2026
f17659b
Merge pull request #114 from TraderAlice/dev
luokerenx4 Apr 9, 2026
da4d5f2
Merge pull request #117 from TraderAlice/dev
luokerenx4 Apr 10, 2026
578b0d1
Merge pull request #119 from TraderAlice/dev
luokerenx4 Apr 11, 2026
953fd91
Merge pull request #121 from TraderAlice/dev
luokerenx4 Apr 14, 2026
83f6035
Merge pull request #122 from TraderAlice/dev
luokerenx4 Apr 14, 2026
3d3a29b
Merge pull request #123 from TraderAlice/dev
luokerenx4 Apr 14, 2026
091f744
Merge pull request #124 from TraderAlice/dev
luokerenx4 Apr 15, 2026
832f06c
Merge pull request #126 from TraderAlice/dev
luokerenx4 Apr 16, 2026
440b6fe
Merge pull request #127 from TraderAlice/dev
luokerenx4 Apr 17, 2026
dd4eb39
Merge pull request #128 from TraderAlice/dev
luokerenx4 Apr 17, 2026
06d6239
Merge pull request #130 from TraderAlice/dev
luokerenx4 Apr 19, 2026
0093075
Merge pull request #131 from TraderAlice/dev
luokerenx4 Apr 19, 2026
5b0d951
Merge pull request #133 from TraderAlice/dev
luokerenx4 Apr 20, 2026
0fd44ba
Merge pull request #134 from TraderAlice/dev
luokerenx4 Apr 21, 2026
8a47b38
Merge pull request #135 from TraderAlice/dev
luokerenx4 Apr 21, 2026
a669fc8
Merge pull request #136 from TraderAlice/dev
luokerenx4 Apr 21, 2026
ba99f0f
fix: CcxtBroker OKX/Bybit initialization and market fetching
Apr 12, 2026
cbd71a2
fix: prevent enableDemoTrading from crashing OKX by overriding urls
Apr 12, 2026
933c998
fix: resolve OKX instType/demo issues and serialize Decimal in tools …
Apr 15, 2026
4ab9abc
fix(trading): resolve CooldownGuard race condition and Telegram UI do…
Apr 16, 2026
43d6b24
docs(learnings): log double-click race condition
Apr 16, 2026
303539b
fix(trading): resolve aliceId to nativeKey in getQuote and getContrac…
Apr 19, 2026
9393ac3
fix(ccxt): include spot balances in portfolio and equity calculation
Apr 19, 2026
f6b9537
fix(heartbeat): implement hot-reloading for heartbeat config and sync…
Apr 22, 2026
d5d2f55
Merge branch 'TraderAlice:master' into fix/ccxt-okx-on-dev
HengWeiBin Apr 22, 2026
27464fb
docs(learnings): log heartbeat config staleness bug and resolution
Apr 22, 2026
f479dbd
fix: resolve setInterval shadowing in UI and missing UNSET_DOUBLE imp…
Apr 22, 2026
dc90e01
docs(learnings): log UI interval shadowing and missing import errors
Apr 22, 2026
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
274 changes: 274 additions & 0 deletions .learnings/LEARNINGS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
# Project Learnings: OpenAlice

This file logs key technical findings, architectural decisions, and bug root causes to prevent regressions and improve future implementation.

---

## [LRN-20260415-001] CCXT Exchange Demo Mode handling (OKX vs Bybit)

**Logged**: 2026-04-15T21:45:00Z
**Priority**: critical
**Status**: resolved
**Area**: backend | trading

### Summary
OKX calls to `enableDemoTrading()` clear `urls.api` in CCXT; OKX needs `setSandboxMode(true)`, while Bybit needs `enableDemoTrading(true)`.

### Details
- Calling `enableDemoTrading()` on OKX in CCXT v4+ triggers an internal logic that clears the `urls.api` if no explicit `demo` URL is defined in the CCXT exchange configuration. This causes all subsequent API calls (like `fetchMarkets`) to fail with "undefined URL" errors.
- **Correct approach for OKX**: Only use `setSandboxMode(true)` which adds the `x-simulated-trading: 1` header.
- **Correct approach for Bybit**: Must use `enableDemoTrading(true)` to route to the correct demo endpoints.

### Suggested Action
Always use the exchange-specific routing logic in `CcxtBroker.ts` for sandbox/demo mode toggles.

### Metadata
- Source: error
- Related Files: src/domain/trading/brokers/ccxt/CcxtBroker.ts
- Tags: ccxt, okx, bybit, demo, sandbox
- Pattern-Key: broker.init.demo_mode

### Resolution
- **Resolved**: 2026-04-15
- **Notes**: Implemented exchange-specific branching in `CcxtBroker` constructor and added unit tests in `CcxtBroker.spec.ts`.

---

## [LRN-20260415-002] OKX fetchMarkets requires explicit `type` parameter

**Logged**: 2026-04-15T21:45:00Z
**Priority**: high
**Status**: resolved
**Area**: backend | trading

### Summary
OKX `fetchMarkets` defaults to `SPOT`; `{ type }` must be passed in `params` to fetch `SWAP` or `FUTURE`.

### Details
- By default, OKX API responses only return SPOT instruments if no `instType` is specified. CCXT's `fetchMarkets` implementation for OKX requires `type` (which maps to `instType`) to be passed in the second parameter object.
- Failure to pass this leads to `instType` missing or incorrect errors when attempting to trade swaps/futures.

### Suggested Action
When calling `fetchMarkets` in any broker, ensure common parameters like `type` are passed to the underlying CCXT method.

### Metadata
- Source: error
- Related Files: src/domain/trading/brokers/ccxt/CcxtBroker.ts
- Tags: okx, fetchMarkets, instType
- Pattern-Key: broker.fetchMarkets.params

### Resolution
- **Resolved**: 2026-04-15
- **Notes**: Fixed by passing `{ ...params, type }` in `CcxtBroker.init()`.

---

## [LRN-20260415-003] AI Tool Output Serialization (DataCloneError)

**Logged**: 2026-04-15T21:45:00Z
**Priority**: critical
**Status**: resolved
**Area**: backend | ai

### Summary
`Decimal` objects (from `decimal.js`) contain methods and fail `structuredClone`; all tool outputs must serialize `Decimal` to `string`.

### Details
- The Vercel AI SDK and Telegram connectors use `structuredClone` (or similar deep serialization) which fails when encountering objects with functions (like `Decimal` instances). This triggers a `DataCloneError`.
- **UNSET_DECIMAL handle**: Must check for the internal `UNSET_DECIMAL` sentinel value to avoid returning the extremely long magic number string to the AI.

### Suggested Action
Use a utility like `summarizeOperation` to wrap all trading tool results and convert `Decimal` fields to strings or numbers before returning to the AI engine.

### Metadata
- Source: error | user_feedback
- Related Files: src/tool/trading.ts
- Tags: serialization, DataCloneError, decimal.js, vercel-ai-sdk
- Pattern-Key: tool.serialization.decimal

### Resolution
- **Resolved**: 2026-04-15
- **Notes**: Implemented `summarizeOperation` and `decimalMaxString` helpers in `src/tool/trading.ts` and updated all trading tools. Added unit tests in `trading.spec.ts`.

---

## [LRN-20260415-004] OperationGuard side effects and double-click race conditions

**Logged**: 2026-04-15T21:45:00Z
**Priority**: high
**Status**: resolved
**Area**: backend | trading

### Summary
Stateful guards like `CooldownGuard` must not mutate state in the `check()` phase, as rapid duplicate requests (e.g., from Telegram double clicks) cause false rejections.

### Details
- In Telegram, pressing the "Approve" button multiple times quickly can dispatch overlapping `push()` operations because Telegram lacks native UI element disablement.
- If a guard (like `CooldownGuard`) mutates state (like `lastTradeTime`) inside its `check()` function, the first push will set the state, causing the second push to fail validation. Worse, the second (failed) operation will clear the staging area and return `rejected` to the UI, obscuring the success of the first operation.

### Suggested Action
State updates for guards must happen only after successful execution using an `onSuccess` hook, ensuring failed or cancelled attempts don't affect subsequent validations. Additionally, critical actions like `push` should employ an explicit atomic lock (`isPushing`). Finally, connectors like Telegram should provide immediate visual feedback (e.g., editing the message to "Processing...") to prevent further clicks.

### Metadata
- Source: bug | user_feedback
- Related Files: src/domain/trading/guards/cooldown.ts, src/domain/trading/git/TradingGit.ts, src/connectors/telegram/telegram-plugin.ts
- Tags: guard, race-condition, double-click, cooldown, telegram
- Pattern-Key: guard.side_effect.check

### Resolution
- **Resolved**: 2026-04-15
- **Notes**: Added `onSuccess` hook to `OperationGuard` and refactored `CooldownGuard`. Added `isPushing` flag to `TradingGit`. Implemented visual feedback for Telegram plugin.

---

## [LRN-20260419-001] Broker Contract Resolution from aliceId

**Logged**: 2026-04-19T10:20:00Z
**Priority**: high
**Status**: resolved
**Area**: backend | trading

### Summary
When an AI tool passes a `Contract` object containing only an `aliceId` (e.g., `ccxt-okx-test|ETH/USDT`), it must be explicitly resolved into broker-native fields (like `localSymbol` or `symbol`) before being sent to the broker adapter (like `CcxtBroker`), otherwise the broker cannot identify the market and fails.

### Details
- The AI uses `aliceId` to uniquely identify an asset across different accounts (format: `{utaId}|{nativeKey}`).
- Broker adapters (like `CcxtBroker` and `AlpacaBroker`) rely on `localSymbol`, `symbol`, or `conId` to perform API calls (e.g., fetching quotes or contract details).
- If `getQuote` or `getContractDetails` simply passes the `Contract` object down to the broker without parsing the `aliceId`, the broker's internal mapping logic (like `contractToCcxt`) will return `null` or throw an error.

### Suggested Action
Always use a central helper method (e.g., `resolveContract` in `UnifiedTradingAccount`) to parse the `aliceId` and populate the `Contract` object via the broker's `resolveNativeKey()` method before dispatching calls like `getQuote` or `getContractDetails`.

### Metadata
- Source: error
- Related Files: src/domain/trading/UnifiedTradingAccount.ts, src/domain/trading/brokers/ccxt/CcxtBroker.ts
- Tags: aliceId, resolution, ccxt, getQuote, getContractDetails
- Pattern-Key: broker.contract.resolution

### Resolution
- **Resolved**: 2026-04-19
- **Notes**: Added `resolveContract` helper to `UnifiedTradingAccount` and updated `getQuote` and `getContractDetails` to use it. Corrected out-of-date CCXT documentation regarding the `aliceId` format.

---

## [LRN-20260419-001] ccxt-spot-balances

**Logged**: 2026-04-19T11:25:00Z
**Priority**: high
**Status**: resolved
**Area**: backend

### Summary
CcxtBroker ignored Spot balances in portfolio view and equity calculation.

### Details
- The original CcxtBroker implementation only fetched futures/margin positions via `fetchPositions()`.
- Spot holdings (e.g. BTC, ETH in Spot wallet) were invisible to the user and their market value was not included in `netLiquidation`.
- This led to misleading account equity and "missing" assets after spot trades.

### Suggested Action
Merge Spot balances from `fetchBalance()` into the results of `getPositions()` and `getAccount()`. Use `fetchTickers()` to batch-fetch current prices for non-stablecoin assets to calculate their USD market value. Filter out "dust" balances (< $1 USD) to keep the view clean.

### Metadata
- Source: user_feedback
- Related Files: src/domain/trading/brokers/ccxt/CcxtBroker.ts, src/domain/trading/brokers/ccxt/CcxtBroker.spec.ts
- Tags: ccxt, spot, balance, equity, okx, bybit
- Pattern-Key: broker.ccxt.spot-visibility

### Resolution
- **Resolved**: 2026-04-19
- **Notes**: Implemented `fetchSpotBalancesAsPositions` helper in CcxtBroker. Updated `getAccount` and `getPositions` to merge spot assets and include them in equity calculations. Added unit tests to verify.

---

## [LRN-20260419-002] heartbeat-config-staleness

**Logged**: 2026-04-19T14:30:00Z
**Priority**: high
**Status**: resolved
**Area**: backend | config

### Summary
Heartbeat configuration (like `activeHours` and `every`) did not hot-reload because the `createHeartbeat` closure captured a stale config reference and the CronEngine wasn't explicitly updated.

### Details
- When the UI modifies `activeHours` or `interval`, the new config is written to disk and global memory is synced (`Object.assign`), but the `Heartbeat` task instance still held the original `config` object reference from startup.
- The `CronEngine` schedule (`every`) also requires an explicit `cronEngine.update()` call; merely changing the config object doesn't reschedule the job.
- The web config route (`/api/config/:section`) only triggered the hot-reload cascade (`onConnectorsChange`) for `connectors` and `marketData`, completely bypassing the heartbeat update flow.
- A secondary issue: using `timezone: "local"` can lead to unexpected `activeHours` skips if the server environment (e.g. Docker) runs in UTC instead of the expected local timezone. Explicit timezones (e.g. `Asia/Taipei`) are safer.

### Suggested Action
1. Ensure long-running tasks expose an `updateConfig(newConfig)` method to re-capture config references and re-register infrastructure like Cron jobs.
2. In routing/API layers, ensure the hot-reload cascade (e.g., `onConnectorsChange`) is triggered for all relevant configuration sections.
3. Prefer explicit IANA timezones over `"local"` to prevent environment-specific bugs.

### Metadata
- Source: user_feedback | bug
- Related Files: src/task/heartbeat/heartbeat.ts, src/main.ts, src/connectors/web/routes/config.ts
- Tags: heartbeat, config, hot-reload, cron, timezone
- Pattern-Key: config.hot_reload.closure

### Resolution
- **Resolved**: 2026-04-19
- **Notes**: Added `updateConfig` to `Heartbeat`. Triggered `updateConfig` inside `reconnectConnectors` in `main.ts`. Added `heartbeat` to the hot-reload trigger in Web UI routes. Changed default config timezone recommendation to explicit strings.

---

## [LRN-20260422-001] React State setter shadowing global API

**Logged**: 2026-04-22T22:15:00Z
**Priority**: high
**Status**: resolved
**Area**: frontend | ui

### Summary
Defining a local function or state setter with the same name as a global browser API (e.g., `const setInterval = ...`) causes TS compilation errors for hooks trying to use the global API.

### Details
- In `KlinePanel.tsx`, a function `const setInterval = (iv: Interval) => ...` was created to manage React state and URL search parameters.
- Further down in the same component, a `useEffect` hook attempted to use the native browser `setInterval()` function for polling market data.
- The TypeScript compiler flagged this as an error (`TS2554` and `TS2769`) because the local `setInterval` expects 1 argument (`iv: Interval`) and returns `void`, but the hook was trying to pass a callback and a number.

### Suggested Action
When a global API name (like `setInterval`, `clearInterval`, `setTimeout`, etc.) is shadowed by a local variable, explicitly prefix the global call with `window.` (e.g., `window.setInterval`, `window.clearInterval`) to bypass the local scope and satisfy the TypeScript compiler.

### Metadata
- Source: error
- Related Files: ui/src/components/market/KlinePanel.tsx
- Tags: react, typescript, shadowing, setInterval
- Pattern-Key: frontend.scope_shadowing.global_api

### Resolution
- **Resolved**: 2026-04-22
- **Notes**: Updated the polling logic in `KlinePanel.tsx` to explicitly use `window.setInterval` and `window.clearInterval`.

---

## [LRN-20260422-002] Missing Sentinel Value Import

**Logged**: 2026-04-22T22:15:00Z
**Priority**: high
**Status**: resolved
**Area**: backend | testing

### Summary
Using a sentinel constant (like `UNSET_DOUBLE`) without importing it causes a runtime `ReferenceError` that breaks test suites and execution.

### Details
- When resolving [LRN-20260415-003] (DataCloneError serialization), `UNSET_DOUBLE` was referenced in `summarizeOperation` to filter out default number values.
- However, `UNSET_DOUBLE` was not imported from `@traderalice/ibkr` alongside `UNSET_DECIMAL`, causing the `trading.spec.ts` test suite to fail entirely with `ReferenceError: UNSET_DOUBLE is not defined`.

### Suggested Action
When adding references to external module constants or sentinels in a file, always ensure the import block at the top of the file is updated to include the new variable. Always run the test suite after modifying serialization boundaries.

### Metadata
- Source: error
- Related Files: src/tool/trading.ts
- Tags: reference_error, import, unset, sentinel
- Pattern-Key: backend.imports.missing_sentinel

### Resolution
- **Resolved**: 2026-04-22
- **Notes**: Added `UNSET_DOUBLE` to the `@traderalice/ibkr` import statement in `src/tool/trading.ts` and verified the fix by running `pnpm test`.

---
20 changes: 17 additions & 3 deletions src/connectors/telegram/telegram-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,13 +165,27 @@ export class TelegramPlugin implements Plugin {
await ctx.editMessageText(text, { reply_markup: keyboard }).catch(() => {})
return
}

// Provide immediate feedback to prevent double-clicks
await ctx.answerCallbackQuery({ text: action === 'push' ? 'Pushing...' : 'Rejecting...' })
const processingText = action === 'push' ? '🚀 Pushing...' : '❌ Rejecting...'
await ctx.editMessageText(
`${processingText}\n\n${status.pendingMessage}`,
{ reply_markup: new InlineKeyboard() },
).catch(() => {})

if (action === 'push') {
const result = await uta.push()
await ctx.answerCallbackQuery({ text: `${result.submitted.length} submitted, ${result.rejected.length} rejected` })
try {
const result = await uta.push()
const summary = `${result.submitted.length} submitted, ${result.rejected.length} rejected`
console.log(`telegram: [${accountId}] push result: ${summary}`)
} catch (err) {
console.error(`telegram: [${accountId}] push failed:`, err)
}
} else {
await uta.reject()
await ctx.answerCallbackQuery({ text: 'Rejected' })
}

// Refresh panel after action
const { text, keyboard } = await this.buildAccountPanel(engineCtx.accountManager, accountId)
await ctx.editMessageText(text, { reply_markup: keyboard }).catch(() => {})
Expand Down
4 changes: 2 additions & 2 deletions src/connectors/web/routes/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,8 @@ export function createConfigRoutes(opts?: ConfigRouteOpts) {
const fresh = await loadConfig()
Object.assign(opts.ctx.config, fresh)
}
// Hot-reload connectors / OpenBB server when their config changes
if (section === 'connectors' || section === 'marketData') {
// Hot-reload connectors / Heartbeat / OpenBB server when their config changes
if (section === 'connectors' || section === 'marketData' || section === 'heartbeat') {
await opts?.onConnectorsChange?.()
}
return c.json(validated)
Expand Down
29 changes: 27 additions & 2 deletions src/domain/trading/UnifiedTradingAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,29 @@ export class UnifiedTradingAccount {
return { utaId: aliceId.slice(0, sep), nativeKey: aliceId.slice(sep + 1) }
}

/**
* Resolve aliceId in contract if present.
* If contract has only aliceId, it fills in broker-native fields (conId, localSymbol, etc.)
* so the broker can recognize it.
*/
private resolveContract(contract: Contract): Contract {
if (contract.aliceId) {
const parsed = UnifiedTradingAccount.parseAliceId(contract.aliceId)
if (parsed) {
if (parsed.utaId !== this.id) {
// Cross-account quoting is allowed in the tool layer, but this UTA can only
// resolve its own IDs. If it doesn't match, we return the original and let
// the broker try to resolve other fields (or fail).
return contract
}
const resolved = this.broker.resolveNativeKey(parsed.nativeKey)
resolved.aliceId = contract.aliceId
return resolved
}
}
return contract
}

// ==================== Stage operations ====================

stagePlaceOrder(params: StagePlaceOrderParams): AddResult {
Expand Down Expand Up @@ -515,7 +538,8 @@ export class UnifiedTradingAccount {
}

async getQuote(contract: Contract): Promise<Quote> {
const quote = await this._callBroker(() => this.broker.getQuote(contract))
const query = this.resolveContract(contract)
const quote = await this._callBroker(() => this.broker.getQuote(query))
this.stampAliceId(quote.contract)
return quote
}
Expand All @@ -531,7 +555,8 @@ export class UnifiedTradingAccount {
}

async getContractDetails(query: Contract): Promise<ContractDetails | null> {
const details = await this._callBroker(() => this.broker.getContractDetails(query))
const resolved = this.resolveContract(query)
const details = await this._callBroker(() => this.broker.getContractDetails(resolved))
if (details) this.stampAliceId(details.contract)
return details
}
Expand Down
Loading