diff --git a/.gitignore b/.gitignore index a67f2602..f53862fa 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,8 @@ /public /dist /.webpack +/open-fin-al/resources/neo4j-win/ +/open-fin-al/resources/jre-win/ # misc .DS_Store diff --git a/open-fin-al/package.json b/open-fin-al/package.json index ae362802..6bbd4062 100644 --- a/open-fin-al/package.json +++ b/open-fin-al/package.json @@ -45,6 +45,7 @@ "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", "@types/jest": "^30.0.0", + "@types/react": "^19.2.7", "@vercel/webpack-asset-relocator-loader": "^1.7.3", "babel-loader": "^10.0.0", "browserify-fs": "^1.0.0", @@ -72,6 +73,7 @@ "cors": "^2.8.5", "crypto": "^1.0.1", "crypto-browserify": "^3.12.1", + "danfojs": "^1.2.0", "electron-squirrel-startup": "^1.0.1", "events": "^3.3.0", "express": "^5.1.0", @@ -80,6 +82,7 @@ "http": "^0.0.1-security", "https": "^1.0.0", "keytar": "^7.9.0", + "neo4j-driver": "^6.0.1", "node-html-parser": "^7.0.1", "os-browserify": "^0.3.0", "pptx-preview": "^1.0.7", diff --git a/open-fin-al/resources/README.md b/open-fin-al/resources/README.md new file mode 100644 index 00000000..9ae32618 --- /dev/null +++ b/open-fin-al/resources/README.md @@ -0,0 +1,3 @@ +## Purpose of this Folder +The resources folder should contain the following folders: +neo4j-win - this folder should include binaries for neo4j community for Windows \ No newline at end of file diff --git a/open-fin-al/resources/slideshows/IntroductionToInvesting.pptx b/open-fin-al/resources/slideshows/IntroductionToInvesting.pptx new file mode 100644 index 00000000..9ab38894 Binary files /dev/null and b/open-fin-al/resources/slideshows/IntroductionToInvesting.pptx differ diff --git a/open-fin-al/src/Asset/Image/thumbnail_unavailable.png b/open-fin-al/src/Asset/Image/thumbnail_unavailable.png new file mode 100644 index 00000000..09da10bb Binary files /dev/null and b/open-fin-al/src/Asset/Image/thumbnail_unavailable.png differ diff --git a/open-fin-al/src/Gateway/Data/EconomicGateway/AlphaVantageEconomicGateway.ts b/open-fin-al/src/Gateway/Data/EconomicGateway/AlphaVantageEconomicGateway.ts index a951711a..c060c05d 100644 --- a/open-fin-al/src/Gateway/Data/EconomicGateway/AlphaVantageEconomicGateway.ts +++ b/open-fin-al/src/Gateway/Data/EconomicGateway/AlphaVantageEconomicGateway.ts @@ -27,7 +27,10 @@ export class AlphaVantageEconomicGateway implements IKeyedDataGateway { throw new Error("This gateway does not have the ability to post content"); } - async read(entity: IEntity, action: string): Promise> { + async read(entity: IEntity, action: string): Promise> { + const sleep = (ms:number) => new Promise(resolve => setTimeout(resolve, ms)); + await sleep(1001); + var url = `${this.baseURL}?`; if(action==="getGDP") { diff --git a/open-fin-al/src/Gateway/Data/ICredentialedDataGateway.ts b/open-fin-al/src/Gateway/Data/ICredentialedDataGateway.ts new file mode 100644 index 00000000..be5bbe73 --- /dev/null +++ b/open-fin-al/src/Gateway/Data/ICredentialedDataGateway.ts @@ -0,0 +1,14 @@ +import {IEntity} from "../../Entity/IEntity"; +import { IDataGateway } from "./IDataGateway"; + +export interface ICredentialedDataGateway extends IDataGateway { + user: string; + key: string; + sourceName: string; + connect(): void; + disconnect(): void; + create(entity: IEntity, action?: string): Promise; + read(entity: IEntity, action?: string): Promise>; + update(entity: IEntity, action?: string): Promise; + delete(entity: IEntity, action?: string): Promise; +} \ No newline at end of file diff --git a/open-fin-al/src/Gateway/Data/IDataGateway.ts b/open-fin-al/src/Gateway/Data/IDataGateway.ts index 8617fdd5..a74f2b89 100644 --- a/open-fin-al/src/Gateway/Data/IDataGateway.ts +++ b/open-fin-al/src/Gateway/Data/IDataGateway.ts @@ -1,6 +1,7 @@ import {IEntity} from "../../Entity/IEntity"; export interface IDataGateway { + user?: string; key?: string; sourceName: string; connect(): void; diff --git a/open-fin-al/src/Gateway/Data/IKeylessDataGateway.ts b/open-fin-al/src/Gateway/Data/IKeylessDataGateway.ts index eb82485c..9705bbb3 100644 --- a/open-fin-al/src/Gateway/Data/IKeylessDataGateway.ts +++ b/open-fin-al/src/Gateway/Data/IKeylessDataGateway.ts @@ -3,8 +3,8 @@ import { IDataGateway } from "./IDataGateway"; export interface IKeylessDataGateway extends IDataGateway { sourceName: string; - connect(): void; - disconnect(): void; + connect(): void | Promise | Promise; + disconnect(): void | Promise | Promise; create(entity: IEntity, action?: string): Promise; read(entity: IEntity, action?: string): Promise>; update(entity: IEntity, action?: string): Promise; diff --git a/open-fin-al/src/Gateway/Data/INeo4JGraphGateway.ts b/open-fin-al/src/Gateway/Data/INeo4JGraphGateway.ts new file mode 100644 index 00000000..6685bf75 --- /dev/null +++ b/open-fin-al/src/Gateway/Data/INeo4JGraphGateway.ts @@ -0,0 +1,17 @@ +import {IEntity} from "../../Entity/IEntity"; +import { IDataGateway } from "./IDataGateway"; + +export interface INeo4JGraphGateway extends IDataGateway { + user: string; + key: string; + sourceName: string; + connect(): Promise; + disconnect(): Promise; + create(entity: IEntity, action?: string): Promise; + read(entity: IEntity, action?: string): Promise>; + update(entity: IEntity, action?: string): Promise; + delete(entity: IEntity, action?: string): Promise; + checkGraphConnected(): Promise; + checkGraphExists(): Promise; + checkLastGraphUpdate() : Promise; +} \ No newline at end of file diff --git a/open-fin-al/src/Gateway/Data/MarketGateway/AlphaVantagMarketGateway.ts b/open-fin-al/src/Gateway/Data/MarketGateway/AlphaVantagMarketGateway.ts index 27f683cd..1986424b 100644 --- a/open-fin-al/src/Gateway/Data/MarketGateway/AlphaVantagMarketGateway.ts +++ b/open-fin-al/src/Gateway/Data/MarketGateway/AlphaVantagMarketGateway.ts @@ -29,6 +29,9 @@ export class AlphaVantageMarketGateway implements IKeyedDataGateway { } async read(entity: IEntity, action: string): Promise> { + const sleep = (ms:number) => new Promise(resolve => setTimeout(resolve, ms)); + await sleep(1001); + var url = `${this.baseURL}?function=MARKET_STATUS&apikey=${entity.getFieldValue("key")}`; const urlObject = new URL(url); diff --git a/open-fin-al/src/Gateway/Data/Neo4J/Neo4JGraphCreationGateway.ts b/open-fin-al/src/Gateway/Data/Neo4J/Neo4JGraphCreationGateway.ts new file mode 100644 index 00000000..f838d6e4 --- /dev/null +++ b/open-fin-al/src/Gateway/Data/Neo4J/Neo4JGraphCreationGateway.ts @@ -0,0 +1,211 @@ +import neo4j, {Record, Node} from "neo4j-driver"; +import { INeo4JGraphGateway } from "../INeo4JGraphGateway"; +import { IEntity } from "@Entity/IEntity"; + +declare global { + interface Window { + neo4j: any + } +} + +export class Neo4JGraphCreationGateway implements INeo4JGraphGateway { + user: string = ""; + key: string = ""; + connection: any = null; + sourceName: string = "Neo4j Local Database"; + + async connect(): Promise { + try { + return await window.neo4j.start(); + } catch (error) { + console.error("Error connecting to Neo4j server:", error); + return false; + } + } + + async disconnect(): Promise { + try { + return await window.neo4j.stop(); + } catch (error) { + console.error("Error disconnecting from Neo4j server:", error); + return false; + } + } + + // used to create and periodically refresh the cache + async create(): Promise { + const schema: string[] = [ + `CREATE CONSTRAINT user_userId_unique IF NOT EXISTS + FOR (u:User) + REQUIRE u.userId IS UNIQUE`, + + `CREATE CONSTRAINT module_moduleId_unique IF NOT EXISTS + FOR (m:Module) + REQUIRE m.moduleId IS UNIQUE`, + + `CREATE CONSTRAINT concept_conceptId_unique IF NOT EXISTS + FOR (c:Concept) + REQUIRE c.conceptId IS UNIQUE`, + + `CREATE INDEX module_minLevel IF NOT EXISTS + FOR (m:Module) ON (m.minLevel)`, + + `CREATE INDEX module_riskTag IF NOT EXISTS + FOR (m:Module) ON (m.riskTag)`, + ]; + + const merges: string[] = [ + `MERGE (u1:User { userId: "u_alice" }) + SET u1.overallLevel = 2, + u1.riskScore = 2`, + + `MERGE (u2:User { userId: "u_bob" }) + SET u2.overallLevel = 1, + u2.riskScore = 1`, + + `MERGE (c_etf:Concept { conceptId: "c_etf", name: "ETFs" })`, + `MERGE (c_div:Concept { conceptId: "c_div", name: "Dividends" })`, + `MERGE (c_opt:Concept { conceptId: "c_opt", name: "Options" })`, + `MERGE (c_cc:Concept { conceptId: "c_cc", name: "Covered Calls" })`, + `MERGE (c_risk:Concept{ conceptId: "c_risk",name: "Risk Management" })`, + + `MERGE (m1:Module { moduleId: "m_etf_basics" }) + SET m1.title = "ETF Basics", + m1.description = "Introduction to exchange-traded funds", + m1.timeEstimate = 45, + m1.minLevel = 1, + m1.riskTag = 1`, + + `MERGE (m2:Module { moduleId: "m_div_income" }) + SET m2.title = "Dividend Investing", + m2.description = "Building income with dividends", + m2.timeEstimate = 60, + m2.minLevel = 2, + m2.riskTag = 1`, + + `MERGE (m3:Module { moduleId: "m_options_101" }) + SET m3.title = "Options 101", + m3.description = "Foundations of options trading", + m3.timeEstimate = 90, + m3.minLevel = 3, + m3.riskTag = 3`, + + `MERGE (m4:Module { moduleId: "m_covered_calls" }) + SET m4.title = "Covered Call Strategies", + m4.description = "Generating income with covered calls", + m4.timeEstimate = 75, + m4.minLevel = 3, + m4.riskTag = 2`, + + `MATCH (m1:Module {moduleId:"m_etf_basics"}), + (c_etf:Concept {conceptId:"c_etf"}) + MERGE (m1)-[:TEACHES {weight: 1.0}]->(c_etf)`, + + `MATCH (m2:Module {moduleId:"m_div_income"}), + (c_div:Concept {conceptId:"c_div"}), + (c_etf:Concept {conceptId:"c_etf"}) + MERGE (m2)-[:TEACHES {weight: 0.7}]->(c_div) + MERGE (m2)-[:TEACHES {weight: 0.3}]->(c_etf)`, + + `MATCH (m3:Module {moduleId:"m_options_101"}), + (c_opt:Concept {conceptId:"c_opt"}), + (c_risk:Concept {conceptId:"c_risk"}) + MERGE (m3)-[:TEACHES {weight: 0.7}]->(c_opt) + MERGE (m3)-[:TEACHES {weight: 0.3}]->(c_risk)`, + + `MATCH (m4:Module {moduleId:"m_covered_calls"}), + (c_cc:Concept {conceptId:"c_cc"}), + (c_opt:Concept {conceptId:"c_opt"}) + MERGE (m4)-[:TEACHES {weight: 0.6}]->(c_cc) + MERGE (m4)-[:TEACHES {weight: 0.4}]->(c_opt)`, + + `MATCH (m2:Module {moduleId:"m_div_income"}), + (m1:Module {moduleId:"m_etf_basics"}) + MERGE (m2)-[:REQUIRES {strict:true}]->(m1)`, + + `MATCH (m3:Module {moduleId:"m_options_101"}), + (m1:Module {moduleId:"m_etf_basics"}) + MERGE (m3)-[:REQUIRES {strict:true}]->(m1)`, + + `MATCH (m4:Module {moduleId:"m_covered_calls"}), + (m3:Module {moduleId:"m_options_101"}) + MERGE (m4)-[:REQUIRES {strict:true}]->(m3)`, + + `MATCH (u:User {userId:"u_alice"}), + (c_etf:Concept {conceptId:"c_etf"}), + (c_div:Concept {conceptId:"c_div"}) + MERGE (u)-[:KNOWS {level:2, confidence:0.8, updatedAt:datetime()}]->(c_etf) + MERGE (u)-[:KNOWS {level:1, confidence:0.6, updatedAt:datetime()}]->(c_div)`, + + `MATCH (u:User {userId:"u_bob"}), + (c_etf:Concept {conceptId:"c_etf"}) + MERGE (u)-[:KNOWS {level:1, confidence:0.5, updatedAt:datetime()}]->(c_etf)`, + + `MATCH (u:User {userId:"u_alice"}), + (m1:Module {moduleId:"m_etf_basics"}) + MERGE (u)-[:COMPLETED {at:datetime()}]->(m1)`, + + `MATCH (u:User {userId:"u_bob"}), + (m1:Module {moduleId:"m_etf_basics"}) + MERGE (u)-[:COMPLETED {at:datetime()}]->(m1)`, + ]; + + try { + await window.neo4j.executeQuery("write", schema); + await window.neo4j.executeQuery("write", merges); + return true; + } catch(error) { + window.console.error("Error creating graph schema/data in Neo4j server:", error); + return false; + } + } + + async read(): Promise { + throw new Error("This gatweay does not allow for reading. This gateway is designed for posting only."); + } + + update(entity: IEntity, action: string): Promise { + throw new Error("This gateway does not have the ability to update content. Updates are handled by periodically deleting and re-entering data"); + } + + async delete(entity: IEntity, action: string): Promise { + throw new Error("This gatweay does not allow deleting data. This gateway is designed for posting only."); + } + + //check to see if the database exists + async checkGraphExists(): Promise { + try { + const query = + `MATCH (n) + RETURN count(n) AS count` + ; + + const result = await window.neo4j.executeQuery("read", query); + window.console.log(result); + const count = result.records[0].count; + window.console.log(count); + if(count > 0) { + return true; + } else { + return false; + } + } catch(error) { + window.console.log(error); + return false; + } + } + + async checkGraphConnected():Promise { + try { + const isConnected = await window.neo4j.isConnected(); + return isConnected; + } catch(error) { + return false; + } + } + + //for database tables that act as cache, check for the last time a table was updated + async checkLastGraphUpdate():Promise { + throw new Error("This gatweay does not allow for checking tables. This gateway is designed for posting only."); + } +} \ No newline at end of file diff --git a/open-fin-al/src/Gateway/Data/Neo4J/Neo4JGraphGateway.ts b/open-fin-al/src/Gateway/Data/Neo4J/Neo4JGraphGateway.ts new file mode 100644 index 00000000..597a45fe --- /dev/null +++ b/open-fin-al/src/Gateway/Data/Neo4J/Neo4JGraphGateway.ts @@ -0,0 +1,66 @@ +import neo4j, {Record, Node} from "neo4j-driver"; +import {IEntity} from "../../../Entity/IEntity"; +import {ICredentialedDataGateway} from "../ICredentialedDataGateway"; + +declare global { + interface Window { + neo4j: any + } +} + +export class Neo4JGraphGateway implements ICredentialedDataGateway { + user: string = ""; + key: string = ""; + sourceName: string = "Neo4j Local Database"; + + async connect(): Promise { + try { + return await window.neo4j.start(); + } catch (error) { + console.error("Error connecting to Neo4j server:", error); + return false; + } + } + + async disconnect(): Promise { + try { + return await window.neo4j.stop(); + } catch (error) { + console.error("Error disconnecting from Neo4j server:", error); + return false; + } + } + + create(entity: IEntity, action: string): Promise { + //at the moment, users will not be permitted to create new graphs + throw new Error("Method not implemented."); + } + + async read(entity: IEntity, action: string): Promise { + const id:any = null; + try { + const query = + ` + MATCH (u:User { id: $id }) + RETURN u + ` + ; + + const results = await window.neo4j.executeQuery(query, {id: id}); + return results; + } catch (error) { + window.console.error("Error reading from Neo4j server:", error); + return null; + } + } + + update(entity: IEntity, action: string): Promise { + //at the moment, users will not be permitted to update graphs; + throw new Error("Method not implemented."); + } + + delete(entity: IEntity, action: string): Promise { + //at the moment, users will not be permitted to delete graphs + throw new Error("Method not implemented."); + } +} \ No newline at end of file diff --git a/open-fin-al/src/Gateway/Data/NewsGateway/AlphaVantageNewsGateway.ts b/open-fin-al/src/Gateway/Data/NewsGateway/AlphaVantageNewsGateway.ts index 128bf8ad..bae85171 100644 --- a/open-fin-al/src/Gateway/Data/NewsGateway/AlphaVantageNewsGateway.ts +++ b/open-fin-al/src/Gateway/Data/NewsGateway/AlphaVantageNewsGateway.ts @@ -28,6 +28,9 @@ export class AlphaVantageNewsGateway implements IKeyedDataGateway { } async read(entity: IEntity, action: string): Promise> { + const sleep = (ms:number) => new Promise(resolve => setTimeout(resolve, ms)); + await sleep(1001); + var url = `${this.baseURL}?function=NEWS_SENTIMENT&apikey=${entity.getFieldValue("key")}`; if(entity.getFieldValue("ticker") !== null) { diff --git a/open-fin-al/src/Gateway/Data/RatioGateway/AlphaVantageRatioGateway.ts b/open-fin-al/src/Gateway/Data/RatioGateway/AlphaVantageRatioGateway.ts index 800c98c8..6c594983 100644 --- a/open-fin-al/src/Gateway/Data/RatioGateway/AlphaVantageRatioGateway.ts +++ b/open-fin-al/src/Gateway/Data/RatioGateway/AlphaVantageRatioGateway.ts @@ -27,6 +27,9 @@ export class AlphaVantageRatioGateway implements IDataGateway { } async read(entity: IEntity, action: string): Promise> { + const sleep = (ms:number) => new Promise(resolve => setTimeout(resolve, ms)); + await sleep(1001); + var url; if (action === "overview"){ url = this.getOverviewUrl(entity); diff --git a/open-fin-al/src/Gateway/Data/StockGateway/AlphaVantageStockGateway.ts b/open-fin-al/src/Gateway/Data/StockGateway/AlphaVantageStockGateway.ts index 9913bd4a..03773f75 100644 --- a/open-fin-al/src/Gateway/Data/StockGateway/AlphaVantageStockGateway.ts +++ b/open-fin-al/src/Gateway/Data/StockGateway/AlphaVantageStockGateway.ts @@ -28,6 +28,9 @@ export class AlphaVantageStockGateway implements IKeyedDataGateway { } async read(entity: IEntity, action: string): Promise> { + const sleep = (ms:number) => new Promise(resolve => setTimeout(resolve, ms)); + await sleep(1001); + try { var url; if (action === "lookup") { @@ -43,7 +46,7 @@ export class AlphaVantageStockGateway implements IKeyedDataGateway { } const urlObject = new URL(url); - + window.console.log(url); var endpointRequest = new JSONRequest(JSON.stringify({ request: { endpoint: { @@ -62,7 +65,8 @@ export class AlphaVantageStockGateway implements IKeyedDataGateway { endpoint.fillWithRequest(endpointRequest); const data = await window.exApi.fetch(this.baseURL, endpoint.toObject()); - + window.console.log(data); + if("Information" in data) { throw Error("The API key used for Alpha Vantage has reached its daily limit"); } diff --git a/open-fin-al/src/Interactor/InitializationInteractor.ts b/open-fin-al/src/Interactor/InitializationInteractor.ts index b13ad7f8..fbde7d03 100644 --- a/open-fin-al/src/Interactor/InitializationInteractor.ts +++ b/open-fin-al/src/Interactor/InitializationInteractor.ts @@ -12,6 +12,10 @@ import { SQLiteCompanyLookupGateway } from "../Gateway/Data/SQLite/SQLiteCompany import { SettingsInteractor } from "./SettingsInteractor"; import { SQLiteAssetGateway } from "../Gateway/Data/SQLite/SQLiteAssetGateway"; +declare global { + interface Window { slideshow: any; } +} + export class InitializationInteractor implements IInputBoundary { requestModel: IRequestModel; responseModel: IResponseModel; @@ -22,7 +26,21 @@ export class InitializationInteractor implements IInputBoundary { const configManager = new ConfigUpdater(); var configCreated; - if(action === "createConfig") { + if(action === "bundelSlideshows") { + try { + window.slideshow.verifyBundle(); + response = new JSONResponse(JSON.stringify({status: 200, ok: true})); + return response; + } catch(error) { + response = new JSONResponse(JSON.stringify({ + status: 500, + data: { + error: `An unknown erorr occurred while trying to bundel the slideshows.}` + }})); + + return response; + } + } else if(action === "createConfig") { try { //create the SQLite database const gateway = new SQLiteTableCreationGateway(); @@ -37,7 +55,7 @@ export class InitializationInteractor implements IInputBoundary { response = new JSONResponse(JSON.stringify({ status: 400, data: { - error: `The application configuration failed. Configurations created: ${configCreated}. Data tables created: ${tablesCreated}.}` + error: `The application configuration failed. Configurations created: ${configCreated}. Data tables created: ${tablesCreated}.` }})); } @@ -75,43 +93,6 @@ export class InitializationInteractor implements IInputBoundary { }})); return response; } - - //load the user table with the OS username - /* don't want to create user automatically during data initialization - TODO: remove user from settings area now that a seaparate user area exists - try { - const username = await window.config.getUsername(); - var firstName; - var lastName; - var email = window.vault.getSecret("Email") || null; - - if(window.config.exists()) { - const config = await window.config.load(); - firstName = config.UserSettings.FirstName; - lastName = config.UserSettings.LastName; - } - - var userInteractor = new UserInteractor(); - var userRequestObj = new JSONRequest(JSON.stringify({ - request: { - user: { - username: username, - firstName: firstName, - lastName: lastName, - email: email - } - } - })); - userResponse = await userInteractor.post(userRequestObj); - } catch(error) { - response = new JSONResponse(JSON.stringify({ - status: 500, - data: { - error: `An unkown error occured while created the application user.` - }})); - return response; - } - */ //return the response based on the outcomes of initialization // removed userReponse if(publicCompaniesResponse.response.ok && userResponse.response.ok) { diff --git a/open-fin-al/src/Interactor/SidecarInitializationInteractor.ts b/open-fin-al/src/Interactor/SidecarInitializationInteractor.ts new file mode 100644 index 00000000..bd864012 --- /dev/null +++ b/open-fin-al/src/Interactor/SidecarInitializationInteractor.ts @@ -0,0 +1,112 @@ +import {IInputBoundary} from "./IInputBoundary"; +import {IRequestModel} from "../Gateway/Request/IRequestModel"; +import {IResponseModel} from "../Gateway/Response/IResponseModel"; +import {JSONResponse} from "../Gateway/Response/JSONResponse"; +import { Neo4JGraphCreationGateway } from "../Gateway/Data/Neo4J/Neo4JGraphCreationGateway"; +import { graph } from "neo4j-driver"; + +export class SidecarInitializationInteractor implements IInputBoundary { + requestModel: IRequestModel; + responseModel: IResponseModel; + + async post(requestModel: IRequestModel, action:string=null): Promise { + var response; + const graphGateway = new Neo4JGraphCreationGateway(); + + if(action === "loadSidecar") { + try { + await graphGateway.connect(); + response = new JSONResponse(JSON.stringify({status: 200, ok: true})); + return response; + } catch(error) { + response = new JSONResponse(JSON.stringify({ + status: 500, + data: { + error: `An unknown erorr occurred while setting up the sidecar configurations.}` + }})); + + return response; + } + } else if (action === "initializeGraph") { + //load the table with company data after database init + try { + const graphCreated = await graphGateway.create(); + + if(graphCreated) { + response = new JSONResponse(JSON.stringify({status: 200, ok: true})); + } else { + response = new JSONResponse(JSON.stringify({ + status: 400, + data: { + error: `The sidecar configuration failed. Graph created: ${graphCreated}}` + }})); + } + + return response; + } catch(error) { + response = new JSONResponse(JSON.stringify({ + status: 500, + data: { + error: `An unkown error occured while creating the graph database.` + }})); + + return response; + } + } + } + + async get(requestModel: IRequestModel, action:string=null): Promise { + var response; + const graphGateway = new Neo4JGraphCreationGateway(); + + try { + if(action==="isLoaded") { + //check if Graph database has been created + const isConnected = await graphGateway.checkGraphConnected(); + + if(!isConnected) { + response = new JSONResponse(JSON.stringify({ + status: 404, + data: { + error: `The graph is not connected.` + }})); + return response; + } + + //return success if other tests passed + response = new JSONResponse(JSON.stringify({status: 200, ok: true})); + return response; + } else if(action==="isGraphInitialized") { + const graphExists = await graphGateway.checkGraphExists(); + + if(!graphExists) { + response = new JSONResponse(JSON.stringify({ + status: 404, + data: { + error: `The graph doesn't exist.` + }})); + return response; + } + + //return success if other tests passed + response = new JSONResponse(JSON.stringify({status: 200, ok: true})); + return response; + } + } catch(error) { + response = new JSONResponse(JSON.stringify({ + status: 500, + data: { + error: `An unkonwn error occured while checking the system initialization.` + }})); + return response; + } + } + + async put(requestModel: IRequestModel): Promise { + return this.post(requestModel); + } + + async delete(requestModel: IRequestModel): Promise { + return this.post(requestModel); + } +} \ No newline at end of file diff --git a/open-fin-al/src/Interactor/StockInteractor.ts b/open-fin-al/src/Interactor/StockInteractor.ts index 53335067..daa267be 100644 --- a/open-fin-al/src/Interactor/StockInteractor.ts +++ b/open-fin-al/src/Interactor/StockInteractor.ts @@ -96,10 +96,10 @@ export class StockInteractor implements IInputBoundary { //add the API key to the stock request object stock.setFieldValue("key", stockGateway.key); } - + //search for the requested information via the API gateway var results = await stockGateway.read(stock, requestModel.request.request.stock.action); - + window.console.log(results); if(results) { //convert the API gateway response to a JSON reponse object response = new JSONResponse(); diff --git a/open-fin-al/src/View/App.jsx b/open-fin-al/src/View/App.jsx index 71aa9269..cac89849 100644 --- a/open-fin-al/src/View/App.jsx +++ b/open-fin-al/src/View/App.jsx @@ -4,24 +4,29 @@ // Disclaimer of Liability // The authors of this software disclaim all liability for any damages, including incidental, consequential, special, or indirect damages, arising from the use or inability to use this software. -import React, { useState, createContext, useEffect } from "react"; +import React, { useState, createContext, useEffect, use } from "react"; // Imports for react pages and assets import AppLoaded from "./App/Loaded"; import { AppPreparing } from "./App/Preparing"; +import { AppSidecarPreparing } from "./App/SidecarPreparing"; import { AppConfiguring } from "./App/Configuring"; import { AuthContainer } from "./Auth/AuthContainer"; import { JSONRequest } from "../Gateway/Request/JSONRequest"; import { InitializationInteractor } from "../Interactor/InitializationInteractor"; +import { SidecarInitializationInteractor } from "../Interactor/SidecarInitializationInteractor"; const DataContext = createContext(); function App(props) { const currentDate = new Date(); const [loading, setLoading] = useState(true); + const [sidecarLoading, setSidecarLoading] = useState(true); const [secureConnectionsValidated, setSecureConnectionsValidated] = useState(false); const [configured, setConfigured] = useState(false); + const [statusMessage, setStatusMessage] = useState(null); const [preparationError, setPreparationError] = useState(null); + const [sidecarPreparationError, setSidecarPreparationError] = useState(null); const [user, setUser] = useState(null); const [isAuthenticated, setIsAuthenticated] = useState(false); const [state, setState] = useState({ @@ -113,14 +118,18 @@ function App(props) { const executeDataInitialization = async() => { try { + setStatusMessage("Checking if system data is initialized..."); const interactor = new InitializationInteractor(); const requestObj = new JSONRequest(`{}`); - const response = await interactor.post(requestObj,"initializeData"); + const response = await interactor.post(requestObj,"initializeData"); + + let slideshowBundleReponse; if(response.response.ok) { setLoading(false); return true; } else { + setStatusMessage("Retreiving data resources for system use..."); //tables may have been deleted and need to be recreated const configurationResponse = await interactor.post(requestObj,"createConfig"); window.console.log(configurationResponse); @@ -140,6 +149,7 @@ function App(props) { const checkIfFullyInitialized = async () => { try { + setStatusMessage("Checking if the system is ready to start..."); //determine if application is fully configured and data initialized const interactor = new InitializationInteractor(); const requestObj = new JSONRequest(`{}`); @@ -149,19 +159,28 @@ function App(props) { setConfigured(true); if(secureConnectionsValidated) { - setLoading(false); checkAuthenticationState(); } else { + setStatusMessage("Updating security certificates..."); const interactor = new InitializationInteractor(); const requestObj = new JSONRequest(`{}`); const response = await interactor.post(requestObj,"refreshPinnedCertificates"); setSecureConnectionsValidated(true); + } + + setStatusMessage("Verifying learning modules are bundled..."); + const slideshowBundleReponse = await interactor.post(requestObj,"bundelSlideshows"); + + if(slideshowBundleReponse.response.ok) { setLoading(false); + } else { + throw new Error("The slideshow bundle did not configure properly"); } return true; } else { //check if the site is uninitialized but configured + setStatusMessage("Checking if the system is configured..."); const configurationResponse = await interactor.get(requestObj,"isConfigured"); if(configurationResponse.response.ok) { @@ -178,6 +197,7 @@ function App(props) { return false; } } else { + setStatusMessage("Creating initial configuration..."); await interactor.post(requestObj,"createConfig"); setConfigured(false); setLoading(true); @@ -191,31 +211,76 @@ function App(props) { } }; + const loadSidecar = async () => { + try { + const interactor = new SidecarInitializationInteractor(); + const requestObj = new JSONRequest(`{}`); + const response = await interactor.post(requestObj,"loadSidecar"); + + setStatusMessage("Starting the graph database..."); + + if(response.response.ok) { + const loadedResponse = await interactor.get(requestObj,"isLoaded"); + if(loadedResponse.response.ok) { + setStatusMessage("Graph database started. Checking if knowledge graph is set up..."); + const graphExistsResponse = await interactor.get(requestObj,"isGraphInitialized"); + + if(graphExistsResponse.response.ok) { + setStatusMessage("Knowledge graph is set up. Checking if system is fully initialized..."); + setSidecarLoading(false); + await checkIfFullyInitialized(); + return true; + } else { + setStatusMessage("Graph database started. Initializing knowledge graph..."); + const graphInitializedResponse = await interactor.post(requestObj,"initializeGraph"); + + if(graphInitializedResponse.response.ok) { + setSidecarLoading(false); + await checkIfFullyInitialized(); + return true; + } else { + throw new Error(); + } + } + } else { + throw new Error(); + } + } else { + throw new Error(); + } + } catch(error) { + setSidecarPreparationError("Failed to initilize the software database. Please contact the software administrator."); + window.console.log(error); + return false; + } + }; + useEffect( () => { - checkIfFullyInitialized(); + loadSidecar(); }, []); return ( - configured ? - ( - loading ? - - : - ( - !isAuthenticated ? - - : - - - - ) - ) - : - + sidecarLoading ? + + : + configured ? + ( loading ? + + : + ( !isAuthenticated ? + + : + + + + ) + ) + : + ); } diff --git a/open-fin-al/src/View/App/Loaded.jsx b/open-fin-al/src/View/App/Loaded.jsx index 5a73f1c8..0634c595 100644 --- a/open-fin-al/src/View/App/Loaded.jsx +++ b/open-fin-al/src/View/App/Loaded.jsx @@ -18,7 +18,7 @@ import { DataContext } from "../App"; import { AppLoadedLayout } from "./LoadedLayout" import Home from "../Home"; import Portfolio from "../Portfolio"; -import { Analysis } from "../Analysis"; +import RiskAnalysis from "../RiskAnalysis"; import BuyReport from "../BuyReport"; import { TimeSeries } from "../Stock"; import { News } from "../News"; @@ -114,7 +114,7 @@ class AppLoaded extends Component {
  • dashboard Dashboard
  • pie_chart Portfolio
  • attach_money Trade
  • -
  • assessment Risk Analysis
  • +
  • assessment Risk Analysis
  • compare Stock Comparison
  • timeline Forecast
  • article News
  • @@ -130,7 +130,7 @@ class AppLoaded extends Component { }> } /> } /> - } /> + } /> } /> } /> } /> diff --git a/open-fin-al/src/View/App/Preparing.jsx b/open-fin-al/src/View/App/Preparing.jsx index da068fc4..f3944b10 100644 --- a/open-fin-al/src/View/App/Preparing.jsx +++ b/open-fin-al/src/View/App/Preparing.jsx @@ -19,7 +19,12 @@ export function AppPreparing(props) {

    {props.preparationError}

    ) : ( <> -

    Downloading data resources...

    + {props.statusMessage ? + ( +

    {props.statusMessage}

    + ) : ( +

    Starting system resources...

    + )}

    This may take a few minutes

    diff --git a/open-fin-al/src/View/App/SidecarPreparing.jsx b/open-fin-al/src/View/App/SidecarPreparing.jsx new file mode 100644 index 00000000..c2e5a86e --- /dev/null +++ b/open-fin-al/src/View/App/SidecarPreparing.jsx @@ -0,0 +1,36 @@ +// No Warranty +// This software is provided "as is" without any warranty of any kind, express or implied. This includes, but is not limited to, the warranties of merchantability, fitness for a particular purpose, and non-infringement. +// +// Disclaimer of Liability +// The authors of this software disclaim all liability for any damages, including incidental, consequential, special, or indirect damages, arising from the use or inability to use this software. + +import React, { useEffect } from "react"; + +//Imports for react pages and assets +import logo from "../../Asset/Image/logo-dark.png"; + +export function AppSidecarPreparing(props) { + return ( +
    +
    +
    Logo
    +
    + {props.sidecarPreparationError ? ( +

    {props.sidecarPreparationError}

    + ) : ( + <> + {props.statusMessage ? + ( +

    {props.statusMessage}

    + ) : ( +

    Starting system resources...

    + )} +

    This may take a few minutes

    +
    + + )} +
    +
    +
    + ); +} diff --git a/open-fin-al/src/View/Component/IViewComponent.ts b/open-fin-al/src/View/Component/IViewComponent.ts new file mode 100644 index 00000000..6bacd267 --- /dev/null +++ b/open-fin-al/src/View/Component/IViewComponent.ts @@ -0,0 +1,21 @@ +export interface IViewComponent { + height: number; // for exact height value + width: number; // for exact width value + isContainer: boolean; // can the component contain other components + resizable: boolean; + maintainAspectRatio: boolean; + heightRatio: number; // for dynamic height + widthRatio: number; // for dynamic width + heightWidthRatioMultiplier: number; // multiplier to maintain aspect ratio when resizing + visible: boolean; // does the component start visible or hidden + enabled: boolean; // is the component usable; allows for visibility but not interaction + + label: string; // for natural language processing search + description: string; // for natural language processing search + tags: string[]; // for natural language processing search + + minimumProficiencyRequirements: Map; // Map of + requiresInternet: boolean; + + calculateRatioMultiplier(): number; +} \ No newline at end of file diff --git a/open-fin-al/src/View/Component/Learn/PowerPointComponent.tsx b/open-fin-al/src/View/Component/Learn/PowerPointComponent.tsx new file mode 100644 index 00000000..c00b8125 --- /dev/null +++ b/open-fin-al/src/View/Component/Learn/PowerPointComponent.tsx @@ -0,0 +1,83 @@ +import React, { Component } from "react"; +import { IViewComponent } from "../IViewComponent"; +import { PowerPoint } from "../../LearningModule/Slideshow/PowerPoint.jsx"; + +interface PowerPointComponentState { + availableWidth: number; +} + +export class PowerPointComponent extends Component<{}, PowerPointComponentState> implements IViewComponent { + height: number = null; + width: number = null; + isContainer: boolean = false; + resizable: boolean = true; + maintainAspectRatio: boolean = true; + widthRatio: number = 16; + heightRatio: number = 9; + heightWidthRatioMultiplier: number = 56; + visible: boolean = true; + enabled: boolean = true; + label: string = "PowerPoint Learning Module"; + description: string = "Component for displaying PowerPoint presentations"; + tags: string[] = ["PowerPoint", "Presentation", "Slideshow", "Learning Module"]; + minimumProficiencyRequirements: Map = null; + requiresInternet: boolean = true; + + containerRef: React.RefObject; + observer: ResizeObserver | null = null; + pptxPath: string = ""; + + constructor(props: any = {}) { + super(props); + this.containerRef = React.createRef(); + this.state = { + availableWidth: 0 + }; + + this.pptxPath = props['pptxPath'] || ""; + + this.calculateRatioMultiplier = this.calculateRatioMultiplier.bind(this); + } + + componentDidMount() { + this.observer = new ResizeObserver(() => { + if (this.containerRef.current) { + this.setState({ + availableWidth: this.containerRef.current.offsetWidth, + }); + } + }); + + if (this.containerRef.current) { + this.observer.observe(this.containerRef.current); + } + } + + componentWillUnmount() { + if (this.observer) { + this.observer.disconnect(); + } + } + + calculateRatioMultiplier(): number { + this.heightWidthRatioMultiplier = Math.floor(this.state.availableWidth / this.widthRatio); + + this.width = this.heightWidthRatioMultiplier * this.widthRatio; + this.height = this.heightWidthRatioMultiplier * this.heightRatio; + + return this.heightWidthRatioMultiplier; + } + + render(): React.ReactNode { + const multiplier = this.calculateRatioMultiplier(); + const ready = multiplier > 0 && this.width > 0 && this.height > 0; + + return ( +
    + {ready && ( + + )} +
    + ); + } +} \ No newline at end of file diff --git a/open-fin-al/src/View/Home.jsx b/open-fin-al/src/View/Home.jsx index 25cae006..cf53620e 100644 --- a/open-fin-al/src/View/Home.jsx +++ b/open-fin-al/src/View/Home.jsx @@ -351,11 +351,14 @@ class Home extends Component {

    trending_up Total Value

    -

    {this.formatter.format(this.state.portfolioValue)} =0 ? {color:"green"}: {color:"red"}}>{this.calculatePercentChange()>0 ? "+" : ""}{this.percentFormatter.format(this.calculatePercentChange())}

    +

    + {this.formatter.format(this.state.portfolioValue)} + {(this.state.portfolioValue ? =0 ? {color:"green"}: {color:"red"}}> {this.calculatePercentChange()>0 ? "+" : ""}{this.percentFormatter.format(this.calculatePercentChange())} : )} +

    account_balance_wallet Total Buying Power

    -

    {this.formatter.format(this.state.buyingPower)} ({this.percentFormatter.format(this.state.buyingPower/this.state.portfolioValue)} cash)

    +

    {this.formatter.format(this.state.buyingPower)}

    diff --git a/open-fin-al/src/View/Learn.jsx b/open-fin-al/src/View/Learn.jsx index 1e223022..a519e563 100644 --- a/open-fin-al/src/View/Learn.jsx +++ b/open-fin-al/src/View/Learn.jsx @@ -88,8 +88,6 @@ export function Learn() { } }; - window.console.log(window.electronApp.getAssetPath()); - return (
    { diff --git a/open-fin-al/src/View/LearningModule/LearningModuleDetails.jsx b/open-fin-al/src/View/LearningModule/LearningModuleDetails.jsx index 8ca97db2..14c47faa 100644 --- a/open-fin-al/src/View/LearningModule/LearningModuleDetails.jsx +++ b/open-fin-al/src/View/LearningModule/LearningModuleDetails.jsx @@ -45,12 +45,8 @@ export function LearningModuleDetails(props) { const handleStartModule = async () => { try { - // get base asset path from Electron (async) - const assetPath = await window.electronApp.getAssetPath(); - - // TODO: handle different OS path separators - const filePath = `${assetPath}\\${location.state.fileName}`; - + const filePath = await window.slideshow.getPath(location.state.fileName); + window.console.log(filePath); // navigate to the learningModulePage route with the full path navigate("/learningModulePage", { state: { diff --git a/open-fin-al/src/View/LearningModule/LearningModulePage.jsx b/open-fin-al/src/View/LearningModule/LearningModulePage.jsx index 49ec1489..5ccebd67 100644 --- a/open-fin-al/src/View/LearningModule/LearningModulePage.jsx +++ b/open-fin-al/src/View/LearningModule/LearningModulePage.jsx @@ -10,6 +10,7 @@ import { } from "react-router-dom"; import { PowerPoint } from "./Slideshow/PowerPoint"; +import { PowerPointComponent } from "../Component/Learn/PowerPointComponent"; export function LearningModulePage(props) { const location = useLocation(); @@ -18,7 +19,7 @@ export function LearningModulePage(props) { window.console.log(location.state); return (
    - +
    ); } diff --git a/open-fin-al/src/View/LearningModule/Slideshow/PowerPoint.jsx b/open-fin-al/src/View/LearningModule/Slideshow/PowerPoint.jsx index 92c50b48..609e7392 100644 --- a/open-fin-al/src/View/LearningModule/Slideshow/PowerPoint.jsx +++ b/open-fin-al/src/View/LearningModule/Slideshow/PowerPoint.jsx @@ -12,14 +12,14 @@ function PowerPoint(props) { const navigate = useNavigate(); const [isDisabled, setIsDisabled] = useState(false); - const containerRef = useRef(null); + const slideshowContainerRef = useRef(null); const viewerRef = useRef(null); const [currentSlide, setCurrentSlide] = useState(0); const [totalSlides, setTotalSlides] = useState(0); + const naturalSizeRef = useRef({ width: 0, height: 0 }); const pptxPath = props.pptxPath; - window.console.log(pptxPath); //navigate to the learn base page @@ -31,15 +31,70 @@ function PowerPoint(props) { // Initialize pptx-preview // ----------------------------- useEffect(() => { - if (!containerRef.current) return; + window.console.log(props.width, props.height); + if (!slideshowContainerRef.current) return; + if (!viewerRef.current) { - viewerRef.current = initPptxPreview(containerRef.current, { - width: 900, - height: 506, + viewerRef.current = initPptxPreview(slideshowContainerRef.current, { + width: props.width, + height: props.height, }); + + naturalSizeRef.current = { + width: props.width, + height: props.height, + }; } }, []); + useEffect(() => { + window.console.log(props.width, props.height); + const container = slideshowContainerRef.current; + /* + // Grab *one* slide's inner content (canvas/svg/img) + const inner = container.querySelector( + ".pptx-preview-slide-wrapper > *" + ); + if (!inner) return;*/ + + const { width: baseW, height: baseH } = naturalSizeRef.current; + if (!baseW || !baseH) return; + + /*const rect = inner.getBoundingClientRect(); + if (!rect.width || !rect.height) return;*/ + + const containerWidth = props.width; + const containerHeight = props.height; + + const scale = Math.min( + containerWidth / baseW, + containerHeight / baseH + ); + + window.console.log("Calculated scale:", scale); + + // Expose scale as a CSS variable on the container + //container.style.setProperty("--pptx-scale", String(scale)); + slideshowContainerRef.current.style.setProperty( + "--pptx-scale", + String(scale) + ); + + slideshowContainerRef.current.style.width = `${containerWidth}px`; + slideshowContainerRef.current.style.height = `${containerHeight}px`; + slideshowContainerRef.current.style.position = "relative"; + + const inner = container.querySelector( + ".pptx-preview-wrapper" + ); + if (!inner) return; + + inner.style.position = 'absolute'; + inner.style.left = `0`; + inner.style.overflow = `hidden`; + + }, [props.width, props.height]); + useEffect(() => { if (!viewerRef.current || !pptxPath) return; @@ -59,8 +114,8 @@ function PowerPoint(props) { await viewerRef.current.preview(arrayBuffer); - if (containerRef.current) { - const slides = containerRef.current.querySelectorAll( + if (slideshowContainerRef.current) { + const slides = slideshowContainerRef.current.querySelectorAll( ".pptx-preview-slide-wrapper" ); setTotalSlides(slides.length); @@ -77,9 +132,9 @@ function PowerPoint(props) { }, [pptxPath]); const showSlide = (index) => { - if (!containerRef.current) return; + if (!slideshowContainerRef.current) return; - const slides = containerRef.current.querySelectorAll( + const slides = slideshowContainerRef.current.querySelectorAll( ".pptx-preview-slide-wrapper" ); @@ -109,11 +164,11 @@ function PowerPoint(props) { {/* Fixed-Size Slide Window */}
    {/* pptx-preview will render into this div */} -
    - Thumbnail + Thumbnail { + // prevent infinite loop if fallback is missing + e.currentTarget.onerror = null; + e.currentTarget.src = defaultThumbnail; + }} />

    - Thumbnail + Thumbnail { + // prevent infinite loop if fallback is missing + e.currentTarget.onerror = null; + e.currentTarget.src = defaultThumbnail; + }} />

    )}

    - {this.state.buyingPowerLoaded && + {this.state.buyingPowerLoaded ? <>
    @@ -461,6 +475,13 @@ class Portfolio extends Component {

    {this.formatter.format(this.state.buyingPower)}

    + + : +
    +
    Retrieving portfolio data...
    +
    + } + <>

    Stock Assets

    @@ -490,7 +511,7 @@ class Portfolio extends Component {
    - } +
    : <> diff --git a/open-fin-al/src/View/RiskAnalysis.jsx b/open-fin-al/src/View/RiskAnalysis.jsx new file mode 100644 index 00000000..6348f8d1 --- /dev/null +++ b/open-fin-al/src/View/RiskAnalysis.jsx @@ -0,0 +1,1083 @@ +// No Warranty +// This software is provided "as is" without any warranty of any kind, express or implied. This includes, but is not limited to, the warranties of merchantability, fitness for a particular purpose, and non-infringement. +// +// Disclaimer of Liability +// The authors of this software disclaim all liability for any damages, including incidental, consequential, special, or indirect damages, arising from the use or inability to use this software. + +import React, { Component } from "react"; +import { PortfolioCreation } from "./Portfolio/Creation"; +import { PortfolioInteractor } from "../Interactor/PortfolioInteractor"; +import {PortfolioTransactionInteractor} from "../Interactor/PortfolioTransactionInteractor"; +import { StockInteractor } from "../Interactor/StockInteractor"; +import {JSONRequest} from "../Gateway/Request/JSONRequest"; +import { HeaderContext } from "./App/LoadedLayout"; + +import { Popover } from "react-tiny-popover"; +import { PieChart, Pie, Sector, LineChart, Line, CartesianGrid, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts'; // For adding charts + +const formatter = new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + minimumFractionDigits: 2, + maximumFractionDigits: 2, +}); + +const renderActiveShape = (props) => { + window.console.log(props); + const RADIAN = Math.PI / 180; + const { cx, cy, midAngle, innerRadius, outerRadius, startAngle, endAngle, fill, payload, percent, value } = props; + const sin = Math.sin(-RADIAN * midAngle); + const cos = Math.cos(-RADIAN * midAngle); + const sx = cx + (outerRadius + 10) * cos; + const sy = cy + (outerRadius + 10) * sin; + const mx = cx + (outerRadius + 30) * cos; + const my = cy + (outerRadius + 30) * sin; + const ex = mx + (cos >= 0 ? 1 : -1) * 22; + const ey = my; + const textAnchor = cos >= 0 ? 'start' : 'end'; + window.console.log(payload); + return ( + + + {payload.name} + + + + + + = 0 ? 1 : -1) * 12} y={ey} textAnchor={textAnchor} fill="#333">{`${formatter.format(value)}`} + = 0 ? 1 : -1) * 12} y={ey} dy={18} textAnchor={textAnchor} fill="#999"> + {`(${(percent * 100).toFixed(2)}%)`} + + + ); +}; + +class RiskAnalysis extends Component { + static contextType = HeaderContext; + + async componentDidMount() { + window.console.log("Portfolio context in componentDidMount:", this.context); + const { setHeader } = this.context || {}; + + if (setHeader) { + setHeader({ + title: "Risk Analysis", + icon: "assessment", + }); + } + + await this.fetchPortfolios(); + const cashId = await this.getCashId(); + + await this.getPortfolioData(); + } + + async getPortfolioData() { + window.console.log("Portfolio id:", this.state.currentPortfolio); + + // Only fetch portfolio data if a portfolio is selected + if(this.state.currentPortfolio) { + await this.getPortfolioValue(); + await this.getPortfolioChartData(); + + if(this.state.cashId) { + await this.getBuyingPower(this.state.cashId); + } + + await this.fetchDailyHistoricalReturnsData(); + } + } + + formatter = new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }); + + percentFormatter = new Intl.NumberFormat('en-US', { + style: 'percent', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }); + + constructor(props) { + super(props); + this.state = { + createPortfolio: true, + currentPortfolio: null, + portfolios: [], + isModalOpen: false, + depositAmount: 0, + cashId: null, + depositMessage: null, + portfolioValue: 0, + assetData: [], + chartData: [], + assetReturnsTimeSeries: {}, + assetReturnsDates: [], + oneYearRisk: {}, + twoYearRisk: {}, + threeYearRisk: {}, + buyingPower: 0, + buyingPowerLoaded: false, + activeIndex: 0, + portfolioName: null, + hoveredInfo: null + }; + + //Bind methods for element events + this.openModal = this.openModal.bind(this); + this.getBuyingPower = this.getBuyingPower.bind(this); + this.onPieEnter = this.onPieEnter.bind(this); + this.handleInfoHover = this.handleInfoHover.bind(this); + this.handleInfoLeave = this.handleInfoLeave.bind(this); + } + + async openModal() { + this.setState({ + depositMessage: null, + isModalOpen: true + }); + } + handleInfoHover(metric) { + this.setState({hoveredInfo: metric}); + } + + handleInfoLeave() { + this.setState({hoveredInfo: null}); + } + + async fetchPortfolios() { + // Get user from localStorage (auth system) + const savedUser = localStorage.getItem('openfinAL_user'); + if (!savedUser) { + console.error('No user found in session'); + return; + } + + const userData = JSON.parse(savedUser); + if (!userData.id) { + console.error('Invalid user session'); + return; + } + + const interactor = new PortfolioInteractor(); + const requestObj = new JSONRequest(JSON.stringify({ + request: { + portfolio: { + userId: userData.id + } + } + })); + + const response = await interactor.get(requestObj); + + var defaultPortfolio = null; + var defaultPortfolioName = null; + for(var portfolio of response.response?.results) { + if(portfolio.isDefault) { + defaultPortfolio = portfolio.id; + defaultPortfolioName = portfolio.name; + this.setState({createPortfolio: false}); + break; + } + } + + this.setState({ + currentPortfolio: defaultPortfolio, + portfolioName: defaultPortfolioName, + portfolios: response.response?.results || [] + }); + } + + async fetchDailyHistoricalReturnsData() { + if(!this.state.currentPortfolio || !this.state.assetData) { + return; + } + + let dates = []; + const assetReturnTimeseries = {}; + + for(var asset of this.state.assetData) { + window.console.log(asset); + if(asset.type==="Stock") { + window.console.log(asset["symbol"]); + const interactor = new StockInteractor(); + const request = new JSONRequest(JSON.stringify({ + request: { + stock: { + action: "interday", + ticker: asset["symbol"], + interval: "5Y" + } + } + })); + + const response = await interactor.get(request); + + if(response?.response?.ok) { + let series = []; + let recordDates = false; + + if(dates.length < response.response.results[0].data.length) { + recordDates = true; + dates = []; + } + + for(var i in response.response.results[0].data) { + var datum = response.response.results[0].data[i]; + var datumMinus1 = i > 0 ? response.response.results[0].data[i-1] : null; + + // don't keep item 0 because there is no t-1 data point for item 0 + if(recordDates && i > 0) { + dates.push(datum.date); + } + + if(i > 0) { + let returnValue = this.calculateReturn(datum.price, datumMinus1.price); + series.push(returnValue); + } + } + + assetReturnTimeseries[asset.symbol] = series; + } + } + } + + window.console.log(assetReturnTimeseries); + window.console.log(dates); + + const df = this.returnsToDataFrame(assetReturnTimeseries, dates); + const weights = this.computeWeights(this.state.assetData, this.state.portfolioValue); + + const oneYear = this.computeRiskBundle(df, weights, 252); + const twoYear = this.computeRiskBundle(df, weights, 504); + const threeYear = this.computeRiskBundle(df, weights, 756); + + this.setState({ + oneYearRisk: oneYear, + twoYearRisk: twoYear, + threeYearRisk: threeYear + }); + + window.console.log(oneYear, twoYear, threeYear); + } + + computeRiskBundle(dfAll, weightsByTicker, windowDays) { + const tickers = Object.keys(weightsByTicker); + + const df = this.sliceLookback(dfAll, tickers, windowDays); + + const cov = this.covarianceDaily(df, tickers); + const corr = this.correlationFromCov(cov); + const { volDaily, volAnnual } = this.portfolioVolFromCov(cov, weightsByTicker); + + const { rows } = this.riskContributionsFromCov(cov, weightsByTicker); + + // annualize MCR/RC consistently if you want annual outputs: + // multiply mcr and rc by sqrt(252) (because they are volatility-like units) + const annualFactor = Math.sqrt(windowDays); + const rowsAnnual = rows.map(r => ({ + ...r, + mcrAnnual: r.mcr * annualFactor, + rcAnnual: r.nrc * annualFactor + })); + + // Max Drawdown (computed from portfolio daily returns in the same lookback window) + const rp = this.portfolioReturnsSeries(df, weightsByTicker, tickers); + const maxDrawdown = this.maxDrawdownFromReturns(rp); + const { var: var95, cvar: cvar95 } = this.varCvarFromReturns(rp, 0.95); + + return { cov, corr, volDaily, volAnnual, maxDrawdown, var95, cvar95, rows: rowsAnnual }; + } + + computeWeights(assetData, portfolioValue) { + const w = {}; + for (const a of assetData) { + if (a.type === "Stock" && a.symbol && a.currentValue != null) { + w[a.symbol] = a.currentValue / portfolioValue; + } + } + return w; + } + + calculateReturn(priceT, priceTMinus1) { + return (priceT - priceTMinus1) / priceTMinus1; + } + + // alpha=0.95 => 95% VaR/CVaR (worst 5% tail) + varCvarFromReturns(rp, alpha = 0.95) { + const clean = rp.filter(x => Number.isFinite(x)).slice().sort((a, b) => a - b); // ascending + if (clean.length < 10) return { var: null, cvar: null }; + + const tailProb = 1 - alpha; // 0.05 + const idx = Math.max(0, Math.floor(tailProb * clean.length)); + + const varReturn = clean[idx]; // typically negative + const tail = clean.slice(0, idx + 1); // worst tail + const cvarReturn = tail.reduce((s, x) => s + x, 0) / tail.length; + + // Return as positive loss magnitudes (more intuitive for UI) + return { var: -varReturn, cvar: -cvarReturn }; + } + + returnsToDataFrame(returnsByTicker, dates) { + window.console.log(returnsByTicker, dates); + + const dfd = window.dfd; + + const df = new dfd.DataFrame(returnsByTicker); + + // Attach dates (as a normal column; safest across Danfo versions) + df.addColumn("date", dates, { inplace: true }); + + // Reorder to put date first (optional) + const cols = ["date", ...Object.keys(returnsByTicker)]; + window.console.log(df.loc({ columns: cols })); + return df.loc({ columns: cols }); + } + + covarianceDaily(df, tickers) { + const dfd = window.dfd; + + // r: DataFrame with only return columns + const r = df.loc({ columns: tickers }); + + // values: rows x cols (T x N) + const Xraw = r.values; + + // Convert to numeric and drop any rows with non-finite values (NaN/undefined) + const X = []; + for (let i = 0; i < Xraw.length; i++) { + const row = Xraw[i].map(v => Number(v)); + if (row.every(v => Number.isFinite(v))) X.push(row); + } + + const T = X.length; + const N = tickers.length; + + if (T < 2) { + // Not enough data; return zeros + const zero = Array.from({ length: N }, () => Array(N).fill(0)); + return new dfd.DataFrame(zero, { columns: tickers, index: tickers }); + } + + // Column means + const means = Array(N).fill(0); + for (let t = 0; t < T; t++) { + for (let j = 0; j < N; j++) means[j] += X[t][j]; + } + for (let j = 0; j < N; j++) means[j] /= T; + + // Covariance (sample covariance, divide by T-1) + const cov = Array.from({ length: N }, () => Array(N).fill(0)); + for (let t = 0; t < T; t++) { + for (let i = 0; i < N; i++) { + const di = X[t][i] - means[i]; + for (let j = 0; j < N; j++) { + cov[i][j] += di * (X[t][j] - means[j]); + } + } + } + const denom = T - 1; + for (let i = 0; i < N; i++) { + for (let j = 0; j < N; j++) cov[i][j] /= denom; + } + + return new dfd.DataFrame(cov, { columns: tickers, index: tickers }); + } + + correlationFromCov(covDf) { + const dfd = window.dfd; + + const cov = covDf.values; // NxN + const cols = covDf.columns; // tickers + const N = cols.length; + + // std devs = sqrt(diagonal) + const std = new Array(N).fill(0); + for (let i = 0; i < N; i++) { + const v = Number(cov[i][i]); + std[i] = v > 0 ? Math.sqrt(v) : 0; + } + + const corr = Array.from({ length: N }, () => Array(N).fill(0)); + for (let i = 0; i < N; i++) { + for (let j = 0; j < N; j++) { + const denom = std[i] * std[j]; + corr[i][j] = denom ? cov[i][j] / denom : 0; + } + } + + return new dfd.DataFrame(corr, { columns: cols, index: cols }); + } + + portfolioVolFromCov(covDf, weightsByTicker, tradingDays = 252) { + const tickers = covDf.columns; + const cov = covDf.values; // 2D array + + // aligned weight vector + const w = tickers.map(t => weightsByTicker[t] ?? 0); + + // variance = wᵀ Σ w + let varP = 0; + for (let i = 0; i < tickers.length; i++) { + for (let j = 0; j < tickers.length; j++) { + varP += w[i] * cov[i][j] * w[j]; + } + } + + const volDaily = Math.sqrt(varP); + const volAnnual = volDaily * Math.sqrt(tradingDays); + + return { varP, volDaily, volAnnual }; + } + + riskContributionsFromCov(covDf, weightsByTicker) { + const tickers = covDf.columns; + const cov = covDf.values; + + const w = tickers.map(t => weightsByTicker[t] ?? 0); + + // compute sigma_p + let varP = 0; + for (let i = 0; i < tickers.length; i++) { + for (let j = 0; j < tickers.length; j++) { + varP += w[i] * cov[i][j] * w[j]; + } + } + const sigmaP = Math.sqrt(varP); + if (sigmaP === 0) { + return { sigmaP: 0, rows: tickers.map(t => ({ ticker: t, weight: weightsByTicker[t] ?? 0, mcr: 0, rc: 0, pctRc: 0 })) }; + } + + // compute Σw + const sigmaW = new Array(tickers.length).fill(0); + for (let i = 0; i < tickers.length; i++) { + for (let j = 0; j < tickers.length; j++) { + sigmaW[i] += cov[i][j] * w[j]; + } + } + + // MCR, RC, %RC + const rows = tickers.map((t, i) => { + const mcr = sigmaW[i] / sigmaP; + const rc = w[i] * mcr; + const pctRc = rc / sigmaP; + return { ticker: t, weight: w[i], mcr, rc, pctRc }; + }); + + return { sigmaP, rows }; + } + + sliceLookback(df, tickers, windowDays) { + const r = df.loc({ columns: ["date", ...tickers] }); + + const n = r.shape[0]; + const start = Math.max(0, n - windowDays); + + return r.iloc({ rows: [`${start}:`] }); + } + + portfolioReturnsSeries(df, weightsByTicker, tickers) { + // df contains date + tickers columns; returns are decimals + const r = df.loc({ columns: tickers }); + const Xraw = r.values; // rows x cols + const w = tickers.map(t => Number(weightsByTicker[t] ?? 0)); + + // build portfolio daily returns rp[t] = sum_i w_i * r_{t,i} + const rp = new Array(Xraw.length).fill(0); + + for (let t = 0; t < Xraw.length; t++) { + let sum = 0; + for (let j = 0; j < tickers.length; j++) { + const v = Number(Xraw[t][j]); + // treat non-finite as 0 to avoid poisoning the series + sum += (Number.isFinite(v) ? v : 0) * w[j]; + } + rp[t] = sum; + } + return rp; + } + + maxDrawdownFromReturns(portfolioReturns) { + // returns decimal negative number (e.g., -0.28 for -28%) + let wealth = 1.0; + let peak = 1.0; + let maxDD = 0; // most negative drawdown + + for (const r of portfolioReturns) { + const rr = Number(r); + wealth *= (1 + (Number.isFinite(rr) ? rr : 0)); + if (wealth > peak) peak = wealth; + const dd = (wealth / peak) - 1; + if (dd < maxDD) maxDD = dd; + } + + return maxDD; + } + + async changeCurrentPortfolio(portfolioId, portfolioName) { + this.setState({ + currentPortfolio: portfolioId, + portfolioName: portfolioName, + portfolioValue: 0, + assetData: [], + chartData: [], + assetReturnsTimeSeries: {}, + assetReturnsDates: [], + oneYearRisk: {}, + twoYearRisk: {}, + threeYearRisk: {}, + buyingPower: 0, + buyingPowerLoaded: false, + }); + await this.sleep(1000); // allow time for state to set + await this.getPortfolioData(); + } + + async getCashId() { + const interactor = new PortfolioInteractor(); + const requestObj = new JSONRequest(JSON.stringify({ + request: { + action: "getCashId" + } + })); + + const response = await interactor.get(requestObj); + + if(response?.response?.ok) { + this.setState({cashId: response.response.results[0].id}); + return response.response.results[0].id; + } + return null; + } + + async getBuyingPower(cashId=null, portfolioId=null) { + try { + if(!cashId) { + cashId = this.state.cashId; + } + + if(!portfolioId) { + portfolioId = this.state.currentPortfolio; + } + + const interactor = new PortfolioTransactionInteractor(); + const requestObj = new JSONRequest(JSON.stringify({ + request: { + action: "getBuyingPower", + transaction: { + portfolioId: portfolioId, + entry: { + assetId: cashId + } + } + + } + })); + + const response = await interactor.get(requestObj); + if(response && response.response && response.response.ok && response.response.results && response.response.results[0]) { + this.setState({buyingPowerLoaded: true, buyingPower: response.response.results[0].buyingPower}); + return true; + } else { + console.error('Buying power API response error:', response); + this.setState({buyingPowerLoaded: true, buyingPower: 0}); + return false; + } + } catch (error) { + console.error('Error fetching buying power:', error); + this.setState({buyingPowerLoaded: true, buyingPower: 0}); + return false; + } + } + + async getPortfolioValue(portfolioId=null) { + if(!portfolioId) { + portfolioId = this.state.currentPortfolio; + } + + const interactor = new PortfolioTransactionInteractor(); + const requestObj = new JSONRequest(JSON.stringify({ + request: { + action: "getPortfolioValue", + transaction: { + portfolioId: portfolioId + } + + } + })); + + const response = await interactor.get(requestObj); + + if(response?.response?.ok) { + var portfolioValue = 0; + for(var i in response.response.results) { + var asset = response.response.results[i]; + + if(asset.type==="Stock") { + const interactor = new StockInteractor(); + const quoteRequestObj = new JSONRequest(JSON.stringify({ + request: { + stock: { + action: "quote", + ticker: asset["symbol"] + } + } + })); + + const quoteResponse = await interactor.get(quoteRequestObj); + + if(quoteResponse?.response?.ok && quoteResponse.response.results[0]?.quotePrice) { + response.response.results[i]["quotePrice"] = quoteResponse.response.results[0].quotePrice; + response.response.results[i]["currentValue"] = asset.quantity * quoteResponse.response.results[0].quotePrice; + portfolioValue += asset.quantity * quoteResponse.response.results[0].quotePrice; + } else { + // If quote not available, use the original asset value + response.response.results[i]["quotePrice"] = asset.assetValue / asset.quantity; + response.response.results[i]["currentValue"] = asset.assetValue; + portfolioValue += asset.assetValue; + } + } else { + portfolioValue += asset.assetValue; + } + } + + this.setState({assetData: response.response.results}); + this.setState({portfolioValue: portfolioValue}); + return true; + } else { + return false; + } + } + + async getPortfolioChartData(portfolioId=null) { + if(!portfolioId) { + portfolioId = this.state.currentPortfolio; + } + + const interactor = new PortfolioTransactionInteractor(); + const requestObj = new JSONRequest(JSON.stringify({ + request: { + action: "getPortfolioValue", + transaction: { + portfolioId: portfolioId + } + + } + })); + + const response = await interactor.get(requestObj); + + if(response?.response?.ok) { + var chartData = []; + for(var asset of response.response.results) { + var assetObj = {}; + assetObj.name = asset.symbol; + assetObj.value = asset.assetValue; + chartData.push(assetObj); + } + + this.setState({chartData: chartData}); + return true; + } else { + return false; + } + } + + onPieEnter(_, index) { + this.setState({ activeIndex: index }); + } + + async sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + corrDfToHeatmapCells(corrDf) { + const tickers = corrDf.columns; + const M = corrDf.values; + + const cells = []; + for (let r = 0; r < tickers.length; r++) { + for (let c = 0; c < tickers.length; c++) { + cells.push({ + row: tickers[r], + col: tickers[c], + value: Number(M[r][c]) + }); + } + } + return { tickers, cells }; + } + + render() { + return ( +
    +
    + +
    + + {!this.state.createPortfolio && this.state.currentPortfolio ? +
    + {/* Portfolio Value and Buying Power Section */} + {this.state.buyingPowerLoaded ? + <> +
    +
    + + + +
    +
    +
    +

    Portfolio Value

    +

    {this.formatter.format(this.state.portfolioValue)}

    +
    +
    +
    + + : +
    +
    Retrieving portfolio data...
    +
    + } + <> +

    + { + this.state.threeYearRisk?.var95 ? "Portfolio Risk" : + + (this.state.buyingPowerLoaded ? + <> + +
    + Calculating Portfolio Risk...
    +
    + + : "Portfolio Risk") + } +

    +
    +
    + + Risk horizon is the length of time over which risk metrics are calculated. Short-term horizons capture short-term risks, while longer-term horizons capture longer-term risks. +
    }> + + +
    +
    + + Daily Volatility is a measure of how much a portfolio’s returns typically move up or down in a single day. For example, a daily volatility of 1% means that on a typical day, the portfolio’s value moves up or down by about 1%. +
    }> + + +
    +
    + + Annual Volatility estimates how much a portfolio’s returns are expected to fluctuate over a full year. For example, an annual volatility of 16% means that over a full year, the portfolio’s return typically varies up or down by about 16%. +
    }> + + +
    +
    + + Maximum Drawdown measures the biggest loss an investment has had from its highest point to its lowest point before recovering to a new high point. For example, a maximum drawdown of 15% means that at some point during the period, the portfolio fell 15% from its highest value to its lowest value before recovering. +
    }> + + +

    +
    + + Value at Risk (VaR), estimates the worst expected loss over a given period 95% of the time. For example, a VaR(95) of 2% means that on 95 out of 100 days, losses should not exceed 2%. +
    }> + + +
    +
    + + Conditional Value at Risk (CVaR) measures the average loss on the worst 5% of days, focusing on how bad losses are when things go wrong. A CVaR(95%) of 4% means that on the worst 5 out of 100 days, the portfolio loses about 4% on average. +
    }> + + +
    +
    + {[ + { label: "1-Year", risk: this.state.oneYearRisk }, + { label: "2-Year", risk: this.state.twoYearRisk }, + { label: "3-Year", risk: this.state.threeYearRisk }, + ].map(({ label, risk }) => ( + ( this.state.threeYearRisk?.cvar95 && +
    +
    {label}
    +
    + {risk?.volDaily != null ? this.percentFormatter.format(risk.volDaily) : "Calculating..."} +
    +
    + {risk?.volAnnual != null ? this.percentFormatter.format(risk.volAnnual) : "Calculating..."} +
    +
    + {risk?.maxDrawdown != null ? this.percentFormatter.format(Math.abs(risk.maxDrawdown)) : "Calculating..."} +
    +
    + {risk?.var95 != null ? this.percentFormatter.format(risk.var95) : "Calculating..."} +
    +
    + {risk?.cvar95 != null ? this.percentFormatter.format(risk.cvar95) : "Calculating..."} +
    +
    + ) + ))} +
    +

    Contributions to Risk (2-Year Time Horizon)

    +
    +
    Symbol
    +
    + + The portfolio weight of an asset is the percentage of the portfolio’s total value that is invested in the specific asset. For example, if a stock has a portfolio weight of 25%, that means 25% of the total portfolio value is invested in that stock. +
    }> + + +
    +
    + + Marginal Contribution to Risk (MCR) measures how much an individual asset adds to the portfolio’s overall risk if its weight increases slightly. For example, if a stock has an MCR of 1.5%, increasing that stock’s weight slightly would increase the portfolio’s total risk by about 1.5%, not considering its weight in the portfolio. +
    }> + + +
    +
    + + Percentage Contribution to Risk (Risk %) shows how much of the portfolio’s total risk comes from a specific asset, taking into account both how risky the asset is (MCR) and how much of the portfolio it represents (weight). For example, if a stock has a Risk % of 35%, that means 35% of the portfolio’s total risk comes from that single stock. +
    }> + + +
    +
    + + {this.state.twoYearRisk?.rows ? + this.state.twoYearRisk.rows.map((asset, index) => ( +
    +
    {asset.ticker}
    +
    {this.percentFormatter.format(asset.weight)}
    +
    {this.percentFormatter.format(asset.mcr)}
    +
    {this.percentFormatter.format(asset.pctRc)}
    +
    + )) : + null + } + +
    + {this.state.twoYearRisk?.corr ? this.renderCorrHeatmap(this.state.twoYearRisk.corr) : null} +
    + + + + : + <> + { } + + } + + ); + } + + renderCorrHeatmap(corrDf, title = "Correlation Heatmap (2-Year Time Horizon)") { + if (!corrDf) return null; + + const { tickers, cells } = this.corrDfToHeatmapCells(corrDf); + + // Map correlation [-1,1] to 0..100 for CSS lightness + const colorFor = (v) => { + // clamp + const x = Math.max(-1, Math.min(1, v)); + // 0 => neutral, -1 => one extreme, +1 => other extreme + // Use HSL: red-ish for negative, green-ish for positive + const hue = x >= 0 ? 120 : 0; // green or red + const sat = 55; + const light = 92 - Math.abs(x) * 45; // stronger corr => darker + return `hsl(${hue} ${sat}% ${light}%)`; + }; + + return ( +
    +

    {title}

    +
    +
    +
    + {tickers.map(t => ( +
    + {t} +
    + ))} + + {tickers.map(r => ( + +
    {r}
    + {tickers.map(c => { + const cell = cells.find(x => x.row === r && x.col === c); + const v = cell?.value ?? 0; + return ( +
    + {v.toFixed(2)} +
    + ); + })} +
    + ))} +
    +
    +
    + Values near +1 move together; values near -1 move opposite; values near 0 are weakly related. +
    +
    + ); + } + + renderStockCard(company, symbol, currentPrice, purchasePrice, quantity, gains, percentGain) { + return ( +
    +

    {company} ({symbol})

    +

    Current Price: ${currentPrice.toFixed(2)}

    +

    Purchase Price: ${purchasePrice.toFixed(2)}

    +

    Quantity: {quantity}

    +

    Gains: = 0 ? 'positive' : 'negative'}`}>${gains.toFixed(2)}

    +

    % Gain: = 0 ? 'positive' : 'negative'}`}>{percentGain.toFixed(2)}%

    +
    + ); + } +} + +export default RiskAnalysis; diff --git a/open-fin-al/src/View/Stock.jsx b/open-fin-al/src/View/Stock.jsx index f1c8709e..2ad6fcbb 100644 --- a/open-fin-al/src/View/Stock.jsx +++ b/open-fin-al/src/View/Stock.jsx @@ -134,7 +134,7 @@ function Stock(props) { const reportResults10K = await secInteractor.get(req10K); const reportResults10Q = await secInteractor.get(req10Q); - + window.console.log(reportResults10K, reportResults10Q); if(reportResults10K && reportResults10Q) { // Update the DataContext state to include reportLinks if they exist setState((prevState) => ({ @@ -163,7 +163,7 @@ function Stock(props) { { state.isLoading === true ? ( <> -

    Loading...

    +

    Retreiving data...

    ) : state.error ? (

    The ticker you entered is not valid or no data is available for this stock.

    diff --git a/open-fin-al/src/View/Stock/TimeSeriesChart.jsx b/open-fin-al/src/View/Stock/TimeSeriesChart.jsx index 8e41d68f..fe179832 100644 --- a/open-fin-al/src/View/Stock/TimeSeriesChart.jsx +++ b/open-fin-al/src/View/Stock/TimeSeriesChart.jsx @@ -11,6 +11,7 @@ import { StockInteractor } from "../../Interactor/StockInteractor"; import { JSONRequest } from "../../Gateway/Request/JSONRequest"; import { UserInteractor } from "../../Interactor/UserInteractor"; import { PortfolioInteractor } from "../../Interactor/PortfolioInteractor"; +import {PortfolioTransactionInteractor} from "../../Interactor/PortfolioTransactionInteractor"; import { OrderInteractor } from "../../Interactor/OrderInteractor"; function TimeSeriesChart(props) { @@ -26,6 +27,8 @@ function TimeSeriesChart(props) { const [orderQuantity, setOrderQuantity] = useState(0); const [timeoutId, setTimeoutId] = useState(null); const [cashId, setCashId] = useState(null); + const [buyingPower, setBuyingPower] = useState(0); + const [buyingPowerLoaded, setBuyingPowerLoaded] = useState(false); const openModal = async () => { setIsModalOpen(true); @@ -114,6 +117,48 @@ function TimeSeriesChart(props) { return null; }; + const getBuyingPower = async(cashIdP=null, portfolioId=null) => { + try { + if(!cashIdP) { + cashIdP = cashId; + } + + if(!portfolioId) { + portfolioId = currentPortfolio; + } + + const interactor = new PortfolioTransactionInteractor(); + const requestObj = new JSONRequest(JSON.stringify({ + request: { + action: "getBuyingPower", + transaction: { + portfolioId: portfolioId, + entry: { + assetId: cashIdP + } + } + } + })); + + const response = await interactor.get(requestObj); + if(response && response.response && response.response.ok && response.response.results && response.response.results[0]) { + setBuyingPowerLoaded(true); + setBuyingPower(response.response.results[0].buyingPower); + return true; + } else { + console.error('Buying power API response error:', response); + setBuyingPowerLoaded(true); + setBuyingPower(0); + return false; + } + } catch (error) { + console.error('Error fetching buying power:', error); + setBuyingPowerLoaded(true); + setBuyingPower(0); + return false; + } + }; + const getCurrentPrice = async () => { if(props.state.data) { const interactor = new StockInteractor(); @@ -141,6 +186,8 @@ function TimeSeriesChart(props) { const placeOrder = async () => { //TODO: check to make sure the order and pending orders don't exceed buying power + getBuyingPower(); + const interactor = new OrderInteractor(); const requestObj = new JSONRequest(JSON.stringify({ request: { @@ -314,10 +361,16 @@ function TimeSeriesChart(props) { - {props.state.secData ? - <> -
    -

    + {data && +
    +

    + +

    {isModalOpen && ( <>
    { @@ -327,7 +380,7 @@ function TimeSeriesChart(props) {
    -

    {header}

    +

    Purchase {header}

    + { buyingPowerLoaded && currentQuote.quotePrice ? + <> +

    Price: {formatterCent.format(currentQuote.quotePrice)}

    +

    Quantity: setOrderQuantity(e.target.value)} />

    +

    Buying Power: {formatterCent.format(buyingPower)}

    +

    Total: {formatterCent.format(currentQuote.quotePrice * orderQuantity)}

    + {orderMessage ?

    {orderMessage}

    : null} + + {(currentQuote.quotePrice * orderQuantity) <= buyingPower ? + + : +

    The order total may not exceed your buying power

    } + + : +
    + Retrieving current quote...
    +
    + } +
    )}
    + } + + {props.state.secData ? + <> { props.fundamentalAnalysis ? <>

    AI Fundamental Analysis

    diff --git a/open-fin-al/src/global.d.ts b/open-fin-al/src/global.d.ts new file mode 100644 index 00000000..c48c26b4 --- /dev/null +++ b/open-fin-al/src/global.d.ts @@ -0,0 +1 @@ +declare module "*.jsx"; \ No newline at end of file diff --git a/open-fin-al/src/index.css b/open-fin-al/src/index.css index 97c22402..848d03d6 100644 --- a/open-fin-al/src/index.css +++ b/open-fin-al/src/index.css @@ -18,9 +18,11 @@ --text-color-medium-dark: #333333; --row-primary-color: #FFFFFF; --row-alternating-color: #c1c9ff; + --always-black: #000000; --footer-height: 90px; --sidebar-width: 220px; --sidebar-footer-width: 190px; + --pptx-scale: 1; } body.dark-mode { @@ -41,6 +43,7 @@ body.dark-mode { --text-color-medium-dark: #BBBBBB; --row-primary-color: #2d3748; --row-alternating-color: #4a5568; + --always-black: #000000; } body { @@ -384,10 +387,17 @@ code { background-color: var(--background-color); } -.pptx-preview-wrapper { +.slideshowContainer { margin: 0 !important; padding: 0 !important; - overflow: hidden !important; + position: relative; + overflow: hidden; +} + +.pptx-preview-wrapper { + transform-origin: top left; + transform: scale(var(--pptx-scale, 1)); + overflow: hidden; } .slide { @@ -658,7 +668,7 @@ code { .popoverContent { max-width: 200px; - padding: 5px; + padding: 15px; border-radius: 10px; background-color: rgba(29, 32, 40, 0.9); color: var(--text-color-light) @@ -820,6 +830,10 @@ code { } .portfolio-card { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; background-color: var(--background-color); padding: 20px; border-radius: 5px; @@ -827,8 +841,12 @@ code { text-align: center; } +.portfolio-card h3 { + margin: 0px; +} + .portfolio-value, .buying-power { - font-size: 24px; + font-size: 20px; font-weight: bold; } @@ -1305,6 +1323,11 @@ nav ul li a:hover { color: var(--text-color-medium-dark); } +.loader-container { + display: flex; + align-items: center; +} + .small-loader { visibility: visible; border: 12px solid #f3f3f3; @@ -1322,6 +1345,25 @@ nav ul li a:hover { display: none; } +.tiny-loader { + visibility: visible; + border: 7px solid #f3f3f3; + border-top: 7px solid #3498db; + border-radius: 50%; + width: 7px; + height: 7px; + margin-left: 10px; + margin-right: 10px; + margin-top: 0px; + margin-bottom: 0px; + animation: spin 2s linear infinite; + display: inline-block; +} + +.tiny-loader.hidden { + display: none; +} + .error { color: red; } diff --git a/open-fin-al/src/index.html b/open-fin-al/src/index.html index 698597a2..743a5370 100644 --- a/open-fin-al/src/index.html +++ b/open-fin-al/src/index.html @@ -4,6 +4,7 @@ OpenFinAL: Open-source Financial Adaptive Learning System +