finalytics
Version:
Node.js wrapper for finalytics Rust library using FFI
378 lines (352 loc) • 15.3 kB
JavaScript
import ffi from '@2060.io/ffi-napi';
import ref from 'ref-napi';
import Polars from 'nodejs-polars';
import { Chart, dfToJSON, getNativeLibPath } from './utils.js';
// Define C types
const PortfolioHandle = ref.types.void; // Opaque pointer
const PortfolioHandlePtr = ref.refType(PortfolioHandle);
const CharPtr = ref.types.CString;
const CharPtrPtr = ref.refType(CharPtr);
// Load the finalytics library
const lib = ffi.Library(getNativeLibPath(), {
finalytics_portfolio_new: [PortfolioHandlePtr, [
CharPtr, CharPtr, CharPtr, CharPtr, CharPtr,
'double', 'double', CharPtr, CharPtr, CharPtr, CharPtr, CharPtr, CharPtr
]],
finalytics_portfolio_free: ['void', [PortfolioHandlePtr]],
finalytics_free_string: ['void', [CharPtr]],
finalytics_portfolio_optimization_results: ['int', [PortfolioHandlePtr, CharPtrPtr]],
finalytics_portfolio_optimization_chart: ['int', [PortfolioHandlePtr, 'uint', 'uint', CharPtrPtr]],
finalytics_portfolio_performance_chart: ['int', [PortfolioHandlePtr, 'uint', 'uint', CharPtrPtr]],
finalytics_portfolio_asset_returns_chart: ['int', [PortfolioHandlePtr, 'uint', 'uint', CharPtrPtr]],
finalytics_portfolio_returns_matrix: ['int', [PortfolioHandlePtr, 'uint', 'uint', CharPtrPtr]],
finalytics_portfolio_report: ['int', [PortfolioHandlePtr, CharPtr, CharPtrPtr]],
});
/**
* Portfolio class representing a portfolio of assets with methods for retrieving optimization results and analytics.
*/
class Portfolio {
/**
* Creates a new Portfolio instance.
* @param {Buffer} handle - Opaque pointer to the underlying C PortfolioHandle.
* @private
*/
constructor(handle) {
this.handle = handle;
}
/**
* Retrieves portfolio optimization results.
* @returns {Promise<Object>} A promise resolving to a JSON object containing optimization results (e.g., weights, expected return, volatility).
* @throws {Error} If optimization results retrieval fails.
* @example
* const portfolio = await new PortfolioBuilder().tickerSymbols(['AAPL', 'MSFT']).build();
* const results = await portfolio.optimizationResults();
* console.log(results);
* portfolio.free();
*/
async optimizationResults() {
return new Promise((resolve, reject) => {
const outputPtr = ref.alloc(CharPtrPtr);
const result = lib.finalytics_portfolio_optimization_results(this.handle, outputPtr);
if (result !== 0) {
return reject(new Error(`Failed to get optimization results: error code ${result}`));
}
const output = ref.readCString(outputPtr.deref(), 0);
lib.finalytics_free_string(outputPtr.deref());
resolve(JSON.parse(output));
});
}
/**
* Retrieves the portfolio optimization chart.
* @param {number} [height=0] - The height of the chart (0 for default).
* @param {number} [width=0] - The width of the chart (0 for default).
* @returns {Promise<Chart>} A promise resolving to a Chart instance containing the optimization chart.
* @throws {Error} If chart retrieval fails.
* @example
* const portfolio = await new PortfolioBuilder().tickerSymbols(['AAPL', 'MSFT']).build();
* const chart = await portfolio.optimizationChart(600, 800);
* chart.show();
* portfolio.free();
*/
async optimizationChart(height = 0, width = 0) {
return new Promise((resolve, reject) => {
const outputPtr = ref.alloc(CharPtrPtr);
const result = lib.finalytics_portfolio_optimization_chart(this.handle, height, width, outputPtr);
if (result !== 0) {
return reject(new Error(`Failed to get optimization chart: error code ${result}`));
}
const output = ref.readCString(outputPtr.deref(), 0);
lib.finalytics_free_string(outputPtr.deref());
resolve(new Chart(output));
});
}
/**
* Retrieves the portfolio performance chart.
* @param {number} [height=0] - The height of the chart (0 for default).
* @param {number} [width=0] - The width of the chart (0 for default).
* @returns {Promise<Chart>} A promise resolving to a Chart instance containing the performance chart.
* @throws {Error} If chart retrieval fails.
* @example
* const portfolio = await new PortfolioBuilder().tickerSymbols(['AAPL', 'MSFT']).build();
* const chart = await portfolio.performanceChart(600, 800);
* chart.show();
* portfolio.free();
*/
async performanceChart(height = 0, width = 0) {
return new Promise((resolve, reject) => {
const outputPtr = ref.alloc(CharPtrPtr);
const result = lib.finalytics_portfolio_performance_chart(this.handle, height, width, outputPtr);
if (result !== 0) {
return reject(new Error(`Failed to get performance chart: error code ${result}`));
}
const output = ref.readCString(outputPtr.deref(), 0);
lib.finalytics_free_string(outputPtr.deref());
resolve(new Chart(output));
});
}
/**
* Retrieves the asset returns chart for the portfolio.
* @param {number} [height=0] - The height of the chart (0 for default).
* @param {number} [width=0] - The width of the chart (0 for default).
* @returns {Promise<Chart>} A promise resolving to a Chart instance containing the asset returns chart.
* @throws {Error} If chart retrieval fails.
* @example
* const portfolio = await new PortfolioBuilder().tickerSymbols(['AAPL', 'MSFT']).build();
* const chart = await portfolio.assetReturnsChart(600, 800);
* chart.show();
* portfolio.free();
*/
async assetReturnsChart(height = 0, width = 0) {
return new Promise((resolve, reject) => {
const outputPtr = ref.alloc(CharPtrPtr);
const result = lib.finalytics_portfolio_asset_returns_chart(this.handle, height, width, outputPtr);
if (result !== 0) {
return reject(new Error(`Failed to get asset returns chart: error code ${result}`));
}
const output = ref.readCString(outputPtr.deref(), 0);
lib.finalytics_free_string(outputPtr.deref());
resolve(new Chart(output));
});
}
/**
* Retrieves the returns correlation matrix for the portfolio.
* @param {number} [height=0] - The height of the chart (0 for default).
* @param {number} [width=0] - The width of the chart (0 for default).
* @returns {Promise<Chart>} A promise resolving to a Chart instance containing the returns correlation matrix.
* @throws {Error} If matrix retrieval fails.
* @example
* const portfolio = await new PortfolioBuilder().tickerSymbols(['AAPL', 'MSFT']).build();
* const matrix = await portfolio.returnsMatrix(600, 800);
* matrix.show();
* portfolio.free();
*/
async returnsMatrix(height = 0, width = 0) {
return new Promise((resolve, reject) => {
const outputPtr = ref.alloc(CharPtrPtr);
const result = lib.finalytics_portfolio_returns_matrix(this.handle, height, width, outputPtr);
if (result !== 0) {
return reject(new Error(`Failed to get returns matrix: error code ${result}`));
}
const output = ref.readCString(outputPtr.deref(), 0);
lib.finalytics_free_string(outputPtr.deref());
resolve(new Chart(output));
});
}
/**
* Retrieves a comprehensive report for the portfolio.
* @param {string} reportType - The type of report to display (e.g., 'performance', 'full').
* @returns {Promise<Chart>} A promise resolving to a Chart instance containing the report.
* @throws {Error} If report retrieval fails.
* @example
* const portfolio = await new PortfolioBuilder().tickerSymbols(['AAPL', 'MSFT']).build();
* const report = await portfolio.report('performance');
* report.show();
* portfolio.free();
*/
async report(reportType) {
return new Promise((resolve, reject) => {
const outputPtr = ref.alloc(CharPtrPtr);
const result = lib.finalytics_portfolio_report(this.handle, reportType, outputPtr);
if (result !== 0) {
return reject(new Error(`Failed to get report: error code ${result}`));
}
const output = ref.readCString(outputPtr.deref(), 0);
lib.finalytics_free_string(outputPtr.deref());
resolve(new Chart(output));
});
}
/**
* Releases resources associated with the Portfolio.
* Should be called when the Portfolio is no longer needed to prevent memory leaks.
* @example
* const portfolio = await new PortfolioBuilder().tickerSymbols(['AAPL', 'MSFT']).build();
* portfolio.free();
*/
free() {
if (this.handle) {
lib.finalytics_portfolio_free(this.handle);
this.handle = null;
}
}
}
/**
* PortfolioBuilder class for constructing Portfolio instances using the builder pattern.
*/
class PortfolioBuilder {
/**
* Initializes a new PortfolioBuilder with default values.
* Defaults:
* - tickerSymbols: []
* - benchmarkSymbol: ''
* - startDate: ''
* - endDate: ''
* - interval: '1d'
* - confidenceLevel: 0.95
* - riskFreeRate: 0.02
* - objectiveFunction: 'max_sharpe'
* - assetConstraints: '{}'
* - categoricalConstraints: '{}'
* - weights: '{}'
* - tickersData: null
* - benchmarkData: null
*/
constructor() {
this.tickerSymbolsValue = [];
this.benchmarkSymbolValue = '';
this.startDateValue = '';
this.endDateValue = '';
this.intervalValue = '1d';
this.confidenceLevelValue = 0.95;
this.riskFreeRateValue = 0.02;
this.objectiveFunctionValue = 'max_sharpe';
this.assetConstraintsValue = '{}';
this.categoricalConstraintsValue = '{}';
this.weightsValue = '{}';
this.tickersDataValue = null;
this.benchmarkDataValue = null;
}
/**
* Sets the ticker symbols.
* @param {string[]} value - Array of ticker symbols (e.g., ['AAPL', 'MSFT']).
* @returns {PortfolioBuilder} The builder instance for method chaining.
*/
tickerSymbols(value) { this.tickerSymbolsValue = value; return this; }
/**
* Sets the benchmark symbol.
* @param {string} value - The benchmark symbol (e.g., '^GSPC').
* @returns {PortfolioBuilder} The builder instance for method chaining.
*/
benchmarkSymbol(value) { this.benchmarkSymbolValue = value; return this; }
/**
* Sets the start date for the data period.
* @param {string} value - The start date in YYYY-MM-DD format.
* @returns {PortfolioBuilder} The builder instance for method chaining.
*/
startDate(value) { this.startDateValue = value; return this; }
/**
* Sets the end date for the data period.
* @param {string} value - The end date in YYYY-MM-DD format.
* @returns {PortfolioBuilder} The builder instance for method chaining.
*/
endDate(value) { this.endDateValue = value; return this; }
/**
* Sets the data interval.
* @param {string} value - The data interval (e.g., '2m', '5m', '15m', '30m', '1h', '1d', '1wk', '1mo', '3mo').
* @returns {PortfolioBuilder} The builder instance for method chaining.
*/
interval(value) { this.intervalValue = value; return this; }
/**
* Sets the confidence level for VaR and ES calculations.
* @param {number} value - The confidence level (e.g., 0.95 for 95% confidence).
* @returns {PortfolioBuilder} The builder instance for method chaining.
*/
confidenceLevel(value) { this.confidenceLevelValue = value; return this; }
/**
* Sets the risk-free rate for calculations.
* @param {number} value - The risk-free rate (e.g., 0.02 for 2%).
* @returns {PortfolioBuilder} The builder instance for method chaining.
*/
riskFreeRate(value) { this.riskFreeRateValue = value; return this; }
/**
* Sets the objective function for optimization.
* @param {string} value - The objective function (e.g., 'max_sharpe', 'max_sortino', 'max_return', 'min_vol', 'min_var', 'min_cvar', 'min_drawdown').
* @returns {PortfolioBuilder} The builder instance for method chaining.
*/
objectiveFunction(value) { this.objectiveFunctionValue = value; return this; }
/**
* Sets the asset-level constraints for optimization.
* @param {string} value - JSON string defining asset-level constraints (e.g., '[[0,1],[0,1]]').
* @returns {PortfolioBuilder} The builder instance for method chaining.
*/
assetConstraints(value) { this.assetConstraintsValue = value; return this; }
/**
* Sets the categorical constraints for optimization.
* @param {string} value - JSON string defining categorical constraints (e.g., '[{"Name":"AssetClass","Categories":["EQUITY","EQUITY"],"Constraints":[["EQUITY",0.0,0.8]]}]').
* @returns {PortfolioBuilder} The builder instance for method chaining.
*/
categoricalConstraints(value) { this.categoricalConstraintsValue = value; return this; }
/**
* Sets the portfolio-level constraints for optimization.
* @param {string} value - JSON string defining portfolio-level constraints (e.g., '{}').
* @returns {PortfolioBuilder} The builder instance for method chaining.
*/
weights(value) { this.weightsValue = value; return this; }
/**
* Sets custom ticker data.
* @param {Polars.DataFrame[]|null} value - Array of Polars DataFrames containing custom ticker data (null if not used).
* @returns {PortfolioBuilder} The builder instance for method chaining.
*/
tickersData(value) { this.tickersDataValue = value; return this; }
/**
* Sets custom benchmark data.
* @param {Polars.DataFrame|null} value - A Polars DataFrame containing custom benchmark data (null if not used).
* @returns {PortfolioBuilder} The builder instance for method chaining.
*/
benchmarkData(value) { this.benchmarkDataValue = value; return this; }
/**
* Constructs the Portfolio instance with the configured parameters.
* The tickerSymbols parameter is required; other parameters are optional and use defaults if not set.
* @returns {Promise<Portfolio>} A promise resolving to the initialized Portfolio object.
* @throws {Error} If Portfolio creation fails or tickerSymbols is empty.
* @example
* const portfolio = await new PortfolioBuilder()
* .tickerSymbols(['AAPL', 'MSFT'])
* .startDate('2023-01-01')
* .endDate('2023-12-31')
* .interval('1d')
* .objectiveFunction('max_sharpe')
* .assetConstraints('[[0,1],[0,1]]')
* .build();
* portfolio.free();
*/
async build() {
if (!this.tickerSymbolsValue.length) throw new Error('tickerSymbols is required and cannot be empty');
const symbolsJson = JSON.stringify(this.tickerSymbolsValue);
const tickersDataJson = this.tickersDataValue
? JSON.stringify(this.tickersDataValue.map(df => dfToJSON(df)))
: '';
const benchmarkDataJson = this.benchmarkDataValue ? dfToJSON(this.benchmarkDataValue) : '';
return new Promise((resolve, reject) => {
const handle = lib.finalytics_portfolio_new(
symbolsJson,
this.benchmarkSymbolValue,
this.startDateValue,
this.endDateValue,
this.intervalValue,
this.confidenceLevelValue,
this.riskFreeRateValue,
this.objectiveFunctionValue,
this.assetConstraintsValue,
this.categoricalConstraintsValue,
this.weightsValue,
tickersDataJson || null,
benchmarkDataJson || null
);
if (!handle || handle.isNull()) {
return reject(new Error('Failed to create Portfolio'));
}
resolve(new Portfolio(handle));
});
}
}
export { Portfolio, PortfolioBuilder };