data fixes; indicator=>workspace sync
This commit is contained in:
@@ -16,6 +16,7 @@ import {
|
||||
type SnapshotMessage,
|
||||
type PatchMessage,
|
||||
} from '../workspace/index.js';
|
||||
import { resolutionToSeconds } from '../types/ohlc.js';
|
||||
|
||||
/**
|
||||
* Safe JSON stringifier that handles BigInt values
|
||||
@@ -486,7 +487,7 @@ export class WebSocketHandler {
|
||||
}
|
||||
const history = await ohlcService.fetchOHLC(
|
||||
payload.symbol,
|
||||
payload.resolution,
|
||||
resolutionToSeconds(payload.resolution),
|
||||
payload.from_time,
|
||||
payload.to_time,
|
||||
payload.countback
|
||||
|
||||
@@ -420,7 +420,7 @@ export class DuckDBClient {
|
||||
WHERE ticker = ?
|
||||
AND period_seconds = ?
|
||||
AND timestamp >= ?
|
||||
AND timestamp <= ?
|
||||
AND timestamp < ?
|
||||
ORDER BY timestamp ASC
|
||||
`;
|
||||
|
||||
@@ -441,10 +441,11 @@ export class DuckDBClient {
|
||||
count: rows.length
|
||||
}, 'Loaded OHLC data from Iceberg');
|
||||
|
||||
// Convert timestamp strings to numbers (microseconds as Number is fine for display)
|
||||
// Keep timestamp as bigint to preserve full microsecond precision.
|
||||
// Convert to seconds (divide first) only when producing TradingView bars.
|
||||
return rows.map((row: any) => ({
|
||||
...row,
|
||||
timestamp: Number(row.timestamp)
|
||||
timestamp: BigInt(row.timestamp)
|
||||
}));
|
||||
} catch (error: any) {
|
||||
this.logger.error({
|
||||
@@ -484,7 +485,7 @@ export class DuckDBClient {
|
||||
WHERE ticker = ?
|
||||
AND period_seconds = ?
|
||||
AND timestamp >= ?
|
||||
AND timestamp <= ?
|
||||
AND timestamp < ?
|
||||
`;
|
||||
|
||||
const params = [
|
||||
@@ -525,6 +526,7 @@ export class DuckDBClient {
|
||||
// For now, simple check: if we have any data, assume complete
|
||||
// TODO: Implement proper gap detection by checking for missing periods
|
||||
const periodMicros = BigInt(period_seconds) * 1000000n;
|
||||
// end_time is exclusive, so expected count = (end - start) / period (no +1)
|
||||
const expectedBars = Number((end_time - start_time) / periodMicros);
|
||||
|
||||
if (data.length < expectedBars * 0.95) { // Allow 5% tolerance
|
||||
|
||||
@@ -39,7 +39,7 @@ export interface IcebergMessage {
|
||||
id: string;
|
||||
user_id: string;
|
||||
session_id: string;
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
role: 'user' | 'assistant' | 'system' | 'workspace';
|
||||
content: string;
|
||||
metadata: string; // JSON string
|
||||
timestamp: number; // microseconds
|
||||
|
||||
@@ -8,8 +8,9 @@ import type { InboundMessage, OutboundMessage } from '../types/messages.js';
|
||||
import { MCPClientConnector } from './mcp-client.js';
|
||||
import { LLMProviderFactory, type ProviderConfig } from '../llm/provider.js';
|
||||
import { ModelRouter, RoutingStrategy } from '../llm/router.js';
|
||||
import type { ModelMiddleware } from '../llm/middleware.js';
|
||||
import type { WorkspaceManager } from '../workspace/workspace-manager.js';
|
||||
import type { ChannelAdapter } from '../workspace/index.js';
|
||||
import type { ChannelAdapter, PathTriggerContext } from '../workspace/index.js';
|
||||
import type { ResearchSubagent } from './subagents/research/index.js';
|
||||
import type { DynamicStructuredTool } from '@langchain/core/tools';
|
||||
import { getToolRegistry } from '../tools/tool-registry.js';
|
||||
@@ -70,10 +71,10 @@ export class AgentHarness {
|
||||
private config: AgentHarnessConfig;
|
||||
private modelFactory: LLMProviderFactory;
|
||||
private modelRouter: ModelRouter;
|
||||
private middleware: ModelMiddleware | undefined;
|
||||
private mcpClient: MCPClientConnector;
|
||||
private workspaceManager?: WorkspaceManager;
|
||||
private channelAdapter?: ChannelAdapter;
|
||||
private isFirstMessage: boolean = true;
|
||||
private researchSubagent?: ResearchSubagent;
|
||||
private availableMCPTools: MCPToolInfo[] = [];
|
||||
private researchImageCapture: Array<{ data: string; mimeType: string }> = [];
|
||||
@@ -94,6 +95,8 @@ export class AgentHarness {
|
||||
mcpServerUrl: config.mcpServerUrl,
|
||||
logger: config.logger,
|
||||
});
|
||||
|
||||
this.registerWorkspaceTriggers();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -193,7 +196,7 @@ export class AgentHarness {
|
||||
const { createResearchSubagent } = await import('./subagents/research/index.js');
|
||||
|
||||
// Create a model for the research subagent
|
||||
const model = await this.modelRouter.route(
|
||||
const { model } = await this.modelRouter.route(
|
||||
'research analysis', // dummy query
|
||||
this.config.license,
|
||||
RoutingStrategy.COMPLEXITY,
|
||||
@@ -429,11 +432,25 @@ export class AgentHarness {
|
||||
|
||||
// 2. Load recent conversation history
|
||||
const channelKey = this.config.channelType ?? ChannelType.WEBSOCKET;
|
||||
const storedMessages = this.conversationStore
|
||||
let storedMessages = this.conversationStore
|
||||
? await this.conversationStore.getRecentMessages(
|
||||
this.config.userId, this.config.sessionId, this.config.historyLimit, channelKey
|
||||
)
|
||||
: [];
|
||||
|
||||
// First turn: seed conversation history with current workspace state
|
||||
if (storedMessages.length === 0 && this.workspaceManager && this.conversationStore) {
|
||||
const workspaceJSON = this.workspaceManager.serializeState();
|
||||
const content = `[Workspace State]\n\`\`\`json\n${workspaceJSON}\n\`\`\``;
|
||||
await this.conversationStore.saveMessage(
|
||||
this.config.userId, this.config.sessionId,
|
||||
'workspace', content, { isWorkspaceContext: true }, channelKey
|
||||
);
|
||||
storedMessages = await this.conversationStore.getRecentMessages(
|
||||
this.config.userId, this.config.sessionId, this.config.historyLimit, channelKey
|
||||
);
|
||||
}
|
||||
|
||||
const history = this.conversationStore
|
||||
? this.conversationStore.toLangChainMessages(storedMessages)
|
||||
: [];
|
||||
@@ -441,12 +458,13 @@ export class AgentHarness {
|
||||
|
||||
// 4. Get the configured model
|
||||
this.config.logger.debug('Routing to model');
|
||||
const model = await this.modelRouter.route(
|
||||
const { model, middleware } = await this.modelRouter.route(
|
||||
message.content,
|
||||
this.config.license,
|
||||
RoutingStrategy.COMPLEXITY,
|
||||
this.config.userId
|
||||
);
|
||||
this.middleware = middleware;
|
||||
this.config.logger.info({ modelName: model.constructor.name }, 'Model selected');
|
||||
|
||||
// 5. Build LangChain messages
|
||||
@@ -489,6 +507,11 @@ export class AgentHarness {
|
||||
'Tools loaded for main agent'
|
||||
);
|
||||
|
||||
// Apply middleware (e.g. Anthropic prompt caching)
|
||||
const processedMessages = this.middleware
|
||||
? this.middleware.processMessages(langchainMessages, tools)
|
||||
: langchainMessages;
|
||||
|
||||
// 7. Bind tools to model
|
||||
const modelWithTools = tools.length > 0 && model.bindTools ? model.bindTools(tools) : model;
|
||||
|
||||
@@ -501,7 +524,7 @@ export class AgentHarness {
|
||||
|
||||
// 8. Call LLM with tool calling loop
|
||||
this.config.logger.info('Invoking LLM with tool support');
|
||||
const assistantMessage = await this.executeWithToolCalling(modelWithTools, langchainMessages, tools);
|
||||
const assistantMessage = await this.executeWithToolCalling(modelWithTools, processedMessages, tools, 10);
|
||||
|
||||
this.config.logger.info(
|
||||
{ responseLength: assistantMessage.length },
|
||||
@@ -518,11 +541,6 @@ export class AgentHarness {
|
||||
);
|
||||
}
|
||||
|
||||
// Mark first message as processed
|
||||
if (this.isFirstMessage) {
|
||||
this.isFirstMessage = false;
|
||||
}
|
||||
|
||||
return {
|
||||
messageId: `msg_${Date.now()}`,
|
||||
sessionId: message.sessionId,
|
||||
@@ -556,16 +574,10 @@ export class AgentHarness {
|
||||
private async buildSystemPrompt(): Promise<string> {
|
||||
// Load template and populate with license info
|
||||
const template = await AgentHarness.loadSystemPromptTemplate();
|
||||
let prompt = template
|
||||
const prompt = template
|
||||
.replace('{{licenseType}}', this.config.license.licenseType)
|
||||
.replace('{{features}}', JSON.stringify(this.config.license.features, null, 2));
|
||||
|
||||
// Add full workspace state from WorkspaceManager (first message only)
|
||||
if (this.isFirstMessage && this.workspaceManager) {
|
||||
const workspaceJSON = this.workspaceManager.serializeState();
|
||||
prompt += `\n\n# Current Workspace State\n\`\`\`json\n${workspaceJSON}\n\`\`\``;
|
||||
}
|
||||
|
||||
return prompt;
|
||||
}
|
||||
|
||||
@@ -574,9 +586,14 @@ export class AgentHarness {
|
||||
*/
|
||||
private getToolLabel(toolName: string): string {
|
||||
const labels: Record<string, string> = {
|
||||
research_agent: 'Researching...',
|
||||
research: 'Researching...',
|
||||
get_chart_data: 'Fetching chart data...',
|
||||
symbol_lookup: 'Looking up symbol...',
|
||||
category_list: 'Seeing what we have...',
|
||||
category_edit: 'Coding...',
|
||||
category_write: 'Coding...',
|
||||
category_read: 'Inspecting...',
|
||||
execute_research: 'Running script...',
|
||||
};
|
||||
return labels[toolName] ?? `Running ${toolName}...`;
|
||||
}
|
||||
@@ -685,6 +702,26 @@ export class AgentHarness {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register workspace path triggers to record state changes into conversation history.
|
||||
*/
|
||||
private registerWorkspaceTriggers(): void {
|
||||
if (!this.workspaceManager || !this.conversationStore) return;
|
||||
const channelKey = this.config.channelType ?? ChannelType.WEBSOCKET;
|
||||
|
||||
for (const store of ['shapes', 'indicators', 'chartState']) {
|
||||
this.workspaceManager.onPathChange(`/${store}/*`, async (_old: unknown, newVal: unknown, ctx: PathTriggerContext) => {
|
||||
const content = `[Workspace Update] ${ctx.store}${ctx.path}\n${JSON.stringify(newVal, null, 2)}`;
|
||||
await this.conversationStore!.saveMessage(
|
||||
this.config.userId, this.config.sessionId,
|
||||
'workspace', content,
|
||||
{ isWorkspaceUpdate: true, store: ctx.store, seq: ctx.seq },
|
||||
channelKey
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* End the session: flush conversation to cold storage, then release resources.
|
||||
* Called by channel handlers on disconnect, session expiry, or graceful shutdown.
|
||||
|
||||
@@ -11,7 +11,7 @@ export interface StoredMessage {
|
||||
id: string;
|
||||
userId: string;
|
||||
sessionId: string;
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
role: 'user' | 'assistant' | 'system' | 'workspace';
|
||||
content: string;
|
||||
timestamp: number; // microseconds (Iceberg convention)
|
||||
metadata?: Record<string, unknown>;
|
||||
@@ -44,7 +44,7 @@ export class ConversationStore {
|
||||
async saveMessage(
|
||||
userId: string,
|
||||
sessionId: string,
|
||||
role: 'user' | 'assistant' | 'system',
|
||||
role: 'user' | 'assistant' | 'system' | 'workspace',
|
||||
content: string,
|
||||
metadata?: Record<string, unknown>,
|
||||
channelType?: string
|
||||
@@ -171,6 +171,8 @@ export class ConversationStore {
|
||||
return new AIMessage(msg.content);
|
||||
case 'system':
|
||||
return new SystemMessage(msg.content);
|
||||
case 'workspace':
|
||||
return new HumanMessage(msg.content);
|
||||
default:
|
||||
throw new Error(`Unknown role: ${msg.role}`);
|
||||
}
|
||||
|
||||
@@ -59,6 +59,11 @@ Example usage:
|
||||
- User: "Does Friday price action correlate with Monday?"
|
||||
- You: Call research tool with instruction="Analyze correlation between Friday and Monday price action during NY trading hours (9:30-4:00 ET)", name="Friday-Monday Correlation"
|
||||
|
||||
### category_list
|
||||
List existing research scripts (category="research").
|
||||
Use this before calling the research tool to check whether a relevant script already exists.
|
||||
If one does, pass its exact name to the research tool so the subagent updates it rather than creating a new one.
|
||||
|
||||
### symbol-lookup
|
||||
Look up trading symbols and get metadata.
|
||||
Use this when users mention tickers or need symbol information.
|
||||
|
||||
@@ -12,6 +12,7 @@ maxTokens: 8192
|
||||
memoryFiles:
|
||||
- api-reference.md
|
||||
- usage-examples.md
|
||||
- pandas-ta-reference.md
|
||||
|
||||
# System prompt file
|
||||
systemPromptFile: system-prompt.md
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
# pandas-ta Reference for Research Scripts
|
||||
|
||||
The sandbox environment uses **pandas-ta** as the standard indicator library. Always use it for technical indicator calculations; do not write manual rolling/ewm implementations.
|
||||
|
||||
```python
|
||||
import pandas_ta as ta
|
||||
```
|
||||
|
||||
## Calling Convention
|
||||
|
||||
pandas-ta functions accept a Series (or OHLCV columns) plus keyword parameters that match pandas-ta's documented argument names:
|
||||
|
||||
```python
|
||||
# Single-series indicator
|
||||
rsi = ta.rsi(df['close'], length=14) # returns Series
|
||||
|
||||
# OHLCV indicator
|
||||
atr = ta.atr(df['high'], df['low'], df['close'], length=14)
|
||||
|
||||
# Multi-output indicator (returns DataFrame)
|
||||
macd_df = ta.macd(df['close'], fast=12, slow=26, signal=9)
|
||||
# columns: MACD_12_26_9, MACDh_12_26_9, MACDs_12_26_9
|
||||
|
||||
bbands_df = ta.bbands(df['close'], length=20, std=2.0)
|
||||
# columns: BBL_20_2.0, BBM_20_2.0, BBU_20_2.0, BBB_20_2.0, BBP_20_2.0
|
||||
```
|
||||
|
||||
## Default Parameters
|
||||
|
||||
Key defaults to keep in mind:
|
||||
- Most period/length indicators: `length=14` (use `length=` not `timeperiod=`)
|
||||
- `bbands`: `length=20, std=2.0` (note: single `std`, not separate upper/lower)
|
||||
- `macd`: `fast=12, slow=26, signal=9`
|
||||
- `stoch`: `k=14, d=3, smooth_k=3`
|
||||
- `psar`: `af0=0.02, af=0.02, max_af=0.2`
|
||||
- `vwap`: `anchor='D'` (requires DatetimeIndex)
|
||||
- `ichimoku`: `tenkan=9, kijun=26, senkou=52`
|
||||
|
||||
## Available Indicators
|
||||
|
||||
These match the indicators supported by the TradingView web client. Use the pandas-ta function name shown here (lowercase):
|
||||
|
||||
### Overlap / Moving Averages — plotted on the price pane
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `sma` | Simple Moving Average — plain arithmetic mean over `length` periods |
|
||||
| `ema` | Exponential Moving Average — more weight on recent prices |
|
||||
| `wma` | Weighted Moving Average — linearly increasing weights |
|
||||
| `dema` | Double EMA — two layers of EMA to reduce lag |
|
||||
| `tema` | Triple EMA — three layers of EMA, even less lag than DEMA |
|
||||
| `trima` | Triangular MA — double-smoothed SMA, very smooth |
|
||||
| `kama` | Kaufman Adaptive MA — adapts speed to market noise/trending conditions |
|
||||
| `t3` | T3 Moving Average — Tillson's smooth, low-lag MA using six EMAs |
|
||||
| `hma` | Hull MA — very low-lag MA using WMAs |
|
||||
| `alma` | Arnaud Legoux MA — Gaussian-weighted MA with reduced lag and noise |
|
||||
| `midpoint` | Midpoint of close over `length` periods: (highest + lowest) / 2 |
|
||||
| `midprice` | Midpoint of high/low over `length` periods |
|
||||
| `supertrend` | Trend-following band (ATR-based) that flips above/below price |
|
||||
| `ichimoku` | Ichimoku Cloud — multi-line Japanese trend/support/resistance system |
|
||||
| `vwap` | Volume-Weighted Average Price — average price weighted by volume, resets on `anchor` |
|
||||
| `vwma` | Volume-Weighted MA — like SMA but candles weighted by volume |
|
||||
| `bbands` | Bollinger Bands — SMA ± N standard deviations; returns upper, mid, lower bands |
|
||||
|
||||
### Momentum — typically plotted in a separate pane
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `rsi` | Relative Strength Index — 0–100 oscillator measuring speed of price changes |
|
||||
| `macd` | MACD — difference of two EMAs plus signal line and histogram |
|
||||
| `stoch` | Stochastic Oscillator — %K/%D, measures close vs recent high/low range |
|
||||
| `stochrsi` | Stochastic RSI — applies stochastic formula to RSI values |
|
||||
| `cci` | Commodity Channel Index — deviation of price from its statistical mean |
|
||||
| `willr` | Williams %R — inverse stochastic, −100 to 0 oscillator |
|
||||
| `mom` | Momentum — raw price change over `length` periods |
|
||||
| `roc` | Rate of Change — percentage price change over `length` periods |
|
||||
| `trix` | TRIX — 1-period % change of a triple-smoothed EMA |
|
||||
| `cmo` | Chande Momentum Oscillator — ratio of up/down momentum, −100 to 100 |
|
||||
| `adx` | Average Directional Index — strength of trend (0–100, direction-agnostic) |
|
||||
| `aroon` | Aroon — measures how recently the highest/lowest price occurred; returns Up, Down, Oscillator |
|
||||
| `ao` | Awesome Oscillator — difference of 5- and 34-period simple MAs of midprice |
|
||||
| `bop` | Balance of Power — measures buying vs selling pressure: (close−open)/(high−low) |
|
||||
| `uo` | Ultimate Oscillator — weighted combo of three period (fast/medium/slow) buying pressure ratios |
|
||||
| `apo` | Absolute Price Oscillator — difference between two EMAs (like MACD without signal line) |
|
||||
| `mfi` | Money Flow Index — RSI-like oscillator using price × volume |
|
||||
| `coppock` | Coppock Curve — long-term momentum oscillator based on rate-of-change |
|
||||
| `dpo` | Detrended Price Oscillator — removes trend to show cycle oscillations |
|
||||
| `fisher` | Fisher Transform — converts price into a Gaussian normal distribution |
|
||||
| `rvgi` | Relative Vigor Index — compares close−open to high−low to measure trend vigor |
|
||||
| `kst` | Know Sure Thing — momentum oscillator from four ROC periods, smoothed |
|
||||
|
||||
### Volatility — plotted on price pane or separate
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `atr` | Average True Range — average of true range (greatest of H−L, H−prevC, L−prevC) |
|
||||
| `kc` | Keltner Channels — EMA ± N × ATR bands around price |
|
||||
| `donchian` | Donchian Channels — highest high / lowest low over `length` periods |
|
||||
|
||||
### Volume — plotted in separate pane
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `obv` | On Balance Volume — cumulative volume, added on up days, subtracted on down days |
|
||||
| `ad` | Accumulation/Distribution — running total of the money flow multiplier × volume |
|
||||
| `adosc` | Chaikin Oscillator — EMA difference of the A/D line |
|
||||
| `cmf` | Chaikin Money Flow — sum of (money flow volume) / sum of volume over `length` |
|
||||
| `eom` | Ease of Movement — relates price change to volume; high = price moves easily |
|
||||
| `efi` | Elder's Force Index — combines price change direction with volume magnitude |
|
||||
| `kvo` | Klinger Volume Oscillator — EMA difference of volume force |
|
||||
| `pvt` | Price Volume Trend — cumulative: volume × percentage price change |
|
||||
|
||||
### Statistics / Price Transforms
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `stdev` | Standard Deviation of close over `length` periods |
|
||||
| `linreg` | Linear Regression Curve — least-squares line endpoint value over `length` periods |
|
||||
| `slope` | Linear Regression Slope — gradient of the regression line |
|
||||
| `hl2` | Median Price — (high + low) / 2 |
|
||||
| `hlc3` | Typical Price — (high + low + close) / 3 |
|
||||
| `ohlc4` | Average Price — (open + high + low + close) / 4 |
|
||||
|
||||
### Trend
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `psar` | Parabolic SAR — trailing stop-and-reverse dots that follow price |
|
||||
| `vortex` | Vortex Indicator — VI+ / VI− lines measuring upward vs downward trend movement |
|
||||
| `chop` | Choppiness Index — 0–100, high = choppy/sideways, low = strong trend |
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Single-output indicators
|
||||
|
||||
```python
|
||||
import pandas_ta as ta
|
||||
|
||||
df['rsi'] = ta.rsi(df['close'], length=14)
|
||||
df['ema_20'] = ta.ema(df['close'], length=20)
|
||||
df['sma_50'] = ta.sma(df['close'], length=50)
|
||||
df['atr'] = ta.atr(df['high'], df['low'], df['close'], length=14)
|
||||
df['obv'] = ta.obv(df['close'], df['volume'])
|
||||
df['adx'] = ta.adx(df['high'], df['low'], df['close'], length=14)['ADX_14']
|
||||
```
|
||||
|
||||
### Multi-output indicators — extract columns by position
|
||||
|
||||
```python
|
||||
# MACD → MACD_12_26_9, MACDh_12_26_9, MACDs_12_26_9
|
||||
macd_df = ta.macd(df['close'], fast=12, slow=26, signal=9)
|
||||
df['macd'] = macd_df.iloc[:, 0] # MACD line
|
||||
df['macd_hist'] = macd_df.iloc[:, 1] # Histogram
|
||||
df['macd_signal'] = macd_df.iloc[:, 2] # Signal line
|
||||
|
||||
# Bollinger Bands → BBL, BBM, BBU, BBB, BBP
|
||||
bb_df = ta.bbands(df['close'], length=20, std=2.0)
|
||||
df['bb_lower'] = bb_df.iloc[:, 0] # BBL
|
||||
df['bb_mid'] = bb_df.iloc[:, 1] # BBM
|
||||
df['bb_upper'] = bb_df.iloc[:, 2] # BBU
|
||||
|
||||
# Stochastic → STOCHk, STOCHd
|
||||
stoch_df = ta.stoch(df['high'], df['low'], df['close'], k=14, d=3, smooth_k=3)
|
||||
df['stoch_k'] = stoch_df.iloc[:, 0]
|
||||
df['stoch_d'] = stoch_df.iloc[:, 1]
|
||||
|
||||
# Keltner Channels → KCLe, KCBe, KCUe
|
||||
kc_df = ta.kc(df['high'], df['low'], df['close'], length=20)
|
||||
df['kc_lower'] = kc_df.iloc[:, 0]
|
||||
df['kc_mid'] = kc_df.iloc[:, 1]
|
||||
df['kc_upper'] = kc_df.iloc[:, 2]
|
||||
|
||||
# ADX → ADX_14, DMP_14, DMN_14
|
||||
adx_df = ta.adx(df['high'], df['low'], df['close'], length=14)
|
||||
df['adx'] = adx_df.iloc[:, 0] # ADX strength
|
||||
df['dmp'] = adx_df.iloc[:, 1] # +DI
|
||||
df['dmn'] = adx_df.iloc[:, 2] # -DI
|
||||
|
||||
# Aroon → AROOND_14, AROONU_14, AROONOSC_14
|
||||
aroon_df = ta.aroon(df['high'], df['low'], length=14)
|
||||
df['aroon_down'] = aroon_df.iloc[:, 0]
|
||||
df['aroon_up'] = aroon_df.iloc[:, 1]
|
||||
|
||||
# Donchian Channels → DCL, DCM, DCU
|
||||
dc_df = ta.donchian(df['high'], df['low'], lower_length=20, upper_length=20)
|
||||
df['dc_lower'] = dc_df.iloc[:, 0]
|
||||
df['dc_mid'] = dc_df.iloc[:, 1]
|
||||
df['dc_upper'] = dc_df.iloc[:, 2]
|
||||
```
|
||||
|
||||
### Charting with indicators
|
||||
|
||||
```python
|
||||
import pandas_ta as ta
|
||||
from dexorder.api import get_api
|
||||
import asyncio
|
||||
|
||||
api = get_api()
|
||||
|
||||
df = asyncio.run(api.data.historical_ohlc(
|
||||
ticker="BINANCE:BTC/USDT",
|
||||
period_seconds=3600,
|
||||
start_time="2024-01-01",
|
||||
end_time="2024-01-08",
|
||||
extra_columns=["volume"]
|
||||
))
|
||||
|
||||
# Compute indicators
|
||||
df['ema_20'] = ta.ema(df['close'], length=20)
|
||||
df['rsi'] = ta.rsi(df['close'], length=14)
|
||||
macd_df = ta.macd(df['close'])
|
||||
df['macd'] = macd_df.iloc[:, 0]
|
||||
df['macd_signal'] = macd_df.iloc[:, 2]
|
||||
|
||||
# Main price chart with EMA overlay
|
||||
fig, ax = api.charting.plot_ohlc(df, title="BTC/USDT 1H", volume=True)
|
||||
ax.plot(df.index, df['ema_20'], label="EMA 20", color="orange", linewidth=1.5)
|
||||
ax.legend()
|
||||
|
||||
# RSI panel
|
||||
rsi_ax = api.charting.add_indicator_panel(fig, df, columns=["rsi"], ylabel="RSI", ylim=(0, 100))
|
||||
rsi_ax.axhline(70, color='red', linestyle='--', alpha=0.5)
|
||||
rsi_ax.axhline(30, color='green', linestyle='--', alpha=0.5)
|
||||
|
||||
# MACD panel
|
||||
api.charting.add_indicator_panel(fig, df, columns=["macd", "macd_signal"], ylabel="MACD")
|
||||
```
|
||||
@@ -112,10 +112,12 @@ fig, ax = api.charting.plot_ohlc(
|
||||
|
||||
### Adding Indicator Panels
|
||||
|
||||
Use **pandas-ta** for all indicator calculations. Do not write manual rolling/ewm implementations.
|
||||
|
||||
```python
|
||||
from dexorder.api import get_api
|
||||
import asyncio
|
||||
import pandas as pd
|
||||
import pandas_ta as ta
|
||||
|
||||
api = get_api()
|
||||
|
||||
@@ -127,8 +129,9 @@ df = asyncio.run(api.data.historical_ohlc(
|
||||
end_time="2021-12-21"
|
||||
))
|
||||
|
||||
# Calculate a simple moving average
|
||||
df['sma_20'] = df['close'].rolling(window=20).mean()
|
||||
# Calculate indicators using pandas-ta
|
||||
df['sma_20'] = ta.sma(df['close'], length=20)
|
||||
df['rsi'] = ta.rsi(df['close'], length=14)
|
||||
|
||||
# Create chart
|
||||
fig, ax = api.charting.plot_ohlc(df, title="BTC/USDT with SMA")
|
||||
@@ -138,7 +141,6 @@ ax.plot(df.index, df['sma_20'], label="SMA 20", color="blue", linewidth=2)
|
||||
ax.legend()
|
||||
|
||||
# Add RSI indicator panel below
|
||||
df['rsi'] = calculate_rsi(df['close'], 14) # Your RSI calculation
|
||||
rsi_ax = api.charting.add_indicator_panel(
|
||||
fig, df,
|
||||
columns=["rsi"],
|
||||
@@ -149,12 +151,40 @@ rsi_ax.axhline(70, color='red', linestyle='--', alpha=0.5)
|
||||
rsi_ax.axhline(30, color='green', linestyle='--', alpha=0.5)
|
||||
```
|
||||
|
||||
### Multi-Output Indicators
|
||||
|
||||
Some pandas-ta indicators return a DataFrame. Extract the columns you need:
|
||||
|
||||
```python
|
||||
import pandas_ta as ta
|
||||
|
||||
# MACD returns: MACD_12_26_9, MACDh_12_26_9, MACDs_12_26_9
|
||||
macd_df = ta.macd(df['close'], fast=12, slow=26, signal=9)
|
||||
df['macd'] = macd_df.iloc[:, 0] # MACD line
|
||||
df['macd_hist'] = macd_df.iloc[:, 1] # Histogram
|
||||
df['macd_signal'] = macd_df.iloc[:, 2] # Signal line
|
||||
|
||||
# Bollinger Bands returns: BBL, BBM, BBU, BBB, BBP
|
||||
bb_df = ta.bbands(df['close'], length=20, std=2.0)
|
||||
df['bb_upper'] = bb_df.iloc[:, 2] # BBU
|
||||
df['bb_mid'] = bb_df.iloc[:, 1] # BBM
|
||||
df['bb_lower'] = bb_df.iloc[:, 0] # BBL
|
||||
|
||||
# Stochastic returns: STOCHk, STOCHd
|
||||
stoch_df = ta.stoch(df['high'], df['low'], df['close'], k=14, d=3, smooth_k=3)
|
||||
df['stoch_k'] = stoch_df.iloc[:, 0]
|
||||
df['stoch_d'] = stoch_df.iloc[:, 1]
|
||||
|
||||
# ATR (uses high, low, close)
|
||||
df['atr'] = ta.atr(df['high'], df['low'], df['close'], length=14)
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
```python
|
||||
from dexorder.api import get_api
|
||||
import asyncio
|
||||
import pandas as pd
|
||||
import pandas_ta as ta
|
||||
|
||||
# Get API instance
|
||||
api = get_api()
|
||||
@@ -168,9 +198,9 @@ df = asyncio.run(api.data.historical_ohlc(
|
||||
extra_columns=["volume"]
|
||||
))
|
||||
|
||||
# Add some analysis
|
||||
df['sma_20'] = df['close'].rolling(window=20).mean()
|
||||
df['sma_50'] = df['close'].rolling(window=50).mean()
|
||||
# Add moving averages using pandas-ta
|
||||
df['sma_20'] = ta.sma(df['close'], length=20)
|
||||
df['ema_50'] = ta.ema(df['close'], length=50)
|
||||
|
||||
# Create chart with volume
|
||||
fig, ax = api.charting.plot_ohlc(
|
||||
@@ -182,7 +212,7 @@ fig, ax = api.charting.plot_ohlc(
|
||||
|
||||
# Overlay moving averages
|
||||
ax.plot(df.index, df['sma_20'], label="SMA 20", color="blue", linewidth=1.5)
|
||||
ax.plot(df.index, df['sma_50'], label="SMA 50", color="red", linewidth=1.5)
|
||||
ax.plot(df.index, df['ema_50'], label="EMA 50", color="red", linewidth=1.5)
|
||||
ax.legend()
|
||||
|
||||
# Print summary statistics
|
||||
|
||||
@@ -51,7 +51,140 @@ The API provides two main components:
|
||||
- `api.data` - DataAPI for fetching OHLC market data
|
||||
- `api.charting` - ChartingAPI for creating financial charts
|
||||
|
||||
See your knowledge base for complete API documentation and examples.
|
||||
See your knowledge base for complete API documentation, examples, and the full pandas-ta indicator reference (see `pandas-ta-reference.md`).
|
||||
|
||||
## Technical Indicators — pandas-ta
|
||||
|
||||
The sandbox environment uses **pandas-ta** as the standard indicator library. Always use it for technical indicator calculations; do not write manual rolling/ewm implementations.
|
||||
|
||||
```python
|
||||
import pandas_ta as ta
|
||||
```
|
||||
|
||||
### Calling Convention
|
||||
|
||||
pandas-ta functions accept a Series (or OHLCV columns) plus keyword parameters that match pandas-ta's documented argument names:
|
||||
|
||||
```python
|
||||
# Single-series indicator
|
||||
rsi = ta.rsi(df['close'], length=14) # returns Series
|
||||
|
||||
# OHLCV indicator
|
||||
atr = ta.atr(df['high'], df['low'], df['close'], length=14)
|
||||
|
||||
# Multi-output indicator (returns DataFrame)
|
||||
macd_df = ta.macd(df['close'], fast=12, slow=26, signal=9)
|
||||
# columns: MACD_12_26_9, MACDh_12_26_9, MACDs_12_26_9
|
||||
|
||||
bbands_df = ta.bbands(df['close'], length=20, std=2.0)
|
||||
# columns: BBL_20_2.0, BBM_20_2.0, BBU_20_2.0, BBB_20_2.0, BBP_20_2.0
|
||||
```
|
||||
|
||||
### Available Indicators (canonical list)
|
||||
|
||||
These match the indicators supported by the TradingView web client. Use the pandas-ta function name shown here (lowercase):
|
||||
|
||||
**Overlap / Moving Averages** — plotted on the price pane
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `sma` | Simple Moving Average — plain arithmetic mean over `length` periods |
|
||||
| `ema` | Exponential Moving Average — more weight on recent prices |
|
||||
| `wma` | Weighted Moving Average — linearly increasing weights |
|
||||
| `dema` | Double EMA — two layers of EMA to reduce lag |
|
||||
| `tema` | Triple EMA — three layers of EMA, even less lag than DEMA |
|
||||
| `trima` | Triangular MA — double-smoothed SMA, very smooth |
|
||||
| `kama` | Kaufman Adaptive MA — adapts speed to market noise/trending conditions |
|
||||
| `t3` | T3 Moving Average — Tillson's smooth, low-lag MA using six EMAs |
|
||||
| `hma` | Hull MA — very low-lag MA using WMAs |
|
||||
| `alma` | Arnaud Legoux MA — Gaussian-weighted MA with reduced lag and noise |
|
||||
| `midpoint` | Midpoint of close over `length` periods: (highest + lowest) / 2 |
|
||||
| `midprice` | Midpoint of high/low over `length` periods |
|
||||
| `supertrend` | Trend-following band (ATR-based) that flips above/below price |
|
||||
| `ichimoku` | Ichimoku Cloud — multi-line Japanese trend/support/resistance system |
|
||||
| `vwap` | Volume-Weighted Average Price — average price weighted by volume, resets on `anchor` |
|
||||
| `vwma` | Volume-Weighted MA — like SMA but candles weighted by volume |
|
||||
| `bbands` | Bollinger Bands — SMA ± N standard deviations; returns upper, mid, lower bands |
|
||||
|
||||
**Momentum** — typically plotted in a separate pane
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `rsi` | Relative Strength Index — 0–100 oscillator measuring speed of price changes |
|
||||
| `macd` | MACD — difference of two EMAs plus signal line and histogram |
|
||||
| `stoch` | Stochastic Oscillator — %K/%D, measures close vs recent high/low range |
|
||||
| `stochrsi` | Stochastic RSI — applies stochastic formula to RSI values |
|
||||
| `cci` | Commodity Channel Index — deviation of price from its statistical mean |
|
||||
| `willr` | Williams %R — inverse stochastic, −100 to 0 oscillator |
|
||||
| `mom` | Momentum — raw price change over `length` periods |
|
||||
| `roc` | Rate of Change — percentage price change over `length` periods |
|
||||
| `trix` | TRIX — 1-period % change of a triple-smoothed EMA |
|
||||
| `cmo` | Chande Momentum Oscillator — ratio of up/down momentum, −100 to 100 |
|
||||
| `adx` | Average Directional Index — strength of trend (0–100, direction-agnostic) |
|
||||
| `aroon` | Aroon — measures how recently the highest/lowest price occurred; returns Up, Down, Oscillator |
|
||||
| `ao` | Awesome Oscillator — difference of 5- and 34-period simple MAs of midprice |
|
||||
| `bop` | Balance of Power — measures buying vs selling pressure: (close−open)/(high−low) |
|
||||
| `uo` | Ultimate Oscillator — weighted combo of three period (fast/medium/slow) buying pressure ratios |
|
||||
| `apo` | Absolute Price Oscillator — difference between two EMAs (like MACD without signal line) |
|
||||
| `mfi` | Money Flow Index — RSI-like oscillator using price × volume |
|
||||
| `coppock` | Coppock Curve — long-term momentum oscillator based on rate-of-change |
|
||||
| `dpo` | Detrended Price Oscillator — removes trend to show cycle oscillations |
|
||||
| `fisher` | Fisher Transform — converts price into a Gaussian normal distribution |
|
||||
| `rvgi` | Relative Vigor Index — compares close−open to high−low to measure trend vigor |
|
||||
| `kst` | Know Sure Thing — momentum oscillator from four ROC periods, smoothed |
|
||||
|
||||
**Volatility** — plotted on price pane or separate
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `atr` | Average True Range — average of true range (greatest of H−L, H−prevC, L−prevC) |
|
||||
| `kc` | Keltner Channels — EMA ± N × ATR bands around price |
|
||||
| `donchian` | Donchian Channels — highest high / lowest low over `length` periods |
|
||||
|
||||
**Volume** — plotted in separate pane
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `obv` | On Balance Volume — cumulative volume, added on up days, subtracted on down days |
|
||||
| `ad` | Accumulation/Distribution — running total of the money flow multiplier × volume |
|
||||
| `adosc` | Chaikin Oscillator — EMA difference of the A/D line |
|
||||
| `cmf` | Chaikin Money Flow — sum of (money flow volume) / sum of volume over `length` |
|
||||
| `eom` | Ease of Movement — relates price change to volume; high = price moves easily |
|
||||
| `efi` | Elder's Force Index — combines price change direction with volume magnitude |
|
||||
| `kvo` | Klinger Volume Oscillator — EMA difference of volume force |
|
||||
| `pvt` | Price Volume Trend — cumulative: volume × percentage price change |
|
||||
|
||||
**Statistics / Price Transforms**
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `stdev` | Standard Deviation of close over `length` periods |
|
||||
| `linreg` | Linear Regression Curve — least-squares line endpoint value over `length` periods |
|
||||
| `slope` | Linear Regression Slope — gradient of the regression line |
|
||||
| `hl2` | Median Price — (high + low) / 2 |
|
||||
| `hlc3` | Typical Price — (high + low + close) / 3 |
|
||||
| `ohlc4` | Average Price — (open + high + low + close) / 4 |
|
||||
|
||||
**Trend**
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `psar` | Parabolic SAR — trailing stop-and-reverse dots that follow price |
|
||||
| `vortex` | Vortex Indicator — VI+ / VI− lines measuring upward vs downward trend movement |
|
||||
| `chop` | Choppiness Index — 0–100, high = choppy/sideways, low = strong trend |
|
||||
|
||||
### Default Parameters
|
||||
|
||||
Key defaults to keep in mind:
|
||||
- Most period/length indicators: `length=14` (use `length=` not `timeperiod=`)
|
||||
- `bbands`: `length=20, std=2.0` (note: single `std`, not separate upper/lower)
|
||||
- `macd`: `fast=12, slow=26, signal=9`
|
||||
- `stoch`: `k=14, d=3, smooth_k=3`
|
||||
- `psar`: `af0=0.02, af=0.02, max_af=0.2`
|
||||
- `vwap`: `anchor='D'` (requires DatetimeIndex)
|
||||
- `ichimoku`: `tenkan=9, kijun=26, senkou=52`
|
||||
|
||||
For multi-output indicator column extraction patterns and complete charting examples, fetch `pandas-ta-reference.md` from your knowledge base.
|
||||
|
||||
## Coding Loop Pattern
|
||||
|
||||
@@ -59,11 +192,9 @@ When a user requests analysis:
|
||||
|
||||
1. **Understand the request**: What data is needed? What analysis? What visualization?
|
||||
|
||||
2. **Check for existing scripts**: Use `category_list` to see if a similar script exists
|
||||
- If exists and suitable: use `category_read` to review it
|
||||
- Consider editing existing script vs creating new one
|
||||
2. **Use the provided name**: The instruction will begin with `Research script name: "<name>"`. Always use that exact name when calling `category_write` or `category_edit`. Check first with `category_read` — if the script already exists, use `category_edit` to update it rather than creating a new one with `category_write`.
|
||||
|
||||
3. **Write the script**: Use `category_write` (or `category_edit`)
|
||||
3. **Write the script**: Use `category_write` (new) or `category_edit` (existing)
|
||||
- Write clean, well-commented Python code
|
||||
- Include proper error handling
|
||||
- Use appropriate ticker symbols, time ranges, and periods
|
||||
@@ -106,10 +237,7 @@ When a user requests analysis:
|
||||
- Add `conda_packages: ["package-name"]` to metadata
|
||||
- Packages are auto-installed during validation
|
||||
|
||||
- **Script naming**: Choose descriptive, unique names. Examples:
|
||||
- "BTC Weekly Analysis"
|
||||
- "ETH Volume Profile"
|
||||
- "Market Correlation Heatmap"
|
||||
- **Script naming**: Always use the name provided in the instruction (`Research script name: "<name>"`). Do not invent a different name.
|
||||
|
||||
- **Error handling**: Wrap data fetching in try/except to provide helpful error messages
|
||||
|
||||
|
||||
89
gateway/src/llm/middleware.ts
Normal file
89
gateway/src/llm/middleware.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import type { BaseMessage } from '@langchain/core/messages';
|
||||
import { SystemMessage, HumanMessage, AIMessage } from '@langchain/core/messages';
|
||||
import type { StructuredTool } from '@langchain/core/tools';
|
||||
|
||||
/**
|
||||
* Provider-agnostic hook to preprocess messages before an LLM call.
|
||||
* Applied transparently by the harness; implementations are provider-specific.
|
||||
*/
|
||||
export interface ModelMiddleware {
|
||||
processMessages(messages: BaseMessage[], tools: StructuredTool[]): BaseMessage[];
|
||||
}
|
||||
|
||||
/**
|
||||
* No-op implementation for providers that don't support prompt caching.
|
||||
*/
|
||||
export class NoopMiddleware implements ModelMiddleware {
|
||||
processMessages(messages: BaseMessage[]): BaseMessage[] {
|
||||
return messages;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mirrors Python's AnthropicPromptCachingMiddleware logic.
|
||||
*
|
||||
* Tags with cache_control: { type: 'ephemeral' }:
|
||||
* 1. The system message last content block (stable prompt prefix — always a cache hit after turn 1)
|
||||
* 2. The last non-current cacheable message (AIMessage or HumanMessage before the final user message)
|
||||
* so the full conversation prefix is cached on the next turn.
|
||||
*
|
||||
* Requires ChatAnthropic to be configured with:
|
||||
* clientOptions: { defaultHeaders: { 'anthropic-beta': 'prompt-caching-2024-07-31' } }
|
||||
*/
|
||||
export class AnthropicCachingMiddleware implements ModelMiddleware {
|
||||
processMessages(messages: BaseMessage[], _tools: StructuredTool[]): BaseMessage[] {
|
||||
if (messages.length === 0) return messages;
|
||||
|
||||
const result = messages.map(msg => cloneMessage(msg));
|
||||
|
||||
// 1. Tag system message
|
||||
const systemMsg = result.find(m => m._getType() === 'system');
|
||||
if (systemMsg) {
|
||||
addCacheControl(systemMsg);
|
||||
}
|
||||
|
||||
// 2. Tag the last cacheable message that isn't the current user input.
|
||||
// The current user message is always the last element; we want the one before it.
|
||||
// We look backwards for the last AIMessage or HumanMessage (excluding the final message).
|
||||
const candidates = result.slice(0, -1);
|
||||
for (let i = candidates.length - 1; i >= 0; i--) {
|
||||
const t = candidates[i]._getType();
|
||||
if (t === 'ai' || t === 'human') {
|
||||
addCacheControl(candidates[i]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shallow-clone a message so we don't mutate history objects.
|
||||
*/
|
||||
function cloneMessage(msg: BaseMessage): BaseMessage {
|
||||
const type = msg._getType();
|
||||
const content = typeof msg.content === 'string'
|
||||
? msg.content
|
||||
: JSON.parse(JSON.stringify(msg.content));
|
||||
|
||||
if (type === 'system') return new SystemMessage({ content, additional_kwargs: { ...msg.additional_kwargs } });
|
||||
if (type === 'human') return new HumanMessage({ content, additional_kwargs: { ...msg.additional_kwargs } });
|
||||
if (type === 'ai') return new AIMessage({ content, additional_kwargs: { ...msg.additional_kwargs }, tool_calls: (msg as AIMessage).tool_calls });
|
||||
// For other types (tool messages etc.), return as-is — we don't tag them
|
||||
return msg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add cache_control to the last content block of a message.
|
||||
* Converts string content to a block array if needed.
|
||||
*/
|
||||
function addCacheControl(msg: BaseMessage): void {
|
||||
if (typeof msg.content === 'string') {
|
||||
// Convert to block array
|
||||
(msg as any).content = [{ type: 'text', text: msg.content, cache_control: { type: 'ephemeral' } }];
|
||||
} else if (Array.isArray(msg.content) && msg.content.length > 0) {
|
||||
const last = msg.content[msg.content.length - 1] as any;
|
||||
last.cache_control = { type: 'ephemeral' };
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||
import { ChatAnthropic } from '@langchain/anthropic';
|
||||
import type { FastifyBaseLogger } from 'fastify';
|
||||
import { type ModelMiddleware, NoopMiddleware, AnthropicCachingMiddleware } from './middleware.js';
|
||||
|
||||
export type { ModelMiddleware };
|
||||
export { NoopMiddleware, AnthropicCachingMiddleware };
|
||||
|
||||
/**
|
||||
* Supported LLM providers
|
||||
@@ -64,7 +68,7 @@ export class LLMProviderFactory {
|
||||
/**
|
||||
* Create a chat model instance
|
||||
*/
|
||||
createModel(modelConfig: ModelConfig): BaseChatModel {
|
||||
createModel(modelConfig: ModelConfig): { model: BaseChatModel; middleware: ModelMiddleware } {
|
||||
this.logger.debug(
|
||||
{ provider: modelConfig.provider, model: modelConfig.model },
|
||||
'Creating LLM model'
|
||||
@@ -82,17 +86,20 @@ export class LLMProviderFactory {
|
||||
/**
|
||||
* Create Anthropic Claude model
|
||||
*/
|
||||
private createAnthropicModel(config: ModelConfig): ChatAnthropic {
|
||||
private createAnthropicModel(config: ModelConfig): { model: ChatAnthropic; middleware: AnthropicCachingMiddleware } {
|
||||
if (!this.config.anthropicApiKey) {
|
||||
throw new Error('Anthropic API key not configured');
|
||||
}
|
||||
|
||||
return new ChatAnthropic({
|
||||
const model = new ChatAnthropic({
|
||||
model: config.model,
|
||||
temperature: config.temperature ?? 0.7,
|
||||
maxTokens: config.maxTokens ?? 4096,
|
||||
anthropicApiKey: this.config.anthropicApiKey,
|
||||
clientOptions: { defaultHeaders: { 'anthropic-beta': 'prompt-caching-2024-07-31' } },
|
||||
});
|
||||
|
||||
return { model, middleware: new AnthropicCachingMiddleware() };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||
import type { FastifyBaseLogger } from 'fastify';
|
||||
import { LLMProviderFactory, type ModelConfig, LLMProvider, type LicenseModelsConfig } from './provider.js';
|
||||
import type { ModelMiddleware } from './middleware.js';
|
||||
import type { License } from '../types/user.js';
|
||||
|
||||
/**
|
||||
@@ -42,7 +43,7 @@ export class ModelRouter {
|
||||
license: License,
|
||||
strategy: RoutingStrategy = RoutingStrategy.USER_PREFERENCE,
|
||||
userId?: string
|
||||
): Promise<BaseChatModel> {
|
||||
): Promise<{ model: BaseChatModel; middleware: ModelMiddleware }> {
|
||||
let modelConfig: ModelConfig;
|
||||
|
||||
switch (strategy) {
|
||||
|
||||
@@ -586,7 +586,7 @@ try {
|
||||
toolRegistry.registerAgentTools({
|
||||
agentName: 'main',
|
||||
platformTools: ['symbol_lookup', 'get_chart_data'],
|
||||
mcpTools: [], // No MCP tools for main agent by default (can be extended later)
|
||||
mcpTools: ['category_list'], // category_list lets the main agent see existing research scripts
|
||||
});
|
||||
|
||||
// Research subagent: only MCP tools for script creation/execution
|
||||
|
||||
@@ -27,7 +27,6 @@ import type {
|
||||
import {
|
||||
secondsToMicros,
|
||||
backendToTradingView,
|
||||
resolutionToSeconds,
|
||||
DEFAULT_SUPPORTED_RESOLUTIONS,
|
||||
} from '../types/ohlc.js';
|
||||
|
||||
@@ -67,25 +66,32 @@ export class OHLCService {
|
||||
*/
|
||||
async fetchOHLC(
|
||||
ticker: string,
|
||||
resolution: string,
|
||||
period_seconds: number,
|
||||
from_time: number, // Unix timestamp in SECONDS
|
||||
to_time: number, // Unix timestamp in SECONDS
|
||||
countback?: number
|
||||
): Promise<HistoryResult> {
|
||||
this.logger.debug({
|
||||
ticker,
|
||||
resolution,
|
||||
period_seconds,
|
||||
from_time,
|
||||
to_time,
|
||||
countback,
|
||||
}, 'Fetching OHLC data');
|
||||
|
||||
// Convert resolution to period_seconds
|
||||
const period_seconds = resolutionToSeconds(resolution);
|
||||
|
||||
// Convert times to microseconds
|
||||
const start_time = secondsToMicros(from_time);
|
||||
const end_time = secondsToMicros(to_time);
|
||||
// Convert times to microseconds, then align to period boundaries using
|
||||
// [ceil(start), ceil(end)) semantics:
|
||||
// - start: ceil to next period boundary — excludes any in-progress candle whose
|
||||
// official timestamp is before from_time.
|
||||
// - end: ceil to next period boundary, used as EXCLUSIVE upper bound — includes
|
||||
// the last candle whose timestamp < to_time, excludes one sitting exactly on
|
||||
// to_time (which would be the next candle, not yet started).
|
||||
const periodMicros = BigInt(period_seconds) * 1_000_000n;
|
||||
const raw_start = secondsToMicros(from_time);
|
||||
const raw_end = secondsToMicros(to_time);
|
||||
// bigint ceiling: ceil(a/b)*b = ((a + b - 1) / b) * b
|
||||
const start_time = ((raw_start + periodMicros - 1n) / periodMicros) * periodMicros;
|
||||
const end_time = ((raw_end + periodMicros - 1n) / periodMicros) * periodMicros; // exclusive
|
||||
|
||||
// Step 1: Check Iceberg for existing data
|
||||
let data = await this.icebergClient.queryOHLC(ticker, period_seconds, start_time, end_time);
|
||||
@@ -100,25 +106,26 @@ export class OHLCService {
|
||||
|
||||
if (missingRanges.length === 0 && data.length > 0) {
|
||||
// All data exists in Iceberg
|
||||
this.logger.debug({ ticker, resolution, cached: true }, 'OHLC data found in cache');
|
||||
return this.formatHistoryResult(data, countback);
|
||||
this.logger.debug({ ticker, period_seconds, cached: true }, 'OHLC data found in cache');
|
||||
return this.formatHistoryResult(data, start_time, end_time, period_seconds, countback);
|
||||
}
|
||||
|
||||
// Step 3: Request missing data via relay
|
||||
this.logger.debug({ ticker, resolution, missingRanges: missingRanges.length }, 'Requesting missing OHLC data');
|
||||
this.logger.debug({ ticker, period_seconds, missingRanges: missingRanges.length }, 'Requesting missing OHLC data');
|
||||
|
||||
try {
|
||||
const notification = await this.relayClient.requestHistoricalOHLC(
|
||||
ticker,
|
||||
period_seconds,
|
||||
start_time,
|
||||
end_time,
|
||||
countback
|
||||
end_time
|
||||
// countback is NOT passed as a limit — the ingestor must fetch the full range.
|
||||
// Countback is applied below after we have the complete dataset.
|
||||
);
|
||||
|
||||
this.logger.info({
|
||||
ticker,
|
||||
resolution,
|
||||
period_seconds,
|
||||
row_count: notification.row_count,
|
||||
status: notification.status,
|
||||
}, 'Historical data request completed');
|
||||
@@ -126,13 +133,13 @@ export class OHLCService {
|
||||
// Step 4: Query Iceberg again for complete dataset
|
||||
data = await this.icebergClient.queryOHLC(ticker, period_seconds, start_time, end_time);
|
||||
|
||||
return this.formatHistoryResult(data, countback);
|
||||
return this.formatHistoryResult(data, start_time, end_time, period_seconds, countback);
|
||||
|
||||
} catch (error: any) {
|
||||
this.logger.error({
|
||||
error,
|
||||
ticker,
|
||||
resolution,
|
||||
period_seconds,
|
||||
}, 'Failed to fetch historical data');
|
||||
|
||||
// Return empty result on error
|
||||
@@ -144,9 +151,22 @@ export class OHLCService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Format OHLC data as TradingView history result
|
||||
* Format OHLC data as TradingView history result.
|
||||
*
|
||||
* Interior gaps (confirmed trading periods with no trades) arrive as null-OHLC
|
||||
* rows from Iceberg. Edge gaps (data not yet ingested, in-progress candles) are
|
||||
* simply absent rows. Both are returned as-is; clients fill as appropriate.
|
||||
*/
|
||||
private formatHistoryResult(data: any[], countback?: number): HistoryResult {
|
||||
private formatHistoryResult(
|
||||
data: any[],
|
||||
// @ts-ignore
|
||||
start_time: bigint,
|
||||
// @ts-ignore
|
||||
end_time: bigint,
|
||||
// @ts-ignore
|
||||
period_seconds: number,
|
||||
countback?: number
|
||||
): HistoryResult {
|
||||
if (data.length === 0) {
|
||||
return {
|
||||
bars: [],
|
||||
@@ -154,13 +174,11 @@ export class OHLCService {
|
||||
};
|
||||
}
|
||||
|
||||
// Convert to TradingView format
|
||||
// Convert to TradingView format without null-filling missing slots.
|
||||
let bars: TradingViewBar[] = data.map(backendToTradingView);
|
||||
|
||||
// Sort by time
|
||||
bars.sort((a, b) => a.time - b.time);
|
||||
|
||||
// Apply countback limit if specified
|
||||
if (countback && bars.length > countback) {
|
||||
bars = bars.slice(-countback);
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ Parameters:
|
||||
|
||||
// Build request with workspace defaults
|
||||
const finalTicker = ticker ?? chartState.symbol;
|
||||
const finalPeriod = period ?? parsePeriod(chartState.period);
|
||||
const finalPeriod = period ?? chartState.period;
|
||||
const finalFromTime = await parseTime(from_time, chartState.start_time, logger);
|
||||
const finalToTime = await parseTime(to_time, chartState.end_time, logger);
|
||||
const requestedColumns = columns ?? [];
|
||||
@@ -83,7 +83,7 @@ Parameters:
|
||||
// Fetch data from OHLCService
|
||||
const historyResult = await ohlcService.fetchOHLC(
|
||||
finalTicker,
|
||||
finalPeriod.toString(),
|
||||
finalPeriod,
|
||||
finalFromTime,
|
||||
finalToTime,
|
||||
countback
|
||||
@@ -167,7 +167,7 @@ async function getChartState(workspaceManager: WorkspaceManager, logger: Fastify
|
||||
symbol: 'BINANCE:BTC/USDT',
|
||||
start_time: null,
|
||||
end_time: null,
|
||||
period: '15',
|
||||
period: 900,
|
||||
selected_shapes: [],
|
||||
};
|
||||
}
|
||||
@@ -180,35 +180,12 @@ async function getChartState(workspaceManager: WorkspaceManager, logger: Fastify
|
||||
symbol: 'BINANCE:BTC/USDT',
|
||||
start_time: null,
|
||||
end_time: null,
|
||||
period: '15',
|
||||
period: 900,
|
||||
selected_shapes: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse period string to seconds
|
||||
* Handles period as either a number (already in seconds) or string (minutes)
|
||||
*/
|
||||
function parsePeriod(period: string | number | null): number | null {
|
||||
if (period === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof period === 'number') {
|
||||
return period;
|
||||
}
|
||||
|
||||
// Period in workspace is stored as string representing minutes
|
||||
// Convert to seconds
|
||||
const minutes = parseInt(period, 10);
|
||||
if (isNaN(minutes)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return minutes * 60;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse time parameter (Unix seconds, date string, or null)
|
||||
* Returns Unix timestamp in seconds
|
||||
|
||||
@@ -30,13 +30,16 @@ Use this tool for:
|
||||
|
||||
The research subagent will write and execute Python scripts, capture output and charts, and return results.`,
|
||||
schema: z.object({
|
||||
name: z.string().describe('The name of the research script to create or update (e.g. "btc_ema_analysis"). Use the same name across calls to revise the same script rather than creating a new one.'),
|
||||
instruction: z.string().describe('The research task or analysis to perform. Be specific about what data, indicators, timeframes, and output you want.'),
|
||||
}),
|
||||
func: async ({ instruction }: { instruction: string }): Promise<string> => {
|
||||
logger.info({ instruction: instruction.substring(0, 100) }, 'Delegating to research subagent');
|
||||
func: async ({ name, instruction }: { name: string; instruction: string }): Promise<string> => {
|
||||
logger.info({ name, instruction: instruction.substring(0, 100) }, 'Delegating to research subagent');
|
||||
|
||||
const prompt = `Research script name: "${name}"\n\n${instruction}`;
|
||||
|
||||
try {
|
||||
const result = await researchSubagent.executeWithImages(context, instruction);
|
||||
const result = await researchSubagent.executeWithImages(context, prompt);
|
||||
|
||||
// Return in the format that AgentHarness.processToolResult() knows how to handle
|
||||
// (extracts images and passes them to channelAdapter)
|
||||
|
||||
@@ -11,12 +11,12 @@
|
||||
* TradingView bar format (used by web frontend)
|
||||
*/
|
||||
export interface TradingViewBar {
|
||||
time: number; // Unix timestamp in SECONDS
|
||||
open: number;
|
||||
high: number;
|
||||
low: number;
|
||||
close: number;
|
||||
volume?: number;
|
||||
time: number; // Unix timestamp in SECONDS
|
||||
open: number | null; // null for gap bars (no trades that period)
|
||||
high: number | null;
|
||||
low: number | null;
|
||||
close: number | null;
|
||||
volume?: number | null;
|
||||
// Optional extra columns from ohlc.proto
|
||||
buy_vol?: number;
|
||||
sell_vol?: number;
|
||||
@@ -31,14 +31,14 @@ export interface TradingViewBar {
|
||||
* Backend OHLC format (from Iceberg)
|
||||
*/
|
||||
export interface BackendOHLC {
|
||||
timestamp: number; // Unix timestamp in MICROSECONDS
|
||||
timestamp: bigint; // Unix timestamp in MICROSECONDS — kept as bigint to preserve precision
|
||||
ticker: string;
|
||||
period_seconds: number;
|
||||
open: number;
|
||||
high: number;
|
||||
low: number;
|
||||
close: number;
|
||||
volume: number;
|
||||
open: number | null; // null for gap bars (no trades that period)
|
||||
high: number | null;
|
||||
low: number | null;
|
||||
close: number | null;
|
||||
volume: number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -171,7 +171,7 @@ export function backendToTradingView(backend: BackendOHLC): TradingViewBar {
|
||||
high: backend.high,
|
||||
low: backend.low,
|
||||
close: backend.close,
|
||||
volume: backend.volume,
|
||||
volume: backend.volume ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -110,11 +110,33 @@ class SyncEntry {
|
||||
return this.history.filter((entry) => entry.seq > sinceSeq);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure intermediate objects exist for all patch add/replace operations.
|
||||
* Called before applying patches to gracefully handle paths into previously-absent sub-objects.
|
||||
*/
|
||||
private ensureIntermediatePaths(doc: any, patch: JsonPatchOp[]): void {
|
||||
for (const op of patch) {
|
||||
if (op.op !== 'add' && op.op !== 'replace') continue;
|
||||
const parts = op.path.split('/').filter(Boolean);
|
||||
if (parts.length <= 1) continue;
|
||||
let current = doc;
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
const part = parts[i];
|
||||
if (current[part] === undefined || current[part] === null) {
|
||||
current[part] = {};
|
||||
}
|
||||
current = current[part];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a patch to state (used when applying local changes).
|
||||
*/
|
||||
applyPatch(patch: JsonPatchOp[]): void {
|
||||
const result = applyPatch(deepClone(this.state), patch, false, false);
|
||||
const doc = deepClone(this.state) as any;
|
||||
this.ensureIntermediatePaths(doc, patch);
|
||||
const result = applyPatch(doc, patch, false, false);
|
||||
this.state = result.newDocument;
|
||||
}
|
||||
|
||||
@@ -130,7 +152,8 @@ class SyncEntry {
|
||||
try {
|
||||
if (clientBaseSeq === this.seq) {
|
||||
// No conflict - apply directly
|
||||
const currentState = deepClone(this.state);
|
||||
const currentState = deepClone(this.state) as any;
|
||||
this.ensureIntermediatePaths(currentState, patch);
|
||||
const result = applyPatch(currentState, patch, false, false);
|
||||
this.state = result.newDocument;
|
||||
this.commitPatch(patch);
|
||||
@@ -160,14 +183,15 @@ class SyncEntry {
|
||||
const frontendPaths = new Set(patch.map((op) => op.path));
|
||||
|
||||
// Apply frontend patch first
|
||||
const currentState = deepClone(this.state);
|
||||
const currentState = deepClone(this.state) as any;
|
||||
this.ensureIntermediatePaths(currentState, patch);
|
||||
let newState: unknown;
|
||||
try {
|
||||
const result = applyPatch(currentState, patch, false, false);
|
||||
newState = result.newDocument;
|
||||
} catch (e) {
|
||||
logger?.warn(
|
||||
{ store: this.storeName, error: e },
|
||||
{ store: this.storeName, err: e },
|
||||
'Failed to apply client patch during conflict resolution'
|
||||
);
|
||||
return { needsSnapshot: true, resolvedState: this.state };
|
||||
@@ -182,7 +206,7 @@ class SyncEntry {
|
||||
newState = result.newDocument;
|
||||
} catch (e) {
|
||||
logger?.debug(
|
||||
{ store: this.storeName, error: e },
|
||||
{ store: this.storeName, err: e },
|
||||
'Skipping backend patch during conflict resolution'
|
||||
);
|
||||
}
|
||||
@@ -209,7 +233,7 @@ class SyncEntry {
|
||||
return { needsSnapshot: true, resolvedState: this.state };
|
||||
} catch (e) {
|
||||
logger?.error(
|
||||
{ store: this.storeName, error: e },
|
||||
{ store: this.storeName, err: e },
|
||||
'Unexpected error applying client patch'
|
||||
);
|
||||
return { needsSnapshot: true, resolvedState: this.state };
|
||||
|
||||
@@ -237,7 +237,7 @@ export interface ChartState {
|
||||
symbol: string;
|
||||
start_time: number | null; // unix timestamp
|
||||
end_time: number | null; // unix timestamp
|
||||
period: string; // OHLC duration (e.g., '15' for 15 minutes)
|
||||
period: number; // OHLC period in seconds (e.g., 900 for 15 minutes)
|
||||
selected_shapes: string[]; // list of shape ID's
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user