diff --git a/models/ExchangeHedge.js b/models/ExchangeHedge.js
new file mode 100644
index 00000000..dd9bce9c
--- /dev/null
+++ b/models/ExchangeHedge.js
@@ -0,0 +1,88 @@
+const mongoose = require('mongoose');
+
+/**
+ * ExchangeHedge Model
+ * Manages FX risk through hedging strategies for multi-currency operations
+ */
+const exchangeHedgeSchema = new mongoose.Schema({
+ userId: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: 'User',
+ required: true,
+ index: true
+ },
+ hedgeId: {
+ type: String,
+ unique: true,
+ required: true
+ },
+ baseCurrency: {
+ type: String,
+ required: true,
+ uppercase: true
+ },
+ targetCurrency: {
+ type: String,
+ required: true,
+ uppercase: true
+ },
+ hedgeType: {
+ type: String,
+ enum: ['forward_contract', 'option', 'swap', 'natural_hedge'],
+ required: true
+ },
+ notionalAmount: {
+ type: Number,
+ required: true
+ },
+ contractRate: {
+ type: Number,
+ required: true
+ },
+ marketRate: {
+ type: Number,
+ default: null
+ },
+ maturityDate: {
+ type: Date,
+ required: true
+ },
+ status: {
+ type: String,
+ enum: ['active', 'settled', 'expired', 'cancelled'],
+ default: 'active'
+ },
+ effectiveness: {
+ hedgeRatio: { type: Number, default: 1.0 },
+ gainLoss: { type: Number, default: 0 },
+ mtmValue: { type: Number, default: 0 } // Mark-to-Market
+ },
+ counterparty: {
+ name: String,
+ rating: String
+ },
+ linkedTransactions: [{
+ type: mongoose.Schema.Types.ObjectId,
+ ref: 'Transaction'
+ }],
+ notes: String
+}, {
+ timestamps: true
+});
+
+// Calculate MTM value before saving
+exchangeHedgeSchema.pre('save', function (next) {
+ if (this.marketRate && this.contractRate) {
+ const rateDiff = this.marketRate - this.contractRate;
+ this.effectiveness.mtmValue = rateDiff * this.notionalAmount;
+ this.effectiveness.gainLoss = this.effectiveness.mtmValue;
+ }
+ next();
+});
+
+// Indexes
+exchangeHedgeSchema.index({ userId: 1, status: 1 });
+exchangeHedgeSchema.index({ maturityDate: 1, status: 1 });
+exchangeHedgeSchema.index({ baseCurrency: 1, targetCurrency: 1 });
+
+module.exports = mongoose.model('ExchangeHedge', exchangeHedgeSchema);
diff --git a/models/LiquidityThreshold.js b/models/LiquidityThreshold.js
new file mode 100644
index 00000000..66abac24
--- /dev/null
+++ b/models/LiquidityThreshold.js
@@ -0,0 +1,76 @@
+const mongoose = require('mongoose');
+
+/**
+ * LiquidityThreshold Model
+ * Defines alert triggers and automated actions when cash runway falls below critical levels
+ */
+const liquidityThresholdSchema = new mongoose.Schema({
+ userId: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: 'User',
+ required: true,
+ index: true
+ },
+ vaultId: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: 'TreasuryVault',
+ required: true
+ },
+ thresholdName: {
+ type: String,
+ required: true
+ },
+ thresholdType: {
+ type: String,
+ enum: ['absolute', 'percentage', 'runway_days'],
+ required: true
+ },
+ triggerValue: {
+ type: Number,
+ required: true
+ },
+ currentValue: {
+ type: Number,
+ default: 0
+ },
+ severity: {
+ type: String,
+ enum: ['info', 'warning', 'critical', 'emergency'],
+ default: 'warning'
+ },
+ alertChannels: [{
+ type: String,
+ enum: ['email', 'sms', 'dashboard', 'webhook']
+ }],
+ automatedActions: [{
+ actionType: {
+ type: String,
+ enum: ['freeze_spending', 'notify_stakeholders', 'trigger_rebalance', 'liquidate_assets']
+ },
+ actionParams: mongoose.Schema.Types.Mixed
+ }],
+ lastTriggered: {
+ type: Date,
+ default: null
+ },
+ triggerCount: {
+ type: Number,
+ default: 0
+ },
+ isActive: {
+ type: Boolean,
+ default: true
+ },
+ cooldownPeriod: {
+ type: Number,
+ default: 24 // hours
+ }
+}, {
+ timestamps: true
+});
+
+// Index for efficient threshold monitoring
+liquidityThresholdSchema.index({ userId: 1, vaultId: 1, isActive: 1 });
+liquidityThresholdSchema.index({ severity: 1, isActive: 1 });
+
+module.exports = mongoose.model('LiquidityThreshold', liquidityThresholdSchema);
diff --git a/models/TreasuryVault.js b/models/TreasuryVault.js
new file mode 100644
index 00000000..70a3d177
--- /dev/null
+++ b/models/TreasuryVault.js
@@ -0,0 +1,75 @@
+const mongoose = require('mongoose');
+
+/**
+ * TreasuryVault Model
+ * Represents a centralized cash/asset pool for enterprise treasury management
+ */
+const treasuryVaultSchema = new mongoose.Schema({
+ userId: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: 'User',
+ required: true,
+ index: true
+ },
+ vaultName: {
+ type: String,
+ required: true,
+ trim: true
+ },
+ vaultType: {
+ type: String,
+ enum: ['operating', 'reserve', 'investment', 'forex'],
+ default: 'operating'
+ },
+ currency: {
+ type: String,
+ required: true,
+ default: 'INR',
+ uppercase: true
+ },
+ balance: {
+ type: Number,
+ required: true,
+ default: 0
+ },
+ allocatedFunds: {
+ type: Number,
+ default: 0
+ },
+ availableLiquidity: {
+ type: Number,
+ default: 0
+ },
+ linkedAccounts: [{
+ accountId: { type: mongoose.Schema.Types.ObjectId, ref: 'Account' },
+ allocationPercentage: { type: Number, min: 0, max: 100 }
+ }],
+ restrictions: {
+ minBalance: { type: Number, default: 0 },
+ maxWithdrawal: { type: Number, default: null },
+ requiresApproval: { type: Boolean, default: false }
+ },
+ metadata: {
+ purpose: String,
+ riskProfile: { type: String, enum: ['conservative', 'moderate', 'aggressive'], default: 'moderate' },
+ autoRebalance: { type: Boolean, default: false }
+ },
+ isActive: {
+ type: Boolean,
+ default: true
+ }
+}, {
+ timestamps: true
+});
+
+// Pre-save hook to calculate available liquidity
+treasuryVaultSchema.pre('save', function (next) {
+ this.availableLiquidity = this.balance - this.allocatedFunds;
+ next();
+});
+
+// Index for performance
+treasuryVaultSchema.index({ userId: 1, vaultType: 1 });
+treasuryVaultSchema.index({ currency: 1, isActive: 1 });
+
+module.exports = mongoose.model('TreasuryVault', treasuryVaultSchema);
diff --git a/public/expensetracker.css b/public/expensetracker.css
index f7307077..a03a8883 100644
--- a/public/expensetracker.css
+++ b/public/expensetracker.css
@@ -9631,3 +9631,335 @@ input:checked + .toggle-slider::before {
}
.checkbox-container input { width: 16px; height: 16px; }
+
+/* ============================================
+ STRATEGIC TREASURY & LIQUIDITY MANAGEMENT
+ Issue #590: Enterprise Treasury Suite
+ ============================================ */
+
+.treasury-header {
+ margin-bottom: 30px;
+}
+
+.header-metrics {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 20px;
+ margin-top: 20px;
+}
+
+.metric-card {
+ padding: 20px;
+ text-align: center;
+}
+
+.metric-card label {
+ font-size: 0.75rem;
+ color: var(--text-secondary);
+ text-transform: uppercase;
+}
+
+.metric-card h2 {
+ font-size: 2rem;
+ margin: 10px 0;
+ color: var(--text-primary);
+}
+
+.metric-trend {
+ font-size: 0.8rem;
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+}
+
+.metric-trend.positive { color: #64ffda; }
+.metric-trend.negative { color: #ff6b6b; }
+.metric-trend.warning { color: #ff9f43; }
+
+.health-bar {
+ width: 100%;
+ height: 6px;
+ background: rgba(255,255,255,0.1);
+ border-radius: 3px;
+ overflow: hidden;
+ margin-top: 10px;
+}
+
+.health-fill {
+ height: 100%;
+ transition: width 0.5s ease, background-color 0.5s ease;
+}
+
+.treasury-grid {
+ display: grid;
+ grid-template-columns: 350px 1fr;
+ gap: 25px;
+}
+
+.treasury-sidebar {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.vaults-list, .thresholds-list, .hedges-list {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.vault-card {
+ padding: 15px;
+}
+
+.vault-header {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin-bottom: 15px;
+}
+
+.vault-icon {
+ width: 40px;
+ height: 40px;
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 1.2rem;
+}
+
+.vault-icon.operating { background: rgba(100, 255, 218, 0.15); color: #64ffda; }
+.vault-icon.reserve { background: rgba(72, 219, 251, 0.15); color: #48dbfb; }
+.vault-icon.investment { background: rgba(255, 159, 67, 0.15); color: #ff9f43; }
+.vault-icon.forex { background: rgba(255, 107, 107, 0.15); color: #ff6b6b; }
+
+.vault-info strong { display: block; font-size: 0.9rem; }
+.vault-info span { font-size: 0.7rem; color: var(--text-secondary); }
+
+.vault-balance {
+ margin: 15px 0;
+}
+
+.vault-balance label { font-size: 0.7rem; color: var(--text-secondary); }
+.vault-balance h3 { margin: 5px 0; font-size: 1.4rem; color: var(--accent-primary); }
+
+.vault-stats {
+ display: flex;
+ justify-content: space-between;
+ padding-top: 10px;
+ border-top: 1px solid rgba(255,255,255,0.05);
+}
+
+.vault-stats .stat label { font-size: 0.65rem; color: var(--text-secondary); display: block; }
+.vault-stats .stat span { font-size: 0.85rem; }
+
+.threshold-item, .hedge-item {
+ padding: 12px;
+ background: rgba(255,255,255,0.02);
+ border-radius: 8px;
+ border-left: 3px solid var(--accent-primary);
+}
+
+.threshold-info {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 5px;
+}
+
+.threshold-info strong { font-size: 0.85rem; }
+
+.severity-pill {
+ font-size: 0.65rem;
+ padding: 2px 8px;
+ border-radius: 4px;
+ text-transform: uppercase;
+}
+
+.severity-pill.info { background: rgba(72, 219, 251, 0.2); color: #48dbfb; }
+.severity-pill.warning { background: rgba(255, 159, 67, 0.2); color: #ff9f43; }
+.severity-pill.critical { background: rgba(255, 107, 107, 0.2); color: #ff6b6b; }
+.severity-pill.emergency { background: rgba(255, 0, 0, 0.3); color: #ff0000; }
+
+.threshold-value {
+ font-size: 0.9rem;
+ color: var(--text-secondary);
+}
+
+.hedge-pair {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 10px;
+}
+
+.hedge-type {
+ font-size: 0.7rem;
+ color: var(--text-secondary);
+ text-transform: capitalize;
+}
+
+.hedge-details {
+ display: flex;
+ gap: 15px;
+}
+
+.hedge-details .detail {
+ flex: 1;
+}
+
+.hedge-details .detail label {
+ font-size: 0.65rem;
+ color: var(--text-secondary);
+ display: block;
+}
+
+.hedge-details .detail span {
+ font-size: 0.85rem;
+}
+
+.hedge-details .detail.positive { color: #64ffda; }
+.hedge-details .detail.negative { color: #ff6b6b; }
+
+.forecast-controls {
+ display: flex;
+ gap: 10px;
+}
+
+.forecast-controls select {
+ background: rgba(255,255,255,0.05);
+ border: 1px solid rgba(255,255,255,0.1);
+ color: white;
+ padding: 5px 10px;
+ border-radius: 4px;
+ font-size: 0.8rem;
+}
+
+.chart-container {
+ height: 350px;
+ padding: 20px 0;
+}
+
+.chart-container-small {
+ height: 250px;
+ padding: 15px 0;
+}
+
+.forecast-insights {
+ margin-top: 20px;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.insight-card {
+ display: flex;
+ align-items: center;
+ gap: 15px;
+ padding: 15px;
+ background: rgba(255,255,255,0.02);
+ border-radius: 8px;
+ border-left: 4px solid;
+}
+
+.insight-card.critical { border-left-color: #ff6b6b; }
+.insight-card.warning { border-left-color: #ff9f43; }
+.insight-card.positive { border-left-color: #64ffda; }
+.insight-card.info { border-left-color: #48dbfb; }
+
+.insight-icon {
+ font-size: 1.5rem;
+}
+
+.insight-card.critical .insight-icon { color: #ff6b6b; }
+.insight-card.warning .insight-icon { color: #ff9f43; }
+.insight-card.positive .insight-icon { color: #64ffda; }
+
+.insight-content {
+ flex: 1;
+}
+
+.insight-content strong {
+ display: block;
+ margin-bottom: 5px;
+}
+
+.insight-content p {
+ font-size: 0.8rem;
+ color: var(--text-secondary);
+ margin: 0;
+}
+
+.severity-badge {
+ font-size: 0.7rem;
+ padding: 3px 10px;
+ border-radius: 12px;
+ text-transform: uppercase;
+}
+
+.severity-badge.low { background: rgba(100, 255, 218, 0.2); color: #64ffda; }
+.severity-badge.medium { background: rgba(255, 159, 67, 0.2); color: #ff9f43; }
+.severity-badge.high { background: rgba(255, 107, 107, 0.2); color: #ff6b6b; }
+.severity-badge.emergency { background: rgba(255, 0, 0, 0.3); color: #ff0000; }
+
+.analytics-row {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 20px;
+}
+
+.portfolio-metrics {
+ display: flex;
+ flex-direction: column;
+ gap: 15px;
+ padding: 10px 0;
+}
+
+.metric-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.metric-item label {
+ font-size: 0.8rem;
+ color: var(--text-secondary);
+}
+
+.metric-item strong {
+ font-size: 1.1rem;
+ color: var(--accent-primary);
+}
+
+.violations-alert {
+ padding: 20px;
+ margin-bottom: 20px;
+ border-left: 4px solid #ff6b6b;
+}
+
+.alert-header {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin-bottom: 15px;
+ color: #ff6b6b;
+}
+
+.violation-item {
+ display: flex;
+ justify-content: space-between;
+ padding: 10px;
+ background: rgba(255,255,255,0.02);
+ border-radius: 4px;
+ margin-bottom: 8px;
+}
+
+.violation-item.critical { border-left: 3px solid #ff6b6b; }
+.violation-item.warning { border-left: 3px solid #ff9f43; }
+
+.no-insights {
+ text-align: center;
+ color: var(--text-secondary);
+ padding: 20px;
+}
diff --git a/public/js/treasury-controller.js b/public/js/treasury-controller.js
new file mode 100644
index 00000000..7ab6bdc9
--- /dev/null
+++ b/public/js/treasury-controller.js
@@ -0,0 +1,494 @@
+/**
+ * Treasury Dashboard Controller
+ * Handles all treasury management UI logic
+ */
+
+let runwayChart = null;
+let vaultDistChart = null;
+let currentForecastData = null;
+
+document.addEventListener('DOMContentLoaded', () => {
+ loadTreasuryDashboard();
+ loadVaults();
+ loadThresholds();
+ loadHedges();
+ setupForms();
+});
+
+async function loadTreasuryDashboard() {
+ try {
+ const res = await fetch('/api/treasury/dashboard', {
+ headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
+ });
+ const { data } = await res.json();
+
+ updateDashboardMetrics(data);
+ loadForecast();
+ } catch (err) {
+ console.error('Failed to load treasury dashboard:', err);
+ }
+}
+
+function updateDashboardMetrics(data) {
+ document.getElementById('total-liquidity').textContent = `₹${data.totalLiquidity.toLocaleString()}`;
+ document.getElementById('cash-runway').textContent = `${data.cashRunway} days`;
+ document.getElementById('health-score').textContent = `${data.healthScore}%`;
+
+ const healthFill = document.getElementById('health-fill');
+ healthFill.style.width = `${data.healthScore}%`;
+ healthFill.style.backgroundColor = data.healthScore > 70 ? '#64ffda' : data.healthScore > 40 ? '#ff9f43' : '#ff6b6b';
+
+ const runwayTrend = document.getElementById('runway-trend');
+ if (data.cashRunway < 30) {
+ runwayTrend.innerHTML = ' Critical';
+ runwayTrend.className = 'metric-trend negative';
+ } else if (data.cashRunway < 60) {
+ runwayTrend.innerHTML = ' Warning';
+ runwayTrend.className = 'metric-trend warning';
+ } else {
+ runwayTrend.innerHTML = ' Healthy';
+ runwayTrend.className = 'metric-trend positive';
+ }
+
+ // Update portfolio metrics
+ if (data.portfolio) {
+ document.getElementById('sharpe-ratio').textContent = data.portfolio.sharpeRatio.toFixed(2);
+ document.getElementById('var-95').textContent = `₹${data.portfolio.var95.toLocaleString()}`;
+ document.getElementById('diversification').textContent = `${data.portfolio.diversificationScore}%`;
+ }
+
+ // Display violations
+ if (data.violations && data.violations.length > 0) {
+ showViolationAlerts(data.violations);
+ }
+}
+
+function showViolationAlerts(violations) {
+ const container = document.createElement('div');
+ container.className = 'violations-alert glass-card';
+ container.innerHTML = `
+
+ ${violations.map(v => `
+
+ ${v.thresholdName}
+ ${v.vaultName}: ${v.currentValue.toFixed(2)} / ${v.triggerValue}
+
+ `).join('')}
+ `;
+
+ const main = document.querySelector('.treasury-main');
+ main.insertBefore(container, main.firstChild);
+}
+
+async function loadForecast() {
+ try {
+ const horizon = document.getElementById('forecast-horizon').value;
+ const res = await fetch(`/api/treasury/forecast?days=${horizon}`, {
+ headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
+ });
+ const { data } = await res.json();
+
+ currentForecastData = data;
+ renderForecastChart(data);
+ renderInsights(data.insights);
+ } catch (err) {
+ console.error('Failed to load forecast:', err);
+ }
+}
+
+function renderForecastChart(data) {
+ const model = document.getElementById('forecast-model').value;
+ const forecastData = data.forecasts[model];
+
+ const ctx = document.getElementById('runwayChart').getContext('2d');
+
+ if (runwayChart) {
+ runwayChart.destroy();
+ }
+
+ runwayChart = new Chart(ctx, {
+ type: 'line',
+ data: {
+ labels: forecastData.map(f => `Day ${f.day}`),
+ datasets: [{
+ label: 'Projected Balance',
+ data: forecastData.map(f => f.balance),
+ borderColor: '#64ffda',
+ backgroundColor: 'rgba(100, 255, 218, 0.1)',
+ fill: true,
+ tension: 0.4
+ }]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ legend: {
+ labels: { color: '#8892b0' }
+ },
+ tooltip: {
+ callbacks: {
+ label: function (context) {
+ return `Balance: ₹${context.parsed.y.toLocaleString()}`;
+ }
+ }
+ }
+ },
+ scales: {
+ x: {
+ ticks: {
+ color: '#8892b0',
+ maxTicksLimit: 10
+ },
+ grid: { color: 'rgba(255,255,255,0.05)' }
+ },
+ y: {
+ ticks: {
+ color: '#8892b0',
+ callback: function (value) {
+ return '₹' + value.toLocaleString();
+ }
+ },
+ grid: { color: 'rgba(255,255,255,0.05)' }
+ }
+ }
+ }
+ });
+}
+
+function renderInsights(insights) {
+ const container = document.getElementById('forecast-insights');
+ if (!insights || insights.length === 0) {
+ container.innerHTML = 'No critical insights at this time.
';
+ return;
+ }
+
+ container.innerHTML = insights.map(insight => `
+
+
+
+
+
+
${insight.message}
+ ${insight.recommendation ? `
${insight.recommendation}
` : ''}
+
+
${insight.severity}
+
+ `).join('');
+}
+
+function getInsightIcon(type) {
+ const icons = {
+ 'critical': 'fa-exclamation-circle',
+ 'warning': 'fa-exclamation-triangle',
+ 'positive': 'fa-check-circle',
+ 'info': 'fa-info-circle'
+ };
+ return icons[type] || 'fa-info-circle';
+}
+
+async function loadVaults() {
+ try {
+ const res = await fetch('/api/treasury/vaults', {
+ headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
+ });
+ const { data } = await res.json();
+
+ renderVaults(data);
+ renderVaultDistribution(data);
+ populateVaultDropdowns(data);
+ } catch (err) {
+ console.error('Failed to load vaults:', err);
+ }
+}
+
+function renderVaults(vaults) {
+ const list = document.getElementById('vaults-list');
+ if (!vaults || vaults.length === 0) {
+ list.innerHTML = 'No vaults created yet.
';
+ return;
+ }
+
+ list.innerHTML = vaults.map(vault => `
+
+
+
+
+
₹${vault.availableLiquidity.toLocaleString()}
+
+
+
+
+ ₹${vault.balance.toLocaleString()}
+
+
+
+ ₹${vault.allocatedFunds.toLocaleString()}
+
+
+
+ `).join('');
+}
+
+function getVaultIcon(type) {
+ const icons = {
+ 'operating': 'fa-wallet',
+ 'reserve': 'fa-piggy-bank',
+ 'investment': 'fa-chart-line',
+ 'forex': 'fa-exchange-alt'
+ };
+ return icons[type] || 'fa-vault';
+}
+
+function renderVaultDistribution(vaults) {
+ const ctx = document.getElementById('vaultDistChart').getContext('2d');
+
+ if (vaultDistChart) {
+ vaultDistChart.destroy();
+ }
+
+ vaultDistChart = new Chart(ctx, {
+ type: 'doughnut',
+ data: {
+ labels: vaults.map(v => v.vaultName),
+ datasets: [{
+ data: vaults.map(v => v.balance),
+ backgroundColor: ['#64ffda', '#48dbfb', '#ff9f43', '#ff6b6b', '#54a0ff'],
+ borderWidth: 0
+ }]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ legend: {
+ position: 'bottom',
+ labels: { color: '#8892b0', font: { size: 10 } }
+ }
+ }
+ }
+ });
+}
+
+function populateVaultDropdowns(vaults) {
+ const select = document.getElementById('threshold-vault');
+ if (select) {
+ select.innerHTML = vaults.map(v =>
+ ``
+ ).join('');
+ }
+}
+
+async function loadThresholds() {
+ try {
+ const res = await fetch('/api/treasury/thresholds', {
+ headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
+ });
+ const { data } = await res.json();
+
+ renderThresholds(data);
+ } catch (err) {
+ console.error('Failed to load thresholds:', err);
+ }
+}
+
+function renderThresholds(thresholds) {
+ const list = document.getElementById('thresholds-list');
+ if (!thresholds || thresholds.length === 0) {
+ list.innerHTML = 'No thresholds configured.
';
+ return;
+ }
+
+ list.innerHTML = thresholds.map(t => `
+
+
+ ${t.thresholdName}
+ ${t.severity}
+
+
+ ${t.triggerValue} ${t.thresholdType === 'percentage' ? '%' : t.thresholdType === 'runway_days' ? 'days' : ''}
+
+
+ `).join('');
+}
+
+async function loadHedges() {
+ try {
+ const res = await fetch('/api/treasury/hedges', {
+ headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
+ });
+ const { data } = await res.json();
+
+ renderHedges(data);
+ } catch (err) {
+ console.error('Failed to load hedges:', err);
+ }
+}
+
+function renderHedges(hedges) {
+ const list = document.getElementById('hedges-list');
+ if (!hedges || hedges.length === 0) {
+ list.innerHTML = 'No FX hedges active.
';
+ return;
+ }
+
+ list.innerHTML = hedges.map(h => `
+
+
+ ${h.baseCurrency}/${h.targetCurrency}
+ ${h.hedgeType.replace('_', ' ')}
+
+
+
+
+ ${h.notionalAmount.toLocaleString()}
+
+
+
+ ${h.contractRate}
+
+
+
+ ${h.effectiveness.gainLoss >= 0 ? '+' : ''}${h.effectiveness.gainLoss.toLocaleString()}
+
+
+
+ `).join('');
+}
+
+function updateForecast() {
+ loadForecast();
+}
+
+// Modal Functions
+function openVaultModal() {
+ document.getElementById('vault-modal').classList.remove('hidden');
+}
+
+function closeVaultModal() {
+ document.getElementById('vault-modal').classList.add('hidden');
+}
+
+function openThresholdModal() {
+ document.getElementById('threshold-modal').classList.remove('hidden');
+}
+
+function closeThresholdModal() {
+ document.getElementById('threshold-modal').classList.add('hidden');
+}
+
+function openHedgeModal() {
+ document.getElementById('hedge-modal').classList.remove('hidden');
+}
+
+function closeHedgeModal() {
+ document.getElementById('hedge-modal').classList.add('hidden');
+}
+
+function setupForms() {
+ // Vault Form
+ document.getElementById('vault-form').addEventListener('submit', async (e) => {
+ e.preventDefault();
+
+ const vaultData = {
+ vaultName: document.getElementById('vault-name').value,
+ vaultType: document.getElementById('vault-type').value,
+ currency: document.getElementById('vault-currency').value,
+ balance: parseFloat(document.getElementById('vault-balance').value)
+ };
+
+ try {
+ const res = await fetch('/api/treasury/vaults', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ },
+ body: JSON.stringify(vaultData)
+ });
+
+ if (res.ok) {
+ closeVaultModal();
+ loadVaults();
+ loadTreasuryDashboard();
+ }
+ } catch (err) {
+ console.error('Failed to create vault:', err);
+ }
+ });
+
+ // Threshold Form
+ document.getElementById('threshold-form').addEventListener('submit', async (e) => {
+ e.preventDefault();
+
+ const thresholdData = {
+ thresholdName: document.getElementById('threshold-name').value,
+ vaultId: document.getElementById('threshold-vault').value,
+ thresholdType: document.getElementById('threshold-type').value,
+ triggerValue: parseFloat(document.getElementById('threshold-value').value),
+ severity: document.getElementById('threshold-severity').value,
+ alertChannels: ['dashboard', 'email']
+ };
+
+ try {
+ const res = await fetch('/api/treasury/thresholds', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ },
+ body: JSON.stringify(thresholdData)
+ });
+
+ if (res.ok) {
+ closeThresholdModal();
+ loadThresholds();
+ }
+ } catch (err) {
+ console.error('Failed to create threshold:', err);
+ }
+ });
+
+ // Hedge Form
+ document.getElementById('hedge-form').addEventListener('submit', async (e) => {
+ e.preventDefault();
+
+ const hedgeData = {
+ baseCurrency: document.getElementById('hedge-base').value,
+ targetCurrency: document.getElementById('hedge-target').value,
+ hedgeType: document.getElementById('hedge-type').value,
+ notionalAmount: parseFloat(document.getElementById('hedge-amount').value),
+ contractRate: parseFloat(document.getElementById('hedge-rate').value),
+ maturityDate: document.getElementById('hedge-maturity').value
+ };
+
+ try {
+ const res = await fetch('/api/treasury/hedges', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ },
+ body: JSON.stringify(hedgeData)
+ });
+
+ if (res.ok) {
+ closeHedgeModal();
+ loadHedges();
+ }
+ } catch (err) {
+ console.error('Failed to create hedge:', err);
+ }
+ });
+}
diff --git a/public/treasury-dashboard.html b/public/treasury-dashboard.html
new file mode 100644
index 00000000..d8f21f2c
--- /dev/null
+++ b/public/treasury-dashboard.html
@@ -0,0 +1,298 @@
+
+
+
+
+
+
+ Strategic Treasury & Liquidity Management - ExpenseFlow
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0.00
+
+
+
+ ₹0
+
+
+
+ 0%
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/routes/treasury.js b/routes/treasury.js
new file mode 100644
index 00000000..2f5b4dda
--- /dev/null
+++ b/routes/treasury.js
@@ -0,0 +1,256 @@
+const express = require('express');
+const router = express.Router();
+const auth = require('../middleware/auth');
+const treasuryService = require('../services/treasuryService');
+const runwayForecaster = require('../services/runwayForecaster');
+const TreasuryVault = require('../models/TreasuryVault');
+const LiquidityThreshold = require('../models/LiquidityThreshold');
+const ExchangeHedge = require('../models/ExchangeHedge');
+
+/**
+ * Get Treasury Dashboard
+ */
+router.get('/dashboard', auth, async (req, res) => {
+ try {
+ const dashboard = await treasuryService.getTreasuryDashboard(req.user._id);
+ const portfolio = await treasuryService.getPortfolioMetrics(req.user._id);
+
+ res.json({
+ success: true,
+ data: {
+ ...dashboard,
+ portfolio
+ }
+ });
+ } catch (err) {
+ res.status(500).json({ success: false, error: err.message });
+ }
+});
+
+/**
+ * Get Cash Runway Forecast
+ */
+router.get('/forecast', auth, async (req, res) => {
+ try {
+ const days = parseInt(req.query.days) || 180;
+ const forecast = await runwayForecaster.generateForecast(req.user._id, days);
+
+ res.json({ success: true, data: forecast });
+ } catch (err) {
+ res.status(500).json({ success: false, error: err.message });
+ }
+});
+
+/**
+ * Get Liquidity Projection
+ */
+router.get('/projection', auth, async (req, res) => {
+ try {
+ const days = parseInt(req.query.days) || 90;
+ const projection = await treasuryService.getLiquidityProjection(req.user._id, days);
+
+ res.json({ success: true, data: projection });
+ } catch (err) {
+ res.status(500).json({ success: false, error: err.message });
+ }
+});
+
+/**
+ * Create Treasury Vault
+ */
+router.post('/vaults', auth, async (req, res) => {
+ try {
+ const vault = new TreasuryVault({
+ ...req.body,
+ userId: req.user._id
+ });
+ await vault.save();
+
+ res.json({ success: true, data: vault });
+ } catch (err) {
+ res.status(400).json({ success: false, error: err.message });
+ }
+});
+
+/**
+ * Get All Vaults
+ */
+router.get('/vaults', auth, async (req, res) => {
+ try {
+ const vaults = await TreasuryVault.find({ userId: req.user._id });
+ res.json({ success: true, data: vaults });
+ } catch (err) {
+ res.status(500).json({ success: false, error: err.message });
+ }
+});
+
+/**
+ * Update Vault Balance
+ */
+router.patch('/vaults/:id/balance', auth, async (req, res) => {
+ try {
+ const { amount, operation } = req.body; // operation: 'add' or 'subtract'
+ const vault = await TreasuryVault.findOne({ _id: req.params.id, userId: req.user._id });
+
+ if (!vault) {
+ return res.status(404).json({ success: false, error: 'Vault not found' });
+ }
+
+ if (operation === 'add') {
+ vault.balance += amount;
+ } else if (operation === 'subtract') {
+ vault.balance -= amount;
+ }
+
+ await vault.save();
+ res.json({ success: true, data: vault });
+ } catch (err) {
+ res.status(400).json({ success: false, error: err.message });
+ }
+});
+
+/**
+ * Transfer Between Vaults
+ */
+router.post('/vaults/transfer', auth, async (req, res) => {
+ try {
+ const { fromVaultId, toVaultId, amount } = req.body;
+ const result = await treasuryService.transferBetweenVaults(
+ fromVaultId,
+ toVaultId,
+ amount,
+ req.user._id
+ );
+
+ res.json({ success: true, data: result });
+ } catch (err) {
+ res.status(400).json({ success: false, error: err.message });
+ }
+});
+
+/**
+ * Auto-Rebalance Vaults
+ */
+router.post('/vaults/rebalance', auth, async (req, res) => {
+ try {
+ const actions = await treasuryService.rebalanceVaults(req.user._id);
+ res.json({ success: true, data: actions });
+ } catch (err) {
+ res.status(400).json({ success: false, error: err.message });
+ }
+});
+
+/**
+ * Create Liquidity Threshold
+ */
+router.post('/thresholds', auth, async (req, res) => {
+ try {
+ const threshold = new LiquidityThreshold({
+ ...req.body,
+ userId: req.user._id
+ });
+ await threshold.save();
+
+ res.json({ success: true, data: threshold });
+ } catch (err) {
+ res.status(400).json({ success: false, error: err.message });
+ }
+});
+
+/**
+ * Get All Thresholds
+ */
+router.get('/thresholds', auth, async (req, res) => {
+ try {
+ const thresholds = await LiquidityThreshold.find({ userId: req.user._id });
+ res.json({ success: true, data: thresholds });
+ } catch (err) {
+ res.status(500).json({ success: false, error: err.message });
+ }
+});
+
+/**
+ * Monitor Thresholds (Manual Trigger)
+ */
+router.post('/thresholds/monitor', auth, async (req, res) => {
+ try {
+ const alerts = await treasuryService.monitorThresholds(req.user._id);
+ res.json({ success: true, data: alerts });
+ } catch (err) {
+ res.status(500).json({ success: false, error: err.message });
+ }
+});
+
+/**
+ * Create FX Hedge
+ */
+router.post('/hedges', auth, async (req, res) => {
+ try {
+ const hedgeId = `HG-${Date.now()}-${req.user._id.toString().substring(0, 4)}`.toUpperCase();
+ const hedge = new ExchangeHedge({
+ ...req.body,
+ hedgeId,
+ userId: req.user._id
+ });
+ await hedge.save();
+
+ res.json({ success: true, data: hedge });
+ } catch (err) {
+ res.status(400).json({ success: false, error: err.message });
+ }
+});
+
+/**
+ * Get All Hedges
+ */
+router.get('/hedges', auth, async (req, res) => {
+ try {
+ const hedges = await ExchangeHedge.find({ userId: req.user._id });
+ res.json({ success: true, data: hedges });
+ } catch (err) {
+ res.status(500).json({ success: false, error: err.message });
+ }
+});
+
+/**
+ * Update Hedge Market Rate (for MTM calculation)
+ */
+router.patch('/hedges/:id/market-rate', auth, async (req, res) => {
+ try {
+ const { marketRate } = req.body;
+ const hedge = await ExchangeHedge.findOne({ _id: req.params.id, userId: req.user._id });
+
+ if (!hedge) {
+ return res.status(404).json({ success: false, error: 'Hedge not found' });
+ }
+
+ hedge.marketRate = marketRate;
+ await hedge.save(); // Pre-save hook will calculate MTM
+
+ res.json({ success: true, data: hedge });
+ } catch (err) {
+ res.status(400).json({ success: false, error: err.message });
+ }
+});
+
+/**
+ * Settle Hedge
+ */
+router.post('/hedges/:id/settle', auth, async (req, res) => {
+ try {
+ const hedge = await ExchangeHedge.findOne({ _id: req.params.id, userId: req.user._id });
+
+ if (!hedge) {
+ return res.status(404).json({ success: false, error: 'Hedge not found' });
+ }
+
+ hedge.status = 'settled';
+ await hedge.save();
+
+ res.json({ success: true, data: hedge });
+ } catch (err) {
+ res.status(400).json({ success: false, error: err.message });
+ }
+});
+
+module.exports = router;
diff --git a/server.js b/server.js
index 620fcffb..30738c49 100644
--- a/server.js
+++ b/server.js
@@ -43,8 +43,8 @@ const server = http.createServer(app);
const io = socketIo(server, {
cors: {
origin: [process.env.FRONTEND_URL ||
- "http://localhost:3000",
- 'https://accounts.clerk.dev',
+ "http://localhost:3000",
+ 'https://accounts.clerk.dev',
'https://*.clerk.accounts.dev'
],
methods: ["GET", "POST"],
@@ -63,10 +63,10 @@ app.use(helmet({
directives: {
defaultSrc: ["'self'"],
styleSrc: [
- "'self'",
- "'unsafe-inline'",
- "https://cdnjs.cloudflare.com",
- "https://fonts.googleapis.com",
+ "'self'",
+ "'unsafe-inline'",
+ "https://cdnjs.cloudflare.com",
+ "https://fonts.googleapis.com",
"https://api.github.com"
],
scriptSrc: [
@@ -83,10 +83,10 @@ app.use(helmet({
scriptSrcAttr: ["'unsafe-inline'"],
workerSrc: ["'self'", "blob:"],
imgSrc: [
- "'self'",
- "data:",
- "https:",
- "https://res.cloudinary.com",
+ "'self'",
+ "data:",
+ "https:",
+ "https://res.cloudinary.com",
"https://api.github.com",
"https://img.clerk.com" // For Clerk user avatars
],
@@ -290,6 +290,7 @@ app.use('/api/folders', require('./routes/folders'));
app.use('/api/procurement', require('./routes/procurement'));
app.use('/api/compliance', require('./routes/compliance'));
app.use('/api/project-billing', require('./routes/project-billing'));
+app.use('/api/treasury', require('./routes/treasury'));
// Import error handling middleware
const { errorHandler, notFoundHandler } = require('./middleware/errorMiddleware');
diff --git a/services/runwayForecaster.js b/services/runwayForecaster.js
new file mode 100644
index 00000000..e58e3cc2
--- /dev/null
+++ b/services/runwayForecaster.js
@@ -0,0 +1,386 @@
+const Transaction = require('../models/Transaction');
+const TreasuryVault = require('../models/TreasuryVault');
+const FinancialModels = require('../utils/financialModels');
+
+class RunwayForecaster {
+ /**
+ * Advanced cash runway forecasting using multiple methodologies
+ */
+ async generateForecast(userId, forecastDays = 180) {
+ // Get historical transaction data
+ const sixMonthsAgo = new Date();
+ sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6);
+
+ const historicalTransactions = await Transaction.find({
+ user: userId,
+ date: { $gte: sixMonthsAgo }
+ }).sort({ date: 1 });
+
+ // Get current liquidity
+ const vaults = await TreasuryVault.find({ userId, isActive: true });
+ const currentLiquidity = vaults.reduce((sum, v) => sum + v.availableLiquidity, 0);
+
+ // Generate forecasts using different methods
+ const simpleForecast = this.simpleLinearForecast(historicalTransactions, currentLiquidity, forecastDays);
+ const movingAvgForecast = this.movingAverageForecast(historicalTransactions, currentLiquidity, forecastDays);
+ const seasonalForecast = this.seasonalForecast(historicalTransactions, currentLiquidity, forecastDays);
+ const mlLikeForecast = this.mlLikeForecast(historicalTransactions, currentLiquidity, forecastDays);
+
+ // Ensemble forecast (weighted average)
+ const ensembleForecast = this.createEnsemble([
+ { forecast: simpleForecast, weight: 0.2 },
+ { forecast: movingAvgForecast, weight: 0.3 },
+ { forecast: seasonalForecast, weight: 0.25 },
+ { forecast: mlLikeForecast, weight: 0.25 }
+ ], forecastDays);
+
+ return {
+ currentLiquidity,
+ forecastHorizon: forecastDays,
+ forecasts: {
+ simple: simpleForecast,
+ movingAverage: movingAvgForecast,
+ seasonal: seasonalForecast,
+ mlLike: mlLikeForecast,
+ ensemble: ensembleForecast
+ },
+ insights: this.generateInsights(ensembleForecast, currentLiquidity),
+ confidence: this.calculateConfidence(historicalTransactions)
+ };
+ }
+
+ /**
+ * Simple linear trend forecast
+ */
+ simpleLinearForecast(transactions, startingBalance, days) {
+ const expenses = transactions.filter(t => t.type === 'expense');
+ const income = transactions.filter(t => t.type === 'income');
+
+ const avgDailyExpense = expenses.reduce((sum, t) => sum + t.amount, 0) /
+ Math.max(1, this.getDaysBetween(transactions));
+ const avgDailyIncome = income.reduce((sum, t) => sum + t.amount, 0) /
+ Math.max(1, this.getDaysBetween(transactions));
+
+ const netDailyFlow = avgDailyIncome - avgDailyExpense;
+
+ const forecast = [];
+ for (let i = 0; i <= days; i++) {
+ const projectedBalance = startingBalance + (netDailyFlow * i);
+ forecast.push({
+ day: i,
+ balance: Math.max(0, projectedBalance),
+ netFlow: netDailyFlow
+ });
+ }
+
+ return forecast;
+ }
+
+ /**
+ * Moving average forecast with trend adjustment
+ */
+ movingAverageForecast(transactions, startingBalance, days) {
+ const windowSize = 30; // 30-day moving average
+ const expenses = transactions.filter(t => t.type === 'expense');
+
+ // Group by day
+ const dailyExpenses = this.groupByDay(expenses);
+ const movingAvgs = this.calculateMovingAverage(dailyExpenses, windowSize);
+
+ const avgExpense = movingAvgs.length > 0 ?
+ movingAvgs[movingAvgs.length - 1] :
+ expenses.reduce((sum, t) => sum + t.amount, 0) / Math.max(1, this.getDaysBetween(transactions));
+
+ // Calculate trend
+ const trend = this.calculateTrend(movingAvgs);
+
+ const forecast = [];
+ for (let i = 0; i <= days; i++) {
+ const adjustedExpense = avgExpense * (1 + trend * i / 100);
+ const projectedBalance = startingBalance - (adjustedExpense * i);
+ forecast.push({
+ day: i,
+ balance: Math.max(0, projectedBalance),
+ dailyExpense: adjustedExpense
+ });
+ }
+
+ return forecast;
+ }
+
+ /**
+ * Seasonal forecast accounting for monthly patterns
+ */
+ seasonalForecast(transactions, startingBalance, days) {
+ // Calculate monthly patterns
+ const monthlyPatterns = this.calculateMonthlyPatterns(transactions);
+
+ const forecast = [];
+ let currentBalance = startingBalance;
+
+ for (let i = 0; i <= days; i++) {
+ const futureDate = new Date();
+ futureDate.setDate(futureDate.getDate() + i);
+ const month = futureDate.getMonth();
+
+ const monthlyFactor = monthlyPatterns[month] || 1.0;
+ const baseExpense = this.getAverageDailyExpense(transactions);
+ const adjustedExpense = baseExpense * monthlyFactor;
+
+ currentBalance -= adjustedExpense;
+
+ forecast.push({
+ day: i,
+ balance: Math.max(0, currentBalance),
+ seasonalFactor: monthlyFactor,
+ dailyExpense: adjustedExpense
+ });
+ }
+
+ return forecast;
+ }
+
+ /**
+ * ML-like forecast using exponential smoothing
+ */
+ mlLikeForecast(transactions, startingBalance, days) {
+ const alpha = 0.3; // Smoothing factor
+ const beta = 0.1; // Trend smoothing factor
+
+ const expenses = transactions.filter(t => t.type === 'expense');
+ const dailyExpenses = this.groupByDay(expenses);
+
+ if (dailyExpenses.length === 0) {
+ return this.simpleLinearForecast(transactions, startingBalance, days);
+ }
+
+ // Initialize
+ let level = dailyExpenses[0];
+ let trend = dailyExpenses.length > 1 ? dailyExpenses[1] - dailyExpenses[0] : 0;
+
+ // Apply Holt's linear trend method
+ for (let i = 1; i < dailyExpenses.length; i++) {
+ const prevLevel = level;
+ level = alpha * dailyExpenses[i] + (1 - alpha) * (level + trend);
+ trend = beta * (level - prevLevel) + (1 - beta) * trend;
+ }
+
+ const forecast = [];
+ let currentBalance = startingBalance;
+
+ for (let i = 0; i <= days; i++) {
+ const forecastExpense = level + trend * i;
+ currentBalance -= forecastExpense;
+
+ forecast.push({
+ day: i,
+ balance: Math.max(0, currentBalance),
+ forecastExpense,
+ level,
+ trend
+ });
+ }
+
+ return forecast;
+ }
+
+ /**
+ * Create ensemble forecast from multiple models
+ */
+ createEnsemble(forecasts, days) {
+ const ensemble = [];
+
+ for (let i = 0; i <= days; i++) {
+ let weightedBalance = 0;
+ let totalWeight = 0;
+
+ forecasts.forEach(({ forecast, weight }) => {
+ if (forecast[i]) {
+ weightedBalance += forecast[i].balance * weight;
+ totalWeight += weight;
+ }
+ });
+
+ ensemble.push({
+ day: i,
+ balance: totalWeight > 0 ? weightedBalance / totalWeight : 0,
+ confidence: this.calculateDayConfidence(i, days)
+ });
+ }
+
+ return ensemble;
+ }
+
+ /**
+ * Generate actionable insights from forecast
+ */
+ generateInsights(forecast, currentLiquidity) {
+ const insights = [];
+
+ // Find when balance hits zero
+ const zeroDay = forecast.findIndex(f => f.balance === 0);
+ if (zeroDay > 0 && zeroDay < forecast.length) {
+ insights.push({
+ type: 'critical',
+ message: `Cash runway depleted in ${zeroDay} days`,
+ severity: zeroDay < 30 ? 'emergency' : zeroDay < 60 ? 'high' : 'medium',
+ actionRequired: true
+ });
+ }
+
+ // Check for declining trend
+ const midPoint = Math.floor(forecast.length / 2);
+ const earlyAvg = forecast.slice(0, 30).reduce((sum, f) => sum + f.balance, 0) / 30;
+ const lateAvg = forecast.slice(midPoint, midPoint + 30).reduce((sum, f) => sum + f.balance, 0) / 30;
+
+ if (lateAvg < earlyAvg * 0.5) {
+ insights.push({
+ type: 'warning',
+ message: 'Significant liquidity decline projected',
+ severity: 'medium',
+ recommendation: 'Consider cost optimization or revenue acceleration'
+ });
+ }
+
+ // Positive insights
+ if (forecast[forecast.length - 1].balance > currentLiquidity * 1.2) {
+ insights.push({
+ type: 'positive',
+ message: 'Liquidity growth projected',
+ severity: 'low',
+ recommendation: 'Consider investment opportunities'
+ });
+ }
+
+ return insights;
+ }
+
+ /**
+ * Calculate forecast confidence based on data quality
+ */
+ calculateConfidence(transactions) {
+ const dataPoints = transactions.length;
+ const timeSpan = this.getDaysBetween(transactions);
+
+ // More data points and longer timespan = higher confidence
+ let confidence = 50; // Base confidence
+
+ if (dataPoints > 100) confidence += 20;
+ else if (dataPoints > 50) confidence += 10;
+
+ if (timeSpan > 120) confidence += 20;
+ else if (timeSpan > 60) confidence += 10;
+
+ // Check for consistency
+ const variance = this.calculateVariance(transactions);
+ if (variance < 0.3) confidence += 10; // Low variance = more predictable
+
+ return Math.min(100, confidence);
+ }
+
+ /**
+ * Helper: Calculate confidence for specific forecast day
+ */
+ calculateDayConfidence(day, totalDays) {
+ // Confidence decreases with forecast horizon
+ return Math.max(20, 100 - (day / totalDays) * 60);
+ }
+
+ /**
+ * Helper: Group transactions by day
+ */
+ groupByDay(transactions) {
+ const grouped = {};
+ transactions.forEach(t => {
+ const day = new Date(t.date).toISOString().split('T')[0];
+ grouped[day] = (grouped[day] || 0) + t.amount;
+ });
+ return Object.values(grouped);
+ }
+
+ /**
+ * Helper: Calculate moving average
+ */
+ calculateMovingAverage(data, windowSize) {
+ const result = [];
+ for (let i = windowSize - 1; i < data.length; i++) {
+ const window = data.slice(i - windowSize + 1, i + 1);
+ const avg = window.reduce((sum, val) => sum + val, 0) / windowSize;
+ result.push(avg);
+ }
+ return result;
+ }
+
+ /**
+ * Helper: Calculate trend from data
+ */
+ calculateTrend(data) {
+ if (data.length < 2) return 0;
+ const recent = data.slice(-10);
+ const older = data.slice(-20, -10);
+
+ const recentAvg = recent.reduce((sum, val) => sum + val, 0) / recent.length;
+ const olderAvg = older.reduce((sum, val) => sum + val, 0) / (older.length || 1);
+
+ return ((recentAvg - olderAvg) / olderAvg) * 100;
+ }
+
+ /**
+ * Helper: Calculate monthly spending patterns
+ */
+ calculateMonthlyPatterns(transactions) {
+ const monthlyTotals = Array(12).fill(0);
+ const monthlyCounts = Array(12).fill(0);
+
+ transactions.filter(t => t.type === 'expense').forEach(t => {
+ const month = new Date(t.date).getMonth();
+ monthlyTotals[month] += t.amount;
+ monthlyCounts[month]++;
+ });
+
+ const avgExpense = monthlyTotals.reduce((sum, val) => sum + val, 0) /
+ Math.max(1, monthlyCounts.reduce((sum, val) => sum + val, 0));
+
+ return monthlyTotals.map((total, i) => {
+ const monthAvg = monthlyCounts[i] > 0 ? total / monthlyCounts[i] : avgExpense;
+ return monthAvg / avgExpense;
+ });
+ }
+
+ /**
+ * Helper: Get average daily expense
+ */
+ getAverageDailyExpense(transactions) {
+ const expenses = transactions.filter(t => t.type === 'expense');
+ const totalExpense = expenses.reduce((sum, t) => sum + t.amount, 0);
+ const days = this.getDaysBetween(transactions);
+ return totalExpense / Math.max(1, days);
+ }
+
+ /**
+ * Helper: Get days between first and last transaction
+ */
+ getDaysBetween(transactions) {
+ if (transactions.length < 2) return 1;
+ const dates = transactions.map(t => new Date(t.date).getTime());
+ const minDate = Math.min(...dates);
+ const maxDate = Math.max(...dates);
+ return Math.ceil((maxDate - minDate) / (1000 * 60 * 60 * 24)) || 1;
+ }
+
+ /**
+ * Helper: Calculate variance in spending
+ */
+ calculateVariance(transactions) {
+ const expenses = transactions.filter(t => t.type === 'expense');
+ if (expenses.length === 0) return 0;
+
+ const amounts = expenses.map(t => t.amount);
+ const mean = amounts.reduce((sum, val) => sum + val, 0) / amounts.length;
+ const variance = amounts.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / amounts.length;
+
+ return Math.sqrt(variance) / mean; // Coefficient of variation
+ }
+}
+
+module.exports = new RunwayForecaster();
diff --git a/services/treasuryService.js b/services/treasuryService.js
new file mode 100644
index 00000000..1e855cdf
--- /dev/null
+++ b/services/treasuryService.js
@@ -0,0 +1,254 @@
+const TreasuryVault = require('../models/TreasuryVault');
+const LiquidityThreshold = require('../models/LiquidityThreshold');
+const Transaction = require('../models/Transaction');
+const Account = require('../models/Account');
+const FinancialModels = require('../utils/financialModels');
+
+class TreasuryService {
+ /**
+ * Get comprehensive treasury dashboard data
+ */
+ async getTreasuryDashboard(userId) {
+ const vaults = await TreasuryVault.find({ userId, isActive: true });
+ const thresholds = await LiquidityThreshold.find({ userId, isActive: true });
+
+ // Calculate total liquidity across all vaults
+ const totalLiquidity = vaults.reduce((sum, v) => {
+ // Convert to base currency (INR) if needed
+ return sum + v.availableLiquidity;
+ }, 0);
+
+ // Get recent transactions for burn rate calculation
+ const thirtyDaysAgo = new Date();
+ thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
+
+ const recentExpenses = await Transaction.find({
+ user: userId,
+ type: 'expense',
+ date: { $gte: thirtyDaysAgo }
+ });
+
+ const dailyBurnRate = FinancialModels.calculateBurnRate(recentExpenses, 30);
+ const cashRunway = FinancialModels.calculateRunway(totalLiquidity, dailyBurnRate);
+
+ // Check threshold violations
+ const violations = [];
+ for (const threshold of thresholds) {
+ const vault = vaults.find(v => v._id.equals(threshold.vaultId));
+ if (!vault) continue;
+
+ let isViolated = false;
+ let currentValue = 0;
+
+ switch (threshold.thresholdType) {
+ case 'absolute':
+ currentValue = vault.availableLiquidity;
+ isViolated = currentValue < threshold.triggerValue;
+ break;
+ case 'percentage':
+ currentValue = (vault.availableLiquidity / vault.balance) * 100;
+ isViolated = currentValue < threshold.triggerValue;
+ break;
+ case 'runway_days':
+ currentValue = cashRunway;
+ isViolated = currentValue < threshold.triggerValue;
+ break;
+ }
+
+ if (isViolated) {
+ violations.push({
+ thresholdId: threshold._id,
+ thresholdName: threshold.thresholdName,
+ severity: threshold.severity,
+ currentValue,
+ triggerValue: threshold.triggerValue,
+ vaultName: vault.vaultName
+ });
+ }
+ }
+
+ return {
+ vaults,
+ totalLiquidity,
+ dailyBurnRate,
+ cashRunway,
+ violations,
+ healthScore: this.calculateHealthScore(totalLiquidity, dailyBurnRate, violations.length)
+ };
+ }
+
+ /**
+ * Calculate treasury health score (0-100)
+ */
+ calculateHealthScore(liquidity, burnRate, violationCount) {
+ let score = 100;
+
+ // Deduct based on runway
+ const runway = FinancialModels.calculateRunway(liquidity, burnRate);
+ if (runway < 30) score -= 40;
+ else if (runway < 60) score -= 20;
+ else if (runway < 90) score -= 10;
+
+ // Deduct based on violations
+ score -= violationCount * 15;
+
+ return Math.max(0, Math.min(100, score));
+ }
+
+ /**
+ * Transfer funds between vaults
+ */
+ async transferBetweenVaults(fromVaultId, toVaultId, amount, userId) {
+ const fromVault = await TreasuryVault.findOne({ _id: fromVaultId, userId });
+ const toVault = await TreasuryVault.findOne({ _id: toVaultId, userId });
+
+ if (!fromVault || !toVault) {
+ throw new Error('Vault not found');
+ }
+
+ if (fromVault.availableLiquidity < amount) {
+ throw new Error('Insufficient liquidity in source vault');
+ }
+
+ // Perform transfer
+ fromVault.balance -= amount;
+ toVault.balance += amount;
+
+ await fromVault.save();
+ await toVault.save();
+
+ return {
+ success: true,
+ fromVault: fromVault.vaultName,
+ toVault: toVault.vaultName,
+ amount
+ };
+ }
+
+ /**
+ * Auto-rebalance vaults based on allocation targets
+ */
+ async rebalanceVaults(userId) {
+ const vaults = await TreasuryVault.find({
+ userId,
+ isActive: true,
+ 'metadata.autoRebalance': true
+ });
+
+ const totalBalance = vaults.reduce((sum, v) => sum + v.balance, 0);
+ const rebalanceActions = [];
+
+ for (const vault of vaults) {
+ // Simple rebalancing: maintain equal distribution
+ const targetBalance = totalBalance / vaults.length;
+ const difference = vault.balance - targetBalance;
+
+ if (Math.abs(difference) > 1000) { // Only rebalance if difference > 1000
+ rebalanceActions.push({
+ vaultId: vault._id,
+ vaultName: vault.vaultName,
+ currentBalance: vault.balance,
+ targetBalance,
+ adjustment: -difference
+ });
+ }
+ }
+
+ return rebalanceActions;
+ }
+
+ /**
+ * Monitor and trigger threshold alerts
+ */
+ async monitorThresholds(userId) {
+ const dashboard = await this.getTreasuryDashboard(userId);
+ const triggeredAlerts = [];
+
+ for (const violation of dashboard.violations) {
+ const threshold = await LiquidityThreshold.findById(violation.thresholdId);
+
+ // Check cooldown period
+ if (threshold.lastTriggered) {
+ const hoursSinceLastTrigger = (Date.now() - threshold.lastTriggered) / (1000 * 60 * 60);
+ if (hoursSinceLastTrigger < threshold.cooldownPeriod) {
+ continue; // Skip if in cooldown
+ }
+ }
+
+ // Update threshold
+ threshold.lastTriggered = new Date();
+ threshold.triggerCount += 1;
+ threshold.currentValue = violation.currentValue;
+ await threshold.save();
+
+ triggeredAlerts.push({
+ thresholdName: threshold.thresholdName,
+ severity: threshold.severity,
+ message: `${threshold.thresholdName} violated: Current ${violation.currentValue.toFixed(2)}, Trigger ${violation.triggerValue}`,
+ automatedActions: threshold.automatedActions
+ });
+ }
+
+ return triggeredAlerts;
+ }
+
+ /**
+ * Get liquidity projection for next N days
+ */
+ async getLiquidityProjection(userId, days = 90) {
+ const dashboard = await this.getTreasuryDashboard(userId);
+ const projection = [];
+
+ for (let i = 0; i <= days; i++) {
+ const projectedBalance = dashboard.totalLiquidity - (dashboard.dailyBurnRate * i);
+ projection.push({
+ day: i,
+ date: new Date(Date.now() + i * 24 * 60 * 60 * 1000),
+ projectedBalance: Math.max(0, projectedBalance),
+ burnRate: dashboard.dailyBurnRate
+ });
+ }
+
+ return projection;
+ }
+
+ /**
+ * Calculate portfolio metrics
+ */
+ async getPortfolioMetrics(userId) {
+ const vaults = await TreasuryVault.find({ userId, isActive: true });
+
+ // Get historical balances (mock data for now)
+ const returns = [0.02, 0.015, -0.01, 0.03, 0.025]; // Mock monthly returns
+
+ const totalValue = vaults.reduce((sum, v) => sum + v.balance, 0);
+
+ return {
+ totalValue,
+ sharpeRatio: FinancialModels.calculateSharpeRatio(returns),
+ var95: FinancialModels.calculateVaR(totalValue, 0.15, 0.95),
+ diversificationScore: this.calculateDiversification(vaults)
+ };
+ }
+
+ /**
+ * Calculate diversification score
+ */
+ calculateDiversification(vaults) {
+ if (vaults.length === 0) return 0;
+
+ const totalBalance = vaults.reduce((sum, v) => sum + v.balance, 0);
+ if (totalBalance === 0) return 0;
+
+ // Herfindahl-Hirschman Index (HHI) for concentration
+ const hhi = vaults.reduce((sum, v) => {
+ const share = v.balance / totalBalance;
+ return sum + Math.pow(share, 2);
+ }, 0);
+
+ // Convert to diversification score (0-100)
+ return Math.round((1 - hhi) * 100);
+ }
+}
+
+module.exports = new TreasuryService();
diff --git a/utils/financialModels.js b/utils/financialModels.js
new file mode 100644
index 00000000..a114215a
--- /dev/null
+++ b/utils/financialModels.js
@@ -0,0 +1,177 @@
+/**
+ * Financial Models Utility
+ * Advanced mathematical functions for treasury operations
+ */
+
+const FinancialModels = {
+ /**
+ * Calculate Internal Rate of Return (IRR)
+ * Uses Newton-Raphson method for approximation
+ */
+ calculateIRR: (cashFlows, guess = 0.1) => {
+ const maxIterations = 100;
+ const tolerance = 0.00001;
+ let rate = guess;
+
+ for (let i = 0; i < maxIterations; i++) {
+ let npv = 0;
+ let dnpv = 0;
+
+ cashFlows.forEach((cf, t) => {
+ npv += cf / Math.pow(1 + rate, t);
+ dnpv -= (t * cf) / Math.pow(1 + rate, t + 1);
+ });
+
+ const newRate = rate - npv / dnpv;
+
+ if (Math.abs(newRate - rate) < tolerance) {
+ return newRate;
+ }
+ rate = newRate;
+ }
+
+ return rate;
+ },
+
+ /**
+ * Calculate Net Present Value (NPV)
+ */
+ calculateNPV: (cashFlows, discountRate) => {
+ return cashFlows.reduce((npv, cf, t) => {
+ return npv + cf / Math.pow(1 + discountRate, t);
+ }, 0);
+ },
+
+ /**
+ * Calculate Cash Runway (days until funds depleted)
+ */
+ calculateRunway: (currentBalance, dailyBurnRate) => {
+ if (dailyBurnRate <= 0) return Infinity;
+ return Math.floor(currentBalance / dailyBurnRate);
+ },
+
+ /**
+ * Calculate Burn Rate from historical data
+ */
+ calculateBurnRate: (expenses, days) => {
+ if (days === 0) return 0;
+ const totalExpenses = expenses.reduce((sum, e) => sum + e.amount, 0);
+ return totalExpenses / days;
+ },
+
+ /**
+ * FX Variance Analysis
+ * Measures volatility of exchange rate movements
+ */
+ calculateFXVariance: (rates) => {
+ if (rates.length < 2) return 0;
+
+ const mean = rates.reduce((sum, r) => sum + r, 0) / rates.length;
+ const squaredDiffs = rates.map(r => Math.pow(r - mean, 2));
+ const variance = squaredDiffs.reduce((sum, sd) => sum + sd, 0) / rates.length;
+
+ return {
+ variance,
+ standardDeviation: Math.sqrt(variance),
+ coefficientOfVariation: (Math.sqrt(variance) / mean) * 100
+ };
+ },
+
+ /**
+ * Value at Risk (VaR) - Parametric Method
+ * Estimates maximum potential loss at given confidence level
+ */
+ calculateVaR: (portfolioValue, volatility, confidenceLevel = 0.95, timeHorizon = 1) => {
+ // Z-score for confidence levels
+ const zScores = {
+ 0.90: 1.28,
+ 0.95: 1.65,
+ 0.99: 2.33
+ };
+
+ const zScore = zScores[confidenceLevel] || 1.65;
+ const var_ = portfolioValue * volatility * zScore * Math.sqrt(timeHorizon);
+
+ return var_;
+ },
+
+ /**
+ * Sharpe Ratio - Risk-adjusted return metric
+ */
+ calculateSharpeRatio: (returns, riskFreeRate = 0.05) => {
+ if (returns.length === 0) return 0;
+
+ const avgReturn = returns.reduce((sum, r) => sum + r, 0) / returns.length;
+ const excessReturn = avgReturn - riskFreeRate;
+
+ const variance = returns.reduce((sum, r) => {
+ return sum + Math.pow(r - avgReturn, 2);
+ }, 0) / returns.length;
+
+ const stdDev = Math.sqrt(variance);
+
+ return stdDev === 0 ? 0 : excessReturn / stdDev;
+ },
+
+ /**
+ * Liquidity Coverage Ratio (LCR)
+ * Basel III metric for short-term resilience
+ */
+ calculateLCR: (highQualityLiquidAssets, netCashOutflows) => {
+ if (netCashOutflows === 0) return Infinity;
+ return (highQualityLiquidAssets / netCashOutflows) * 100;
+ },
+
+ /**
+ * Compound Annual Growth Rate (CAGR)
+ */
+ calculateCAGR: (beginningValue, endingValue, years) => {
+ if (beginningValue === 0 || years === 0) return 0;
+ return (Math.pow(endingValue / beginningValue, 1 / years) - 1) * 100;
+ },
+
+ /**
+ * Weighted Average Cost of Capital (WACC)
+ */
+ calculateWACC: (equityValue, debtValue, costOfEquity, costOfDebt, taxRate) => {
+ const totalValue = equityValue + debtValue;
+ if (totalValue === 0) return 0;
+
+ const equityWeight = equityValue / totalValue;
+ const debtWeight = debtValue / totalValue;
+
+ return (equityWeight * costOfEquity) + (debtWeight * costOfDebt * (1 - taxRate));
+ },
+
+ /**
+ * Moving Average Convergence Divergence (MACD) for trend analysis
+ */
+ calculateMACD: (prices, shortPeriod = 12, longPeriod = 26, signalPeriod = 9) => {
+ const ema = (data, period) => {
+ const k = 2 / (period + 1);
+ let emaValue = data[0];
+ const emaArray = [emaValue];
+
+ for (let i = 1; i < data.length; i++) {
+ emaValue = (data[i] * k) + (emaValue * (1 - k));
+ emaArray.push(emaValue);
+ }
+ return emaArray;
+ };
+
+ const shortEMA = ema(prices, shortPeriod);
+ const longEMA = ema(prices, longPeriod);
+
+ const macdLine = shortEMA.map((val, i) => val - longEMA[i]);
+ const signalLine = ema(macdLine, signalPeriod);
+ const histogram = macdLine.map((val, i) => val - signalLine[i]);
+
+ return {
+ macdLine: macdLine[macdLine.length - 1],
+ signalLine: signalLine[signalLine.length - 1],
+ histogram: histogram[histogram.length - 1]
+ };
+ }
+};
+
+module.exports = FinancialModels;