aiden-shared-calculations-unified 1.0.81 → 1.0.83
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.MD +126 -49
- package/calculations/core/price-metrics.js +372 -0
- package/calculations/helix/winner-loser-flow.js +4 -2
- package/node-graphviz/README.md +123 -0
- package/node-graphviz/bin/graphvizlib.wasm +0 -0
- package/node-graphviz/index.d.ts +33 -0
- package/node-graphviz/index.js +4911 -0
- package/node-graphviz/package.json +16 -0
- package/package.json +14 -6
package/README.MD
CHANGED
|
@@ -1,78 +1,155 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# Quantum Test Harness for Computation System
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
The Quantum Test Harness is a **dynamic, dependency-aware testing framework** for running, validating, and profiling the entire computation system.
|
|
8
|
+
|
|
9
|
+
The new architecture is built around a `run-suite.js` orchestrator. It automatically builds a dependency graph of all calculations, ensuring they are tested in the correct order. This "ground-up" approach allows for robust, efficient, and realistic testing that mimics the production environment.
|
|
10
|
+
|
|
11
|
+
The harness still tests calculations **without modifying their source code**. It:
|
|
12
|
+
|
|
13
|
+
* Builds a dependency graph of all calculations.
|
|
14
|
+
* Dynamically generates mock data for a consistent "universe" of users and tickers.
|
|
15
|
+
* Executes calculations in the correct order, passing the results of one test as dependencies to the next.
|
|
16
|
+
* Monitors performance and validates output against each calculation's `getSchema()`.
|
|
17
|
+
* Generates rich, interactive HTML reports for each calculation run.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Core Concepts
|
|
22
|
+
|
|
23
|
+
The harness is built on **four main components**:
|
|
24
|
+
|
|
25
|
+
### 1. Test Suite Orchestrator (`run-suite.js`)
|
|
26
|
+
|
|
27
|
+
The main CLI entry point for running automated test suites. Responsibilities:
|
|
28
|
+
|
|
29
|
+
* Scans all calculation files and builds a computation graph.
|
|
30
|
+
* Topologically sorts the graph to determine the correct execution order.
|
|
31
|
+
* Parses CLI commands (`--all`, `--target`, `--product`) to decide which tests to run.
|
|
32
|
+
* Manages a results cache to efficiently pass outputs as dependencies.
|
|
33
|
+
|
|
34
|
+
### 2. Test Worker (`test-worker.js`)
|
|
35
|
+
|
|
36
|
+
Executes the test for a single calculation. Responsibilities:
|
|
37
|
+
|
|
38
|
+
* Receives a calculation to test from the orchestrator, along with its pre-computed dependencies.
|
|
39
|
+
* Generates fresh, temporal mock data for the specific test.
|
|
40
|
+
* Instantiates the calculation and wraps it in the Quantum Monitor.
|
|
41
|
+
* Executes `process()` and `getResult()`.
|
|
42
|
+
* Triggers the report generation.
|
|
43
|
+
|
|
44
|
+
### 3. Dynamic Data Generation (`data-generator.js`)
|
|
45
|
+
|
|
46
|
+
Generates **temporal mock data**:
|
|
47
|
+
|
|
48
|
+
1. The orchestrator creates a consistent "universe" (tickers and userIds) for the entire test run.
|
|
49
|
+
2. The test worker generates "today" and optional "yesterday" data snapshots using this universe, ensuring data is consistent and comparable across tests.
|
|
50
|
+
|
|
51
|
+
### 4. Instrumentation & Validation (`test-harness.js`)
|
|
8
52
|
|
|
9
|
-
|
|
53
|
+
Provides:
|
|
10
54
|
|
|
11
|
-
|
|
55
|
+
* **Instrumentation**: Wraps the calculation instance in a Proxy via `createQuantumRecorder`. Logs performance, memory, and errors without changing the calculation logic.
|
|
56
|
+
* **Validation**:
|
|
57
|
+
* **Schema Check**: Uses Ajv to validate `getResult()` against `getSchema()`.
|
|
58
|
+
* **Sanity Check**: Flags "valid but empty" results like `0`, `null`, `[]`, or `{}` as warnings.
|
|
12
59
|
|
|
13
|
-
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Usage: Automated Test Suite
|
|
63
|
+
|
|
64
|
+
To run automated, dependency-aware test suites, use the `run-suite.js` orchestrator. This is the recommended method for all testing.
|
|
65
|
+
|
|
66
|
+
### Prerequisites
|
|
67
|
+
|
|
68
|
+
Graphviz is required to render data-flow diagrams. Ensure `dot` is available in your PATH.
|
|
69
|
+
|
|
70
|
+
| OS | Installation |
|
|
71
|
+
| ------- | ------------------------------- |
|
|
72
|
+
| macOS | `brew install graphviz` |
|
|
73
|
+
| Windows | `choco install graphviz` |
|
|
74
|
+
| Linux | `sudo apt-get install graphviz` |
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
### Commands
|
|
14
79
|
|
|
15
|
-
|
|
16
|
-
* `sector_mapping_provider.js`: Provides functions (`loadInstrumentMappings`, `getInstrumentSectorMap`) to fetch and cache instrument-to-ticker and instrument-to-sector mappings from Firestore.
|
|
80
|
+
#### 1. Run ALL Computations
|
|
17
81
|
|
|
18
|
-
|
|
82
|
+
Tests every non-legacy computation, ordered by complexity (dependency-free calculations first).
|
|
19
83
|
|
|
20
|
-
|
|
84
|
+
```bash
|
|
85
|
+
node run-suite.js --all
|
|
86
|
+
```
|
|
21
87
|
|
|
22
|
-
|
|
23
|
-
* `constructor()`: Initializes any internal state needed for aggregation.
|
|
24
|
-
* `process(portfolioData, userId, context)` OR `process(todayPortfolio, yesterdayPortfolio, userId, context)`: Processes a single user's data. The signature depends on whether the calculation requires historical comparison. `context` provides shared data like mappings.
|
|
25
|
-
* `getResult()`: Returns the final, calculated result for the aggregation period. **Crucially, this method must perform any final averaging (e.g., sum/count) itself.** It should return the final value or object ready for storage, not raw components.
|
|
26
|
-
* `reset()`: (Optional but recommended) Resets the internal state, often used by the calling system between processing batches or days.
|
|
88
|
+
#### 2. Run a Specific Target
|
|
27
89
|
|
|
28
|
-
|
|
29
|
-
* `/asset_metrics`: Calculations focused on individual assets (e.g., `asset_dollar_metrics`, `asset_position_size`).
|
|
30
|
-
* `/behavioural`: Calculations analyzing user trading patterns (e.g., `drawdown_response`, `gain_response`, `paper_vs_diamond_hands`, `position_count_pnl`, `smart_money_flow`).
|
|
31
|
-
* `/pnl`: Profit and Loss related calculations (e.g., `asset_pnl_status`, `average_daily_pnl_all_users`, `average_daily_pnl_per_sector`, `average_daily_pnl_per_stock`, `average_daily_position_pnl`, `pnl_distribution_per_stock`, `profitability_migration`, `profitability_ratio_per_stock`, `profitability_skew_per_stock`, `user_profitability_tracker`).
|
|
32
|
-
* `/sanity`: Basic checks and counts (e.g., `users_processed`).
|
|
33
|
-
* `/sectors`: Calculations aggregated by market sector (e.g., `diversification_pnl`, `sector_dollar_metrics`, `sector_rotation`, `total_long_per_sector`, `total_short_per_sector`).
|
|
34
|
-
* `/sentiment`: Calculations related to market sentiment (e.g., `crowd_conviction_score`).
|
|
35
|
-
* `/short_and_long_stats`: Specific counts for short and long positions (e.g., `long_position_per_stock`, `sentiment_per_stock`, `short_position_per_stock`, `total_long_figures`, `total_short_figures`).
|
|
36
|
-
* `/speculators`: Calculations **specifically** for the 'speculator' user type, often involving leverage, stop-loss, or take-profit data (e.g., `distance_to_stop_loss_per_leverage`, `distance_to_tp_per_leverage`, `entry_distance_to_sl_per_leverage`, `entry_distance_to_tp_per_leverage`, `holding_duration_per_asset`, `leverage_per_asset`, `leverage_per_sector`, `risk_appetite_change`, `risk_reward_ratio_per_asset`, `speculator_asset_sentiment`, `speculator_danger_zone`, `stop_loss_distance_by_sector_short_long_breakdown`, `stop_loss_distance_by_ticker_short_long_breakdown`, `stop_loss_per_asset`, `take_profit_per_asset`, `tsl_effectiveness`, `tsl_per_asset`).
|
|
90
|
+
Tests a single computation and all of its dependencies in the correct order.
|
|
37
91
|
|
|
38
|
-
|
|
92
|
+
```bash
|
|
93
|
+
# Syntax
|
|
94
|
+
node run-suite.js --target [computation-name]
|
|
39
95
|
|
|
40
|
-
|
|
96
|
+
# Example (note: uses the name, not the file path)
|
|
97
|
+
node run-suite.js --target asset-pnl-status
|
|
98
|
+
```
|
|
41
99
|
|
|
42
|
-
|
|
100
|
+
#### 3. Run a Full Product Line
|
|
43
101
|
|
|
44
|
-
|
|
45
|
-
// Example usage within Computation System
|
|
46
|
-
const { calculations, utils } = require('aiden-shared-calculations-unified');
|
|
102
|
+
Tests all computations belonging to a specific product category (defined in `getMetadata`) and all of their dependencies.
|
|
47
103
|
|
|
48
|
-
|
|
49
|
-
|
|
104
|
+
```bash
|
|
105
|
+
# Syntax
|
|
106
|
+
node run-suite.js --product [product-name]
|
|
50
107
|
|
|
51
|
-
|
|
108
|
+
# Example
|
|
109
|
+
node run-suite.js --product gem
|
|
110
|
+
```
|
|
52
111
|
|
|
53
|
-
|
|
54
|
-
calculator.process(portfolioData, userId, context);
|
|
112
|
+
---
|
|
55
113
|
|
|
56
|
-
|
|
57
|
-
const results = await calculator.getResult();
|
|
114
|
+
### Output
|
|
58
115
|
|
|
59
|
-
|
|
60
|
-
````
|
|
116
|
+
Running a test produces:
|
|
61
117
|
|
|
62
|
-
|
|
118
|
+
* **Console Output**: Live logs of the orchestration and test progress.
|
|
119
|
+
* **Test Reports**: Generated for each executed calculation at `test_reports/[CalculationName]/`:
|
|
120
|
+
* `timeline.html` – Interactive performance report.
|
|
121
|
+
* `dataflow.svg` – Diagram of the calculation's data flow.
|
|
122
|
+
* `computation_result.json` – Raw JSON output from `getResult()`.
|
|
123
|
+
* `report.json` – Full metrics collected by the harness.
|
|
63
124
|
|
|
64
|
-
|
|
125
|
+
---
|
|
65
126
|
|
|
66
|
-
|
|
67
|
-
2. **Create File:** Create a new `.js` file using kebab-case (e.g., `my-new-metric.js`).
|
|
68
|
-
3. **Implement Class:** Define the calculation class, ensuring it has `constructor`, `process`, and `getResult` methods adhering to the standards. Remember `getResult` must return the *final* computed value.
|
|
69
|
-
4. **Add to Manifest:** The main `index.js` uses `require-all`, so the new calculation should be automatically included in the exports upon the next package build/publish, assuming the file naming and structure are correct.
|
|
70
|
-
5. **Publish:** Bump the package version (`npm version patch` or minor/major as appropriate) and publish (`npm publish --access public`).
|
|
71
|
-
6. **Update Consumer:** Update the version dependency in the Computation System's `package.json`.
|
|
127
|
+
## Adding a New Calculation
|
|
72
128
|
|
|
73
|
-
|
|
129
|
+
The process remains the same.
|
|
74
130
|
|
|
131
|
+
1. **Create Your File**: `calculations/my_product/my-new-calc.js`
|
|
132
|
+
2. **Implement the Class**: Must include `constructor()`, `process()`, and `getResult()`.
|
|
133
|
+
3. **Add `getDependencies()`**: Define the other calculations this one depends on.
|
|
134
|
+
4. **Add `getMetadata()`**: Defines the contract for the harness.
|
|
135
|
+
5. **Add `getSchema()`**: Enables automatic output validation.
|
|
136
|
+
6. **Run the Test**: Use the `run-suite.js` commands.
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
# Test your new calculation and its dependencies
|
|
140
|
+
node run-suite.js --target my-new-calc
|
|
75
141
|
```
|
|
76
142
|
|
|
77
143
|
---
|
|
78
|
-
|
|
144
|
+
|
|
145
|
+
## Troubleshooting
|
|
146
|
+
|
|
147
|
+
| Issue | Solution |
|
|
148
|
+
| ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
|
|
149
|
+
| Graphviz rendering failed | Ensure Graphviz is installed and `dot` is in your system's PATH. |
|
|
150
|
+
| `SchemaValidation` errors in `report.json` | The calculation's output does not match its `getSchema()`. Check `computation_result.json` to see the actual output and fix the logic. |
|
|
151
|
+
| `SanityCheck` warnings in `report.json` | The output is valid according to the schema but is empty (`null`, `0`, `[]`, `{}`). This may be expected, but it's worth verifying. |
|
|
152
|
+
| `Error: Unknown computation dependency: [name]` | A calculation lists a dependency in `getDependencies()` that doesn't exist or has a typo. Check the name for errors. |
|
|
153
|
+
| `Error: Circular dependency detected!` | Two or more calculations depend on each other, creating an impossible loop. Review the `getDependencies()` lists for the involved calculations. |
|
|
154
|
+
|
|
155
|
+
---
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Calculation (Pass 1 - Meta) for historical price metrics.
|
|
3
|
+
*
|
|
4
|
+
* This metric answers: "What is the Volatility, Sharpe Ratio, and Max Drawdown
|
|
5
|
+
* for all instruments over 7, 30, 90, and 365-day periods?"
|
|
6
|
+
*
|
|
7
|
+
* It also aggregates these metrics as an average for each sector.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const RANGES = [7, 30, 90, 365];
|
|
11
|
+
const TRADING_DAYS_PER_YEAR = 252;
|
|
12
|
+
const MAX_LOOKBACK_DAYS = 5; // For finding a non-holiday/weekend price
|
|
13
|
+
|
|
14
|
+
class CorePriceMetrics {
|
|
15
|
+
|
|
16
|
+
// #region --- Static Metadata & Schema ---
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Statically defines all metadata for the manifest builder.
|
|
20
|
+
*/
|
|
21
|
+
static getMetadata() {
|
|
22
|
+
return {
|
|
23
|
+
type: 'meta',
|
|
24
|
+
rootDataDependencies: [], // Relies on price data, not root data
|
|
25
|
+
isHistorical: true, // Needs up to 365d of price history
|
|
26
|
+
userType: 'n/a',
|
|
27
|
+
category: 'core_metrics' // Fits with other price/metric calcs
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* This is a Pass 1 calculation and has no dependencies on other calculations.
|
|
33
|
+
*/
|
|
34
|
+
static getDependencies() {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Defines the output schema for this calculation.
|
|
40
|
+
*/
|
|
41
|
+
static getSchema() {
|
|
42
|
+
// This is the sub-schema for a single instrument's metrics
|
|
43
|
+
const instrumentMetricsSchema = {
|
|
44
|
+
"type": "object",
|
|
45
|
+
"properties": {
|
|
46
|
+
"stdev_7d": { "type": ["number", "null"], "description": "7-day standard deviation of daily returns" },
|
|
47
|
+
"volatility_annualized_7d": { "type": ["number", "null"], "description": "7-day annualized volatility" },
|
|
48
|
+
"sharpe_ratio_7d": { "type": ["number", "null"], "description": "7-day annualized Sharpe ratio (rf=0)" },
|
|
49
|
+
"max_drawdown_7d": { "type": ["number", "null"], "description": "7-day max peak-to-trough drawdown" },
|
|
50
|
+
|
|
51
|
+
"stdev_30d": { "type": ["number", "null"], "description": "30-day standard deviation of daily returns" },
|
|
52
|
+
"volatility_annualized_30d": { "type": ["number", "null"], "description": "30-day annualized volatility" },
|
|
53
|
+
"sharpe_ratio_30d": { "type": ["number", "null"], "description": "30-day annualized Sharpe ratio (rf=0)" },
|
|
54
|
+
"max_drawdown_30d": { "type": ["number", "null"], "description": "30-day max peak-to-trough drawdown" },
|
|
55
|
+
|
|
56
|
+
"stdev_90d": { "type": ["number", "null"], "description": "90-day standard deviation of daily returns" },
|
|
57
|
+
"volatility_annualized_90d": { "type": ["number", "null"], "description": "90-day annualized volatility" },
|
|
58
|
+
"sharpe_ratio_90d": { "type": ["number", "null"], "description": "90-day annualized Sharpe ratio (rf=0)" },
|
|
59
|
+
"max_drawdown_90d": { "type": ["number", "null"], "description": "90-day max peak-to-trough drawdown" },
|
|
60
|
+
|
|
61
|
+
"stdev_365d": { "type": ["number", "null"], "description": "365-day standard deviation of daily returns" },
|
|
62
|
+
"volatility_annualized_365d": { "type": ["number", "null"], "description": "365-day annualized volatility" },
|
|
63
|
+
"sharpe_ratio_365d": { "type": ["number", "null"], "description": "365-day annualized Sharpe ratio (rf=0)" },
|
|
64
|
+
"max_drawdown_365d": { "type": ["number", "null"], "description": "365-day max peak-to-trough drawdown" }
|
|
65
|
+
},
|
|
66
|
+
"additionalProperties": false
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// This is the sub-schema for a single sector's *averages*
|
|
70
|
+
const sectorMetricsSchema = {
|
|
71
|
+
"type": "object",
|
|
72
|
+
"properties": {
|
|
73
|
+
"average_stdev_7d": { "type": ["number", "null"], "description": "Average 7-day standard deviation for the sector" },
|
|
74
|
+
"average_volatility_annualized_7d": { "type": ["number", "null"], "description": "Average 7-day annualized volatility for the sector" },
|
|
75
|
+
"average_sharpe_ratio_7d": { "type": ["number", "null"], "description": "Average 7-day annualized Sharpe ratio for the sector" },
|
|
76
|
+
"average_max_drawdown_7d": { "type": ["number", "null"], "description": "Average 7-day max drawdown for the sector" },
|
|
77
|
+
|
|
78
|
+
"average_stdev_30d": { "type": ["number", "null"], "description": "Average 30-day standard deviation for the sector" },
|
|
79
|
+
"average_volatility_annualized_30d": { "type": ["number", "null"], "description": "Average 30-day annualized volatility for the sector" },
|
|
80
|
+
"average_sharpe_ratio_30d": { "type": ["number", "null"], "description": "Average 30-day annualized Sharpe ratio for the sector" },
|
|
81
|
+
"average_max_drawdown_30d": { "type": ["number", "null"], "description": "Average 30-day max drawdown for the sector" },
|
|
82
|
+
|
|
83
|
+
"average_stdev_90d": { "type": ["number", "null"], "description": "Average 90-day standard deviation for the sector" },
|
|
84
|
+
"average_volatility_annualized_90d": { "type": ["number", "null"], "description": "Average 90-day annualized volatility for the sector" },
|
|
85
|
+
"average_sharpe_ratio_90d": { "type": ["number", "null"], "description": "Average 90-day annualized Sharpe ratio for the sector" },
|
|
86
|
+
"average_max_drawdown_90d": { "type": ["number", "null"], "description": "Average 90-day max drawdown for the sector" },
|
|
87
|
+
|
|
88
|
+
"average_stdev_365d": { "type": ["number", "null"], "description": "Average 365-day standard deviation for the sector" },
|
|
89
|
+
"average_volatility_annualized_365d": { "type": ["number", "null"], "description": "Average 365-day annualized volatility for the sector" },
|
|
90
|
+
"average_sharpe_ratio_365d": { "type": ["number", "null"], "description": "Average 365-day annualized Sharpe ratio for the sector" },
|
|
91
|
+
"average_max_drawdown_365d": { "type": ["number", "null"], "description": "Average 365-day max drawdown for the sector" }
|
|
92
|
+
},
|
|
93
|
+
"additionalProperties": false
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// This is the final, top-level schema
|
|
97
|
+
return {
|
|
98
|
+
"type": "object",
|
|
99
|
+
"description": "Calculates risk/return metrics (StdDev, Sharpe, Vol, Drawdown) for instruments and sectors.",
|
|
100
|
+
"properties": {
|
|
101
|
+
"by_instrument": {
|
|
102
|
+
"type": "object",
|
|
103
|
+
"description": "Metrics per instrument, keyed by Ticker.",
|
|
104
|
+
"patternProperties": { "^[A-Z\\.]+$": instrumentMetricsSchema }, // Match tickers like 'NVDA' or 'BRK.B'
|
|
105
|
+
"additionalProperties": instrumentMetricsSchema
|
|
106
|
+
},
|
|
107
|
+
"by_sector": {
|
|
108
|
+
"type": "object",
|
|
109
|
+
"description": "Average metrics per sector, keyed by Sector Name.",
|
|
110
|
+
"patternProperties": { "^[a-zA-Z0-9_ ]+$": sectorMetricsSchema }, // Match sector names
|
|
111
|
+
"additionalProperties": sectorMetricsSchema
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
"required": ["by_instrument", "by_sector"]
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// #endregion --- Static Metadata & Schema ---
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
// #region --- Main Process ---
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* This is a 'meta' calculation. It runs once.
|
|
125
|
+
* @param {string} dateStr - The date string 'YYYY-MM-DD'.
|
|
126
|
+
* @param {object} dependencies - The shared dependencies (e.g., logger, calculationUtils).
|
|
127
|
+
* @param {object} config - The computation system configuration.
|
|
128
|
+
* @returns {Promise<object>} The calculation result.
|
|
129
|
+
*/
|
|
130
|
+
async process(dateStr, dependencies, config) {
|
|
131
|
+
const { logger, calculationUtils } = dependencies;
|
|
132
|
+
|
|
133
|
+
const priceMap = await calculationUtils.loadAllPriceData();
|
|
134
|
+
const mappings = await calculationUtils.loadInstrumentMappings();
|
|
135
|
+
|
|
136
|
+
if (!priceMap || !mappings || !mappings.instrumentToTicker || !mappings.instrumentToSector) {
|
|
137
|
+
logger.log('ERROR', '[core-price-metrics] Failed to load priceMap or mappings.');
|
|
138
|
+
return { by_instrument: {}, by_sector: {} };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const { instrumentToTicker, instrumentToSector } = mappings;
|
|
142
|
+
const by_instrument = {};
|
|
143
|
+
|
|
144
|
+
// 1. Calculate Per-Instrument Metrics
|
|
145
|
+
for (const instrumentId in priceMap) {
|
|
146
|
+
const ticker = instrumentToTicker[instrumentId];
|
|
147
|
+
if (!ticker) continue;
|
|
148
|
+
|
|
149
|
+
const priceHistoryObj = priceMap[instrumentId];
|
|
150
|
+
const instrumentMetrics = {};
|
|
151
|
+
|
|
152
|
+
// Null-out all metrics by default to ensure consistent schema
|
|
153
|
+
for (const range of RANGES) {
|
|
154
|
+
instrumentMetrics[`stdev_${range}d`] = null;
|
|
155
|
+
instrumentMetrics[`volatility_annualized_${range}d`] = null;
|
|
156
|
+
instrumentMetrics[`sharpe_ratio_${range}d`] = null;
|
|
157
|
+
instrumentMetrics[`max_drawdown_${range}d`] = null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
for (const range of RANGES) {
|
|
161
|
+
// Get the price slice for the range (e.g., last 30 days)
|
|
162
|
+
// We need range + 1 prices to calculate `range` number of returns
|
|
163
|
+
const priceArray = this._getHistoricalPriceArray(priceHistoryObj, dateStr, range + 1);
|
|
164
|
+
|
|
165
|
+
if (priceArray.length < 2) {
|
|
166
|
+
continue; // Not enough data for this range
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Calculate drawdown on prices
|
|
170
|
+
instrumentMetrics[`max_drawdown_${range}d`] = this._calculateMaxDrawdown(priceArray);
|
|
171
|
+
|
|
172
|
+
// Calculate returns and return-based metrics
|
|
173
|
+
const dailyReturns = this._calculateDailyReturns(priceArray);
|
|
174
|
+
if (dailyReturns.length < 2) {
|
|
175
|
+
continue; // Not enough returns to calculate stddev
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const meanReturn = this._calculateMean(dailyReturns);
|
|
179
|
+
const stdDev = this._calculateStdDev(dailyReturns, meanReturn);
|
|
180
|
+
|
|
181
|
+
instrumentMetrics[`stdev_${range}d`] = stdDev;
|
|
182
|
+
|
|
183
|
+
if (stdDev > 0) {
|
|
184
|
+
instrumentMetrics[`sharpe_ratio_${range}d`] = (meanReturn / stdDev) * Math.sqrt(TRADING_DAYS_PER_YEAR);
|
|
185
|
+
instrumentMetrics[`volatility_annualized_${range}d`] = stdDev * Math.sqrt(TRADING_DAYS_PER_YEAR);
|
|
186
|
+
} else {
|
|
187
|
+
instrumentMetrics[`sharpe_ratio_${range}d`] = 0;
|
|
188
|
+
instrumentMetrics[`volatility_annualized_${range}d`] = 0;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
by_instrument[ticker] = instrumentMetrics;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 2. Calculate Sector Aggregates
|
|
196
|
+
const by_sector = this._aggregateMetricsBySector(by_instrument, instrumentToTicker, instrumentToSector);
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
by_instrument,
|
|
200
|
+
by_sector,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// #endregion --- Main Process ---
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
// #region --- Aggregation Helpers ---
|
|
208
|
+
|
|
209
|
+
_aggregateMetricsBySector(by_instrument, instrumentToTicker, instrumentToSector) {
|
|
210
|
+
const sectorAggregates = {}; // { [sector]: { metrics: { [metricName]: sum }, counts: { [metricName]: count } } }
|
|
211
|
+
const tickerToInstrument = Object.fromEntries(Object.entries(instrumentToTicker).map(([id, ticker]) => [ticker, id]));
|
|
212
|
+
|
|
213
|
+
for (const ticker in by_instrument) {
|
|
214
|
+
const instrumentId = tickerToInstrument[ticker];
|
|
215
|
+
const sector = instrumentToSector[instrumentId] || "Unknown";
|
|
216
|
+
const metrics = by_instrument[ticker];
|
|
217
|
+
|
|
218
|
+
if (!sectorAggregates[sector]) {
|
|
219
|
+
sectorAggregates[sector] = { metrics: {}, counts: {} };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
for (const metricName in metrics) {
|
|
223
|
+
const value = metrics[metricName];
|
|
224
|
+
// Check for valid, non-null, finite numbers
|
|
225
|
+
if (value !== null && typeof value === 'number' && isFinite(value)) {
|
|
226
|
+
if (!sectorAggregates[sector].metrics[metricName]) {
|
|
227
|
+
sectorAggregates[sector].metrics[metricName] = 0;
|
|
228
|
+
sectorAggregates[sector].counts[metricName] = 0;
|
|
229
|
+
}
|
|
230
|
+
sectorAggregates[sector].metrics[metricName] += value;
|
|
231
|
+
sectorAggregates[sector].counts[metricName]++;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Finalize averages
|
|
237
|
+
const by_sector = {};
|
|
238
|
+
for (const sector in sectorAggregates) {
|
|
239
|
+
by_sector[sector] = {};
|
|
240
|
+
const agg = sectorAggregates[sector];
|
|
241
|
+
|
|
242
|
+
// Get all unique metric names from this sector's aggregation
|
|
243
|
+
const allMetricNames = Object.keys(agg.metrics);
|
|
244
|
+
|
|
245
|
+
for (const metricName of allMetricNames) {
|
|
246
|
+
const count = agg.counts[metricName];
|
|
247
|
+
if (count > 0) {
|
|
248
|
+
by_sector[sector][`average_${metricName}`] = agg.metrics[metricName] / count;
|
|
249
|
+
} else {
|
|
250
|
+
by_sector[sector][`average_${metricName}`] = null; // Ensure null if no valid data
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return by_sector;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// #endregion --- Aggregation Helpers ---
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
// #region --- Math & Price Helpers ---
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Re-implementation of the logic from price_data_provider.js's private helper.
|
|
264
|
+
* Finds the most recent available price on or before a given date.
|
|
265
|
+
*/
|
|
266
|
+
_findPriceOnOrBefore(priceHistory, dateStr) {
|
|
267
|
+
if (!priceHistory) return null;
|
|
268
|
+
|
|
269
|
+
let checkDate = new Date(dateStr + 'T00:00:00Z');
|
|
270
|
+
|
|
271
|
+
for (let i = 0; i < MAX_LOOKBACK_DAYS; i++) {
|
|
272
|
+
const checkDateStr = checkDate.toISOString().slice(0, 10);
|
|
273
|
+
const price = priceHistory[checkDateStr];
|
|
274
|
+
|
|
275
|
+
if (price !== undefined && price !== null && price > 0) {
|
|
276
|
+
return price; // Found it
|
|
277
|
+
}
|
|
278
|
+
// If not found, look back one more day
|
|
279
|
+
checkDate.setUTCDate(checkDate.getUTCDate() - 1);
|
|
280
|
+
}
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Gets a gap-filled array of prices for a historical range.
|
|
286
|
+
* @param {object} priceHistoryObj - The map of { "YYYY-MM-DD": price }
|
|
287
|
+
* @param {string} endDateStr - The end date of the period (e.g., today).
|
|
288
|
+
* @param {number} numDays - The number of calendar days to fetch prices for.
|
|
289
|
+
* @returns {number[]} A sorted array of prices, oldest to newest.
|
|
290
|
+
*/
|
|
291
|
+
_getHistoricalPriceArray(priceHistoryObj, endDateStr, numDays) {
|
|
292
|
+
const prices = [];
|
|
293
|
+
let currentDate = new Date(endDateStr + 'T00:00:00Z');
|
|
294
|
+
let lastPrice = null;
|
|
295
|
+
|
|
296
|
+
for (let i = 0; i < numDays; i++) {
|
|
297
|
+
const targetDateStr = currentDate.toISOString().slice(0, 10);
|
|
298
|
+
let price = this._findPriceOnOrBefore(priceHistoryObj, targetDateStr);
|
|
299
|
+
|
|
300
|
+
// If price is null (e.g., new instrument), try to use the last known price
|
|
301
|
+
if (price === null) {
|
|
302
|
+
price = lastPrice;
|
|
303
|
+
} else {
|
|
304
|
+
lastPrice = price; // Update last known price
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (price !== null) {
|
|
308
|
+
prices.push(price);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Go back one calendar day for the next data point
|
|
312
|
+
currentDate.setUTCDate(currentDate.getUTCDate() - 1);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// We built the array from newest to oldest, so reverse it.
|
|
316
|
+
// And filter out any initial nulls if lastPrice was null at the start
|
|
317
|
+
return prices.reverse().filter(p => p !== null);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
_calculateMean(arr) {
|
|
322
|
+
if (!arr || arr.length === 0) return 0;
|
|
323
|
+
const sum = arr.reduce((acc, val) => acc + val, 0);
|
|
324
|
+
return sum / arr.length;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
_calculateStdDev(arr, mean) {
|
|
328
|
+
if (!arr || arr.length < 2) return 0;
|
|
329
|
+
const avg = mean === undefined ? this._calculateMean(arr) : mean;
|
|
330
|
+
// Use N-1 for sample standard deviation
|
|
331
|
+
const variance = arr.reduce((acc, val) => acc + (val - avg) ** 2, 0) / (arr.length - 1);
|
|
332
|
+
return Math.sqrt(variance);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
_calculateDailyReturns(prices) {
|
|
336
|
+
const returns = [];
|
|
337
|
+
for (let i = 1; i < prices.length; i++) {
|
|
338
|
+
const prevPrice = prices[i - 1];
|
|
339
|
+
const currPrice = prices[i];
|
|
340
|
+
if (prevPrice !== 0 && prevPrice !== null && currPrice !== null) {
|
|
341
|
+
returns.push((currPrice - prevPrice) / prevPrice);
|
|
342
|
+
} else {
|
|
343
|
+
returns.push(0);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return returns;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
_calculateMaxDrawdown(prices) {
|
|
350
|
+
if (!prices || prices.length < 2) return 0;
|
|
351
|
+
let maxDrawdown = 0;
|
|
352
|
+
let peak = -Infinity;
|
|
353
|
+
|
|
354
|
+
for (const price of prices) {
|
|
355
|
+
if (price > peak) {
|
|
356
|
+
peak = price;
|
|
357
|
+
}
|
|
358
|
+
if (peak > 0) { // Only calculate drawdown if peak is positive
|
|
359
|
+
const drawdown = (price - peak) / peak;
|
|
360
|
+
if (drawdown < maxDrawdown) {
|
|
361
|
+
maxDrawdown = drawdown;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
// Ensure result is a finite number, default to 0
|
|
366
|
+
return isFinite(maxDrawdown) ? maxDrawdown : 0;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// #endregion --- Math & Price Helpers ---
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
module.exports = CorePriceMetrics;
|
|
@@ -119,8 +119,10 @@ class WinnerLoserFlow {
|
|
|
119
119
|
return;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
|
|
123
|
+
|
|
122
124
|
if (!this.mappings) {
|
|
123
|
-
|
|
125
|
+
this.mappings = context.instrumentToTickerMap;
|
|
124
126
|
}
|
|
125
127
|
|
|
126
128
|
const yHoldings = this._getHoldingsWithPnl(yesterdayPortfolio); // Map<InstID, {pnl}>
|
|
@@ -132,7 +134,7 @@ class WinnerLoserFlow {
|
|
|
132
134
|
}
|
|
133
135
|
|
|
134
136
|
for (const instrumentId of allInstrumentIds) {
|
|
135
|
-
const ticker = this.mappings
|
|
137
|
+
const ticker = this.mappings[instrumentId];
|
|
136
138
|
if (!ticker) continue;
|
|
137
139
|
|
|
138
140
|
// --- MODIFIED ---
|