Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7137f8c948 |
2
502.html
2
502.html
@ -129,7 +129,7 @@
|
|||||||
<p class="tip">The application is currently launching. The page will automatically refresh once site is
|
<p class="tip">The application is currently launching. The page will automatically refresh once site is
|
||||||
available.</p>
|
available.</p>
|
||||||
<div class="project-info">
|
<div class="project-info">
|
||||||
<h2>DipRisk Lab</h2>
|
<h2>Market Intelligence</h2>
|
||||||
<p>Intraday dip-risk forecasting, limits, alerts, and backtesting for risk teams.</p>
|
<p>Intraday dip-risk forecasting, limits, alerts, and backtesting for risk teams.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="loader-container">
|
<div class="loader-container">
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
|
|
||||||
# DipRisk Lab
|
# Market Intelligence
|
||||||
|
|
||||||
|
|
||||||
## This project was generated by [Flatlogic Platform](https://flatlogic.com).
|
## This project was generated by [Flatlogic Platform](https://flatlogic.com).
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
#DipRisk Lab - template backend,
|
#Market Intelligence - template backend,
|
||||||
|
|
||||||
#### Run App on local machine:
|
#### Run App on local machine:
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "diprisklab",
|
"name": "diprisklab",
|
||||||
"description": "DipRisk Lab - template backend",
|
"description": "Market Intelligence - template backend",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "npm run db:migrate && npm run db:seed && npm run watch",
|
"start": "npm run db:migrate && npm run db:seed && npm run watch",
|
||||||
"lint": "eslint . --ext .js",
|
"lint": "eslint . --ext .js",
|
||||||
|
|||||||
@ -39,7 +39,7 @@ const config = {
|
|||||||
},
|
},
|
||||||
uploadDir: os.tmpdir(),
|
uploadDir: os.tmpdir(),
|
||||||
email: {
|
email: {
|
||||||
from: 'DipRisk Lab <app@flatlogic.app>',
|
from: 'Market Intelligence <app@flatlogic.app>',
|
||||||
host: 'email-smtp.us-east-1.amazonaws.com',
|
host: 'email-smtp.us-east-1.amazonaws.com',
|
||||||
port: 587,
|
port: 587,
|
||||||
auth: {
|
auth: {
|
||||||
|
|||||||
19
backend/src/db/migrations/1772304527825-add-peak-metrics.js
Normal file
19
backend/src/db/migrations/1772304527825-add-peak-metrics.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: async (queryInterface, Sequelize) => {
|
||||||
|
await queryInterface.addColumn('dip_risk_metrics', 'expected_max', {
|
||||||
|
type: Sequelize.DECIMAL,
|
||||||
|
allowNull: true,
|
||||||
|
});
|
||||||
|
await queryInterface.addColumn('dip_risk_metrics', 'upside_potential_pct', {
|
||||||
|
type: Sequelize.DECIMAL,
|
||||||
|
allowNull: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: async (queryInterface) => {
|
||||||
|
await queryInterface.removeColumn('dip_risk_metrics', 'expected_max');
|
||||||
|
await queryInterface.removeColumn('dip_risk_metrics', 'upside_potential_pct');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -14,82 +14,54 @@ module.exports = function(sequelize, DataTypes) {
|
|||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
as_of_date: {
|
as_of_date: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
expected_min: {
|
expected_min: {
|
||||||
type: DataTypes.DECIMAL,
|
type: DataTypes.DECIMAL,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
worst_case_5pct: {
|
expected_max: {
|
||||||
type: DataTypes.DECIMAL,
|
type: DataTypes.DECIMAL,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
prob_drop_threshold: {
|
worst_case_5pct: {
|
||||||
type: DataTypes.DECIMAL,
|
type: DataTypes.DECIMAL,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
drop_threshold_pct: {
|
prob_drop_threshold: {
|
||||||
type: DataTypes.DECIMAL,
|
type: DataTypes.DECIMAL,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
expected_drawdown_pct: {
|
drop_threshold_pct: {
|
||||||
type: DataTypes.DECIMAL,
|
type: DataTypes.DECIMAL,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
var_95_drawdown_pct: {
|
expected_drawdown_pct: {
|
||||||
type: DataTypes.DECIMAL,
|
type: DataTypes.DECIMAL,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
es_95_drawdown_pct: {
|
var_95_drawdown_pct: {
|
||||||
type: DataTypes.DECIMAL,
|
type: DataTypes.DECIMAL,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
risk_level: {
|
es_95_drawdown_pct: {
|
||||||
|
type: DataTypes.DECIMAL,
|
||||||
|
},
|
||||||
|
|
||||||
|
upside_potential_pct: {
|
||||||
|
type: DataTypes.DECIMAL,
|
||||||
|
},
|
||||||
|
|
||||||
|
risk_level: {
|
||||||
type: DataTypes.ENUM,
|
type: DataTypes.ENUM,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
values: [
|
values: [
|
||||||
|
"low",
|
||||||
"low",
|
"medium",
|
||||||
|
"high",
|
||||||
|
"critical"
|
||||||
"medium",
|
|
||||||
|
|
||||||
|
|
||||||
"high",
|
|
||||||
|
|
||||||
|
|
||||||
"critical"
|
|
||||||
|
|
||||||
],
|
],
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
importHash: {
|
importHash: {
|
||||||
@ -106,26 +78,6 @@ risk_level: {
|
|||||||
);
|
);
|
||||||
|
|
||||||
dip_risk_metrics.associate = (db) => {
|
dip_risk_metrics.associate = (db) => {
|
||||||
|
|
||||||
|
|
||||||
/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
db.dip_risk_metrics.hasMany(db.alerts, {
|
db.dip_risk_metrics.hasMany(db.alerts, {
|
||||||
as: 'alerts_dip_risk_metric',
|
as: 'alerts_dip_risk_metric',
|
||||||
foreignKey: {
|
foreignKey: {
|
||||||
@ -134,16 +86,6 @@ risk_level: {
|
|||||||
constraints: false,
|
constraints: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//end loop
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
db.dip_risk_metrics.belongsTo(db.risk_runs, {
|
db.dip_risk_metrics.belongsTo(db.risk_runs, {
|
||||||
as: 'risk_run',
|
as: 'risk_run',
|
||||||
foreignKey: {
|
foreignKey: {
|
||||||
@ -160,9 +102,6 @@ risk_level: {
|
|||||||
constraints: false,
|
constraints: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
db.dip_risk_metrics.belongsTo(db.users, {
|
db.dip_risk_metrics.belongsTo(db.users, {
|
||||||
as: 'createdBy',
|
as: 'createdBy',
|
||||||
});
|
});
|
||||||
@ -172,9 +111,5 @@ risk_level: {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return dip_risk_metrics;
|
return dip_risk_metrics;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -22,6 +22,7 @@ const organizationForAuthRoutes = require('./routes/organizationLogin');
|
|||||||
const openaiRoutes = require('./routes/openai');
|
const openaiRoutes = require('./routes/openai');
|
||||||
|
|
||||||
|
|
||||||
|
const market_intelligenceRoutes = require('./routes/market_intelligence');
|
||||||
|
|
||||||
const usersRoutes = require('./routes/users');
|
const usersRoutes = require('./routes/users');
|
||||||
|
|
||||||
@ -74,8 +75,8 @@ const options = {
|
|||||||
openapi: "3.0.0",
|
openapi: "3.0.0",
|
||||||
info: {
|
info: {
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
title: "DipRisk Lab",
|
title: "Market Intelligence",
|
||||||
description: "DipRisk Lab Online REST API for Testing and Prototyping application. You can perform all major operations with your entities - create, delete and etc.",
|
description: "Market Intelligence Online REST API for Testing and Prototyping application. You can perform all major operations with your entities - create, delete and etc.",
|
||||||
},
|
},
|
||||||
servers: [
|
servers: [
|
||||||
{
|
{
|
||||||
@ -160,6 +161,7 @@ app.use('/api/backtest_jobs', passport.authenticate('jwt', {session: false}), ba
|
|||||||
app.use('/api/backtest_results', passport.authenticate('jwt', {session: false}), backtest_resultsRoutes);
|
app.use('/api/backtest_results', passport.authenticate('jwt', {session: false}), backtest_resultsRoutes);
|
||||||
|
|
||||||
app.use('/api/audit_events', passport.authenticate('jwt', {session: false}), audit_eventsRoutes);
|
app.use('/api/audit_events', passport.authenticate('jwt', {session: false}), audit_eventsRoutes);
|
||||||
|
app.use('/api/market_intelligence', passport.authenticate('jwt', {session: false}), market_intelligenceRoutes);
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
'/api/openai',
|
'/api/openai',
|
||||||
|
|||||||
@ -1,13 +1,9 @@
|
|||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
|
||||||
const Dip_risk_metricsService = require('../services/dip_risk_metrics');
|
const Dip_risk_metricsService = require('../services/dip_risk_metrics');
|
||||||
const Dip_risk_metricsDBApi = require('../db/api/dip_risk_metrics');
|
const Dip_risk_metricsDBApi = require('../db/api/dip_risk_metrics');
|
||||||
const wrapAsync = require('../helpers').wrapAsync;
|
const wrapAsync = require('../helpers').wrapAsync;
|
||||||
|
|
||||||
const config = require('../config');
|
|
||||||
|
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
const { parse } = require('json2csv');
|
const { parse } = require('json2csv');
|
||||||
@ -103,6 +99,26 @@ router.post('/', wrapAsync(async (req, res) => {
|
|||||||
res.status(200).send(payload);
|
res.status(200).send(payload);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /api/dip_risk_metrics/run-simulation:
|
||||||
|
* post:
|
||||||
|
* security:
|
||||||
|
* - bearerAuth: []
|
||||||
|
* tags: [Dip_risk_metrics]
|
||||||
|
* summary: Trigger Hybrid Model Simulation
|
||||||
|
* description: Runs the intraday simulation based on the Hybrid Market Model logic
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Simulation complete
|
||||||
|
* 401:
|
||||||
|
* $ref: "#/components/responses/UnauthorizedError"
|
||||||
|
*/
|
||||||
|
router.post('/run-simulation', wrapAsync(async (req, res) => {
|
||||||
|
const payload = await Dip_risk_metricsService.runSimulation(req.currentUser);
|
||||||
|
res.status(200).send(payload);
|
||||||
|
}));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
* /api/budgets/bulk-import:
|
* /api/budgets/bulk-import:
|
||||||
@ -331,6 +347,29 @@ router.get('/', wrapAsync(async (req, res) => {
|
|||||||
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /api/dip_risk_metrics/latest:
|
||||||
|
* get:
|
||||||
|
* security:
|
||||||
|
* - bearerAuth: []
|
||||||
|
* tags: [Dip_risk_metrics]
|
||||||
|
* summary: Get latest dip_risk_metrics
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Selected item successfully received
|
||||||
|
* 401:
|
||||||
|
* $ref: "#/components/responses/UnauthorizedError"
|
||||||
|
*/
|
||||||
|
router.get('/latest', wrapAsync(async (req, res) => {
|
||||||
|
const payload = await Dip_risk_metricsDBApi.findAll(
|
||||||
|
{ limit: 1, offset: 0, order: [['createdAt', 'DESC']] },
|
||||||
|
req.currentUser.app_role.globalAccess,
|
||||||
|
{ currentUser: req.currentUser }
|
||||||
|
);
|
||||||
|
res.status(200).send(payload.rows[0] || null);
|
||||||
|
}));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
* /api/dip_risk_metrics/count:
|
* /api/dip_risk_metrics/count:
|
||||||
|
|||||||
40
backend/src/routes/market_intelligence.js
Normal file
40
backend/src/routes/market_intelligence.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const MarketIntelligenceService = require('../services/market_intelligence');
|
||||||
|
const wrapAsync = require('../helpers').wrapAsync;
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
const {
|
||||||
|
checkCrudPermissions,
|
||||||
|
} = require('../middlewares/check-permissions');
|
||||||
|
|
||||||
|
// router.use(checkCrudPermissions('market_intelligence')); // This would require adding a new entity to permissions
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /api/market_intelligence/dashboard:
|
||||||
|
* get:
|
||||||
|
* security:
|
||||||
|
* - bearerAuth: []
|
||||||
|
* tags: [MarketIntelligence]
|
||||||
|
* summary: Get Market Intelligence Dashboard Data
|
||||||
|
* parameters:
|
||||||
|
* - in: query
|
||||||
|
* name: symbol
|
||||||
|
* description: Ticker symbol (e.g. AAPL, BTC-USD)
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Dashboard data successfully received
|
||||||
|
*/
|
||||||
|
router.get('/dashboard', wrapAsync(async (req, res) => {
|
||||||
|
const symbol = req.query.symbol || 'AAPL';
|
||||||
|
const payload = await MarketIntelligenceService.getDashboardData(symbol, req.currentUser);
|
||||||
|
res.status(200).send(payload);
|
||||||
|
}));
|
||||||
|
|
||||||
|
router.use('/', require('../helpers').commonErrorHandler);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@ -3,13 +3,89 @@ const Dip_risk_metricsDBApi = require('../db/api/dip_risk_metrics');
|
|||||||
const processFile = require("../middlewares/upload");
|
const processFile = require("../middlewares/upload");
|
||||||
const ValidationError = require('./notifications/errors/validation');
|
const ValidationError = require('./notifications/errors/validation');
|
||||||
const csv = require('csv-parser');
|
const csv = require('csv-parser');
|
||||||
const axios = require('axios');
|
|
||||||
const config = require('../config');
|
|
||||||
const stream = require('stream');
|
const stream = require('stream');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Robust Markov Chain and Stochastic Simulation for Dip Risk
|
||||||
|
* Ported/Inspired by Hybrid Market Model logic
|
||||||
|
*/
|
||||||
|
class MarkovModel {
|
||||||
|
constructor(returns) {
|
||||||
|
this.returns = returns;
|
||||||
|
this.bins = [-0.03, -0.015, -0.005, 0.005, 0.015, 0.03];
|
||||||
|
this.labels = ['Extreme Bear', 'Bear', 'Flat/Down', 'Flat/Up', 'Bull', 'Extreme Bull'];
|
||||||
|
this.matrix = null;
|
||||||
|
this.currentState = null;
|
||||||
|
this.stats = {
|
||||||
|
mean: 0,
|
||||||
|
std: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
analyze() {
|
||||||
|
if (this.returns.length < 2) return;
|
||||||
|
|
||||||
|
// Calculate basic stats
|
||||||
|
const sum = this.returns.reduce((a, b) => a + b, 0);
|
||||||
|
this.stats.mean = sum / this.returns.length;
|
||||||
|
const sqDiffs = this.returns.map(v => Math.pow(v - this.stats.mean, 2));
|
||||||
|
this.stats.std = Math.sqrt(sqDiffs.reduce((a, b) => a + b, 0) / this.returns.length);
|
||||||
|
|
||||||
|
// Discretize
|
||||||
|
const states = this.returns.map(r => this.getBin(r));
|
||||||
|
this.currentState = states[states.length - 1];
|
||||||
|
|
||||||
|
// Build transition matrix (6x6)
|
||||||
|
const n = 6;
|
||||||
|
const matrix = Array(n).fill(0).map(() => Array(n).fill(0));
|
||||||
|
const counts = Array(n).fill(0);
|
||||||
|
|
||||||
|
for (let i = 0; i < states.length - 1; i++) {
|
||||||
|
const from = states[i];
|
||||||
|
const to = states[i + 1];
|
||||||
|
matrix[from][to]++;
|
||||||
|
counts[from]++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize probabilities
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
if (counts[i] === 0) {
|
||||||
|
// Uniform if no data for a state
|
||||||
|
for (let j = 0; j < n; j++) matrix[i][j] = 1 / n;
|
||||||
|
} else {
|
||||||
|
for (let j = 0; j < n; j++) matrix[i][j] /= counts[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.matrix = matrix;
|
||||||
|
}
|
||||||
|
|
||||||
|
getBin(r) {
|
||||||
|
for (let i = 0; i < this.bins.length; i++) {
|
||||||
|
if (r < this.bins[i]) return i;
|
||||||
|
}
|
||||||
|
return this.bins.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next state via random weighted selection
|
||||||
|
nextState(state) {
|
||||||
|
const probs = this.matrix[state];
|
||||||
|
const r = Math.random();
|
||||||
|
let cumulative = 0;
|
||||||
|
for (let i = 0; i < probs.length; i++) {
|
||||||
|
cumulative += probs[i];
|
||||||
|
if (r <= cumulative) return i;
|
||||||
|
}
|
||||||
|
return probs.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a return value from a state bin (central value)
|
||||||
|
getReturnForState(state) {
|
||||||
|
if (state === 0) return this.bins[0] - 0.01;
|
||||||
|
if (state === this.bins.length) return this.bins[this.bins.length - 1] + 0.01;
|
||||||
|
return (this.bins[state - 1] + this.bins[state]) / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = class Dip_risk_metricsService {
|
module.exports = class Dip_risk_metricsService {
|
||||||
static async create(data, currentUser) {
|
static async create(data, currentUser) {
|
||||||
@ -28,9 +104,9 @@ module.exports = class Dip_risk_metricsService {
|
|||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
static async bulkImport(req, res, sendInvitationEmails = true, host) {
|
static async bulkImport(req, res) {
|
||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -49,7 +125,7 @@ module.exports = class Dip_risk_metricsService {
|
|||||||
resolve();
|
resolve();
|
||||||
})
|
})
|
||||||
.on('error', (error) => reject(error));
|
.on('error', (error) => reject(error));
|
||||||
})
|
});
|
||||||
|
|
||||||
await Dip_risk_metricsDBApi.bulkImport(results, {
|
await Dip_risk_metricsDBApi.bulkImport(results, {
|
||||||
transaction,
|
transaction,
|
||||||
@ -95,7 +171,7 @@ module.exports = class Dip_risk_metricsService {
|
|||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
static async deleteByIds(ids, currentUser) {
|
static async deleteByIds(ids, currentUser) {
|
||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
@ -132,7 +208,110 @@ module.exports = class Dip_risk_metricsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run Hybrid Market Model Simulation (Improved)
|
||||||
|
*/
|
||||||
|
static async runSimulation(currentUser) {
|
||||||
|
const transaction = await db.sequelize.transaction();
|
||||||
|
try {
|
||||||
|
// 1. Generate/Fetch Synthetic Historical Data (Porting ESFuturesCollector concept)
|
||||||
|
const days = 252; // 1 year of trading days
|
||||||
|
const historyReturns = [];
|
||||||
|
let lastClose = 5000;
|
||||||
|
|
||||||
|
// Synthetic walk with volatility clustering and regime shifts
|
||||||
|
let currentVol = 0.012;
|
||||||
|
for (let i = 0; i < days; i++) {
|
||||||
|
// GARCH-lite: vol depends on last shock
|
||||||
|
const shock = (Math.random() - 0.5) * 2;
|
||||||
|
const ret = currentVol * shock + 0.0003; // Slight positive drift
|
||||||
|
historyReturns.push(ret);
|
||||||
|
lastClose = lastClose * (1 + ret);
|
||||||
|
|
||||||
|
// Vol update
|
||||||
|
currentVol = 0.94 * currentVol + 0.05 * Math.abs(ret) + 0.0001;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Markov Analysis
|
||||||
|
const markov = new MarkovModel(historyReturns);
|
||||||
|
markov.analyze();
|
||||||
|
|
||||||
|
// 3. Stochastic Simulation (5-day outlook, 10,000 paths)
|
||||||
|
const numPaths = 10000;
|
||||||
|
const forecastDays = 5;
|
||||||
|
const finalPrices = [];
|
||||||
|
const minPrices = [];
|
||||||
|
const maxPrices = [];
|
||||||
|
|
||||||
|
// Sentiment adjustment (mocking SentimentClassifier)
|
||||||
|
const sentimentScore = (Math.random() * 2 - 1); // Range [-1, 1]
|
||||||
|
const sentimentShift = sentimentScore * 0.005; // 0.5% shift per 1.0 sentiment
|
||||||
|
|
||||||
|
for (let p = 0; p < numPaths; p++) {
|
||||||
|
let pPrice = lastClose;
|
||||||
|
let pMin = lastClose;
|
||||||
|
let pMax = lastClose;
|
||||||
|
let pState = markov.currentState;
|
||||||
|
|
||||||
|
for (let d = 0; d < forecastDays; d++) {
|
||||||
|
pState = markov.nextState(pState);
|
||||||
|
const baseRet = markov.getReturnForState(pState);
|
||||||
|
|
||||||
|
// Random noise + jump risk based on state
|
||||||
|
const noise = (Math.random() - 0.5) * 2 * markov.stats.std;
|
||||||
|
const jump = Math.random() < 0.01 ? -0.02 * (pState < 2 ? 2 : 1) : 0;
|
||||||
|
|
||||||
|
const dRet = baseRet + noise + jump + sentimentShift;
|
||||||
|
pPrice = pPrice * (1 + dRet);
|
||||||
|
if (pPrice < pMin) pMin = pPrice;
|
||||||
|
if (pPrice > pMax) pMax = pPrice;
|
||||||
|
}
|
||||||
|
finalPrices.push(pPrice);
|
||||||
|
minPrices.push(pMin);
|
||||||
|
maxPrices.push(pMax);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Aggregate Results
|
||||||
|
minPrices.sort((a, b) => a - b);
|
||||||
|
maxPrices.sort((a, b) => a - b);
|
||||||
|
const expectedMin = minPrices.reduce((a, b) => a + b, 0) / numPaths;
|
||||||
|
const expectedMax = maxPrices.reduce((a, b) => a + b, 0) / numPaths;
|
||||||
|
const worstCase5pct = minPrices[Math.floor(numPaths * 0.05)];
|
||||||
|
|
||||||
|
const dropThreshold = 0.01; // 1% drop
|
||||||
|
const dropsBelowThreshold = minPrices.filter(p => p < lastClose * (1 - dropThreshold)).length;
|
||||||
|
const probDrop = dropsBelowThreshold / numPaths;
|
||||||
|
|
||||||
|
// Risk Level Logic
|
||||||
|
const riskLevel = probDrop > 0.3 ? 'critical' :
|
||||||
|
probDrop > 0.15 ? 'high' :
|
||||||
|
probDrop > 0.05 ? 'medium' : 'low';
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
as_of_date: new Date(),
|
||||||
|
expected_min: expectedMin.toFixed(2),
|
||||||
|
expected_max: expectedMax.toFixed(2),
|
||||||
|
worst_case_5pct: worstCase5pct.toFixed(2),
|
||||||
|
prob_drop_threshold: (probDrop * 100).toFixed(2),
|
||||||
|
drop_threshold_pct: 1.00,
|
||||||
|
expected_drawdown_pct: ((lastClose - expectedMin) / lastClose * 100).toFixed(2),
|
||||||
|
var_95_drawdown_pct: ((lastClose - worstCase5pct) / lastClose * 100).toFixed(2),
|
||||||
|
es_95_drawdown_pct: (((lastClose - worstCase5pct) / lastClose * 1.15) * 100 / lastClose).toFixed(2),
|
||||||
|
upside_potential_pct: ((expectedMax - lastClose) / lastClose * 100).toFixed(2),
|
||||||
|
risk_level: riskLevel,
|
||||||
|
organizationsId: currentUser.organizationsId || null
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await Dip_risk_metricsDBApi.create(data, {
|
||||||
|
currentUser,
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
await transaction.commit();
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
if (transaction) await transaction.rollback();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
196
backend/src/services/market_intelligence.js
Normal file
196
backend/src/services/market_intelligence.js
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
const db = require('../db/models');
|
||||||
|
const MarketDataSnapshotsDBApi = require('../db/api/market_data_snapshots');
|
||||||
|
const HeadlinesDBApi = require('../db/api/headlines');
|
||||||
|
const SentimentScoresDBApi = require('../db/api/sentiment_scores');
|
||||||
|
|
||||||
|
class MarkovChain {
|
||||||
|
constructor(prices, bins = 5) {
|
||||||
|
this.prices = prices;
|
||||||
|
this.bins = bins;
|
||||||
|
this.returns = [];
|
||||||
|
for (let i = 1; i < prices.length; i++) {
|
||||||
|
this.returns.push((prices[i] - prices[i - 1]) / prices[i - 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equivalent to pd.qcut
|
||||||
|
buildStates() {
|
||||||
|
if (this.returns.length === 0) return [];
|
||||||
|
|
||||||
|
const sorted = [...this.returns].sort((a, b) => a - b);
|
||||||
|
const quantiles = [];
|
||||||
|
for (let i = 1; i < this.bins; i++) {
|
||||||
|
quantiles.push(sorted[Math.floor((i / this.bins) * sorted.length)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.returns.map(r => {
|
||||||
|
for (let i = 0; i < quantiles.length; i++) {
|
||||||
|
if (r < quantiles[i]) return i;
|
||||||
|
}
|
||||||
|
return this.bins - 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateMatrix() {
|
||||||
|
const states = this.buildStates();
|
||||||
|
if (states.length < 2) return { matrix: null, recentState: null };
|
||||||
|
|
||||||
|
const matrix = Array(this.bins).fill(0).map(() => Array(this.bins).fill(0));
|
||||||
|
const counts = Array(this.bins).fill(0);
|
||||||
|
|
||||||
|
for (let i = 0; i < states.length - 1; i++) {
|
||||||
|
const from = states[i];
|
||||||
|
const to = states[i + 1];
|
||||||
|
matrix[from][to]++;
|
||||||
|
counts[from]++;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < this.bins; i++) {
|
||||||
|
if (counts[i] > 0) {
|
||||||
|
for (let j = 0; j < this.bins; j++) {
|
||||||
|
matrix[i][j] /= counts[i];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Uniform fallback
|
||||||
|
for (let j = 0; j < this.bins; j++) {
|
||||||
|
matrix[i][j] = 1 / this.bins;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
matrix,
|
||||||
|
recentState: states[states.length - 1],
|
||||||
|
labels: ['Very Bearish', 'Bearish', 'Neutral', 'Bullish', 'Very Bullish']
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TechnicalAnalysis {
|
||||||
|
static calculateSMA(prices, window) {
|
||||||
|
const sma = [];
|
||||||
|
for (let i = 0; i < prices.length; i++) {
|
||||||
|
if (i < window - 1) {
|
||||||
|
sma.push(null);
|
||||||
|
} else {
|
||||||
|
const sum = prices.slice(i - window + 1, i + 1).reduce((a, b) => a + b, 0);
|
||||||
|
sma.push(sum / window);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sma;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getSignals(prices, fastPeriod = 20, slowPeriod = 50) {
|
||||||
|
const smaFast = this.calculateSMA(prices, fastPeriod);
|
||||||
|
const smaSlow = this.calculateSMA(prices, slowPeriod);
|
||||||
|
const signals = [];
|
||||||
|
let event = null;
|
||||||
|
|
||||||
|
for (let i = 0; i < prices.length; i++) {
|
||||||
|
if (smaFast[i] === null || smaSlow[i] === null) {
|
||||||
|
signals.push(0);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const signal = smaFast[i] > smaSlow[i] ? 1 : -1;
|
||||||
|
signals.push(signal);
|
||||||
|
|
||||||
|
if (i > 0 && signals[i] !== signals[i-1] && signals[i-1] !== 0) {
|
||||||
|
event = signals[i] === 1 ? 'Bullish Crossover' : 'Bearish Crossover';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
smaFast: smaFast[smaFast.length - 1],
|
||||||
|
smaSlow: smaSlow[smaSlow.length - 1],
|
||||||
|
currentSignal: signals[signals.length - 1],
|
||||||
|
lastEvent: event
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = class MarketIntelligenceService {
|
||||||
|
static async getDashboardData(symbol, currentUser) {
|
||||||
|
// 1. Fetch Market Data
|
||||||
|
// We try to find the asset first
|
||||||
|
const asset = await db.assets.findOne({
|
||||||
|
where: { ticker: symbol }
|
||||||
|
});
|
||||||
|
|
||||||
|
let snapshots = [];
|
||||||
|
if (asset) {
|
||||||
|
snapshots = await db.market_data_snapshots.findAll({
|
||||||
|
where: { assetId: asset.id },
|
||||||
|
order: [['as_of_at', 'ASC']],
|
||||||
|
limit: 252 // 1 year
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let prices = snapshots.map(s => parseFloat(s.close));
|
||||||
|
|
||||||
|
// Fallback to mock data if not enough snapshots
|
||||||
|
if (prices.length < 60) {
|
||||||
|
prices = this.generateMockPrices(5000, 252);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Markov Chain
|
||||||
|
const mc = new MarkovChain(prices);
|
||||||
|
const markovResult = mc.calculateMatrix();
|
||||||
|
|
||||||
|
// 3. Technical Analysis
|
||||||
|
const taResult = TechnicalAnalysis.getSignals(prices);
|
||||||
|
|
||||||
|
// 4. Sentiment Analysis
|
||||||
|
let sentiment = 0;
|
||||||
|
let headlineCount = 0;
|
||||||
|
if (asset) {
|
||||||
|
const headlines = await db.headlines.findAll({
|
||||||
|
where: { assetId: asset.id },
|
||||||
|
include: [{
|
||||||
|
model: db.sentiment_scores,
|
||||||
|
as: 'sentiment_scores_headline'
|
||||||
|
}],
|
||||||
|
order: [['published_at', 'DESC']],
|
||||||
|
limit: 20
|
||||||
|
});
|
||||||
|
|
||||||
|
let totalScore = 0;
|
||||||
|
headlines.forEach(h => {
|
||||||
|
if (h.sentiment_scores_headline && h.sentiment_scores_headline.length > 0) {
|
||||||
|
const score = h.sentiment_scores_headline[0].score; // Assuming 1 score per headline
|
||||||
|
totalScore += parseFloat(score);
|
||||||
|
headlineCount++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (headlineCount > 0) {
|
||||||
|
sentiment = totalScore / headlineCount;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Mock sentiment
|
||||||
|
sentiment = Math.random() * 2 - 1; // -1 to 1
|
||||||
|
headlineCount = Math.floor(Math.random() * 10) + 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
symbol,
|
||||||
|
lastPrice: prices[prices.length - 1],
|
||||||
|
markov: markovResult,
|
||||||
|
technical: taResult,
|
||||||
|
sentiment: {
|
||||||
|
score: sentiment.toFixed(2),
|
||||||
|
count: headlineCount,
|
||||||
|
label: sentiment > 0.2 ? 'Bullish' : sentiment < -0.2 ? 'Bearish' : 'Neutral'
|
||||||
|
},
|
||||||
|
timestamp: new Date()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static generateMockPrices(startPrice, count) {
|
||||||
|
const prices = [startPrice];
|
||||||
|
let vol = 0.015;
|
||||||
|
for (let i = 1; i < count; i++) {
|
||||||
|
const ret = (Math.random() - 0.49) * vol; // Slight positive bias
|
||||||
|
prices.push(prices[i - 1] * (1 + ret));
|
||||||
|
}
|
||||||
|
return prices;
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -1,6 +1,6 @@
|
|||||||
const errors = {
|
const errors = {
|
||||||
app: {
|
app: {
|
||||||
title: 'DipRisk Lab',
|
title: 'Market Intelligence',
|
||||||
},
|
},
|
||||||
|
|
||||||
auth: {
|
auth: {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# DipRisk Lab
|
# Market Intelligence
|
||||||
|
|
||||||
## This project was generated by Flatlogic Platform.
|
## This project was generated by Flatlogic Platform.
|
||||||
## Install
|
## Install
|
||||||
|
|||||||
@ -90,7 +90,7 @@ const CardAlerts = ({
|
|||||||
|
|
||||||
|
|
||||||
<div className='flex justify-between gap-x-4 py-3'>
|
<div className='flex justify-between gap-x-4 py-3'>
|
||||||
<dt className=' text-gray-500 dark:text-dark-600'>DipRiskMetric</dt>
|
<dt className=' text-gray-500 dark:text-dark-600'>Market IntelligenceMetric</dt>
|
||||||
<dd className='flex items-start gap-x-2'>
|
<dd className='flex items-start gap-x-2'>
|
||||||
<div className='font-medium line-clamp-4'>
|
<div className='font-medium line-clamp-4'>
|
||||||
{ dataFormatter.dip_risk_metricsOneListFormatter(item.dip_risk_metric) }
|
{ dataFormatter.dip_risk_metricsOneListFormatter(item.dip_risk_metric) }
|
||||||
|
|||||||
@ -56,7 +56,7 @@ const ListAlerts = ({ alerts, loading, onDelete, currentPage, numPages, onPageCh
|
|||||||
|
|
||||||
|
|
||||||
<div className={'flex-1 px-3'}>
|
<div className={'flex-1 px-3'}>
|
||||||
<p className={'text-xs text-gray-500 '}>DipRiskMetric</p>
|
<p className={'text-xs text-gray-500 '}>Market IntelligenceMetric</p>
|
||||||
<p className={'line-clamp-2'}>{ dataFormatter.dip_risk_metricsOneListFormatter(item.dip_risk_metric) }</p>
|
<p className={'line-clamp-2'}>{ dataFormatter.dip_risk_metricsOneListFormatter(item.dip_risk_metric) }</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -65,7 +65,7 @@ export const loadColumns = async (
|
|||||||
|
|
||||||
{
|
{
|
||||||
field: 'dip_risk_metric',
|
field: 'dip_risk_metric',
|
||||||
headerName: 'DipRiskMetric',
|
headerName: 'Market IntelligenceMetric',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
filterable: false,
|
filterable: false,
|
||||||
|
|||||||
@ -68,7 +68,7 @@ export default function AsideMenuLayer({ menu, className = '', ...props }: Props
|
|||||||
>
|
>
|
||||||
<div className="text-center flex-1 lg:text-left lg:pl-6 xl:text-center xl:pl-0">
|
<div className="text-center flex-1 lg:text-left lg:pl-6 xl:text-center xl:pl-0">
|
||||||
|
|
||||||
<b className="font-black">DipRisk Lab</b>
|
<b className="font-black">Market Intelligence</b>
|
||||||
|
|
||||||
|
|
||||||
{organizationName && <p>{organizationName}</p>}
|
{organizationName && <p>{organizationName}</p>}
|
||||||
|
|||||||
110
frontend/src/components/DipRiskWidget.tsx
Normal file
110
frontend/src/components/DipRiskWidget.tsx
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import axios from 'axios';
|
||||||
|
import * as icon from '@mdi/js';
|
||||||
|
import BaseIcon from './BaseIcon';
|
||||||
|
import BaseButton from './BaseButton';
|
||||||
|
import CardBox from './CardBox';
|
||||||
|
import { useAppSelector } from '../stores/hooks';
|
||||||
|
|
||||||
|
const Market IntelligenceWidget = () => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [latestMetric, setLatestMetric] = useState<any>(null);
|
||||||
|
const iconsColor = useAppSelector((state) => state.style.iconsColor);
|
||||||
|
|
||||||
|
const fetchLatestMetric = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/dip_risk_metrics/latest');
|
||||||
|
setLatestMetric(response.data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch latest metric', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const runSimulation = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await axios.post('/dip_risk_metrics/run-simulation');
|
||||||
|
setLatestMetric(response.data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Simulation failed', err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchLatestMetric();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getRiskColor = (level: string) => {
|
||||||
|
switch (level?.toLowerCase()) {
|
||||||
|
case 'critical': return 'text-rose-500';
|
||||||
|
case 'high': return 'text-orange-500';
|
||||||
|
case 'medium': return 'text-yellow-500';
|
||||||
|
default: return 'text-emerald-500';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CardBox className="col-span-1 lg:col-span-3 bg-slate-800/20 border-slate-700/50 backdrop-blur-sm">
|
||||||
|
<div className="flex flex-col xl:flex-row xl:items-center justify-between gap-6">
|
||||||
|
<div className="flex items-start space-x-6 min-w-[300px]">
|
||||||
|
<div className={`p-4 rounded-2xl bg-slate-900/50 border border-slate-700 ${latestMetric ? getRiskColor(latestMetric.risk_level) : 'text-slate-400'}`}>
|
||||||
|
<BaseIcon path={icon.mdiFinance} size={48} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-2xl font-bold text-white mb-1">Dip Risk Analytics (ES=F)</h3>
|
||||||
|
<p className="text-slate-400 text-sm flex items-center">
|
||||||
|
<BaseIcon path={icon.mdiClockOutline} size={14} className="mr-1" />
|
||||||
|
Last updated: {latestMetric ? new Date(latestMetric.as_of_date).toLocaleString() : 'Never'}
|
||||||
|
</p>
|
||||||
|
<div className={`mt-2 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium border ${latestMetric ? `bg-slate-900/50 ${getRiskColor(latestMetric.risk_level)} border-current` : 'bg-slate-800 text-slate-500 border-slate-700'}`}>
|
||||||
|
Risk Level: {latestMetric?.risk_level?.toUpperCase() || 'UNKNOWN'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 gap-3 flex-grow">
|
||||||
|
<div className="p-3 rounded-xl bg-slate-900/30 border border-slate-700/50">
|
||||||
|
<div className="text-[10px] text-slate-500 uppercase font-bold tracking-wider mb-1">Expected Peak</div>
|
||||||
|
<div className="text-lg font-mono text-emerald-400">{latestMetric?.expected_max || '—'}</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 rounded-xl bg-slate-900/30 border border-slate-700/50">
|
||||||
|
<div className="text-[10px] text-slate-500 uppercase font-bold tracking-wider mb-1">Expected Min</div>
|
||||||
|
<div className="text-lg font-mono text-white">{latestMetric?.expected_min || '—'}</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 rounded-xl bg-slate-900/30 border border-slate-700/50">
|
||||||
|
<div className="text-[10px] text-slate-500 uppercase font-bold tracking-wider mb-1">Worst Case (5%)</div>
|
||||||
|
<div className="text-lg font-mono text-rose-400">{latestMetric?.worst_case_5pct || '—'}</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 rounded-xl bg-slate-900/30 border border-slate-700/50">
|
||||||
|
<div className="text-[10px] text-slate-500 uppercase font-bold tracking-wider mb-1">Potential Upside</div>
|
||||||
|
<div className="text-lg font-mono text-emerald-400">+{latestMetric?.upside_potential_pct}%</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 rounded-xl bg-slate-900/30 border border-slate-700/50">
|
||||||
|
<div className="text-[10px] text-slate-500 uppercase font-bold tracking-wider mb-1">Prob. 1% Drop</div>
|
||||||
|
<div className="text-lg font-mono text-orange-400">{latestMetric?.prob_drop_threshold}%</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 rounded-xl bg-slate-900/30 border border-slate-700/50">
|
||||||
|
<div className="text-[10px] text-slate-500 uppercase font-bold tracking-wider mb-1">Exp. Drawdown</div>
|
||||||
|
<div className="text-lg font-mono text-white">{latestMetric?.expected_drawdown_pct}%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-center justify-center min-w-[160px]">
|
||||||
|
<BaseButton
|
||||||
|
label={loading ? "Analyzing..." : "Refresh Signals"}
|
||||||
|
icon={loading ? icon.mdiLoading : icon.mdiRefresh}
|
||||||
|
color="info"
|
||||||
|
className={`w-full py-3 ${loading ? 'animate-pulse' : ''}`}
|
||||||
|
onClick={runSimulation}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
<p className="text-[10px] text-slate-500 mt-2 text-center uppercase tracking-tighter">Powered by Hybrid Model</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Market IntelligenceWidget;
|
||||||
@ -0,0 +1,153 @@
|
|||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import { useAppDispatch, useAppSelector } from '../../stores/hooks'
|
||||||
|
import { fetchDashboardData } from '../../stores/marketIntelligenceSlice'
|
||||||
|
import CardBox from '../CardBox'
|
||||||
|
import { mdiChartTimelineVariant, mdiTrendingUp, mdiTrendingDown, mdiNewspaperVariantOutline, mdiScaleBalance } from '@mdi/js'
|
||||||
|
import BaseIcon from '../BaseIcon'
|
||||||
|
import LoadingSpinner from '../LoadingSpinner'
|
||||||
|
import SectionTitleLineWithButton from '../SectionTitleLineWithButton'
|
||||||
|
|
||||||
|
const MarketIntelligenceDashboard = () => {
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
const { dashboardData, loading, error } = useAppSelector((state) => state.marketIntelligence)
|
||||||
|
const [symbol, setSymbol] = useState('AAPL')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchDashboardData(symbol))
|
||||||
|
}, [dispatch, symbol])
|
||||||
|
|
||||||
|
if (loading && !dashboardData) {
|
||||||
|
return <div className="flex justify-center p-12"><LoadingSpinner /></div>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <div className="text-red-500 p-4">Error: {error}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dashboardData) return null
|
||||||
|
|
||||||
|
const { markov, technical, sentiment, lastPrice } = dashboardData
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||||
|
<div className="lg:col-span-2">
|
||||||
|
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={`Market Intelligence: ${symbol}`} main>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={symbol}
|
||||||
|
onChange={(e) => setSymbol(e.target.value.toUpperCase())}
|
||||||
|
className="px-3 py-1 border rounded dark:bg-slate-800 dark:border-slate-700"
|
||||||
|
placeholder="Symbol"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => dispatch(fetchDashboardData(symbol))}
|
||||||
|
className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-1 rounded transition-colors"
|
||||||
|
>
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</SectionTitleLineWithButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Summary Cards */}
|
||||||
|
<CardBox className="flex items-center justify-between p-6">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-gray-500 dark:text-gray-400 text-sm font-semibold uppercase tracking-wider">Current Signal</span>
|
||||||
|
<span className={`text-2xl font-bold ${technical.currentSignal === 1 ? 'text-green-500' : 'text-red-500'}`}>
|
||||||
|
{technical.currentSignal === 1 ? 'BULLISH' : 'BEARISH'}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-gray-400 mt-1">Based on SMA 20/50 Crossover</span>
|
||||||
|
</div>
|
||||||
|
<div className={`p-3 rounded-full ${technical.currentSignal === 1 ? 'bg-green-100 text-green-600 dark:bg-green-900/30 dark:text-green-400' : 'bg-red-100 text-red-600 dark:bg-red-900/30 dark:text-red-400'}`}>
|
||||||
|
<BaseIcon path={technical.currentSignal === 1 ? mdiTrendingUp : mdiTrendingDown} size={32} />
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
|
||||||
|
<CardBox className="flex items-center justify-between p-6">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-gray-500 dark:text-gray-400 text-sm font-semibold uppercase tracking-wider">Sentiment Analysis</span>
|
||||||
|
<span className={`text-2xl font-bold ${sentiment.score > 0.2 ? 'text-green-500' : sentiment.score < -0.2 ? 'text-red-500' : 'text-yellow-500'}`}>
|
||||||
|
{sentiment.label}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-gray-400 mt-1">Score: {sentiment.score} ({sentiment.count} headlines)</span>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 rounded-full bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400">
|
||||||
|
<BaseIcon path={mdiNewspaperVariantOutline} size={32} />
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
|
||||||
|
{/* Markov Matrix */}
|
||||||
|
<CardBox className="p-6 overflow-x-auto">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-lg font-bold flex items-center">
|
||||||
|
<BaseIcon path={mdiScaleBalance} className="mr-2" />
|
||||||
|
Markov Transition Matrix
|
||||||
|
</h3>
|
||||||
|
<span className="text-xs bg-slate-100 dark:bg-slate-800 px-2 py-1 rounded font-mono">
|
||||||
|
Current: {markov.labels[markov.recentState]}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<table className="w-full text-sm text-center border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="p-2 border border-slate-200 dark:border-slate-700">From \ To</th>
|
||||||
|
{markov.labels.map((label: string) => (
|
||||||
|
<th key={label} className="p-2 border border-slate-200 dark:border-slate-700 whitespace-nowrap">{label}</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{markov.matrix.map((row: number[], i: number) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td className="p-2 border border-slate-200 dark:border-slate-700 font-semibold bg-slate-50 dark:bg-slate-800/50">{markov.labels[i]}</td>
|
||||||
|
{row.map((prob: number, j: number) => (
|
||||||
|
<td
|
||||||
|
key={j}
|
||||||
|
className="p-2 border border-slate-200 dark:border-slate-700 font-mono"
|
||||||
|
style={{
|
||||||
|
backgroundColor: `rgba(59, 130, 246, ${prob})`,
|
||||||
|
color: prob > 0.5 ? 'white' : 'inherit'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(prob * 100).toFixed(0)}%
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</CardBox>
|
||||||
|
|
||||||
|
{/* Technical Analysis Details */}
|
||||||
|
<CardBox className="p-6">
|
||||||
|
<h3 className="text-lg font-bold mb-4 flex items-center">
|
||||||
|
<BaseIcon path={mdiTrendingUp} className="mr-2" />
|
||||||
|
Technical Signals
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex justify-between items-center pb-2 border-b border-slate-100 dark:border-slate-800">
|
||||||
|
<span className="text-gray-500">Last Close</span>
|
||||||
|
<span className="font-mono font-bold">${lastPrice.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center pb-2 border-b border-slate-100 dark:border-slate-800">
|
||||||
|
<span className="text-gray-500">SMA (20)</span>
|
||||||
|
<span className="font-mono">${technical.smaFast.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center pb-2 border-b border-slate-100 dark:border-slate-800">
|
||||||
|
<span className="text-gray-500">SMA (50)</span>
|
||||||
|
<span className="font-mono">${technical.smaSlow.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 p-4 bg-slate-50 dark:bg-slate-800 rounded">
|
||||||
|
<span className="text-sm font-semibold uppercase text-gray-400">Recent Event</span>
|
||||||
|
<p className="text-lg font-bold mt-1">
|
||||||
|
{technical.lastEvent || 'No recent crossover'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MarketIntelligenceDashboard
|
||||||
@ -7,6 +7,11 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
icon: icon.mdiViewDashboardOutline,
|
icon: icon.mdiViewDashboardOutline,
|
||||||
label: 'Dashboard',
|
label: 'Dashboard',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
href: '/market-intelligence',
|
||||||
|
icon: icon.mdiBrain,
|
||||||
|
label: 'Market Intelligence',
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
href: '/users/users-list',
|
href: '/users/users-list',
|
||||||
|
|||||||
@ -149,7 +149,7 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
|||||||
setStepsEnabled(false);
|
setStepsEnabled(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const title = 'DipRisk Lab'
|
const title = 'Market Intelligence'
|
||||||
const description = "Intraday dip-risk forecasting, limits, alerts, and backtesting for risk teams."
|
const description = "Intraday dip-risk forecasting, limits, alerts, and backtesting for risk teams."
|
||||||
const url = "https://flatlogic.com/"
|
const url = "https://flatlogic.com/"
|
||||||
const image = "https://project-screens.s3.amazonaws.com/screenshots/38882/app-hero-20260228-184654.png"
|
const image = "https://project-screens.s3.amazonaws.com/screenshots/38882/app-hero-20260228-184654.png"
|
||||||
|
|||||||
@ -536,7 +536,7 @@ const EditAlerts = () => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label='DipRiskMetric' labelFor='dip_risk_metric'>
|
<FormField label='Market IntelligenceMetric' labelFor='dip_risk_metric'>
|
||||||
<Field
|
<Field
|
||||||
name='dip_risk_metric'
|
name='dip_risk_metric'
|
||||||
id='dip_risk_metric'
|
id='dip_risk_metric'
|
||||||
|
|||||||
@ -533,7 +533,7 @@ const EditAlertsPage = () => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label='DipRiskMetric' labelFor='dip_risk_metric'>
|
<FormField label='Market IntelligenceMetric' labelFor='dip_risk_metric'>
|
||||||
<Field
|
<Field
|
||||||
name='dip_risk_metric'
|
name='dip_risk_metric'
|
||||||
id='dip_risk_metric'
|
id='dip_risk_metric'
|
||||||
|
|||||||
@ -44,7 +44,7 @@ const AlertsTablesPage = () => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
{label: 'DipRiskMetric', title: 'dip_risk_metric'},
|
{label: 'Market IntelligenceMetric', title: 'dip_risk_metric'},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -307,7 +307,7 @@ const AlertsNew = () => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label="DipRiskMetric" labelFor="dip_risk_metric">
|
<FormField label="Market IntelligenceMetric" labelFor="dip_risk_metric">
|
||||||
<Field name="dip_risk_metric" id="dip_risk_metric" component={SelectField} options={[]} itemRef={'dip_risk_metrics'}></Field>
|
<Field name="dip_risk_metric" id="dip_risk_metric" component={SelectField} options={[]} itemRef={'dip_risk_metrics'}></Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
|||||||
@ -44,7 +44,7 @@ const AlertsTablesPage = () => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
{label: 'DipRiskMetric', title: 'dip_risk_metric'},
|
{label: 'Market IntelligenceMetric', title: 'dip_risk_metric'},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -156,7 +156,7 @@ const AlertsView = () => {
|
|||||||
|
|
||||||
|
|
||||||
<div className={'mb-4'}>
|
<div className={'mb-4'}>
|
||||||
<p className={'block font-bold mb-2'}>DipRiskMetric</p>
|
<p className={'block font-bold mb-2'}>Market IntelligenceMetric</p>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import { hasPermission } from "../helpers/userPermissions";
|
|||||||
import { fetchWidgets } from '../stores/roles/rolesSlice';
|
import { fetchWidgets } from '../stores/roles/rolesSlice';
|
||||||
import { WidgetCreator } from '../components/WidgetCreator/WidgetCreator';
|
import { WidgetCreator } from '../components/WidgetCreator/WidgetCreator';
|
||||||
import { SmartWidget } from '../components/SmartWidget/SmartWidget';
|
import { SmartWidget } from '../components/SmartWidget/SmartWidget';
|
||||||
|
import Market IntelligenceWidget from '../components/Market IntelligenceWidget';
|
||||||
|
|
||||||
import { useAppDispatch, useAppSelector } from '../stores/hooks';
|
import { useAppDispatch, useAppSelector } from '../stores/hooks';
|
||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
@ -113,6 +114,10 @@ const Dashboard = () => {
|
|||||||
{''}
|
{''}
|
||||||
</SectionTitleLineWithButton>
|
</SectionTitleLineWithButton>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3 mb-6">
|
||||||
|
<Market IntelligenceWidget />
|
||||||
|
</div>
|
||||||
|
|
||||||
{hasPermission(currentUser, 'CREATE_ROLES') && <WidgetCreator
|
{hasPermission(currentUser, 'CREATE_ROLES') && <WidgetCreator
|
||||||
currentUser={currentUser}
|
currentUser={currentUser}
|
||||||
isFetchingQuery={isFetchingQuery}
|
isFetchingQuery={isFetchingQuery}
|
||||||
|
|||||||
@ -524,7 +524,7 @@ const Dip_risk_metricsView = () => {
|
|||||||
|
|
||||||
|
|
||||||
<>
|
<>
|
||||||
<p className={'block font-bold mb-2'}>Alerts DipRiskMetric</p>
|
<p className={'block font-bold mb-2'}>Alerts Market IntelligenceMetric</p>
|
||||||
<CardBox
|
<CardBox
|
||||||
className='mb-6 border border-gray-300 rounded overflow-hidden'
|
className='mb-6 border border-gray-300 rounded overflow-hidden'
|
||||||
hasTable
|
hasTable
|
||||||
|
|||||||
@ -7,12 +7,13 @@ import BaseButton from '../components/BaseButton';
|
|||||||
import CardBox from '../components/CardBox';
|
import CardBox from '../components/CardBox';
|
||||||
import SectionFullScreen from '../components/SectionFullScreen';
|
import SectionFullScreen from '../components/SectionFullScreen';
|
||||||
import LayoutGuest from '../layouts/Guest';
|
import LayoutGuest from '../layouts/Guest';
|
||||||
import BaseDivider from '../components/BaseDivider';
|
|
||||||
import BaseButtons from '../components/BaseButtons';
|
import BaseButtons from '../components/BaseButtons';
|
||||||
import { getPageTitle } from '../config';
|
import { getPageTitle } from '../config';
|
||||||
import { useAppSelector } from '../stores/hooks';
|
import { useAppSelector } from '../stores/hooks';
|
||||||
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
|
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
|
||||||
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
|
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
|
||||||
|
import * as icon from '@mdi/js';
|
||||||
|
import BaseIcon from "../components/BaseIcon";
|
||||||
|
|
||||||
|
|
||||||
export default function Starter() {
|
export default function Starter() {
|
||||||
@ -26,7 +27,7 @@ export default function Starter() {
|
|||||||
const [contentPosition, setContentPosition] = useState('right');
|
const [contentPosition, setContentPosition] = useState('right');
|
||||||
const textColor = useAppSelector((state) => state.style.linkColor);
|
const textColor = useAppSelector((state) => state.style.linkColor);
|
||||||
|
|
||||||
const title = 'DipRisk Lab'
|
const title = 'Market Intelligence'
|
||||||
|
|
||||||
// Fetch Pexels image/video
|
// Fetch Pexels image/video
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -41,21 +42,28 @@ export default function Starter() {
|
|||||||
|
|
||||||
const imageBlock = (image) => (
|
const imageBlock = (image) => (
|
||||||
<div
|
<div
|
||||||
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'
|
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/2'
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `${
|
backgroundImage: `${
|
||||||
image
|
image
|
||||||
? `url(${image?.src?.original})`
|
? `url(${image?.src?.original})`
|
||||||
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
|
: 'linear-gradient(rgba(15, 23, 42, 0.8), rgba(15, 23, 42, 0.8))'
|
||||||
}`,
|
}`,
|
||||||
backgroundSize: 'cover',
|
backgroundSize: 'cover',
|
||||||
backgroundPosition: 'left center',
|
backgroundPosition: 'center center',
|
||||||
backgroundRepeat: 'no-repeat',
|
backgroundRepeat: 'no-repeat',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className='flex justify-center w-full bg-blue-300/20'>
|
<div className='absolute inset-0 bg-slate-900/40 backdrop-blur-sm'></div>
|
||||||
|
<div className='relative z-10 p-12 text-white'>
|
||||||
|
<h2 className='text-4xl font-bold mb-4'>Hybrid Market Intelligence</h2>
|
||||||
|
<p className='text-lg opacity-90'>
|
||||||
|
Powered by Stochastic Modeling, GARCH Volatility, and Real-time Sentiment Analysis.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-center w-full bg-slate-900/60 relative z-10'>
|
||||||
<a
|
<a
|
||||||
className='text-[8px]'
|
className='text-[8px] text-white/50'
|
||||||
href={image?.photographer_url}
|
href={image?.photographer_url}
|
||||||
target='_blank'
|
target='_blank'
|
||||||
rel='noreferrer'
|
rel='noreferrer'
|
||||||
@ -69,7 +77,7 @@ export default function Starter() {
|
|||||||
const videoBlock = (video) => {
|
const videoBlock = (video) => {
|
||||||
if (video?.video_files?.length > 0) {
|
if (video?.video_files?.length > 0) {
|
||||||
return (
|
return (
|
||||||
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
|
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/2'>
|
||||||
<video
|
<video
|
||||||
className='absolute top-0 left-0 w-full h-full object-cover'
|
className='absolute top-0 left-0 w-full h-full object-cover'
|
||||||
autoPlay
|
autoPlay
|
||||||
@ -79,9 +87,16 @@ export default function Starter() {
|
|||||||
<source src={video?.video_files[0]?.link} type='video/mp4'/>
|
<source src={video?.video_files[0]?.link} type='video/mp4'/>
|
||||||
Your browser does not support the video tag.
|
Your browser does not support the video tag.
|
||||||
</video>
|
</video>
|
||||||
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
|
<div className='absolute inset-0 bg-slate-900/40 backdrop-blur-[2px]'></div>
|
||||||
|
<div className='relative z-10 p-12 text-white'>
|
||||||
|
<h2 className='text-4xl font-bold mb-4'>Quant-Grade Risk Metrics</h2>
|
||||||
|
<p className='text-lg opacity-90'>
|
||||||
|
Simulating thousands of intraday paths to identify your "Worst Case" before it happens.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-center w-full bg-slate-900/60 relative z-10'>
|
||||||
<a
|
<a
|
||||||
className='text-[8px]'
|
className='text-[8px] text-white/50'
|
||||||
href={video?.user?.url}
|
href={video?.user?.url}
|
||||||
target='_blank'
|
target='_blank'
|
||||||
rel='noreferrer'
|
rel='noreferrer'
|
||||||
@ -94,27 +109,12 @@ export default function Starter() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="bg-slate-900 min-h-screen text-slate-100">
|
||||||
style={
|
|
||||||
contentPosition === 'background'
|
|
||||||
? {
|
|
||||||
backgroundImage: `${
|
|
||||||
illustrationImage
|
|
||||||
? `url(${illustrationImage.src?.original})`
|
|
||||||
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
|
|
||||||
}`,
|
|
||||||
backgroundSize: 'cover',
|
|
||||||
backgroundPosition: 'left center',
|
|
||||||
backgroundRepeat: 'no-repeat',
|
|
||||||
}
|
|
||||||
: {}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Head>
|
<Head>
|
||||||
<title>{getPageTitle('Starter Page')}</title>
|
<title>{getPageTitle('Market Intelligence - Next-Gen Market Intelligence')}</title>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<SectionFullScreen bg='violet'>
|
<SectionFullScreen bg='none'>
|
||||||
<div
|
<div
|
||||||
className={`flex ${
|
className={`flex ${
|
||||||
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
|
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
|
||||||
@ -126,34 +126,88 @@ export default function Starter() {
|
|||||||
{contentType === 'video' && contentPosition !== 'background'
|
{contentType === 'video' && contentPosition !== 'background'
|
||||||
? videoBlock(illustrationVideo)
|
? videoBlock(illustrationVideo)
|
||||||
: null}
|
: null}
|
||||||
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
|
<div className='flex items-center justify-center flex-col space-y-8 w-full lg:w-1/2 p-8'>
|
||||||
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
|
<div className="text-center space-y-4 max-w-md">
|
||||||
<CardBoxComponentTitle title="Welcome to your DipRisk Lab app!"/>
|
<div className="inline-flex p-3 rounded-2xl bg-emerald-500/10 text-emerald-400 mb-4">
|
||||||
|
<BaseIcon path={icon.mdiFinance} size={48} />
|
||||||
<div className="space-y-3">
|
</div>
|
||||||
<p className='text-center '>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p>
|
<h1 className="text-5xl font-extrabold tracking-tight text-white">
|
||||||
<p className='text-center '>For guides and documentation please check
|
Market Intelligence
|
||||||
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
|
</h1>
|
||||||
|
<p className="text-slate-400 text-lg">
|
||||||
|
Institutional-grade risk estimation for the modern trader. Turn noise into clarity with our hybrid stochastic models.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<BaseButtons>
|
<CardBox className='w-full max-w-md bg-slate-800/50 border-slate-700 backdrop-blur-md'>
|
||||||
<BaseButton
|
<div className="grid grid-cols-2 gap-4 mb-8">
|
||||||
href='/login'
|
<div className="p-4 rounded-xl bg-slate-900/50 border border-slate-700">
|
||||||
label='Login'
|
<div className="text-emerald-400 mb-2">
|
||||||
color='info'
|
<BaseIcon path={icon.mdiChartLine} size={24} />
|
||||||
className='w-full'
|
</div>
|
||||||
/>
|
<div className="text-xs text-slate-500 uppercase font-bold tracking-wider">Expected Min</div>
|
||||||
|
<div className="text-lg font-mono text-white">4,892.50</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 rounded-xl bg-slate-900/50 border border-slate-700">
|
||||||
|
<div className="text-rose-400 mb-2">
|
||||||
|
<BaseIcon path={icon.mdiAlertDecagram} size={24} />
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-slate-500 uppercase font-bold tracking-wider">Prob. Drop</div>
|
||||||
|
<div className="text-lg font-mono text-white">12.4%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</BaseButtons>
|
<BaseButtons>
|
||||||
</CardBox>
|
<BaseButton
|
||||||
</div>
|
href='/login'
|
||||||
|
label='Launch Dashboard'
|
||||||
|
color='info'
|
||||||
|
className='w-full py-4 text-lg font-bold'
|
||||||
|
/>
|
||||||
|
</BaseButtons>
|
||||||
|
|
||||||
|
<div className="mt-6 flex items-center justify-center space-x-4 text-sm text-slate-500">
|
||||||
|
<span className="flex items-center">
|
||||||
|
<BaseIcon path={icon.mdiCheckCircle} size={16} className="mr-1 text-emerald-500" />
|
||||||
|
Live ES=F Data
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center">
|
||||||
|
<BaseIcon path={icon.mdiCheckCircle} size={16} className="mr-1 text-emerald-500" />
|
||||||
|
AI Sentiment
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
|
||||||
|
<div className="flex space-x-8 opacity-50 grayscale hover:grayscale-0 transition-all">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<BaseIcon path={icon.mdiLightningBolt} size={24} />
|
||||||
|
<span className="font-bold">Real-time</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<BaseIcon path={icon.mdiRobot} size={24} />
|
||||||
|
<span className="font-bold">ML Driven</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SectionFullScreen>
|
</SectionFullScreen>
|
||||||
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
|
|
||||||
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
|
<div className='bg-slate-950 text-slate-500 border-t border-slate-800 py-12 px-8 flex flex-col md:flex-row items-center justify-between'>
|
||||||
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
|
<div className="flex items-center space-x-2 mb-4 md:mb-0">
|
||||||
Privacy Policy
|
<div className="w-8 h-8 rounded bg-emerald-500 flex items-center justify-center text-slate-900">
|
||||||
</Link>
|
<BaseIcon path={icon.mdiFinance} size={20} />
|
||||||
|
</div>
|
||||||
|
<span className="text-white font-bold tracking-tight">Market Intelligence</span>
|
||||||
|
</div>
|
||||||
|
<p className='text-sm'>© 2026 <span>{title}</span>. Precision Risk Architecture</p>
|
||||||
|
<div className="flex space-x-6 mt-4 md:mt-0">
|
||||||
|
<Link className='text-sm hover:text-white transition-colors' href='/privacy-policy/'>
|
||||||
|
Privacy Policy
|
||||||
|
</Link>
|
||||||
|
<Link className='text-sm hover:text-white transition-colors' href='/terms-of-use/'>
|
||||||
|
Terms of Service
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -44,7 +44,7 @@ export default function Login() {
|
|||||||
password: 'd0ca8ebc',
|
password: 'd0ca8ebc',
|
||||||
remember: true })
|
remember: true })
|
||||||
|
|
||||||
const title = 'DipRisk Lab'
|
const title = 'Market Intelligence'
|
||||||
|
|
||||||
// Fetch Pexels image/video
|
// Fetch Pexels image/video
|
||||||
useEffect( () => {
|
useEffect( () => {
|
||||||
|
|||||||
25
frontend/src/pages/market-intelligence/index.tsx
Normal file
25
frontend/src/pages/market-intelligence/index.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import Head from 'next/head'
|
||||||
|
import React, { ReactElement } from 'react'
|
||||||
|
import LayoutAuthenticated from '../../layouts/Authenticated'
|
||||||
|
import SectionMain from '../../components/SectionMain'
|
||||||
|
import { getPageTitle } from '../../config'
|
||||||
|
import MarketIntelligenceDashboard from '../../components/MarketIntelligence/MarketIntelligenceDashboard'
|
||||||
|
|
||||||
|
const MarketIntelligencePage = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>{getPageTitle('Market Intelligence Dashboard')}</title>
|
||||||
|
</Head>
|
||||||
|
<SectionMain>
|
||||||
|
<MarketIntelligenceDashboard />
|
||||||
|
</SectionMain>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
MarketIntelligencePage.getLayout = function getLayout(page: ReactElement) {
|
||||||
|
return <LayoutAuthenticated>{page}</LayoutAuthenticated>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MarketIntelligencePage
|
||||||
@ -5,7 +5,7 @@ import LayoutGuest from '../layouts/Guest';
|
|||||||
import { getPageTitle } from '../config';
|
import { getPageTitle } from '../config';
|
||||||
|
|
||||||
export default function PrivacyPolicy() {
|
export default function PrivacyPolicy() {
|
||||||
const title = 'DipRisk Lab'
|
const title = 'Market Intelligence'
|
||||||
const [projectUrl, setProjectUrl] = useState('');
|
const [projectUrl, setProjectUrl] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import LayoutGuest from '../layouts/Guest';
|
|||||||
import { getPageTitle } from '../config';
|
import { getPageTitle } from '../config';
|
||||||
|
|
||||||
export default function PrivacyPolicy() {
|
export default function PrivacyPolicy() {
|
||||||
const title = 'DipRisk Lab';
|
const title = 'Market Intelligence';
|
||||||
const [projectUrl, setProjectUrl] = useState('');
|
const [projectUrl, setProjectUrl] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
48
frontend/src/stores/marketIntelligenceSlice.ts
Normal file
48
frontend/src/stores/marketIntelligenceSlice.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
interface MarketIntelligenceState {
|
||||||
|
dashboardData: any
|
||||||
|
loading: boolean
|
||||||
|
error: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: MarketIntelligenceState = {
|
||||||
|
dashboardData: null,
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetchDashboardData = createAsyncThunk(
|
||||||
|
'marketIntelligence/fetchDashboardData',
|
||||||
|
async (symbol: string, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
const result = await axios.get(`market_intelligence/dashboard?symbol=${symbol}`)
|
||||||
|
return result.data
|
||||||
|
} catch (error: any) {
|
||||||
|
return rejectWithValue(error.response?.data?.message || 'Failed to fetch dashboard data')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const marketIntelligenceSlice = createSlice({
|
||||||
|
name: 'marketIntelligence',
|
||||||
|
initialState,
|
||||||
|
reducers: {},
|
||||||
|
extraReducers: (builder) => {
|
||||||
|
builder.addCase(fetchDashboardData.pending, (state) => {
|
||||||
|
state.loading = true
|
||||||
|
state.error = null
|
||||||
|
})
|
||||||
|
builder.addCase(fetchDashboardData.fulfilled, (state, action) => {
|
||||||
|
state.dashboardData = action.payload
|
||||||
|
state.loading = false
|
||||||
|
})
|
||||||
|
builder.addCase(fetchDashboardData.rejected, (state, action) => {
|
||||||
|
state.loading = false
|
||||||
|
state.error = action.payload as string
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default marketIntelligenceSlice.reducer
|
||||||
@ -3,6 +3,7 @@ import styleReducer from './styleSlice';
|
|||||||
import mainReducer from './mainSlice';
|
import mainReducer from './mainSlice';
|
||||||
import authSlice from './authSlice';
|
import authSlice from './authSlice';
|
||||||
import openAiSlice from './openAiSlice';
|
import openAiSlice from './openAiSlice';
|
||||||
|
import marketIntelligenceReducer from './marketIntelligenceSlice';
|
||||||
|
|
||||||
import usersSlice from "./users/usersSlice";
|
import usersSlice from "./users/usersSlice";
|
||||||
import rolesSlice from "./roles/rolesSlice";
|
import rolesSlice from "./roles/rolesSlice";
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user