Skip to content

Commit b5dd106

Browse files
added scripts to compare vs python package
1 parent e87b830 commit b5dd106

File tree

3 files changed

+236
-1
lines changed

3 files changed

+236
-1
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@
3232
"plot": "tsx scripts/plot.ts",
3333
"plot:iris": "tsx scripts/plot.ts --iris",
3434
"plot:captor": "tsx scripts/plot.ts",
35-
"efficient-frontier": "tsx scripts/efficient-frontier.ts"
35+
"efficient-frontier": "tsx scripts/efficient-frontier.ts",
36+
"compare-metrics": "tsx scripts/compare-metrics.ts"
3637
},
3738
"keywords": [
3839
"finance",

scripts/compare-metrics.ts

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Compare TypeScript openseries-ts metrics vs Python metrics (from data.json).
4+
*
5+
* Loads iris.json into OpenTimeSeries, computes metrics with the TS implementation,
6+
* and displays them side-by-side with Python-calculated metrics from data.json.
7+
*
8+
* Run: npx tsx scripts/compare-metrics.ts [--decimals=N]
9+
* npm run compare-metrics -- --decimals=6
10+
*
11+
* Options:
12+
* --decimals=N Number of decimal places for comparison (default: 4)
13+
* --decimals N Same, space-separated
14+
* COMPARE_DECIMALS Env var (avoids npm -- when using npm run)
15+
*
16+
* Requires iris.json and data.json in ~/Documents (from the Python fetch snippet).
17+
*/
18+
19+
import { existsSync, readFileSync } from "node:fs";
20+
import { homedir } from "node:os";
21+
import { join } from "node:path";
22+
23+
import { OpenTimeSeries } from "../src/series";
24+
25+
const DOCUMENTS = join(homedir(), "Documents");
26+
const IRIS_PATH = join(DOCUMENTS, "iris.json");
27+
const DATA_PATH = join(DOCUMENTS, "data.json");
28+
29+
interface IrisJsonItem {
30+
dates: string[];
31+
values: number[];
32+
name?: string;
33+
timeseries_id?: string;
34+
}
35+
36+
function loadSeriesFromIrisJson(path: string): OpenTimeSeries {
37+
const raw = JSON.parse(readFileSync(path, "utf-8"));
38+
const item: IrisJsonItem = Array.isArray(raw) ? raw[0] : raw;
39+
return OpenTimeSeries.fromArrays(
40+
item.name ?? "Series",
41+
item.dates,
42+
item.values,
43+
{ timeseriesId: item.timeseries_id ?? "", countries: "SE" },
44+
);
45+
}
46+
47+
function loadPythonMetrics(path: string): Record<string, unknown> {
48+
const raw = JSON.parse(readFileSync(path, "utf-8"));
49+
// data.json: {"Captor Iris Bond": {"value_ret": 0.025, "geo_ret": ..., ...}}
50+
const col = typeof raw === "object" && raw !== null ? Object.values(raw)[0] : raw;
51+
return (col as Record<string, unknown>) ?? {};
52+
}
53+
54+
function tsDateStr(v: unknown): string {
55+
if (typeof v === "string") return v.slice(0, 10);
56+
return String(v);
57+
}
58+
59+
function roundToStr(n: number, decimals: number): string {
60+
if (Number.isNaN(n)) return "NaN";
61+
if (!Number.isFinite(n)) return String(n);
62+
if (decimals <= 0) return String(Math.round(n));
63+
const rounded = Number(n.toFixed(decimals));
64+
return Number.isInteger(rounded) ? String(rounded) : n.toFixed(decimals);
65+
}
66+
67+
function formatForDisplay(v: unknown, decimals: number): string {
68+
if (v === undefined) return "—";
69+
if (typeof v === "number") return roundToStr(v, decimals);
70+
return tsDateStr(v);
71+
}
72+
73+
function parseDecimals(args: string[]): number {
74+
const def = 4;
75+
// --decimals=N
76+
const eqArg = args.find((a) => a.startsWith("--decimals="));
77+
if (eqArg) {
78+
const n = parseInt(eqArg.slice(11), 10);
79+
if (Number.isInteger(n) && n >= 0) return n;
80+
}
81+
// --decimals N
82+
const idx = args.indexOf("--decimals");
83+
if (idx >= 0 && args[idx + 1] != null) {
84+
const n = parseInt(args[idx + 1]!, 10);
85+
if (Number.isInteger(n) && n >= 0) return n;
86+
}
87+
// env COMPARE_DECIMALS (works with npm run even without --)
88+
const env = process.env.COMPARE_DECIMALS;
89+
if (env) {
90+
const n = parseInt(env, 10);
91+
if (Number.isInteger(n) && n >= 0) return n;
92+
}
93+
return def;
94+
}
95+
96+
function main(): void {
97+
const decimals = parseDecimals(process.argv.slice(2));
98+
99+
if (!existsSync(IRIS_PATH)) {
100+
console.error(`iris.json not found at ${IRIS_PATH}`);
101+
process.exit(1);
102+
}
103+
if (!existsSync(DATA_PATH)) {
104+
console.error(`data.json not found at ${DATA_PATH}`);
105+
process.exit(1);
106+
}
107+
108+
console.log("Loading timeseries from iris.json...");
109+
const series = loadSeriesFromIrisJson(IRIS_PATH);
110+
111+
console.log("Loading Python metrics from data.json...");
112+
const pythonMetrics = loadPythonMetrics(DATA_PATH);
113+
114+
// Compute TS metrics; map Python property names to TS getters/methods
115+
const tsMetrics: Record<string, string | number> = {};
116+
117+
const metrics: { pyKey: string; getTs: () => number | string }[] = [
118+
{ pyKey: "value_ret", getTs: () => series.valueRet() },
119+
{ pyKey: "geo_ret", getTs: () => series.geoRet() },
120+
{ pyKey: "arithmetic_ret", getTs: () => series.arithmeticRet() },
121+
{ pyKey: "vol", getTs: () => series.vol() },
122+
{ pyKey: "downside_deviation", getTs: () => series.downsideDeviation() },
123+
{ pyKey: "ret_vol_ratio", getTs: () => series.retVolRatio(0) },
124+
{ pyKey: "sortino_ratio", getTs: () => series.sortinoRatio(0, 0) },
125+
{ pyKey: "z_score", getTs: () => series.zScore() },
126+
{ pyKey: "skew", getTs: () => series.skew() },
127+
{ pyKey: "kurtosis", getTs: () => series.kurtosis() },
128+
{ pyKey: "positive_share", getTs: () => series.positiveShare() },
129+
{ pyKey: "var_down", getTs: () => series.varDown(0.95) },
130+
{ pyKey: "cvar_down", getTs: () => series.cvarDown(0.95) },
131+
{ pyKey: "vol_from_var", getTs: () => series.volFromVar(0.95) },
132+
{ pyKey: "worst", getTs: () => series.worst(1) },
133+
{ pyKey: "worst_month", getTs: () => series.worstMonth() },
134+
{ pyKey: "max_drawdown", getTs: () => series.maxDrawdown() },
135+
{ pyKey: "first_idx", getTs: () => series.firstIdx },
136+
{ pyKey: "last_idx", getTs: () => series.lastIdx },
137+
{ pyKey: "length", getTs: () => series.length },
138+
{ pyKey: "span_of_days", getTs: () => series.spanOfDays },
139+
{ pyKey: "yearfrac", getTs: () => series.yearfrac },
140+
{ pyKey: "periods_in_a_year", getTs: () => series.periodsInAYear },
141+
];
142+
143+
for (const { pyKey, getTs } of metrics) {
144+
try {
145+
const v = getTs();
146+
tsMetrics[pyKey] = typeof v === "number" ? v : v;
147+
} catch {
148+
tsMetrics[pyKey] = NaN;
149+
}
150+
}
151+
152+
// Build comparison table
153+
const allKeys = new Set([
154+
...Object.keys(pythonMetrics),
155+
...Object.keys(tsMetrics),
156+
]);
157+
const sortedKeys = [...allKeys].sort();
158+
159+
const rows: string[] = [];
160+
let matches = 0;
161+
162+
const col1 = 35;
163+
const col2 = 28;
164+
const col3 = 28;
165+
const col4 = 6;
166+
167+
const header =
168+
"Metric".padEnd(col1) +
169+
"Python (data.json)".padEnd(col2) +
170+
"TypeScript (openseries-ts)".padEnd(col3) +
171+
"Match".padEnd(col4);
172+
rows.push(header);
173+
rows.push("=".repeat(col1 + col2 + col3 + col4));
174+
175+
for (const key of sortedKeys) {
176+
const pyVal = pythonMetrics[key];
177+
const tsVal = tsMetrics[key];
178+
179+
const pyStr = formatForDisplay(pyVal, decimals);
180+
const tsStr = formatForDisplay(tsVal, decimals);
181+
182+
let match = "";
183+
if (pyVal !== undefined && tsVal !== undefined) {
184+
let matchStr = false;
185+
if (typeof pyVal === "number" && typeof tsVal === "number") {
186+
matchStr = roundToStr(pyVal, decimals) === roundToStr(tsVal, decimals);
187+
} else {
188+
matchStr = tsDateStr(pyVal) === String(tsVal).slice(0, 10);
189+
}
190+
if (matchStr) {
191+
match = "✓";
192+
matches++;
193+
}
194+
}
195+
196+
rows.push(
197+
key.padEnd(col1) +
198+
pyStr.padEnd(col2) +
199+
tsStr.padEnd(col3) +
200+
match.padEnd(col4),
201+
);
202+
}
203+
204+
console.log("\n" + rows.join("\n"));
205+
console.log("=".repeat(col1 + col2 + col3 + col4));
206+
207+
const compared = sortedKeys.filter((k) => pythonMetrics[k] !== undefined && tsMetrics[k] !== undefined);
208+
console.log(`\nMatched: ${matches}/${compared.length} (rounded to ${decimals} decimals or same date)`);
209+
}
210+
211+
main();

scripts/load_py_openseries_data.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"""Module to create metrics from the openseries Python package."""
2+
3+
from requests import get as requests_get
4+
from pathlib import Path
5+
from openseries import OpenTimeSeries
6+
7+
if __name__ == "__main__":
8+
tid = "5b72a10c23d27735104e0576"
9+
urliris = f"https://api.captor.se/public/api/opentimeseries/{tid}"
10+
resp_iris = requests_get(urliris, timeout=10)
11+
resp_iris.raise_for_status()
12+
idata = resp_iris.json()
13+
iris = OpenTimeSeries.from_arrays(
14+
name="Captor Iris Bond",
15+
dates=idata["dates"],
16+
values=idata["values"],
17+
timeseries_id=tid
18+
)
19+
_ = iris.to_json(what_output="values", filename="iris.json")
20+
data = iris.all_properties()
21+
data.columns = data.columns.droplevel(level=1)
22+
data_path = Path.home() / "Documents" / "data.json"
23+
data.to_json(data_path, date_format="iso")

0 commit comments

Comments
 (0)