Skip to content

Commit 44d067b

Browse files
committed
Add stock peers demo apps to React Workspace
1 parent d2b8439 commit 44d067b

File tree

6 files changed

+274
-1
lines changed

6 files changed

+274
-1
lines changed

frameworks/react/workspace/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

33
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
44

5+
## Quick Start
6+
7+
```
8+
npm install
9+
npm start
10+
```
11+
12+
Open [fin://localhost:3000/platform/manifest.fin.json](fin://localhost:3000/platform/manifest.fin.json) in your web browser to launch OpenFin.
13+
14+
515
## Available Scripts
616

717
In the project directory, you can run:

frameworks/react/workspace/public/platform/manifest.fin.json

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,44 @@
6363
"intents": [],
6464
"images": [],
6565
"tags": ["view", "openfin"]
66+
},
67+
{
68+
"appId": "stock-peers",
69+
"name": "stock-peers",
70+
"title": "Streamlit Stock Peers",
71+
"description": "Streamlit Stock Peers",
72+
"manifestType": "view",
73+
"manifest": "http://localhost:3000/views/stock-peers.fin.json",
74+
"icons": [
75+
{
76+
"src": "http://localhost:3000/favicon.ico"
77+
}
78+
],
79+
"contactEmail": "contact@example.com",
80+
"supportEmail": "support@example.com",
81+
"publisher": "OpenFin",
82+
"intents": [],
83+
"images": [],
84+
"tags": ["view", "openfin"]
85+
},
86+
{
87+
"appId": "stock-peers-monitor",
88+
"name": "stock-peers-monitor",
89+
"title": "Streamlit Monitor",
90+
"description": "Streamlit Monitor",
91+
"manifestType": "view",
92+
"manifest": "http://localhost:3000/views/stock-peers-monitor.fin.json",
93+
"icons": [
94+
{
95+
"src": "http://localhost:3000/favicon.ico"
96+
}
97+
],
98+
"contactEmail": "contact@example.com",
99+
"supportEmail": "support@example.com",
100+
"publisher": "OpenFin",
101+
"intents": [],
102+
"images": [],
103+
"tags": ["view", "openfin"]
66104
}
67105
]
68106
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"url": "http://localhost:3000/views/stock-peers-monitor",
3+
"fdc3InteropApi": "2.0",
4+
"interop": {
5+
"currentContextGroup": "green"
6+
}
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"url": "http://localhost:8501",
3+
"fdc3InteropApi": "2.0",
4+
"interop": {
5+
"currentContextGroup": "green"
6+
}
7+
}

frameworks/react/workspace/src/index.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import ReactDOM from 'react-dom/client';
33
import { BrowserRouter, Route, Routes } from "react-router-dom";
44
import App from './App';
55
import './index.css';
6+
import { Monitor } from "./views/monitor";
67

78
const Provider = React.lazy(() => import('./platform/Provider'));
89
const View1 = React.lazy(() => import('./views/View1'));
@@ -18,8 +19,9 @@ root.render(
1819
<Route path="/" element={<App />}></Route>
1920
<Route path="/views/view1" element={<View1 />}></Route>
2021
<Route path="/views/view2" element={<View2 />}></Route>
22+
<Route path="/views/stock-peers-monitor" element={<Monitor />}></Route>
2123
<Route path="/platform/provider" element={<Provider />}></Route>
2224
</Routes>
2325
</BrowserRouter>
2426
</React.StrictMode>
25-
);
27+
);
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import type { Context } from "@finos/fdc3";
2+
import { useEffect, useMemo, useState } from "react";
3+
4+
// Simple mock quote generator
5+
function hash(str: string): number {
6+
let h = 2166136261;
7+
for (let i = 0; i < str.length; i++) {
8+
h ^= str.charCodeAt(i);
9+
h += (h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24);
10+
}
11+
return Math.abs(h >>> 0);
12+
}
13+
14+
function getMockQuote(ticker: string, horizonValue?: string) {
15+
const base = hash(ticker) % 500 + 20; // $20 - $520
16+
// Map horizon to a simple volatility multiplier so the UI "feels" different per selection
17+
let volMultiplier = 1;
18+
if (horizonValue) {
19+
if (horizonValue.includes("1d")) volMultiplier = 0.2;
20+
else if (horizonValue.includes("1w")) volMultiplier = 0.5;
21+
else if (horizonValue.includes("1mo")) volMultiplier = 1.0;
22+
else if (horizonValue.includes("3mo")) volMultiplier = 1.05;
23+
else if (horizonValue.includes("6mo")) volMultiplier = 1.1;
24+
else if (horizonValue.includes("1y")) volMultiplier = 1.15;
25+
else if (horizonValue.includes("5y")) volMultiplier = 1.2;
26+
else if (horizonValue.includes("10y")) volMultiplier = 1.25;
27+
else if (horizonValue.includes("20y")) volMultiplier = 1.3;
28+
}
29+
const change = (((hash(ticker + "chg") % 2000) - 1000) / 100) * volMultiplier; // -10%..+10% scaled
30+
const price = Math.max(1, base * (1 + change / 100));
31+
const prevClose = price / (1 + change / 100);
32+
return {
33+
price: Number(price.toFixed(2)),
34+
changePct: Number(change.toFixed(2)),
35+
changeAbs: Number((price - prevClose).toFixed(2)),
36+
volume: (hash(ticker + "vol") % 9_000_000) + 1_000_000,
37+
dayHigh: Number((price * 1.02).toFixed(2)),
38+
dayLow: Number((price * 0.98).toFixed(2)),
39+
};
40+
}
41+
42+
export function Monitor() {
43+
44+
// Demo state populated from interop contexts
45+
const [tickers, setTickers] = useState<string[]>([]);
46+
const [lastContextAt, setLastContextAt] = useState<string | null>(null);
47+
const [horizonLabel, setHorizonLabel] = useState<string>("—");
48+
const [horizonValue, setHorizonValue] = useState<string | undefined>(undefined);
49+
const [horizonTs, setHorizonTs] = useState<string | null>(null);
50+
const [sessionContextGroup, setSessionContextGroup] = useState<any | null>(null);
51+
52+
const quotes = useMemo(() => {
53+
return tickers.map((t) => ({
54+
ticker: t,
55+
...getMockQuote(t, horizonValue),
56+
}));
57+
}, [tickers, horizonValue]);
58+
59+
// Outbound: broadcast contexts back to the session group
60+
const handleBroadcastInstruments = async () => {
61+
if (!sessionContextGroup) return;
62+
const list = (tickers.length > 0 ? tickers : ["AAPL", "AMZN", "META"]).map((t) => ({
63+
type: "instrument",
64+
id: { ticker: t.toUpperCase() },
65+
}));
66+
console.log('Setting instrument context:', list);
67+
await sessionContextGroup.setContext({
68+
type: "instrumentList",
69+
instruments: list,
70+
});
71+
};
72+
73+
const handleSetHorizon = async (label: string, value: string) => {
74+
if (!sessionContextGroup) return;
75+
console.log('Setting horizon context:', label, value);
76+
await sessionContextGroup.setContext({
77+
type: "streamlit.timeHorizon",
78+
horizon: label,
79+
horizonValue: value,
80+
timestamp: new Date().toISOString(),
81+
});
82+
};
83+
84+
useEffect(() => {
85+
if (!fin) {
86+
return;
87+
}
88+
89+
(async () => {
90+
console.log("Listening to session context group");
91+
92+
const interop = fin.Interop.connectSync(fin.me.uuid, {});
93+
const contextGroup = await interop.joinSessionContextGroup("stockpeers");
94+
setSessionContextGroup(contextGroup);
95+
await contextGroup.addContextHandler((context: Context) => {
96+
console.log("Received context", context);
97+
98+
// Handle instrument list
99+
if (context?.type === "instrumentList") {
100+
const list = context?.instruments as any[] | undefined;
101+
const next = (list ?? [])
102+
.map((i) => i?.id?.ticker as string)
103+
.filter((t): t is string => Boolean(t))
104+
.map((t) => t.toUpperCase());
105+
// de-dup and keep order
106+
const unique: string[] = [];
107+
next.forEach((t) => {
108+
if (!unique.includes(t)) unique.push(t);
109+
});
110+
setTickers(unique);
111+
setLastContextAt(new Date().toISOString());
112+
return;
113+
}
114+
115+
// Handle time horizon
116+
if (context.type === "streamlit.timeHorizon") {
117+
const c: Context = context;
118+
setHorizonLabel(c.horizon ?? "—");
119+
setHorizonValue(c.horizonValue);
120+
setHorizonTs(c.timestamp ?? new Date().toISOString());
121+
return;
122+
}
123+
});
124+
})();
125+
}, []);
126+
127+
return (
128+
<div style={{ display: "flex", flexDirection: "column", gap: 16, padding: 16 }}>
129+
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
130+
<h2 style={{ margin: 0 }}>Market Monitor</h2>
131+
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
132+
<div
133+
style={{
134+
border: "1px solid #e0e0e0",
135+
borderRadius: 8,
136+
padding: "6px 10px",
137+
background: "#fafafa",
138+
}}
139+
title={horizonTs ? `Updated ${new Date(horizonTs).toLocaleString()}` : undefined}
140+
>
141+
<span style={{ color: "#666", marginRight: 6 }}>Horizon:&nbsp;
142+
<strong>{horizonLabel}</strong></span>
143+
</div>
144+
</div>
145+
</div>
146+
147+
<div style={{ fontSize: 12, color: "#666" }}>
148+
{lastContextAt ? `Last context @ ${new Date(lastContextAt).toLocaleTimeString()}` : "Awaiting context…"}
149+
</div>
150+
151+
{/* Outbound controls: send contexts to the session group */}
152+
<div style={{ display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" }}>
153+
<button type="button" onClick={handleBroadcastInstruments} disabled={!sessionContextGroup} title={!sessionContextGroup ? "Joining context group…" : undefined}>
154+
Broadcast instrumentList
155+
</button>
156+
<span style={{ color: "#888" }}>|</span>
157+
<span style={{ color: "#666" }}>Set Horizon:</span>
158+
<button type="button" onClick={() => handleSetHorizon("1 Month", "1mo")} disabled={!sessionContextGroup}>1M</button>
159+
<button type="button" onClick={() => handleSetHorizon("3 Months", "3mo")} disabled={!sessionContextGroup}>3M</button>
160+
<button type="button" onClick={() => handleSetHorizon("6 Months", "6mo")} disabled={!sessionContextGroup}>6M</button>
161+
<button type="button" onClick={() => handleSetHorizon("1 Year", "1y")} disabled={!sessionContextGroup}>1Y</button>
162+
<button type="button" onClick={() => handleSetHorizon("5 Years", "5y")} disabled={!sessionContextGroup}>5Y</button>
163+
<button type="button" onClick={() => handleSetHorizon("10 Years", "10y")} disabled={!sessionContextGroup}>10Y</button>
164+
<button type="button" onClick={() => handleSetHorizon("20 Years", "20y")} disabled={!sessionContextGroup}>20Y</button>
165+
</div>
166+
167+
<div style={{ overflow: "auto", border: "1px solid #eee", borderRadius: 8 }}>
168+
<table style={{ width: "100%", borderCollapse: "separate", borderSpacing: 0 }}>
169+
<thead>
170+
<tr style={{ background: "#f5f7fa" }}>
171+
<th style={{ textAlign: "left", padding: "8px 12px", borderBottom: "1px solid #e5e7eb" }}>Ticker</th>
172+
<th style={{ textAlign: "right", padding: "8px 12px", borderBottom: "1px solid #e5e7eb" }}>Price</th>
173+
<th style={{ textAlign: "right", padding: "8px 12px", borderBottom: "1px solid #e5e7eb" }}>Change</th>
174+
<th style={{ textAlign: "right", padding: "8px 12px", borderBottom: "1px solid #e5e7eb" }}>%</th>
175+
<th style={{ textAlign: "right", padding: "8px 12px", borderBottom: "1px solid #e5e7eb" }}>Volume</th>
176+
<th style={{ textAlign: "right", padding: "8px 12px", borderBottom: "1px solid #e5e7eb" }}>Day Range</th>
177+
</tr>
178+
</thead>
179+
<tbody>
180+
{quotes.length === 0 ? (
181+
<tr>
182+
<td colSpan={6} style={{ padding: 16, textAlign: "center", color: "#888" }}>
183+
Send an `instrumentList` context to populate the blotter
184+
</td>
185+
</tr>
186+
) : (
187+
quotes.map((q) => {
188+
const up = q.changePct >= 0;
189+
const color = up ? "#059669" : "#dc2626";
190+
return (
191+
<tr key={q.ticker}>
192+
<td style={{ padding: "8px 12px", borderBottom: "1px solid #f0f0f0", fontWeight: 600 }}>{q.ticker}</td>
193+
<td style={{ padding: "8px 12px", borderBottom: "1px solid #f0f0f0", textAlign: "right" }}>${q.price.toFixed(2)}</td>
194+
<td style={{ padding: "8px 12px", borderBottom: "1px solid #f0f0f0", textAlign: "right", color }}>{up ? "+" : ""}{q.changeAbs.toFixed(2)}</td>
195+
<td style={{ padding: "8px 12px", borderBottom: "1px solid #f0f0f0", textAlign: "right", color }}>{up ? "+" : ""}{q.changePct.toFixed(2)}%</td>
196+
<td style={{ padding: "8px 12px", borderBottom: "1px solid #f0f0f0", textAlign: "right" }}>{q.volume.toLocaleString()}</td>
197+
<td style={{ padding: "8px 12px", borderBottom: "1px solid #f0f0f0", textAlign: "right" }}>
198+
${q.dayLow.toFixed(2)} – ${q.dayHigh.toFixed(2)}
199+
</td>
200+
</tr>
201+
);
202+
})
203+
)}
204+
</tbody>
205+
</table>
206+
</div>
207+
</div>
208+
);
209+
}

0 commit comments

Comments
 (0)