@agenticmail/enterprise 0.5.441 → 0.5.443
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/README.md +3 -2
- package/dist/agent-heartbeat-QJ3WTWUK.js +518 -0
- package/dist/agent-status-CS5NNYIO.js +11 -0
- package/dist/agent-tools-3MD7EPO6.js +14629 -0
- package/dist/agent-tools-62HGQYDL.js +14629 -0
- package/dist/agent-tools-BTUVVOWT.js +14629 -0
- package/dist/agent-tools-BV3JMJPN.js +14629 -0
- package/dist/agent-tools-JVNQ4RPM.js +14629 -0
- package/dist/agent-tools-LD7LELVT.js +14629 -0
- package/dist/agent-tools-NRKQ5NMW.js +14629 -0
- package/dist/agent-tools-OXZJOD4D.js +14629 -0
- package/dist/agent-tools-PT2ES6G5.js +14629 -0
- package/dist/agent-tools-SS335E2E.js +14629 -0
- package/dist/chunk-2ACTQSPC.js +1728 -0
- package/dist/chunk-2XZE5NT3.js +5387 -0
- package/dist/chunk-325NTNE6.js +2180 -0
- package/dist/chunk-3AHKVWCO.js +5387 -0
- package/dist/chunk-3DM6QDVR.js +1728 -0
- package/dist/chunk-3L2N6LR2.js +7517 -0
- package/dist/chunk-3XP7VYIS.js +7437 -0
- package/dist/chunk-3ZD3XOV6.js +1728 -0
- package/dist/chunk-44VXWYPE.js +2570 -0
- package/dist/chunk-4UOSUXFR.js +7529 -0
- package/dist/chunk-54SHHX2S.js +2180 -0
- package/dist/chunk-5DVY3CNK.js +1728 -0
- package/dist/chunk-5F5DGV63.js +5596 -0
- package/dist/chunk-6IUUOWZY.js +7526 -0
- package/dist/chunk-77LFFLQX.js +7526 -0
- package/dist/chunk-7BKNDBSA.js +2191 -0
- package/dist/chunk-7BS2XPLW.js +5359 -0
- package/dist/chunk-7GCSGDXQ.js +1636 -0
- package/dist/chunk-7IAI45PR.js +4735 -0
- package/dist/chunk-7QPZGO27.js +4976 -0
- package/dist/chunk-7S5XAXRD.js +4747 -0
- package/dist/chunk-AD6ZR3QD.js +5357 -0
- package/dist/chunk-AO556OXZ.js +2570 -0
- package/dist/chunk-APUWQHXC.js +7437 -0
- package/dist/chunk-BKSSHV6C.js +1728 -0
- package/dist/chunk-BMUNQHLU.js +1208 -0
- package/dist/chunk-BPF5DKUM.js +392 -0
- package/dist/chunk-CFR5OSMI.js +1220 -0
- package/dist/chunk-CM4GUHXP.js +1728 -0
- package/dist/chunk-CVFIM72Q.js +501 -0
- package/dist/chunk-DFXVJNKW.js +4734 -0
- package/dist/chunk-DJ7LHRL7.js +2214 -0
- package/dist/chunk-DJFI6NR6.js +1624 -0
- package/dist/chunk-DJHYV5VY.js +1624 -0
- package/dist/chunk-DYM4PQO3.js +4727 -0
- package/dist/chunk-DZ75RF35.js +1728 -0
- package/dist/chunk-E5H64U5H.js +472 -0
- package/dist/chunk-EOD4EGFI.js +1728 -0
- package/dist/chunk-EP74QR5B.js +4980 -0
- package/dist/chunk-FE2M4FV5.js +5273 -0
- package/dist/chunk-FQWJMPKW.js +305 -0
- package/dist/chunk-FUH6NWIX.js +7540 -0
- package/dist/chunk-G5ADHHAD.js +1728 -0
- package/dist/chunk-GAYLPSM7.js +7529 -0
- package/dist/chunk-GDAFZJTJ.js +7540 -0
- package/dist/chunk-GLAN2JBA.js +1636 -0
- package/dist/chunk-GNAEBGU7.js +4812 -0
- package/dist/chunk-HDDA2Q3Q.js +1728 -0
- package/dist/chunk-HG62FWWQ.js +7437 -0
- package/dist/chunk-HV5VIS5K.js +1624 -0
- package/dist/chunk-IEVA23WK.js +4976 -0
- package/dist/chunk-IFMZ2IYC.js +7540 -0
- package/dist/chunk-IN7VIORK.js +2641 -0
- package/dist/chunk-KAX3ZDP2.js +7529 -0
- package/dist/chunk-KECO53GP.js +1728 -0
- package/dist/chunk-KGZ74UMA.js +1728 -0
- package/dist/chunk-M2ZLRUMX.js +7540 -0
- package/dist/chunk-MKQWC6KF.js +5374 -0
- package/dist/chunk-MQ3JBGLU.js +1636 -0
- package/dist/chunk-MVB6JARX.js +7529 -0
- package/dist/chunk-N37MOOFE.js +2210 -0
- package/dist/chunk-NELCAZUQ.js +5357 -0
- package/dist/chunk-NHLOKTUV.js +26305 -0
- package/dist/chunk-OPAO5QQS.js +1728 -0
- package/dist/chunk-OUHU3VW6.js +1728 -0
- package/dist/chunk-OWEXZVZ6.js +1728 -0
- package/dist/chunk-P4DHSJJY.js +7437 -0
- package/dist/chunk-Q7QA6MNJ.js +1728 -0
- package/dist/chunk-QKKTNPGV.js +7592 -0
- package/dist/chunk-REMMXZVU.js +7529 -0
- package/dist/chunk-RK3KATD4.js +4756 -0
- package/dist/chunk-RQ33L5T3.js +5374 -0
- package/dist/chunk-RU77F65Q.js +1728 -0
- package/dist/chunk-SAM3CVIU.js +5374 -0
- package/dist/chunk-SFMXIKWZ.js +1728 -0
- package/dist/chunk-SPZ5JBGW.js +1624 -0
- package/dist/chunk-TDOC6WSK.js +7529 -0
- package/dist/chunk-TPMT5WTW.js +5357 -0
- package/dist/chunk-TVXUR3PB.js +2180 -0
- package/dist/chunk-TZEEVEKG.js +5343 -0
- package/dist/chunk-U327O3ZR.js +7540 -0
- package/dist/chunk-ULD6C5DB.js +2180 -0
- package/dist/chunk-ULVRCJZV.js +5374 -0
- package/dist/chunk-UOCDOM2S.js +1624 -0
- package/dist/chunk-VINDZLLX.js +1636 -0
- package/dist/chunk-VODW5GJL.js +2180 -0
- package/dist/chunk-WEWL7Z3C.js +2180 -0
- package/dist/chunk-WG2MEMS6.js +5343 -0
- package/dist/chunk-WYV6QWOJ.js +1188 -0
- package/dist/chunk-X5AIAD77.js +1636 -0
- package/dist/chunk-XUETIRDV.js +2214 -0
- package/dist/chunk-XWSMX7RK.js +7529 -0
- package/dist/chunk-YUSYXHKB.js +1728 -0
- package/dist/chunk-ZC2RMHQD.js +5374 -0
- package/dist/chunk-ZDZQQGFG.js +5374 -0
- package/dist/chunk-ZJ2HCWKF.js +1728 -0
- package/dist/chunk-ZK4QCOJ5.js +5273 -0
- package/dist/chunk-ZMKVEJKR.js +392 -0
- package/dist/cli-agent-2UFGFO24.js +2761 -0
- package/dist/cli-agent-5FPUCFPG.js +2761 -0
- package/dist/cli-agent-7AB6NIYQ.js +2761 -0
- package/dist/cli-agent-7CAFSYOM.js +2761 -0
- package/dist/cli-agent-7OVOINHR.js +2757 -0
- package/dist/cli-agent-AWKBFIRS.js +2761 -0
- package/dist/cli-agent-ES3XOPHJ.js +2757 -0
- package/dist/cli-agent-FSO2N7I6.js +2757 -0
- package/dist/cli-agent-KPXZN5JK.js +2761 -0
- package/dist/cli-agent-LGTCFHGS.js +2757 -0
- package/dist/cli-agent-LLUYUHHF.js +2757 -0
- package/dist/cli-agent-NHZWYX5Q.js +2761 -0
- package/dist/cli-agent-TYUOTYCO.js +2757 -0
- package/dist/cli-agent-VQO6HI65.js +2757 -0
- package/dist/cli-agent-VV5JWRU7.js +2757 -0
- package/dist/cli-agent-Z5B23XED.js +2757 -0
- package/dist/cli-serve-2FLQXJW7.js +322 -0
- package/dist/cli-serve-34YFCCUX.js +322 -0
- package/dist/cli-serve-3NS6MNUS.js +322 -0
- package/dist/cli-serve-4PWFEPNA.js +322 -0
- package/dist/cli-serve-4VIBMXMT.js +322 -0
- package/dist/cli-serve-5ZUF5MGH.js +322 -0
- package/dist/cli-serve-6HGL56GB.js +322 -0
- package/dist/cli-serve-BBBWEKKW.js +322 -0
- package/dist/cli-serve-D2HQC4SB.js +322 -0
- package/dist/cli-serve-IKCMNUNM.js +322 -0
- package/dist/cli-serve-LTUKYMEF.js +322 -0
- package/dist/cli-serve-MIWI5PBE.js +322 -0
- package/dist/cli-serve-R6K4SZ3L.js +322 -0
- package/dist/cli-serve-RQIOBQGF.js +322 -0
- package/dist/cli-serve-S57ROYQ6.js +322 -0
- package/dist/cli-serve-WVSLOISY.js +322 -0
- package/dist/cli-serve-XDFSJIQV.js +322 -0
- package/dist/cli-serve-YKZSNR3P.js +322 -0
- package/dist/cli-serve-ZGKXWDMZ.js +322 -0
- package/dist/cli-serve-ZZOJWREY.js +322 -0
- package/dist/cli.js +3 -3
- package/dist/dashboard/components/utils.js +1 -1
- package/dist/dashboard/docs/polymarket.html +1 -1
- package/dist/dashboard/pages/polymarket.js +222 -80
- package/dist/index.js +12 -12
- package/dist/pipeline-C4C3ZF4X.js +15 -0
- package/dist/pipeline-DAF3EV7Q.js +15 -0
- package/dist/pipeline-MMESLRQG.js +15 -0
- package/dist/polymarket-3LGJSQZK.js +17 -0
- package/dist/polymarket-4OIWQFBO.js +17 -0
- package/dist/polymarket-AZEV7C6E.js +17 -0
- package/dist/polymarket-DBQYJC57.js +7 -0
- package/dist/polymarket-EVTKWLUO.js +7 -0
- package/dist/polymarket-GMKDVVQH.js +17 -0
- package/dist/polymarket-HZOAD4W4.js +17 -0
- package/dist/polymarket-PKGRF7ID.js +17 -0
- package/dist/polymarket-QUNR2H7U.js +17 -0
- package/dist/polymarket-TM624BN5.js +17 -0
- package/dist/polymarket-VT2EDGD7.js +17 -0
- package/dist/polymarket-W7B4TLP3.js +17 -0
- package/dist/polymarket-WS4VE6U7.js +7 -0
- package/dist/polymarket-runtime-772ADZJW.js +108 -0
- package/dist/polymarket-runtime-AIYHQKYQ.js +108 -0
- package/dist/polymarket-runtime-APJ5HW4Y.js +108 -0
- package/dist/polymarket-runtime-BX3ODZZD.js +108 -0
- package/dist/polymarket-runtime-ISD4CKKL.js +108 -0
- package/dist/polymarket-runtime-JU4PQZGJ.js +108 -0
- package/dist/polymarket-runtime-OFLBSPCK.js +108 -0
- package/dist/polymarket-runtime-POJRRWFS.js +108 -0
- package/dist/polymarket-runtime-XN5TVHU5.js +108 -0
- package/dist/polymarket-runtime-ZYFTKEJ4.js +108 -0
- package/dist/polymarket-watcher-2CKLTIXI.js +23 -0
- package/dist/polymarket-watcher-3C72X36B.js +23 -0
- package/dist/polymarket-watcher-IHRZ2URM.js +23 -0
- package/dist/polymarket-watcher-J7UWIN7I.js +23 -0
- package/dist/polymarket-watcher-M6CDW63Y.js +23 -0
- package/dist/polymarket-watcher-OSI73EDX.js +23 -0
- package/dist/polymarket-watcher-PN4JJV3V.js +23 -0
- package/dist/polymarket-watcher-PXK4NS5F.js +23 -0
- package/dist/polymarket-watcher-VDJ4SVHP.js +23 -0
- package/dist/polymarket-watcher-W4BV6JTG.js +23 -0
- package/dist/routes-PRBWZ4MQ.js +94 -0
- package/dist/runtime-22E2WCL6.js +46 -0
- package/dist/runtime-7GYS4FQU.js +46 -0
- package/dist/runtime-7OVDHKNW.js +46 -0
- package/dist/runtime-BR6K3Q3N.js +46 -0
- package/dist/runtime-CTTRDVJ6.js +46 -0
- package/dist/runtime-GIIPTHK5.js +46 -0
- package/dist/runtime-HYAKLZWI.js +46 -0
- package/dist/runtime-IQ2KIV2L.js +50 -0
- package/dist/runtime-ISXECIAB.js +46 -0
- package/dist/runtime-IV4BY7S4.js +46 -0
- package/dist/runtime-O65GGHL2.js +46 -0
- package/dist/runtime-PKHEGQYK.js +50 -0
- package/dist/runtime-RFKPIFK4.js +46 -0
- package/dist/runtime-S35B6PIY.js +46 -0
- package/dist/runtime-WEZJ4YRK.js +46 -0
- package/dist/runtime-WLWB62ZI.js +46 -0
- package/dist/screener-FB47G4YX.js +26 -0
- package/dist/screener-WQVQO4WF.js +26 -0
- package/dist/server-6GWGIGBH.js +36 -0
- package/dist/server-7VZ6XXBC.js +36 -0
- package/dist/server-7XBTD3FW.js +36 -0
- package/dist/server-BN56IMC4.js +36 -0
- package/dist/server-GD6SPDER.js +36 -0
- package/dist/server-IJHU2FIK.js +36 -0
- package/dist/server-J5WZGZX3.js +36 -0
- package/dist/server-JVUH2E3H.js +36 -0
- package/dist/server-MYXWTXSE.js +36 -0
- package/dist/server-ORGV7O7L.js +36 -0
- package/dist/server-QPE3WVUQ.js +36 -0
- package/dist/server-SUBGS5BJ.js +36 -0
- package/dist/server-SWAYJTFF.js +36 -0
- package/dist/server-TFIWDU2H.js +36 -0
- package/dist/server-VH4W6LWW.js +36 -0
- package/dist/server-WQXRTEKF.js +36 -0
- package/dist/server-X5UMJJ3J.js +36 -0
- package/dist/server-XOK5LHNU.js +36 -0
- package/dist/server-ZLK24KFY.js +36 -0
- package/dist/server-ZNSQGCRM.js +36 -0
- package/dist/setup-32ZRMW7R.js +20 -0
- package/dist/setup-3WF7PBIS.js +20 -0
- package/dist/setup-4TM64QG4.js +20 -0
- package/dist/setup-5SGUMKN7.js +20 -0
- package/dist/setup-7ENTPINX.js +20 -0
- package/dist/setup-AGYKOCIU.js +20 -0
- package/dist/setup-EIP4IHZN.js +20 -0
- package/dist/setup-F5SAGQJJ.js +20 -0
- package/dist/setup-LMF76UUM.js +20 -0
- package/dist/setup-MA5QR3J4.js +20 -0
- package/dist/setup-MLIPR2U3.js +20 -0
- package/dist/setup-MLIZYTT6.js +20 -0
- package/dist/setup-OPVZ3GM7.js +20 -0
- package/dist/setup-PWFHUXOK.js +20 -0
- package/dist/setup-QF5GCTPE.js +20 -0
- package/dist/setup-SGF4OX3A.js +20 -0
- package/dist/setup-TLWKGWC6.js +20 -0
- package/dist/setup-TYCRH63P.js +20 -0
- package/dist/setup-USUBKTVX.js +20 -0
- package/dist/setup-VMNTNEB4.js +20 -0
- package/dist/shared-S5ROHYCX.js +69 -0
- package/dist/shared-TLWZZZSK.js +69 -0
- package/dist/system-prompts-7ZRL27FN.js +69 -0
- package/dist/system-prompts-CNXUF2AV.js +69 -0
- package/dist/system-prompts-SYGQRBRG.js +69 -0
- package/logs/cloudflared-error.log +4 -57
- package/logs/cloudflared-error__2026-03-11_00-00-00.log +289 -0
- package/package.json +1 -1
- package/logs/cloudflared-error__2026-03-06_00-00-00.log +0 -245
|
@@ -0,0 +1,2641 @@
|
|
|
1
|
+
import {
|
|
2
|
+
screenMarkets
|
|
3
|
+
} from "./chunk-ZMKVEJKR.js";
|
|
4
|
+
import {
|
|
5
|
+
CLOB_API,
|
|
6
|
+
CTF_ADDRESS,
|
|
7
|
+
GAMMA_API,
|
|
8
|
+
NEGATIVE_WORDS,
|
|
9
|
+
POSITIVE_WORDS,
|
|
10
|
+
USDC_ADDRESS,
|
|
11
|
+
apiFetch,
|
|
12
|
+
cachedFetchJSON,
|
|
13
|
+
cachedFetchText,
|
|
14
|
+
calculateHurst,
|
|
15
|
+
calculateVolatility,
|
|
16
|
+
ewma,
|
|
17
|
+
extractTopics,
|
|
18
|
+
fetchPriceHistory,
|
|
19
|
+
fetchPriceSeries,
|
|
20
|
+
linearRegression,
|
|
21
|
+
normalCDF,
|
|
22
|
+
normalInv,
|
|
23
|
+
normalPDF,
|
|
24
|
+
parseRSSItems,
|
|
25
|
+
pearsonCorrelation,
|
|
26
|
+
scoreSentiment,
|
|
27
|
+
sma,
|
|
28
|
+
std
|
|
29
|
+
} from "./chunk-CVFIM72Q.js";
|
|
30
|
+
|
|
31
|
+
// src/polymarket-engines/quant.ts
|
|
32
|
+
async function calculateKelly(params) {
|
|
33
|
+
let price = params.market_price;
|
|
34
|
+
if (!price && params.token_id) {
|
|
35
|
+
const mid = await apiFetch(`${CLOB_API}/midpoint?token_id=${params.token_id}`);
|
|
36
|
+
price = parseFloat(mid?.mid || "0.5");
|
|
37
|
+
}
|
|
38
|
+
if (!price) throw new Error("Provide market_price or token_id");
|
|
39
|
+
const prob = params.true_probability;
|
|
40
|
+
const q = 1 - prob;
|
|
41
|
+
const b = 1 / price - 1;
|
|
42
|
+
const kellyFraction = (prob * b - q) / b;
|
|
43
|
+
const halfKelly = kellyFraction / 2;
|
|
44
|
+
const quarterKelly = kellyFraction / 4;
|
|
45
|
+
const maxF = params.max_fraction || 0.25;
|
|
46
|
+
const cappedKelly = Math.min(Math.max(kellyFraction, 0), maxF);
|
|
47
|
+
const ev = prob * (1 / price - 1) - q;
|
|
48
|
+
const bankroll = params.bankroll || 0;
|
|
49
|
+
const optimalBet = bankroll * cappedKelly;
|
|
50
|
+
return {
|
|
51
|
+
formula: "f* = (p\xB7b - q) / b",
|
|
52
|
+
inputs: { true_probability: prob, market_price: price, odds: b.toFixed(4) },
|
|
53
|
+
kelly: {
|
|
54
|
+
full: parseFloat(kellyFraction.toFixed(6)),
|
|
55
|
+
half: parseFloat(halfKelly.toFixed(6)),
|
|
56
|
+
quarter: parseFloat(quarterKelly.toFixed(6)),
|
|
57
|
+
capped: parseFloat(cappedKelly.toFixed(6))
|
|
58
|
+
},
|
|
59
|
+
expected_value_per_dollar: parseFloat(ev.toFixed(6)),
|
|
60
|
+
edge_pct: parseFloat(((prob - price) * 100).toFixed(2)),
|
|
61
|
+
signal: kellyFraction > 0 ? "BUY" : kellyFraction < -0.01 ? "SELL" : "NO_EDGE",
|
|
62
|
+
recommended_bet: bankroll > 0 ? {
|
|
63
|
+
full_kelly: parseFloat((bankroll * Math.max(kellyFraction, 0)).toFixed(2)),
|
|
64
|
+
half_kelly: parseFloat((bankroll * Math.max(halfKelly, 0)).toFixed(2)),
|
|
65
|
+
quarter_kelly: parseFloat((bankroll * Math.max(quarterKelly, 0)).toFixed(2)),
|
|
66
|
+
capped: parseFloat(optimalBet.toFixed(2))
|
|
67
|
+
} : void 0,
|
|
68
|
+
warnings: [
|
|
69
|
+
kellyFraction > 0.5 ? "WARNING: Full Kelly > 50% \u2014 extremely aggressive, use half or quarter Kelly" : "",
|
|
70
|
+
kellyFraction <= 0 ? "No edge detected at this price \u2014 do not bet" : "",
|
|
71
|
+
Math.abs(prob - price) < 0.02 ? "Edge is very thin (<2%) \u2014 transaction costs may eliminate profit" : ""
|
|
72
|
+
].filter(Boolean)
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
async function priceBinaryOption(params) {
|
|
76
|
+
let price = params.current_price;
|
|
77
|
+
let vol = params.volatility;
|
|
78
|
+
if (!price && params.token_id) {
|
|
79
|
+
const mid = await apiFetch(`${CLOB_API}/midpoint?token_id=${params.token_id}`);
|
|
80
|
+
price = parseFloat(mid?.mid || "0.5");
|
|
81
|
+
}
|
|
82
|
+
if (!price) throw new Error("Provide current_price or token_id");
|
|
83
|
+
let T;
|
|
84
|
+
if (params.time_to_expiry_hours) {
|
|
85
|
+
T = params.time_to_expiry_hours / 8760;
|
|
86
|
+
} else if (params.end_date) {
|
|
87
|
+
T = Math.max(0, (new Date(params.end_date).getTime() - Date.now()) / (8760 * 36e5));
|
|
88
|
+
} else {
|
|
89
|
+
throw new Error("Provide time_to_expiry_hours or end_date");
|
|
90
|
+
}
|
|
91
|
+
if (!vol && params.token_id) {
|
|
92
|
+
const prices = await fetchPriceSeries(params.token_id, 100);
|
|
93
|
+
if (prices.length > 10) {
|
|
94
|
+
const returns = prices.slice(1).map((pr, i) => Math.log(pr / prices[i]));
|
|
95
|
+
vol = std(returns) * Math.sqrt(365 * 24);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
vol = vol || 1;
|
|
99
|
+
const clampedPrice = Math.max(0.01, Math.min(0.99, price));
|
|
100
|
+
const logOdds = Math.log(clampedPrice / (1 - clampedPrice));
|
|
101
|
+
const sqrtT = Math.sqrt(T);
|
|
102
|
+
const d1 = (logOdds + 0.5 * vol * vol * T) / (vol * sqrtT || 1);
|
|
103
|
+
const d2 = d1 - vol * sqrtT;
|
|
104
|
+
const theoreticalPrice = normalCDF(d2);
|
|
105
|
+
const delta = normalPDF(d2) / (vol * sqrtT * clampedPrice * (1 - clampedPrice) || 1);
|
|
106
|
+
const gamma = normalPDF(d2) * d1 / (vol * sqrtT * clampedPrice * clampedPrice * (1 - clampedPrice) * (1 - clampedPrice) || 1);
|
|
107
|
+
const theta = -normalPDF(d2) * vol / (2 * sqrtT || 1) / 8760;
|
|
108
|
+
const vega = normalPDF(d2) * sqrtT * 0.01;
|
|
109
|
+
const mispricing = params.true_probability ? {
|
|
110
|
+
your_estimate: params.true_probability,
|
|
111
|
+
market_price: price,
|
|
112
|
+
theoretical_price: parseFloat(theoreticalPrice.toFixed(4)),
|
|
113
|
+
edge: parseFloat(((params.true_probability - price) * 100).toFixed(2)) + "%",
|
|
114
|
+
signal: params.true_probability > price + 0.03 ? "BUY" : params.true_probability < price - 0.03 ? "SELL" : "FAIR"
|
|
115
|
+
} : void 0;
|
|
116
|
+
return {
|
|
117
|
+
model: "Binary Option (Black-Scholes Analog)",
|
|
118
|
+
inputs: { price, volatility: parseFloat(vol.toFixed(4)), time_to_expiry_years: parseFloat(T.toFixed(6)), time_to_expiry_hours: parseFloat((T * 8760).toFixed(1)) },
|
|
119
|
+
theoretical_price: parseFloat(theoreticalPrice.toFixed(4)),
|
|
120
|
+
market_price: price,
|
|
121
|
+
difference: parseFloat((theoreticalPrice - price).toFixed(4)),
|
|
122
|
+
greeks: {
|
|
123
|
+
delta: parseFloat(delta.toFixed(6)),
|
|
124
|
+
gamma: parseFloat(gamma.toFixed(6)),
|
|
125
|
+
theta_per_hour: parseFloat(theta.toFixed(8)),
|
|
126
|
+
theta_per_day: parseFloat((theta * 24).toFixed(6)),
|
|
127
|
+
vega: parseFloat(vega.toFixed(6))
|
|
128
|
+
},
|
|
129
|
+
interpretation: {
|
|
130
|
+
delta: `Price moves ~${(Math.abs(delta) * 100).toFixed(1)}\xA2 per 1% true probability change`,
|
|
131
|
+
gamma: gamma > 0.5 ? "HIGH \u2014 price very sensitive near 50/50" : "MODERATE",
|
|
132
|
+
theta: `Losing ${(Math.abs(theta) * 24 * 100).toFixed(3)}\xA2/day to time decay`,
|
|
133
|
+
time_premium: parseFloat(((price - (price > 0.5 ? 1 : 0)) * (price > 0.5 ? -1 : 1)).toFixed(4))
|
|
134
|
+
},
|
|
135
|
+
mispricing
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
async function bayesianUpdate(params) {
|
|
139
|
+
let prior = params.prior;
|
|
140
|
+
if (!prior && params.token_id) {
|
|
141
|
+
const mid = await apiFetch(`${CLOB_API}/midpoint?token_id=${params.token_id}`);
|
|
142
|
+
prior = parseFloat(mid?.mid || "0.5");
|
|
143
|
+
}
|
|
144
|
+
prior = prior || 0.5;
|
|
145
|
+
const updates = [];
|
|
146
|
+
let current = prior;
|
|
147
|
+
for (const ev of params.evidence || []) {
|
|
148
|
+
const prevProb = current;
|
|
149
|
+
let lr;
|
|
150
|
+
if (ev.likelihood_ratio) {
|
|
151
|
+
lr = ev.likelihood_ratio;
|
|
152
|
+
} else if (ev.likelihood_if_true !== void 0 && ev.likelihood_if_false !== void 0) {
|
|
153
|
+
lr = ev.likelihood_if_true / (ev.likelihood_if_false || 1e-3);
|
|
154
|
+
} else {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const priorOdds = current / (1 - current);
|
|
158
|
+
const posteriorOdds = lr * priorOdds;
|
|
159
|
+
current = posteriorOdds / (1 + posteriorOdds);
|
|
160
|
+
current = Math.max(1e-3, Math.min(0.999, current));
|
|
161
|
+
updates.push({
|
|
162
|
+
evidence: ev.description || "unnamed",
|
|
163
|
+
likelihood_ratio: parseFloat(lr.toFixed(4)),
|
|
164
|
+
log_lr: parseFloat(Math.log2(lr).toFixed(4)),
|
|
165
|
+
prior: parseFloat(prevProb.toFixed(4)),
|
|
166
|
+
posterior: parseFloat(current.toFixed(4)),
|
|
167
|
+
shift: parseFloat(((current - prevProb) * 100).toFixed(2)) + "%",
|
|
168
|
+
bits_of_evidence: parseFloat(Math.abs(Math.log2(lr)).toFixed(2))
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
const totalShift = current - prior;
|
|
172
|
+
const marketPrice = params.token_id ? parseFloat((await apiFetch(`${CLOB_API}/midpoint?token_id=${params.token_id}`).catch(() => ({ mid: prior }))).mid || String(prior)) : prior;
|
|
173
|
+
return {
|
|
174
|
+
formula: "P(H|E) = LR \xB7 P(H) / [LR \xB7 P(H) + P(\xACH)]",
|
|
175
|
+
initial_prior: parseFloat(prior.toFixed(4)),
|
|
176
|
+
final_posterior: parseFloat(current.toFixed(4)),
|
|
177
|
+
total_shift: parseFloat((totalShift * 100).toFixed(2)) + "%",
|
|
178
|
+
total_log_odds_shift: parseFloat((Math.log(current / (1 - current)) - Math.log(prior / (1 - prior))).toFixed(4)),
|
|
179
|
+
updates,
|
|
180
|
+
market_comparison: {
|
|
181
|
+
market_price: parseFloat(marketPrice.toFixed(4)),
|
|
182
|
+
your_posterior: parseFloat(current.toFixed(4)),
|
|
183
|
+
edge: parseFloat(((current - marketPrice) * 100).toFixed(2)) + "%",
|
|
184
|
+
signal: current > marketPrice + 0.03 ? "BUY" : current < marketPrice - 0.03 ? "SELL" : "FAIR"
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
async function runMonteCarlo(params) {
|
|
189
|
+
const N = Math.min(params.simulations || 1e4, 1e5);
|
|
190
|
+
const T = (params.time_horizon_hours || 24) / 8760;
|
|
191
|
+
const vol = params.volatility || 1;
|
|
192
|
+
const corr = params.correlation || 0;
|
|
193
|
+
const positions = params.positions || [];
|
|
194
|
+
if (positions.length === 0) throw new Error("No positions provided");
|
|
195
|
+
for (const pos of positions) {
|
|
196
|
+
if (pos.token_id && !pos.current_price) {
|
|
197
|
+
try {
|
|
198
|
+
const mid = await apiFetch(`${CLOB_API}/midpoint?token_id=${pos.token_id}`);
|
|
199
|
+
pos.current_price = parseFloat(mid?.mid || String(pos.entry_price || 0.5));
|
|
200
|
+
} catch {
|
|
201
|
+
pos.current_price = pos.entry_price || 0.5;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const pnlDistribution = [];
|
|
206
|
+
const sqrtT = Math.sqrt(T);
|
|
207
|
+
for (let sim = 0; sim < N; sim++) {
|
|
208
|
+
let totalPnl = 0;
|
|
209
|
+
const z1 = normalInv(Math.random());
|
|
210
|
+
for (const pos of positions) {
|
|
211
|
+
const z2 = normalInv(Math.random());
|
|
212
|
+
const z = corr * z1 + Math.sqrt(1 - corr * corr) * z2;
|
|
213
|
+
const currentP = pos.current_price || 0.5;
|
|
214
|
+
const trueP = pos.true_probability || currentP;
|
|
215
|
+
const meanReversion = 0.5;
|
|
216
|
+
const logOdds = Math.log(currentP / (1 - currentP));
|
|
217
|
+
const targetLogOdds = Math.log(trueP / (1 - trueP));
|
|
218
|
+
const newLogOdds = logOdds + meanReversion * (targetLogOdds - logOdds) * T + vol * sqrtT * z;
|
|
219
|
+
let simPrice = 1 / (1 + Math.exp(-newLogOdds));
|
|
220
|
+
simPrice = Math.max(0.01, Math.min(0.99, simPrice));
|
|
221
|
+
const entryPrice = pos.entry_price || currentP;
|
|
222
|
+
const size = pos.size || 1;
|
|
223
|
+
const pnl = pos.side === "BUY" ? (simPrice - entryPrice) * size : (entryPrice - simPrice) * size;
|
|
224
|
+
totalPnl += pnl;
|
|
225
|
+
}
|
|
226
|
+
pnlDistribution.push(totalPnl);
|
|
227
|
+
}
|
|
228
|
+
pnlDistribution.sort((a, b) => a - b);
|
|
229
|
+
const mean = pnlDistribution.reduce((s, v) => s + v, 0) / N;
|
|
230
|
+
const stdDev = std(pnlDistribution);
|
|
231
|
+
const profitCount = pnlDistribution.filter((v) => v > 0).length;
|
|
232
|
+
const pctl = (pct) => pnlDistribution[Math.floor(N * pct / 100)];
|
|
233
|
+
return {
|
|
234
|
+
model: "Mean-Reverting Geometric Brownian Motion (Ornstein-Uhlenbeck)",
|
|
235
|
+
simulations: N,
|
|
236
|
+
time_horizon_hours: params.time_horizon_hours || 24,
|
|
237
|
+
positions: positions.length,
|
|
238
|
+
results: {
|
|
239
|
+
expected_pnl: parseFloat(mean.toFixed(2)),
|
|
240
|
+
std_dev: parseFloat(stdDev.toFixed(2)),
|
|
241
|
+
probability_of_profit: parseFloat((profitCount / N * 100).toFixed(1)) + "%",
|
|
242
|
+
sharpe_ratio: stdDev > 0 ? parseFloat((mean / stdDev).toFixed(3)) : null
|
|
243
|
+
},
|
|
244
|
+
risk_metrics: {
|
|
245
|
+
var_95: parseFloat(pctl(5).toFixed(2)),
|
|
246
|
+
var_99: parseFloat(pctl(1).toFixed(2)),
|
|
247
|
+
cvar_95: parseFloat((pnlDistribution.slice(0, Math.floor(N * 0.05)).reduce((s, v) => s + v, 0) / Math.floor(N * 0.05)).toFixed(2)),
|
|
248
|
+
max_loss: parseFloat(pnlDistribution[0].toFixed(2)),
|
|
249
|
+
max_gain: parseFloat(pnlDistribution[N - 1].toFixed(2))
|
|
250
|
+
},
|
|
251
|
+
distribution: {
|
|
252
|
+
p1: parseFloat(pctl(1).toFixed(2)),
|
|
253
|
+
p5: parseFloat(pctl(5).toFixed(2)),
|
|
254
|
+
p10: parseFloat(pctl(10).toFixed(2)),
|
|
255
|
+
p25: parseFloat(pctl(25).toFixed(2)),
|
|
256
|
+
p50: parseFloat(pctl(50).toFixed(2)),
|
|
257
|
+
p75: parseFloat(pctl(75).toFixed(2)),
|
|
258
|
+
p90: parseFloat(pctl(90).toFixed(2)),
|
|
259
|
+
p95: parseFloat(pctl(95).toFixed(2)),
|
|
260
|
+
p99: parseFloat(pctl(99).toFixed(2))
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
async function calculateTechnicalIndicators(params) {
|
|
265
|
+
let prices = params.prices;
|
|
266
|
+
if (!prices && params.token_id) {
|
|
267
|
+
prices = await fetchPriceSeries(params.token_id, 200);
|
|
268
|
+
}
|
|
269
|
+
if (!prices || prices.length < 15) throw new Error("Need at least 15 price points");
|
|
270
|
+
const indicators = params.indicators || ["all"];
|
|
271
|
+
const doAll = indicators.includes("all");
|
|
272
|
+
const result = { data_points: prices.length, latest_price: prices[prices.length - 1] };
|
|
273
|
+
if (doAll || indicators.includes("rsi")) {
|
|
274
|
+
const period = params.rsi_period || 14;
|
|
275
|
+
const gains = [], losses = [];
|
|
276
|
+
for (let i = 1; i < prices.length; i++) {
|
|
277
|
+
const change = prices[i] - prices[i - 1];
|
|
278
|
+
gains.push(change > 0 ? change : 0);
|
|
279
|
+
losses.push(change < 0 ? -change : 0);
|
|
280
|
+
}
|
|
281
|
+
let avgGain = gains.slice(0, period).reduce((s, v) => s + v, 0) / period;
|
|
282
|
+
let avgLoss = losses.slice(0, period).reduce((s, v) => s + v, 0) / period;
|
|
283
|
+
for (let i = period; i < gains.length; i++) {
|
|
284
|
+
avgGain = (avgGain * (period - 1) + gains[i]) / period;
|
|
285
|
+
avgLoss = (avgLoss * (period - 1) + losses[i]) / period;
|
|
286
|
+
}
|
|
287
|
+
const rs = avgLoss > 0 ? avgGain / avgLoss : 100;
|
|
288
|
+
const rsi = 100 - 100 / (1 + rs);
|
|
289
|
+
result.rsi = {
|
|
290
|
+
value: parseFloat(rsi.toFixed(2)),
|
|
291
|
+
period,
|
|
292
|
+
signal: rsi > 70 ? "OVERBOUGHT" : rsi < 30 ? "OVERSOLD" : "NEUTRAL",
|
|
293
|
+
interpretation: rsi > 70 ? "Price may be overextended \u2014 consider selling" : rsi < 30 ? "Price may be undervalued \u2014 consider buying" : "No extreme detected"
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
if (doAll || indicators.includes("macd")) {
|
|
297
|
+
const fast = ewma(prices, params.macd_fast || 12);
|
|
298
|
+
const slow = ewma(prices, params.macd_slow || 26);
|
|
299
|
+
const macdLine = fast.map((f, i) => f - slow[i]);
|
|
300
|
+
const signalLine = ewma(macdLine, params.macd_signal || 9);
|
|
301
|
+
const histogram = macdLine.map((m, i) => m - signalLine[i]);
|
|
302
|
+
const latestMacd = macdLine[macdLine.length - 1];
|
|
303
|
+
const latestSignal = signalLine[signalLine.length - 1];
|
|
304
|
+
const latestHist = histogram[histogram.length - 1];
|
|
305
|
+
const prevHist = histogram[histogram.length - 2];
|
|
306
|
+
result.macd = {
|
|
307
|
+
macd_line: parseFloat(latestMacd.toFixed(6)),
|
|
308
|
+
signal_line: parseFloat(latestSignal.toFixed(6)),
|
|
309
|
+
histogram: parseFloat(latestHist.toFixed(6)),
|
|
310
|
+
signal: latestMacd > latestSignal ? "BULLISH" : "BEARISH",
|
|
311
|
+
crossover: latestHist > 0 && prevHist <= 0 ? "BULLISH_CROSS" : latestHist < 0 && prevHist >= 0 ? "BEARISH_CROSS" : "NONE",
|
|
312
|
+
momentum: latestHist > prevHist ? "INCREASING" : "DECREASING"
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
if (doAll || indicators.includes("bollinger")) {
|
|
316
|
+
const period = params.bollinger_period || 20;
|
|
317
|
+
const numStd = params.bollinger_std || 2;
|
|
318
|
+
const ma = sma(prices, period);
|
|
319
|
+
const latestMa = ma[ma.length - 1];
|
|
320
|
+
const recentPrices = prices.slice(-period);
|
|
321
|
+
const stdDev = std(recentPrices);
|
|
322
|
+
const upper = latestMa + numStd * stdDev;
|
|
323
|
+
const lower = latestMa - numStd * stdDev;
|
|
324
|
+
const currentPrice = prices[prices.length - 1];
|
|
325
|
+
const bandwidth = (upper - lower) / latestMa;
|
|
326
|
+
const pctB = (currentPrice - lower) / (upper - lower);
|
|
327
|
+
result.bollinger = {
|
|
328
|
+
upper: parseFloat(upper.toFixed(4)),
|
|
329
|
+
middle: parseFloat(latestMa.toFixed(4)),
|
|
330
|
+
lower: parseFloat(lower.toFixed(4)),
|
|
331
|
+
bandwidth: parseFloat(bandwidth.toFixed(4)),
|
|
332
|
+
percent_b: parseFloat(pctB.toFixed(4)),
|
|
333
|
+
signal: pctB > 1 ? "ABOVE_UPPER (overbought)" : pctB < 0 ? "BELOW_LOWER (oversold)" : bandwidth < 0.05 ? "SQUEEZE (breakout imminent)" : "WITHIN_BANDS"
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
if (doAll || indicators.includes("ema")) {
|
|
337
|
+
const ema9 = ewma(prices, 9);
|
|
338
|
+
const ema21 = ewma(prices, 21);
|
|
339
|
+
const ema50 = ewma(prices, Math.min(50, prices.length));
|
|
340
|
+
const latest9 = ema9[ema9.length - 1];
|
|
341
|
+
const latest21 = ema21[ema21.length - 1];
|
|
342
|
+
const latest50 = ema50[ema50.length - 1];
|
|
343
|
+
result.ema = {
|
|
344
|
+
ema_9: parseFloat(latest9.toFixed(4)),
|
|
345
|
+
ema_21: parseFloat(latest21.toFixed(4)),
|
|
346
|
+
ema_50: parseFloat(latest50.toFixed(4)),
|
|
347
|
+
trend: latest9 > latest21 && latest21 > latest50 ? "STRONG_UPTREND" : latest9 < latest21 && latest21 < latest50 ? "STRONG_DOWNTREND" : latest9 > latest21 ? "MILD_UPTREND" : "MILD_DOWNTREND",
|
|
348
|
+
golden_cross: latest9 > latest50 && ema9[ema9.length - 2] <= ema50[ema50.length - 2],
|
|
349
|
+
death_cross: latest9 < latest50 && ema9[ema9.length - 2] >= ema50[ema50.length - 2]
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
if (doAll || indicators.includes("momentum")) {
|
|
353
|
+
const roc5 = (prices[prices.length - 1] - prices[prices.length - 6]) / prices[prices.length - 6] * 100;
|
|
354
|
+
const roc10 = prices.length > 10 ? (prices[prices.length - 1] - prices[prices.length - 11]) / prices[prices.length - 11] * 100 : null;
|
|
355
|
+
const roc20 = prices.length > 20 ? (prices[prices.length - 1] - prices[prices.length - 21]) / prices[prices.length - 21] * 100 : null;
|
|
356
|
+
result.momentum = {
|
|
357
|
+
roc_5: parseFloat(roc5.toFixed(2)) + "%",
|
|
358
|
+
roc_10: roc10 !== null ? parseFloat(roc10.toFixed(2)) + "%" : null,
|
|
359
|
+
roc_20: roc20 !== null ? parseFloat(roc20.toFixed(2)) + "%" : null,
|
|
360
|
+
acceleration: roc10 !== null ? parseFloat((roc5 - roc10).toFixed(2)) : null
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
const signals = [];
|
|
364
|
+
if (result.rsi?.signal === "OVERSOLD") signals.push("BUY");
|
|
365
|
+
if (result.rsi?.signal === "OVERBOUGHT") signals.push("SELL");
|
|
366
|
+
if (result.macd?.signal === "BULLISH") signals.push("BUY");
|
|
367
|
+
if (result.macd?.signal === "BEARISH") signals.push("SELL");
|
|
368
|
+
if (result.bollinger?.signal?.includes("oversold")) signals.push("BUY");
|
|
369
|
+
if (result.bollinger?.signal?.includes("overbought")) signals.push("SELL");
|
|
370
|
+
if (result.ema?.trend?.includes("UPTREND")) signals.push("BUY");
|
|
371
|
+
if (result.ema?.trend?.includes("DOWNTREND")) signals.push("SELL");
|
|
372
|
+
const buyCount = signals.filter((s) => s === "BUY").length;
|
|
373
|
+
const sellCount = signals.filter((s) => s === "SELL").length;
|
|
374
|
+
result.composite_signal = {
|
|
375
|
+
buy_signals: buyCount,
|
|
376
|
+
sell_signals: sellCount,
|
|
377
|
+
total_indicators: buyCount + sellCount,
|
|
378
|
+
consensus: buyCount > sellCount ? "BUY" : sellCount > buyCount ? "SELL" : "MIXED",
|
|
379
|
+
confidence: buyCount + sellCount > 0 ? parseFloat((Math.abs(buyCount - sellCount) / (buyCount + sellCount) * 100).toFixed(0)) + "%" : "N/A"
|
|
380
|
+
};
|
|
381
|
+
return result;
|
|
382
|
+
}
|
|
383
|
+
async function analyzeVolatility(params) {
|
|
384
|
+
let prices = params.prices;
|
|
385
|
+
if (!prices && params.token_id) prices = await fetchPriceSeries(params.token_id, 200);
|
|
386
|
+
if (!prices || prices.length < 20) throw new Error("Need at least 20 price points");
|
|
387
|
+
const returns = prices.slice(1).map((pr, i) => Math.log(pr / prices[i]));
|
|
388
|
+
const windows = params.windows || [5, 10, 20, 50];
|
|
389
|
+
const realizedVol = {};
|
|
390
|
+
for (const w of windows) {
|
|
391
|
+
if (returns.length >= w) {
|
|
392
|
+
const windowReturns = returns.slice(-w);
|
|
393
|
+
realizedVol[`${w}_period`] = parseFloat((std(windowReturns) * Math.sqrt(365 * 24)).toFixed(4));
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
const ewmaVol = ewma(returns.map((r) => r * r), 10).map((v) => Math.sqrt(v * 365 * 24));
|
|
397
|
+
const latestEwmaVol = ewmaVol[ewmaVol.length - 1];
|
|
398
|
+
let hurst = 0.5;
|
|
399
|
+
if (returns.length >= 20) {
|
|
400
|
+
const logNs = [], logRS = [];
|
|
401
|
+
for (const n of [5, 10, 15, 20, Math.min(40, Math.floor(returns.length / 2))]) {
|
|
402
|
+
if (n > returns.length) continue;
|
|
403
|
+
const chunks = Math.floor(returns.length / n);
|
|
404
|
+
let totalRS = 0;
|
|
405
|
+
for (let c = 0; c < chunks; c++) {
|
|
406
|
+
const chunk = returns.slice(c * n, (c + 1) * n);
|
|
407
|
+
const mean = chunk.reduce((s, v) => s + v, 0) / n;
|
|
408
|
+
const deviations = chunk.map((v) => v - mean);
|
|
409
|
+
const cumDev = [];
|
|
410
|
+
let sum = 0;
|
|
411
|
+
for (const d of deviations) {
|
|
412
|
+
sum += d;
|
|
413
|
+
cumDev.push(sum);
|
|
414
|
+
}
|
|
415
|
+
const R = Math.max(...cumDev) - Math.min(...cumDev);
|
|
416
|
+
const S = std(chunk);
|
|
417
|
+
if (S > 0) totalRS += R / S;
|
|
418
|
+
}
|
|
419
|
+
if (totalRS > 0 && chunks > 0) {
|
|
420
|
+
logNs.push(Math.log(n));
|
|
421
|
+
logRS.push(Math.log(totalRS / chunks));
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (logNs.length >= 3) {
|
|
425
|
+
const reg = linearRegression(logNs, logRS);
|
|
426
|
+
hurst = reg.slope;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
const currentVol = realizedVol["10_period"] || latestEwmaVol;
|
|
430
|
+
const longVol = realizedVol["50_period"] || currentVol;
|
|
431
|
+
return {
|
|
432
|
+
realized_volatility: realizedVol,
|
|
433
|
+
ewma_volatility: parseFloat(latestEwmaVol.toFixed(4)),
|
|
434
|
+
hurst_exponent: {
|
|
435
|
+
value: parseFloat(hurst.toFixed(4)),
|
|
436
|
+
interpretation: hurst > 0.6 ? "TRENDING \u2014 momentum strategies may work" : hurst < 0.4 ? "MEAN_REVERTING \u2014 contrarian strategies may work" : "RANDOM_WALK \u2014 market is efficient at this timescale",
|
|
437
|
+
regime: hurst > 0.6 ? "trending" : hurst < 0.4 ? "mean_reverting" : "random_walk"
|
|
438
|
+
},
|
|
439
|
+
volatility_regime: {
|
|
440
|
+
current_vs_long: parseFloat((currentVol / (longVol || 1)).toFixed(2)),
|
|
441
|
+
regime: currentVol > longVol * 1.5 ? "HIGH_VOL (elevated risk)" : currentVol < longVol * 0.5 ? "LOW_VOL (compression \u2014 breakout possible)" : "NORMAL"
|
|
442
|
+
},
|
|
443
|
+
data_points: prices.length
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
async function analyzeStatArb(params) {
|
|
447
|
+
const [prices1, prices2] = await Promise.all([
|
|
448
|
+
fetchPriceSeries(params.token_id_1, 200),
|
|
449
|
+
fetchPriceSeries(params.token_id_2, 200)
|
|
450
|
+
]);
|
|
451
|
+
const len = Math.min(prices1.length, prices2.length);
|
|
452
|
+
if (len < 20) throw new Error("Need at least 20 data points for both tokens");
|
|
453
|
+
const p1 = prices1.slice(-len);
|
|
454
|
+
const p2 = prices2.slice(-len);
|
|
455
|
+
const reg = linearRegression(p2, p1);
|
|
456
|
+
const spread = p1.map((v, i) => v - reg.slope * p2[i] - reg.intercept);
|
|
457
|
+
const lookback = Math.min(params.lookback || 50, len);
|
|
458
|
+
const recentSpread = spread.slice(-lookback);
|
|
459
|
+
const meanSpread = recentSpread.reduce((s, v) => s + v, 0) / lookback;
|
|
460
|
+
const stdSpread = std(recentSpread);
|
|
461
|
+
const currentZScore = stdSpread > 0 ? (spread[spread.length - 1] - meanSpread) / stdSpread : 0;
|
|
462
|
+
const mean1 = p1.reduce((s, v) => s + v, 0) / len;
|
|
463
|
+
const mean2 = p2.reduce((s, v) => s + v, 0) / len;
|
|
464
|
+
let cov = 0, var1 = 0, var2 = 0;
|
|
465
|
+
for (let i = 0; i < len; i++) {
|
|
466
|
+
cov += (p1[i] - mean1) * (p2[i] - mean2);
|
|
467
|
+
var1 += (p1[i] - mean1) ** 2;
|
|
468
|
+
var2 += (p2[i] - mean2) ** 2;
|
|
469
|
+
}
|
|
470
|
+
const correlation = Math.sqrt(var1 * var2) > 0 ? cov / Math.sqrt(var1 * var2) : 0;
|
|
471
|
+
const entryZ = params.entry_zscore || 2;
|
|
472
|
+
const exitZ = params.exit_zscore || 0.5;
|
|
473
|
+
return {
|
|
474
|
+
model: "Pairs Trading / Statistical Arbitrage",
|
|
475
|
+
pair: { token_1: params.token_id_1, token_2: params.token_id_2 },
|
|
476
|
+
correlation: parseFloat(correlation.toFixed(4)),
|
|
477
|
+
cointegration: {
|
|
478
|
+
hedge_ratio: parseFloat(reg.slope.toFixed(4)),
|
|
479
|
+
intercept: parseFloat(reg.intercept.toFixed(4)),
|
|
480
|
+
r_squared: parseFloat(reg.r2.toFixed(4)),
|
|
481
|
+
is_cointegrated: reg.r2 > 0.5 && Math.abs(correlation) > 0.6
|
|
482
|
+
},
|
|
483
|
+
spread: {
|
|
484
|
+
current: parseFloat(spread[spread.length - 1].toFixed(4)),
|
|
485
|
+
mean: parseFloat(meanSpread.toFixed(4)),
|
|
486
|
+
std: parseFloat(stdSpread.toFixed(4)),
|
|
487
|
+
z_score: parseFloat(currentZScore.toFixed(4))
|
|
488
|
+
},
|
|
489
|
+
signal: {
|
|
490
|
+
action: Math.abs(currentZScore) > entryZ ? currentZScore > 0 ? "SHORT_SPREAD (sell 1, buy 2)" : "LONG_SPREAD (buy 1, sell 2)" : Math.abs(currentZScore) < exitZ ? "CLOSE_POSITION" : "HOLD",
|
|
491
|
+
strength: Math.abs(currentZScore) > entryZ * 1.5 ? "STRONG" : Math.abs(currentZScore) > entryZ ? "MODERATE" : "WEAK",
|
|
492
|
+
entry_threshold: entryZ,
|
|
493
|
+
exit_threshold: exitZ
|
|
494
|
+
},
|
|
495
|
+
warnings: [
|
|
496
|
+
Math.abs(correlation) < 0.3 ? "LOW CORRELATION \u2014 these markets may not be related enough for stat arb" : "",
|
|
497
|
+
reg.r2 < 0.3 ? "POOR FIT \u2014 hedge ratio may be unreliable" : ""
|
|
498
|
+
].filter(Boolean)
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
async function calculateVaR(params) {
|
|
502
|
+
const confidence = params.confidence || 0.95;
|
|
503
|
+
const horizon = (params.horizon_hours || 24) / (24 * 365);
|
|
504
|
+
let portfolioReturns = [];
|
|
505
|
+
let totalExposure = 0;
|
|
506
|
+
for (const pos of params.positions || []) {
|
|
507
|
+
const prices = await fetchPriceSeries(pos.token_id, 100);
|
|
508
|
+
if (prices.length < 10) continue;
|
|
509
|
+
const returns = prices.slice(1).map((pr, i) => Math.log(pr / prices[i]));
|
|
510
|
+
const scaledReturns = returns.map((r) => r * (pos.size || 1) * (pos.side === "SELL" ? -1 : 1));
|
|
511
|
+
if (portfolioReturns.length === 0) {
|
|
512
|
+
portfolioReturns = scaledReturns;
|
|
513
|
+
} else {
|
|
514
|
+
const len = Math.min(portfolioReturns.length, scaledReturns.length);
|
|
515
|
+
portfolioReturns = portfolioReturns.slice(-len).map((r, i) => r + scaledReturns.slice(-len)[i]);
|
|
516
|
+
}
|
|
517
|
+
totalExposure += pos.size || 1;
|
|
518
|
+
}
|
|
519
|
+
if (portfolioReturns.length < 10) throw new Error("Insufficient data for VaR calculation");
|
|
520
|
+
const mean = portfolioReturns.reduce((s, v) => s + v, 0) / portfolioReturns.length;
|
|
521
|
+
const sigma = std(portfolioReturns);
|
|
522
|
+
const sqrtH = Math.sqrt(horizon * 365 * 24);
|
|
523
|
+
const results = { confidence, horizon_hours: params.horizon_hours || 24, total_exposure: totalExposure, data_points: portfolioReturns.length };
|
|
524
|
+
const method = params.method || "all";
|
|
525
|
+
if (method === "all" || method === "parametric") {
|
|
526
|
+
const z = normalInv(1 - confidence);
|
|
527
|
+
results.parametric = {
|
|
528
|
+
var: parseFloat((-(mean * sqrtH + z * sigma * sqrtH)).toFixed(2)),
|
|
529
|
+
note: "Assumes normal distribution of returns"
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
if (method === "all" || method === "historical") {
|
|
533
|
+
const sorted = [...portfolioReturns].sort((a, b) => a - b);
|
|
534
|
+
const idx = Math.floor(sorted.length * (1 - confidence));
|
|
535
|
+
const var_ = -sorted[idx] * sqrtH;
|
|
536
|
+
const cvar = -(sorted.slice(0, idx + 1).reduce((s, v) => s + v, 0) / (idx + 1)) * sqrtH;
|
|
537
|
+
results.historical = {
|
|
538
|
+
var: parseFloat(var_.toFixed(2)),
|
|
539
|
+
cvar_expected_shortfall: parseFloat(cvar.toFixed(2)),
|
|
540
|
+
note: "Based on actual return distribution"
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
if (method === "all" || method === "cornish_fisher") {
|
|
544
|
+
const n = portfolioReturns.length;
|
|
545
|
+
const skew = portfolioReturns.reduce((s, r) => s + Math.pow((r - mean) / sigma, 3), 0) / n;
|
|
546
|
+
const kurt = portfolioReturns.reduce((s, r) => s + Math.pow((r - mean) / sigma, 4), 0) / n - 3;
|
|
547
|
+
const z = normalInv(1 - confidence);
|
|
548
|
+
const zCF = z + (z * z - 1) * skew / 6 + (z * z * z - 3 * z) * kurt / 24 - (2 * z * z * z - 5 * z) * skew * skew / 36;
|
|
549
|
+
results.cornish_fisher = {
|
|
550
|
+
var: parseFloat((-(mean * sqrtH + zCF * sigma * sqrtH)).toFixed(2)),
|
|
551
|
+
skewness: parseFloat(skew.toFixed(4)),
|
|
552
|
+
excess_kurtosis: parseFloat(kurt.toFixed(4)),
|
|
553
|
+
note: "Adjusts for fat tails and asymmetry in return distribution"
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
return results;
|
|
557
|
+
}
|
|
558
|
+
async function calculateEntropy(params) {
|
|
559
|
+
let probs = params.market_prices;
|
|
560
|
+
if (!probs && params.token_id) {
|
|
561
|
+
const mid = await apiFetch(`${CLOB_API}/midpoint?token_id=${params.token_id}`);
|
|
562
|
+
const pr = parseFloat(mid?.mid || "0.5");
|
|
563
|
+
probs = [pr, 1 - pr];
|
|
564
|
+
}
|
|
565
|
+
if (!probs) throw new Error("Provide market_prices or token_id");
|
|
566
|
+
const entropy = -probs.reduce((s, p) => {
|
|
567
|
+
if (p <= 0 || p >= 1) return s;
|
|
568
|
+
return s + p * Math.log2(p);
|
|
569
|
+
}, 0);
|
|
570
|
+
const maxEntropy = Math.log2(probs.length);
|
|
571
|
+
const normalizedEntropy = maxEntropy > 0 ? entropy / maxEntropy : 0;
|
|
572
|
+
const result = {
|
|
573
|
+
shannon_entropy: parseFloat(entropy.toFixed(4)),
|
|
574
|
+
max_entropy: parseFloat(maxEntropy.toFixed(4)),
|
|
575
|
+
normalized_entropy: parseFloat(normalizedEntropy.toFixed(4)),
|
|
576
|
+
interpretation: normalizedEntropy > 0.9 ? "MAXIMUM UNCERTAINTY \u2014 market has no conviction" : normalizedEntropy > 0.7 ? "HIGH UNCERTAINTY \u2014 wide range of outcomes" : normalizedEntropy > 0.3 ? "MODERATE \u2014 some conviction forming" : "DECISIVE \u2014 market strongly favors one outcome",
|
|
577
|
+
bits_to_resolve: parseFloat(entropy.toFixed(4))
|
|
578
|
+
};
|
|
579
|
+
if (params.your_estimates) {
|
|
580
|
+
const q = params.your_estimates;
|
|
581
|
+
const klDiv = probs.reduce((s, pi, i) => {
|
|
582
|
+
if (q[i] <= 0 || pi <= 0) return s;
|
|
583
|
+
return s + q[i] * Math.log2(q[i] / pi);
|
|
584
|
+
}, 0);
|
|
585
|
+
result.kl_divergence = {
|
|
586
|
+
value: parseFloat(klDiv.toFixed(6)),
|
|
587
|
+
interpretation: klDiv > 0.5 ? "LARGE DISAGREEMENT \u2014 strong potential edge" : klDiv > 0.1 ? "MODERATE DISAGREEMENT" : "SMALL DISAGREEMENT \u2014 little edge",
|
|
588
|
+
direction: "Your beliefs diverge from market by " + klDiv.toFixed(4) + " bits"
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
return result;
|
|
592
|
+
}
|
|
593
|
+
function analyzeSentiment(texts) {
|
|
594
|
+
if (texts.length === 0) throw new Error("No texts provided");
|
|
595
|
+
const analyzed = texts.map((text) => {
|
|
596
|
+
const words = text.toLowerCase().split(/\s+/);
|
|
597
|
+
let score = 0, posCount = 0, negCount = 0;
|
|
598
|
+
for (const w of words) {
|
|
599
|
+
const clean = w.replace(/[^a-z]/g, "");
|
|
600
|
+
if (POSITIVE_WORDS[clean]) {
|
|
601
|
+
score += POSITIVE_WORDS[clean];
|
|
602
|
+
posCount++;
|
|
603
|
+
}
|
|
604
|
+
if (NEGATIVE_WORDS[clean]) {
|
|
605
|
+
score += NEGATIVE_WORDS[clean];
|
|
606
|
+
negCount++;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
const normalized = words.length > 0 ? score / Math.sqrt(words.length) : 0;
|
|
610
|
+
return {
|
|
611
|
+
text: text.slice(0, 200),
|
|
612
|
+
raw_score: score,
|
|
613
|
+
normalized_score: parseFloat(normalized.toFixed(3)),
|
|
614
|
+
sentiment: normalized > 0.5 ? "POSITIVE" : normalized < -0.5 ? "NEGATIVE" : "NEUTRAL",
|
|
615
|
+
positive_words: posCount,
|
|
616
|
+
negative_words: negCount
|
|
617
|
+
};
|
|
618
|
+
});
|
|
619
|
+
const avgScore = analyzed.reduce((s, a) => s + a.normalized_score, 0) / analyzed.length;
|
|
620
|
+
const posTexts = analyzed.filter((a) => a.sentiment === "POSITIVE").length;
|
|
621
|
+
const negTexts = analyzed.filter((a) => a.sentiment === "NEGATIVE").length;
|
|
622
|
+
return {
|
|
623
|
+
total_texts: texts.length,
|
|
624
|
+
aggregate: {
|
|
625
|
+
average_score: parseFloat(avgScore.toFixed(3)),
|
|
626
|
+
overall_sentiment: avgScore > 0.3 ? "BULLISH" : avgScore < -0.3 ? "BEARISH" : "MIXED",
|
|
627
|
+
positive_count: posTexts,
|
|
628
|
+
negative_count: negTexts,
|
|
629
|
+
neutral_count: texts.length - posTexts - negTexts,
|
|
630
|
+
consensus_strength: parseFloat((Math.abs(posTexts - negTexts) / texts.length * 100).toFixed(1)) + "%"
|
|
631
|
+
},
|
|
632
|
+
details: analyzed.slice(0, 30),
|
|
633
|
+
market_signal: {
|
|
634
|
+
direction: avgScore > 0.3 ? "BUY" : avgScore < -0.3 ? "SELL" : "HOLD",
|
|
635
|
+
confidence: Math.abs(avgScore) > 1 ? "HIGH" : Math.abs(avgScore) > 0.5 ? "MEDIUM" : "LOW"
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
async function fetchNewsFeed(params) {
|
|
640
|
+
let query = params.query;
|
|
641
|
+
if (!query && params.market_id) {
|
|
642
|
+
const m = await apiFetch(`${GAMMA_API}/markets/${params.market_id}`).catch(() => null);
|
|
643
|
+
query = m?.question;
|
|
644
|
+
}
|
|
645
|
+
if (!query) throw new Error("Provide query or market_id");
|
|
646
|
+
const googleNewsUrl = `https://news.google.com/rss/search?q=${encodeURIComponent(query)}&hl=en-US&gl=US&ceid=US:en`;
|
|
647
|
+
try {
|
|
648
|
+
const rssXml = await cachedFetchText(googleNewsUrl, 15e3);
|
|
649
|
+
const items = [];
|
|
650
|
+
const itemRegex = /<item>([\s\S]*?)<\/item>/g;
|
|
651
|
+
let match;
|
|
652
|
+
while ((match = itemRegex.exec(typeof rssXml === "string" ? rssXml : "")) !== null) {
|
|
653
|
+
const get = (tag) => {
|
|
654
|
+
const m2 = match[1].match(new RegExp(`<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`));
|
|
655
|
+
return m2 ? m2[1].replace(/<!\[CDATA\[|\]\]>/g, "").trim() : "";
|
|
656
|
+
};
|
|
657
|
+
const title = get("title");
|
|
658
|
+
const link = get("link");
|
|
659
|
+
const pubDate = get("pubDate");
|
|
660
|
+
const source = get("source");
|
|
661
|
+
if (title) {
|
|
662
|
+
if (pubDate && params.hours) {
|
|
663
|
+
const articleDate = new Date(pubDate);
|
|
664
|
+
const cutoff = new Date(Date.now() - (params.hours || 24) * 36e5);
|
|
665
|
+
if (articleDate < cutoff) continue;
|
|
666
|
+
}
|
|
667
|
+
items.push({
|
|
668
|
+
title,
|
|
669
|
+
link,
|
|
670
|
+
published: pubDate,
|
|
671
|
+
source: source || "Google News",
|
|
672
|
+
impact_score: title.toLowerCase().includes("breaking") || title.toLowerCase().includes("urgent") ? "HIGH" : title.toLowerCase().includes("update") || title.toLowerCase().includes("report") ? "MEDIUM" : "STANDARD"
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
if (items.length >= (params.limit || 20)) break;
|
|
676
|
+
}
|
|
677
|
+
return { query, source: "Google News RSS", articles: items.length, results: items, note: "For deeper analysis, use analyzeSentiment on individual articles." };
|
|
678
|
+
} catch {
|
|
679
|
+
return { query, source: "Google News RSS", articles: 0, results: [], note: "RSS unavailable \u2014 use web_search as fallback" };
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
async function generateCompositeSignal(params) {
|
|
683
|
+
const [midData, bookData, marketData, trades] = await Promise.all([
|
|
684
|
+
apiFetch(`${CLOB_API}/midpoint?token_id=${params.token_id}`).catch(() => null),
|
|
685
|
+
apiFetch(`${CLOB_API}/book?token_id=${params.token_id}`).catch(() => null),
|
|
686
|
+
params.market_id ? apiFetch(`${GAMMA_API}/markets/${params.market_id}`).catch(() => null) : null,
|
|
687
|
+
apiFetch(`${CLOB_API}/trades?asset_id=${params.token_id}&limit=100`).catch(() => [])
|
|
688
|
+
]);
|
|
689
|
+
const price = parseFloat(midData?.mid || "0.5");
|
|
690
|
+
const prices = Array.isArray(trades) ? trades.reverse().map((t) => parseFloat(t.price || "0")).filter((v) => v > 0) : [];
|
|
691
|
+
const signals = [];
|
|
692
|
+
if (bookData) {
|
|
693
|
+
const bidVol = (bookData.bids || []).reduce((s, b) => s + parseFloat(b.size || "0") * parseFloat(b.price || "0"), 0);
|
|
694
|
+
const askVol = (bookData.asks || []).reduce((s, a) => s + parseFloat(a.size || "0") * parseFloat(a.price || "0"), 0);
|
|
695
|
+
const imbalance = (bidVol - askVol) / (bidVol + askVol || 1);
|
|
696
|
+
signals.push({
|
|
697
|
+
name: "Order Book Imbalance",
|
|
698
|
+
direction: imbalance > 0.2 ? "BUY" : imbalance < -0.2 ? "SELL" : "NEUTRAL",
|
|
699
|
+
weight: Math.min(Math.abs(imbalance) * 3, 1),
|
|
700
|
+
detail: `Bid/Ask ratio: ${(bidVol / (askVol || 1)).toFixed(2)}, Imbalance: ${(imbalance * 100).toFixed(1)}%`
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
if (prices.length >= 15) {
|
|
704
|
+
const period = 14;
|
|
705
|
+
const gains = prices.slice(1).map((pr, i) => Math.max(0, pr - prices[i]));
|
|
706
|
+
const losses = prices.slice(1).map((pr, i) => Math.max(0, prices[i] - pr));
|
|
707
|
+
let avgGain = gains.slice(0, period).reduce((s, v) => s + v, 0) / period;
|
|
708
|
+
let avgLoss = losses.slice(0, period).reduce((s, v) => s + v, 0) / period;
|
|
709
|
+
for (let i = period; i < gains.length; i++) {
|
|
710
|
+
avgGain = (avgGain * (period - 1) + gains[i]) / period;
|
|
711
|
+
avgLoss = (avgLoss * (period - 1) + losses[i]) / period;
|
|
712
|
+
}
|
|
713
|
+
const rsi = 100 - 100 / (1 + (avgLoss > 0 ? avgGain / avgLoss : 100));
|
|
714
|
+
signals.push({
|
|
715
|
+
name: "RSI",
|
|
716
|
+
direction: rsi < 30 ? "BUY" : rsi > 70 ? "SELL" : "NEUTRAL",
|
|
717
|
+
weight: rsi < 20 || rsi > 80 ? 0.9 : rsi < 30 || rsi > 70 ? 0.6 : 0.2,
|
|
718
|
+
detail: `RSI(14) = ${rsi.toFixed(1)}`
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
if (prices.length >= 10) {
|
|
722
|
+
const roc = (prices[prices.length - 1] - prices[prices.length - 6]) / prices[prices.length - 6];
|
|
723
|
+
signals.push({
|
|
724
|
+
name: "Momentum",
|
|
725
|
+
direction: roc > 0.03 ? "BUY" : roc < -0.03 ? "SELL" : "NEUTRAL",
|
|
726
|
+
weight: Math.min(Math.abs(roc) * 10, 1),
|
|
727
|
+
detail: `5-period ROC: ${(roc * 100).toFixed(1)}%`
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
if (prices.length >= 20) {
|
|
731
|
+
const ma20 = prices.slice(-20).reduce((s, v) => s + v, 0) / 20;
|
|
732
|
+
const deviation = (price - ma20) / ma20;
|
|
733
|
+
signals.push({
|
|
734
|
+
name: "Mean Reversion",
|
|
735
|
+
direction: deviation < -0.05 ? "BUY" : deviation > 0.05 ? "SELL" : "NEUTRAL",
|
|
736
|
+
weight: Math.min(Math.abs(deviation) * 5, 1),
|
|
737
|
+
detail: `Price ${(deviation * 100).toFixed(1)}% from 20-MA (${ma20.toFixed(3)})`
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
if (params.true_probability) {
|
|
741
|
+
const edge = params.true_probability - price;
|
|
742
|
+
signals.push({
|
|
743
|
+
name: "Fundamental Edge",
|
|
744
|
+
direction: edge > 0.02 ? "BUY" : edge < -0.02 ? "SELL" : "NEUTRAL",
|
|
745
|
+
weight: Math.min(Math.abs(edge) * 10, 1),
|
|
746
|
+
detail: `Your estimate: ${(params.true_probability * 100).toFixed(1)}%, Market: ${(price * 100).toFixed(1)}%, Edge: ${(edge * 100).toFixed(1)}%`
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
if (Array.isArray(trades) && trades.length >= 20) {
|
|
750
|
+
const recentVol = trades.slice(-10).reduce((s, t) => s + parseFloat(t.size || "0"), 0);
|
|
751
|
+
const olderVol = trades.slice(-20, -10).reduce((s, t) => s + parseFloat(t.size || "0"), 0);
|
|
752
|
+
const volChange = olderVol > 0 ? (recentVol - olderVol) / olderVol : 0;
|
|
753
|
+
const priceDir = prices.length >= 10 ? prices[prices.length - 1] - prices[prices.length - 10] : 0;
|
|
754
|
+
signals.push({
|
|
755
|
+
name: "Volume-Price",
|
|
756
|
+
direction: volChange > 0.3 && priceDir > 0 ? "BUY" : volChange > 0.3 && priceDir < 0 ? "SELL" : "NEUTRAL",
|
|
757
|
+
weight: Math.min(Math.abs(volChange) * 0.5, 0.7),
|
|
758
|
+
detail: `Volume ${volChange > 0 ? "+" : ""}${(volChange * 100).toFixed(0)}%, Price ${priceDir > 0 ? "up" : "down"}`
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
let buyScore = 0, sellScore = 0, totalWeight = 0;
|
|
762
|
+
for (const sig of signals) {
|
|
763
|
+
totalWeight += sig.weight;
|
|
764
|
+
if (sig.direction === "BUY") buyScore += sig.weight;
|
|
765
|
+
else if (sig.direction === "SELL") sellScore += sig.weight;
|
|
766
|
+
}
|
|
767
|
+
const netScore = totalWeight > 0 ? (buyScore - sellScore) / totalWeight : 0;
|
|
768
|
+
const confidence = totalWeight > 0 ? Math.abs(buyScore - sellScore) / totalWeight : 0;
|
|
769
|
+
const direction = netScore > 0.15 ? "BUY" : netScore < -0.15 ? "SELL" : "HOLD";
|
|
770
|
+
let kellySize = null;
|
|
771
|
+
if (params.bankroll && direction !== "HOLD") {
|
|
772
|
+
const trueP = params.true_probability || (direction === "BUY" ? price + confidence * 0.1 : price - confidence * 0.1);
|
|
773
|
+
const b = 1 / price - 1;
|
|
774
|
+
const f = (trueP * b - (1 - trueP)) / b;
|
|
775
|
+
const riskMultiplier = params.risk_tolerance === "conservative" ? 0.25 : params.risk_tolerance === "aggressive" ? 0.75 : 0.5;
|
|
776
|
+
kellySize = parseFloat((params.bankroll * Math.max(f, 0) * riskMultiplier).toFixed(2));
|
|
777
|
+
}
|
|
778
|
+
return {
|
|
779
|
+
token_id: params.token_id,
|
|
780
|
+
current_price: price,
|
|
781
|
+
signal: {
|
|
782
|
+
direction,
|
|
783
|
+
confidence: parseFloat((confidence * 100).toFixed(1)) + "%",
|
|
784
|
+
net_score: parseFloat(netScore.toFixed(3)),
|
|
785
|
+
buy_weight: parseFloat(buyScore.toFixed(2)),
|
|
786
|
+
sell_weight: parseFloat(sellScore.toFixed(2))
|
|
787
|
+
},
|
|
788
|
+
recommended_position: kellySize !== null ? {
|
|
789
|
+
size_usdc: kellySize,
|
|
790
|
+
risk_tolerance: params.risk_tolerance || "moderate",
|
|
791
|
+
method: "Kelly Criterion (risk-adjusted)"
|
|
792
|
+
} : void 0,
|
|
793
|
+
components: signals,
|
|
794
|
+
warning: confidence < 0.3 ? "LOW CONFIDENCE \u2014 signals are mixed, consider waiting" : void 0
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
async function calculateCorrelationMatrix(params) {
|
|
798
|
+
const ids = (params.token_ids || []).slice(0, 10);
|
|
799
|
+
if (ids.length < 2) throw new Error("Need at least 2 token IDs");
|
|
800
|
+
const allPrices = await Promise.all(ids.map((id) => fetchPriceSeries(id, params.lookback || 50)));
|
|
801
|
+
const minLen = Math.min(...allPrices.map((pr) => pr.length));
|
|
802
|
+
if (minLen < 10) throw new Error("Insufficient data points");
|
|
803
|
+
const aligned = allPrices.map((pr) => pr.slice(-minLen));
|
|
804
|
+
const returns = aligned.map((pr) => pr.slice(1).map((v, i) => Math.log(v / pr[i])));
|
|
805
|
+
const n = ids.length;
|
|
806
|
+
const matrix = Array.from({ length: n }, () => Array(n).fill(0));
|
|
807
|
+
const labels = params.labels || ids.map((_, i) => `Token ${i + 1}`);
|
|
808
|
+
for (let i = 0; i < n; i++) {
|
|
809
|
+
matrix[i][i] = 1;
|
|
810
|
+
for (let j = i + 1; j < n; j++) {
|
|
811
|
+
const len = Math.min(returns[i].length, returns[j].length);
|
|
812
|
+
const r1 = returns[i].slice(-len), r2 = returns[j].slice(-len);
|
|
813
|
+
const m1 = r1.reduce((s, v) => s + v, 0) / len;
|
|
814
|
+
const m2 = r2.reduce((s, v) => s + v, 0) / len;
|
|
815
|
+
let cv = 0, v1 = 0, v2 = 0;
|
|
816
|
+
for (let k = 0; k < len; k++) {
|
|
817
|
+
cv += (r1[k] - m1) * (r2[k] - m2);
|
|
818
|
+
v1 += (r1[k] - m1) ** 2;
|
|
819
|
+
v2 += (r2[k] - m2) ** 2;
|
|
820
|
+
}
|
|
821
|
+
const corr = Math.sqrt(v1 * v2) > 0 ? cv / Math.sqrt(v1 * v2) : 0;
|
|
822
|
+
matrix[i][j] = corr;
|
|
823
|
+
matrix[j][i] = corr;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
const pairs = [];
|
|
827
|
+
for (let i = 0; i < n; i++) {
|
|
828
|
+
for (let j = i + 1; j < n; j++) {
|
|
829
|
+
pairs.push({ pair: `${labels[i]} / ${labels[j]}`, correlation: parseFloat(matrix[i][j].toFixed(4)) });
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
pairs.sort((a, b) => Math.abs(b.correlation) - Math.abs(a.correlation));
|
|
833
|
+
return {
|
|
834
|
+
tokens: ids.length,
|
|
835
|
+
data_points: minLen,
|
|
836
|
+
matrix: matrix.map((row, i) => ({
|
|
837
|
+
token: labels[i],
|
|
838
|
+
correlations: Object.fromEntries(row.map((v, j) => [labels[j], parseFloat(v.toFixed(4))]))
|
|
839
|
+
})),
|
|
840
|
+
most_correlated: pairs[0],
|
|
841
|
+
least_correlated: pairs[pairs.length - 1],
|
|
842
|
+
diversification_score: parseFloat((1 - pairs.reduce((s, p) => s + Math.abs(p.correlation), 0) / pairs.length).toFixed(3)),
|
|
843
|
+
all_pairs: pairs
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
async function testMarketEfficiency(params) {
|
|
847
|
+
let prices = params.prices;
|
|
848
|
+
if (!prices && params.token_id) prices = await fetchPriceSeries(params.token_id, 200);
|
|
849
|
+
if (!prices || prices.length < 30) throw new Error("Need at least 30 data points");
|
|
850
|
+
const returns = prices.slice(1).map((pr, i) => Math.log(pr / prices[i]));
|
|
851
|
+
const n = returns.length;
|
|
852
|
+
const meanR = returns.reduce((s, v) => s + v, 0) / n;
|
|
853
|
+
const autoCorrs = [];
|
|
854
|
+
for (let lag = 1; lag <= 5; lag++) {
|
|
855
|
+
let num = 0, den = 0;
|
|
856
|
+
for (let i = lag; i < n; i++) num += (returns[i] - meanR) * (returns[i - lag] - meanR);
|
|
857
|
+
for (let i = 0; i < n; i++) den += (returns[i] - meanR) ** 2;
|
|
858
|
+
autoCorrs.push(den > 0 ? num / den : 0);
|
|
859
|
+
}
|
|
860
|
+
const signs = returns.map((r) => r >= 0 ? 1 : -1);
|
|
861
|
+
let runs = 1;
|
|
862
|
+
for (let i = 1; i < signs.length; i++) {
|
|
863
|
+
if (signs[i] !== signs[i - 1]) runs++;
|
|
864
|
+
}
|
|
865
|
+
const nPos = signs.filter((s) => s === 1).length;
|
|
866
|
+
const nNeg = signs.filter((s) => s === -1).length;
|
|
867
|
+
const expectedRuns = 1 + 2 * nPos * nNeg / (nPos + nNeg);
|
|
868
|
+
const stdRuns = Math.sqrt(2 * nPos * nNeg * (2 * nPos * nNeg - nPos - nNeg) / ((nPos + nNeg) ** 2 * (nPos + nNeg - 1)));
|
|
869
|
+
const runsZ = stdRuns > 0 ? (runs - expectedRuns) / stdRuns : 0;
|
|
870
|
+
const q = 5;
|
|
871
|
+
const qReturns = [];
|
|
872
|
+
for (let i = 0; i <= n - q; i++) {
|
|
873
|
+
qReturns.push(returns.slice(i, i + q).reduce((s, v) => s + v, 0));
|
|
874
|
+
}
|
|
875
|
+
const var1 = returns.reduce((s, r) => s + (r - meanR) ** 2, 0) / (n - 1);
|
|
876
|
+
const varQ = qReturns.reduce((s, r) => s + (r - q * meanR) ** 2, 0) / (qReturns.length - 1);
|
|
877
|
+
const vr = varQ / (q * var1);
|
|
878
|
+
const vrZ = (vr - 1) * Math.sqrt(n * 2 * (2 * q - 1) * (q - 1) / (3 * q));
|
|
879
|
+
let lbStat = 0;
|
|
880
|
+
for (let k = 0; k < autoCorrs.length; k++) {
|
|
881
|
+
lbStat += n * (n + 2) / (n - k - 1) * autoCorrs[k] ** 2;
|
|
882
|
+
}
|
|
883
|
+
const isEfficient = Math.abs(runsZ) < 1.96 && Math.abs(vrZ) < 1.96 && lbStat < 11.07;
|
|
884
|
+
return {
|
|
885
|
+
data_points: prices.length,
|
|
886
|
+
autocorrelation: {
|
|
887
|
+
lag_1: parseFloat(autoCorrs[0].toFixed(4)),
|
|
888
|
+
lag_2: parseFloat(autoCorrs[1].toFixed(4)),
|
|
889
|
+
lag_3: parseFloat(autoCorrs[2].toFixed(4)),
|
|
890
|
+
lag_4: parseFloat(autoCorrs[3].toFixed(4)),
|
|
891
|
+
lag_5: parseFloat(autoCorrs[4].toFixed(4)),
|
|
892
|
+
significant: autoCorrs.some((ac) => Math.abs(ac) > 2 / Math.sqrt(n))
|
|
893
|
+
},
|
|
894
|
+
runs_test: {
|
|
895
|
+
runs,
|
|
896
|
+
expected: parseFloat(expectedRuns.toFixed(1)),
|
|
897
|
+
z_score: parseFloat(runsZ.toFixed(3)),
|
|
898
|
+
p_value: parseFloat((2 * (1 - normalCDF(Math.abs(runsZ)))).toFixed(4)),
|
|
899
|
+
result: Math.abs(runsZ) > 1.96 ? "NON_RANDOM" : "RANDOM"
|
|
900
|
+
},
|
|
901
|
+
variance_ratio: {
|
|
902
|
+
vr: parseFloat(vr.toFixed(4)),
|
|
903
|
+
z_score: parseFloat(vrZ.toFixed(3)),
|
|
904
|
+
interpretation: vr > 1.1 ? "MOMENTUM (trending)" : vr < 0.9 ? "MEAN_REVERTING" : "RANDOM_WALK"
|
|
905
|
+
},
|
|
906
|
+
ljung_box: {
|
|
907
|
+
statistic: parseFloat(lbStat.toFixed(2)),
|
|
908
|
+
significant: lbStat > 11.07
|
|
909
|
+
},
|
|
910
|
+
overall: {
|
|
911
|
+
efficient: isEfficient,
|
|
912
|
+
exploitable_patterns: !isEfficient,
|
|
913
|
+
suggested_strategy: !isEfficient ? vr > 1.1 ? "TREND_FOLLOWING \u2014 momentum is persistent" : vr < 0.9 ? "MEAN_REVERSION \u2014 buy dips, sell rallies" : "PATTERN detected but type unclear" : "Market appears efficient \u2014 edge must come from fundamental analysis, not technicals"
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// src/polymarket-engines/analytics.ts
|
|
919
|
+
async function findCorrelations(tokenIds, minCorrelation = 0.5) {
|
|
920
|
+
const tokens = tokenIds.slice(0, 10);
|
|
921
|
+
if (tokens.length < 2) throw new Error("Need at least 2 token IDs");
|
|
922
|
+
const histories = await Promise.all(tokens.map(async (tid) => ({
|
|
923
|
+
token_id: tid,
|
|
924
|
+
prices: await fetchPriceHistory(tid)
|
|
925
|
+
})));
|
|
926
|
+
const correlations = [];
|
|
927
|
+
for (let i = 0; i < histories.length; i++) {
|
|
928
|
+
for (let j = i + 1; j < histories.length; j++) {
|
|
929
|
+
const corr = pearsonCorrelation(histories[i].prices, histories[j].prices);
|
|
930
|
+
if (Math.abs(corr) >= minCorrelation) {
|
|
931
|
+
correlations.push({
|
|
932
|
+
token_a: tokens[i],
|
|
933
|
+
token_b: tokens[j],
|
|
934
|
+
correlation: corr,
|
|
935
|
+
strength: Math.abs(corr) > 0.8 ? "STRONG" : Math.abs(corr) > 0.6 ? "MODERATE" : "WEAK",
|
|
936
|
+
direction: corr > 0 ? "POSITIVE" : "NEGATIVE",
|
|
937
|
+
data_points: Math.min(histories[i].prices.length, histories[j].prices.length)
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
correlations.sort((a, b) => Math.abs(b.correlation) - Math.abs(a.correlation));
|
|
943
|
+
return {
|
|
944
|
+
tokens_analyzed: tokens.length,
|
|
945
|
+
pairs_checked: tokens.length * (tokens.length - 1) / 2,
|
|
946
|
+
significant_correlations: correlations.length,
|
|
947
|
+
correlations,
|
|
948
|
+
arbitrage_candidates: correlations.filter((c) => Math.abs(c.correlation) > 0.8).map((c) => ({
|
|
949
|
+
...c,
|
|
950
|
+
note: c.direction === "NEGATIVE" ? "Negatively correlated \u2014 if one goes up, the other goes down. Check if pricing is consistent." : "Strongly correlated \u2014 prices should move together. Divergence = opportunity."
|
|
951
|
+
}))
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
async function scanArbitrage(params) {
|
|
955
|
+
const opportunities = [];
|
|
956
|
+
let markets = [];
|
|
957
|
+
if (params.market_slugs?.length) {
|
|
958
|
+
const fetches = await Promise.all(params.market_slugs.map(
|
|
959
|
+
(s) => cachedFetchJSON(`${GAMMA_API}/markets?slug=${s}`).catch(() => [])
|
|
960
|
+
));
|
|
961
|
+
markets = fetches.flat();
|
|
962
|
+
} else {
|
|
963
|
+
markets = await cachedFetchJSON(`${GAMMA_API}/markets?active=true&closed=false&limit=50&order=volume24hr&ascending=false`).catch(() => []);
|
|
964
|
+
}
|
|
965
|
+
const scanType = params.scan_type || "all";
|
|
966
|
+
const minProfit = params.min_profit_pct || 0.5;
|
|
967
|
+
if (scanType === "yes_no" || scanType === "all") {
|
|
968
|
+
for (const m of markets) {
|
|
969
|
+
try {
|
|
970
|
+
const prices = m.outcomePrices ? JSON.parse(m.outcomePrices) : [];
|
|
971
|
+
if (prices.length === 2) {
|
|
972
|
+
const yes = parseFloat(prices[0]);
|
|
973
|
+
const no = parseFloat(prices[1]);
|
|
974
|
+
const sum = yes + no;
|
|
975
|
+
const deviation = Math.abs(sum - 1) * 100;
|
|
976
|
+
if (deviation >= minProfit) {
|
|
977
|
+
opportunities.push({
|
|
978
|
+
type: "yes_no_mispricing",
|
|
979
|
+
market: m.question,
|
|
980
|
+
slug: m.slug,
|
|
981
|
+
yes_price: yes,
|
|
982
|
+
no_price: no,
|
|
983
|
+
sum: +sum.toFixed(4),
|
|
984
|
+
profit_pct: +deviation.toFixed(2),
|
|
985
|
+
action: sum > 1 ? "SELL both YES and NO (overpriced)" : "BUY both YES and NO (underpriced)"
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
} catch {
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
if (scanType === "multi_outcome" || scanType === "all") {
|
|
994
|
+
for (const m of markets) {
|
|
995
|
+
try {
|
|
996
|
+
const prices = m.outcomePrices ? JSON.parse(m.outcomePrices) : [];
|
|
997
|
+
if (prices.length > 2) {
|
|
998
|
+
const sum = prices.reduce((s, p) => s + parseFloat(p), 0);
|
|
999
|
+
const deviation = Math.abs(sum - 1) * 100;
|
|
1000
|
+
if (deviation >= minProfit) {
|
|
1001
|
+
opportunities.push({
|
|
1002
|
+
type: "multi_outcome_mispricing",
|
|
1003
|
+
market: m.question,
|
|
1004
|
+
slug: m.slug,
|
|
1005
|
+
outcomes: prices.length,
|
|
1006
|
+
prices: prices.map(Number),
|
|
1007
|
+
sum: +sum.toFixed(4),
|
|
1008
|
+
profit_pct: +deviation.toFixed(2),
|
|
1009
|
+
action: sum > 1 ? "SELL all outcomes (combined price exceeds 100%)" : "BUY all outcomes (combined price below 100%)"
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
} catch {
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
if (scanType === "cross_market" || scanType === "all") {
|
|
1018
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1019
|
+
for (const m of markets) {
|
|
1020
|
+
const key = (m.question || "").toLowerCase().replace(/[^a-z0-9]/g, " ").replace(/\s+/g, " ").trim().slice(0, 50);
|
|
1021
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
1022
|
+
groups.get(key).push(m);
|
|
1023
|
+
}
|
|
1024
|
+
for (const [key, group] of Array.from(groups.entries())) {
|
|
1025
|
+
if (group.length < 2) continue;
|
|
1026
|
+
const prices = group.map((m2) => {
|
|
1027
|
+
const p = m2.outcomePrices ? JSON.parse(m2.outcomePrices) : [];
|
|
1028
|
+
return { slug: m2.slug, question: m2.question, yes: parseFloat(p[0] || "0") };
|
|
1029
|
+
});
|
|
1030
|
+
const maxPrice = Math.max(...prices.map((p) => p.yes));
|
|
1031
|
+
const minPrice = Math.min(...prices.map((p) => p.yes));
|
|
1032
|
+
const diff = (maxPrice - minPrice) * 100;
|
|
1033
|
+
if (diff >= minProfit) {
|
|
1034
|
+
opportunities.push({
|
|
1035
|
+
type: "cross_market_divergence",
|
|
1036
|
+
topic: key,
|
|
1037
|
+
markets: prices,
|
|
1038
|
+
price_spread: +diff.toFixed(2),
|
|
1039
|
+
action: `Buy cheapest (${minPrice.toFixed(3)}), sell most expensive (${maxPrice.toFixed(3)})`
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
opportunities.sort((a, b) => (b.profit_pct || b.price_spread || 0) - (a.profit_pct || a.price_spread || 0));
|
|
1045
|
+
return {
|
|
1046
|
+
markets_scanned: markets.length,
|
|
1047
|
+
opportunities_found: opportunities.length,
|
|
1048
|
+
opportunities: opportunities.slice(0, 20),
|
|
1049
|
+
total_potential_profit: opportunities.reduce((s, o) => s + (o.profit_pct || o.price_spread || 0), 0).toFixed(2) + "%"
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
async function detectRegime(tokenId, lookback = 72) {
|
|
1053
|
+
const prices = await fetchPriceHistory(tokenId);
|
|
1054
|
+
if (prices.length < 20) throw new Error("Insufficient price history (need 20+ data points)");
|
|
1055
|
+
const lb = Math.min(lookback, prices.length);
|
|
1056
|
+
const recentPrices = prices.slice(-lb);
|
|
1057
|
+
const hurst = calculateHurst(recentPrices);
|
|
1058
|
+
const vol = calculateVolatility(recentPrices);
|
|
1059
|
+
const n = recentPrices.length;
|
|
1060
|
+
const xMean = (n - 1) / 2;
|
|
1061
|
+
const yMean = recentPrices.reduce((s, p) => s + p, 0) / n;
|
|
1062
|
+
let num = 0, den = 0;
|
|
1063
|
+
for (let i = 0; i < n; i++) {
|
|
1064
|
+
num += (i - xMean) * (recentPrices[i] - yMean);
|
|
1065
|
+
den += (i - xMean) ** 2;
|
|
1066
|
+
}
|
|
1067
|
+
const slope = den > 0 ? num / den : 0;
|
|
1068
|
+
let regime;
|
|
1069
|
+
let confidence;
|
|
1070
|
+
if (hurst > 0.6) {
|
|
1071
|
+
regime = "TRENDING";
|
|
1072
|
+
confidence = Math.min(1, (hurst - 0.5) * 5);
|
|
1073
|
+
} else if (hurst < 0.4) {
|
|
1074
|
+
regime = "MEAN_REVERTING";
|
|
1075
|
+
confidence = Math.min(1, (0.5 - hurst) * 5);
|
|
1076
|
+
} else {
|
|
1077
|
+
regime = "RANDOM_WALK";
|
|
1078
|
+
confidence = 1 - Math.abs(hurst - 0.5) * 5;
|
|
1079
|
+
}
|
|
1080
|
+
const strategies = {
|
|
1081
|
+
TRENDING: [
|
|
1082
|
+
"Follow the trend \u2014 buy on dips in an uptrend, sell rallies in a downtrend",
|
|
1083
|
+
"Use momentum indicators (moving averages, breakout levels)",
|
|
1084
|
+
"Set trailing stops to ride the trend",
|
|
1085
|
+
"DO NOT mean-revert \u2014 the trend is your friend"
|
|
1086
|
+
],
|
|
1087
|
+
MEAN_REVERTING: [
|
|
1088
|
+
"Buy when price dips below recent mean, sell when it rises above",
|
|
1089
|
+
"Use Bollinger Bands or standard deviation channels",
|
|
1090
|
+
"Set target at mean price \u2014 profits come from reversion",
|
|
1091
|
+
"DO NOT chase breakouts \u2014 they will likely reverse"
|
|
1092
|
+
],
|
|
1093
|
+
RANDOM_WALK: [
|
|
1094
|
+
"No persistent pattern \u2014 fundamentals-only trading",
|
|
1095
|
+
"Only trade if you have informational edge (news, insider knowledge)",
|
|
1096
|
+
"Keep positions small \u2014 no statistical edge from technicals",
|
|
1097
|
+
"Focus on event catalysts, not price patterns"
|
|
1098
|
+
]
|
|
1099
|
+
};
|
|
1100
|
+
return {
|
|
1101
|
+
token_id: tokenId,
|
|
1102
|
+
regime,
|
|
1103
|
+
confidence: +confidence.toFixed(3),
|
|
1104
|
+
hurst_exponent: hurst,
|
|
1105
|
+
volatility_annualized: vol,
|
|
1106
|
+
trend_direction: slope > 0 ? "UP" : slope < 0 ? "DOWN" : "FLAT",
|
|
1107
|
+
slope: +slope.toFixed(6),
|
|
1108
|
+
data_points: recentPrices.length,
|
|
1109
|
+
current_price: recentPrices[recentPrices.length - 1],
|
|
1110
|
+
price_range: { min: +Math.min(...recentPrices).toFixed(4), max: +Math.max(...recentPrices).toFixed(4) },
|
|
1111
|
+
strategies: strategies[regime] || []
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
async function calculateSmartMoneyIndex(tokenId, marketQuestion) {
|
|
1115
|
+
const signals = {};
|
|
1116
|
+
try {
|
|
1117
|
+
const book = await cachedFetchJSON(`${CLOB_API}/book?token_id=${tokenId}`);
|
|
1118
|
+
const bids = (book?.bids || []).reduce((s, b) => s + parseFloat(b.size) * parseFloat(b.price), 0);
|
|
1119
|
+
const asks = (book?.asks || []).reduce((s, a) => s + parseFloat(a.size) * parseFloat(a.price), 0);
|
|
1120
|
+
const total = bids + asks;
|
|
1121
|
+
signals.orderbook_imbalance = total > 0 ? +((bids / total - 0.5) * 2).toFixed(3) : 0;
|
|
1122
|
+
} catch {
|
|
1123
|
+
signals.orderbook_imbalance = 0;
|
|
1124
|
+
}
|
|
1125
|
+
try {
|
|
1126
|
+
const trades = await cachedFetchJSON(`${CLOB_API}/trades?token_id=${tokenId}&limit=100`);
|
|
1127
|
+
let buyVol = 0, sellVol = 0;
|
|
1128
|
+
for (const t of trades || []) {
|
|
1129
|
+
const val = parseFloat(t.size || "0") * parseFloat(t.price || "0");
|
|
1130
|
+
if (t.side === "BUY") buyVol += val;
|
|
1131
|
+
else sellVol += val;
|
|
1132
|
+
}
|
|
1133
|
+
const total = buyVol + sellVol;
|
|
1134
|
+
signals.trade_flow = total > 0 ? +((buyVol / total - 0.5) * 2).toFixed(3) : 0;
|
|
1135
|
+
} catch {
|
|
1136
|
+
signals.trade_flow = 0;
|
|
1137
|
+
}
|
|
1138
|
+
try {
|
|
1139
|
+
const prices = await fetchPriceHistory(tokenId);
|
|
1140
|
+
if (prices.length >= 10) {
|
|
1141
|
+
const recent = prices.slice(-5).reduce((s, p) => s + p, 0) / 5;
|
|
1142
|
+
const older = prices.slice(-10, -5).reduce((s, p) => s + p, 0) / 5;
|
|
1143
|
+
signals.momentum = older > 0 ? +((recent - older) / older).toFixed(4) : 0;
|
|
1144
|
+
} else {
|
|
1145
|
+
signals.momentum = 0;
|
|
1146
|
+
}
|
|
1147
|
+
} catch {
|
|
1148
|
+
signals.momentum = 0;
|
|
1149
|
+
}
|
|
1150
|
+
signals.news_sentiment = 0;
|
|
1151
|
+
if (marketQuestion) {
|
|
1152
|
+
try {
|
|
1153
|
+
const xml = await (await fetch(`https://news.google.com/rss/search?q=${encodeURIComponent(marketQuestion)}&hl=en-US&gl=US&ceid=US:en`, { signal: AbortSignal.timeout(8e3) })).text();
|
|
1154
|
+
const titles = (xml.match(/<title>([\s\S]*?)<\/title>/g) || []).map((t) => t.replace(/<\/?title>/g, "").replace(/<!\[CDATA\[|\]\]>/g, ""));
|
|
1155
|
+
const positive = ["yes", "win", "likely", "confirmed", "ahead", "surge", "up"];
|
|
1156
|
+
const negative = ["no", "lose", "unlikely", "denied", "behind", "drop", "down"];
|
|
1157
|
+
let sentScore = 0;
|
|
1158
|
+
for (const t of titles) {
|
|
1159
|
+
const lower = t.toLowerCase();
|
|
1160
|
+
for (const p of positive) if (lower.includes(p)) sentScore += 0.1;
|
|
1161
|
+
for (const nn of negative) if (lower.includes(nn)) sentScore -= 0.1;
|
|
1162
|
+
}
|
|
1163
|
+
signals.news_sentiment = +Math.max(-1, Math.min(1, sentScore)).toFixed(3);
|
|
1164
|
+
} catch {
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
const weights = { orderbook_imbalance: 0.3, trade_flow: 0.3, momentum: 0.2, news_sentiment: 0.2 };
|
|
1168
|
+
const composite = +(signals.orderbook_imbalance * weights.orderbook_imbalance + signals.trade_flow * weights.trade_flow + signals.momentum * weights.momentum + signals.news_sentiment * weights.news_sentiment).toFixed(4);
|
|
1169
|
+
const action = composite > 0.3 ? "STRONG_BUY" : composite > 0.1 ? "BUY" : composite < -0.3 ? "STRONG_SELL" : composite < -0.1 ? "SELL" : "HOLD";
|
|
1170
|
+
return {
|
|
1171
|
+
token_id: tokenId,
|
|
1172
|
+
smart_money_index: composite,
|
|
1173
|
+
action,
|
|
1174
|
+
signals,
|
|
1175
|
+
weights,
|
|
1176
|
+
interpretation: {
|
|
1177
|
+
"-1 to -0.3": "Smart money selling aggressively",
|
|
1178
|
+
"-0.3 to -0.1": "Slight sell pressure from informed traders",
|
|
1179
|
+
"-0.1 to 0.1": "No clear smart money direction",
|
|
1180
|
+
"0.1 to 0.3": "Slight buy pressure from informed traders",
|
|
1181
|
+
"0.3 to 1": "Smart money buying aggressively"
|
|
1182
|
+
}
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
async function analyzeMicrostructure(tokenId, orderSizes) {
|
|
1186
|
+
const book = await cachedFetchJSON(`${CLOB_API}/book?token_id=${tokenId}`);
|
|
1187
|
+
const bids = (book?.bids || []).map((b) => ({ price: parseFloat(b.price), size: parseFloat(b.size) }));
|
|
1188
|
+
const asks = (book?.asks || []).map((a) => ({ price: parseFloat(a.price), size: parseFloat(a.size) }));
|
|
1189
|
+
const bestBid = bids.length ? Math.max(...bids.map((b) => b.price)) : 0;
|
|
1190
|
+
const bestAsk = asks.length ? Math.min(...asks.map((a) => a.price)) : 1;
|
|
1191
|
+
const midPrice = (bestBid + bestAsk) / 2;
|
|
1192
|
+
const spread = bestAsk - bestBid;
|
|
1193
|
+
const sizes = orderSizes || [100, 500, 1e3, 5e3];
|
|
1194
|
+
const buySimulations = [];
|
|
1195
|
+
const sellSimulations = [];
|
|
1196
|
+
for (const size of sizes) {
|
|
1197
|
+
let remaining = size;
|
|
1198
|
+
let totalCost = 0;
|
|
1199
|
+
let levelsUsed = 0;
|
|
1200
|
+
const sortedAsks = [...asks].sort((a, b) => a.price - b.price);
|
|
1201
|
+
for (const ask of sortedAsks) {
|
|
1202
|
+
if (remaining <= 0) break;
|
|
1203
|
+
const fillSize = Math.min(remaining / ask.price, ask.size);
|
|
1204
|
+
totalCost += fillSize * ask.price;
|
|
1205
|
+
remaining -= fillSize * ask.price;
|
|
1206
|
+
levelsUsed++;
|
|
1207
|
+
}
|
|
1208
|
+
const filled = size - Math.max(0, remaining);
|
|
1209
|
+
const avgPrice = filled > 0 ? totalCost / (totalCost / midPrice) : midPrice;
|
|
1210
|
+
const slippage = midPrice > 0 ? (avgPrice - midPrice) / midPrice * 100 : 0;
|
|
1211
|
+
buySimulations.push({
|
|
1212
|
+
order_size_usdc: size,
|
|
1213
|
+
estimated_fill_pct: +(filled / size * 100).toFixed(1),
|
|
1214
|
+
avg_fill_price: +avgPrice.toFixed(4),
|
|
1215
|
+
slippage_pct: +slippage.toFixed(3),
|
|
1216
|
+
levels_consumed: levelsUsed,
|
|
1217
|
+
recommendation: slippage > 2 ? "USE LIMIT ORDER \u2014 high slippage" : slippage > 0.5 ? "Consider limit order" : "Market order acceptable"
|
|
1218
|
+
});
|
|
1219
|
+
remaining = size;
|
|
1220
|
+
let totalProceeds = 0;
|
|
1221
|
+
levelsUsed = 0;
|
|
1222
|
+
const sortedBids = [...bids].sort((a, b) => b.price - a.price);
|
|
1223
|
+
for (const bid of sortedBids) {
|
|
1224
|
+
if (remaining <= 0) break;
|
|
1225
|
+
const fillSize = Math.min(remaining / bid.price, bid.size);
|
|
1226
|
+
totalProceeds += fillSize * bid.price;
|
|
1227
|
+
remaining -= fillSize * bid.price;
|
|
1228
|
+
levelsUsed++;
|
|
1229
|
+
}
|
|
1230
|
+
const filledSell = size - Math.max(0, remaining);
|
|
1231
|
+
const avgSellPrice = filledSell > 0 ? totalProceeds / (totalProceeds / midPrice) : midPrice;
|
|
1232
|
+
const sellSlippage = midPrice > 0 ? (midPrice - avgSellPrice) / midPrice * 100 : 0;
|
|
1233
|
+
sellSimulations.push({
|
|
1234
|
+
order_size_usdc: size,
|
|
1235
|
+
estimated_fill_pct: +(filledSell / size * 100).toFixed(1),
|
|
1236
|
+
avg_fill_price: +avgSellPrice.toFixed(4),
|
|
1237
|
+
slippage_pct: +sellSlippage.toFixed(3),
|
|
1238
|
+
levels_consumed: levelsUsed
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
let recentTrades = [];
|
|
1242
|
+
try {
|
|
1243
|
+
const trades = await cachedFetchJSON(`${CLOB_API}/trades?token_id=${tokenId}&limit=50`);
|
|
1244
|
+
recentTrades = (trades || []).map((t) => ({ size: parseFloat(t.size || "0"), price: parseFloat(t.price || "0"), side: t.side }));
|
|
1245
|
+
} catch {
|
|
1246
|
+
}
|
|
1247
|
+
const avgTradeSize = recentTrades.length ? +(recentTrades.reduce((s, t) => s + t.size * t.price, 0) / recentTrades.length).toFixed(2) : 0;
|
|
1248
|
+
return {
|
|
1249
|
+
token_id: tokenId,
|
|
1250
|
+
mid_price: +midPrice.toFixed(4),
|
|
1251
|
+
spread: +spread.toFixed(4),
|
|
1252
|
+
spread_bps: +(spread / midPrice * 1e4).toFixed(1),
|
|
1253
|
+
bid_levels: bids.length,
|
|
1254
|
+
ask_levels: asks.length,
|
|
1255
|
+
total_bid_liquidity: +bids.reduce((s, b) => s + b.size * b.price, 0).toFixed(2),
|
|
1256
|
+
total_ask_liquidity: +asks.reduce((s, a) => s + a.size * a.price, 0).toFixed(2),
|
|
1257
|
+
avg_recent_trade_size: avgTradeSize,
|
|
1258
|
+
buy_simulations: buySimulations,
|
|
1259
|
+
sell_simulations: sellSimulations,
|
|
1260
|
+
optimal_order_type: spread > 0.02 ? "LIMIT" : "MARKET",
|
|
1261
|
+
market_quality: spread < 0.01 ? "EXCELLENT" : spread < 0.03 ? "GOOD" : spread < 0.05 ? "FAIR" : "POOR"
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// src/polymarket-engines/onchain.ts
|
|
1266
|
+
async function analyzeOrderbookDepth(tokenId) {
|
|
1267
|
+
let book = null;
|
|
1268
|
+
try {
|
|
1269
|
+
book = await cachedFetchJSON(`${CLOB_API}/book?token_id=${tokenId}`, 15e3);
|
|
1270
|
+
} catch {
|
|
1271
|
+
try {
|
|
1272
|
+
const gammaData = await cachedFetchJSON(`${GAMMA_API}/markets?clob_token_ids=${tokenId}&limit=1`);
|
|
1273
|
+
if (gammaData?.[0]?.outcomePrices) {
|
|
1274
|
+
const prices = JSON.parse(gammaData[0].outcomePrices);
|
|
1275
|
+
const yesPrice = parseFloat(prices[0]) || 0.5;
|
|
1276
|
+
return {
|
|
1277
|
+
bestBid: +(yesPrice - 0.01).toFixed(4),
|
|
1278
|
+
bestAsk: +(yesPrice + 0.01).toFixed(4),
|
|
1279
|
+
spread: 0.02,
|
|
1280
|
+
spreadPct: +(0.02 / yesPrice * 100).toFixed(2),
|
|
1281
|
+
midpoint: yesPrice,
|
|
1282
|
+
bidDepth: 0,
|
|
1283
|
+
askDepth: 0,
|
|
1284
|
+
imbalance: 0,
|
|
1285
|
+
topBidSize: 0,
|
|
1286
|
+
topAskSize: 0,
|
|
1287
|
+
levels: 0,
|
|
1288
|
+
totalBidLiquidity: 0,
|
|
1289
|
+
totalAskLiquidity: 0,
|
|
1290
|
+
bidWalls: [],
|
|
1291
|
+
askWalls: [],
|
|
1292
|
+
bidDepthByLevel: {},
|
|
1293
|
+
askDepthByLevel: {},
|
|
1294
|
+
spoofIndicators: [],
|
|
1295
|
+
recommendation: "CLOB rate limited \u2014 orderbook data estimated from Gamma. Re-check with poly_orderbook_depth before trading."
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
} catch {
|
|
1299
|
+
}
|
|
1300
|
+
throw new Error("CLOB rate limited and no Gamma fallback available");
|
|
1301
|
+
}
|
|
1302
|
+
const bids = (book?.bids || []).map((b) => ({ price: parseFloat(b.price), size: parseFloat(b.size) }));
|
|
1303
|
+
const asks = (book?.asks || []).map((a) => ({ price: parseFloat(a.price), size: parseFloat(a.size) }));
|
|
1304
|
+
const totalBidLiquidity = bids.reduce((s, b) => s + b.size * b.price, 0);
|
|
1305
|
+
const totalAskLiquidity = asks.reduce((s, a) => s + a.size * a.price, 0);
|
|
1306
|
+
const bestBid = bids.length ? Math.max(...bids.map((b) => b.price)) : 0;
|
|
1307
|
+
const bestAsk = asks.length ? Math.min(...asks.map((a) => a.price)) : 1;
|
|
1308
|
+
const spread = bestAsk - bestBid;
|
|
1309
|
+
const spreadPct = bestBid > 0 ? spread / bestBid * 100 : 0;
|
|
1310
|
+
const midPrice = (bestBid + bestAsk) / 2;
|
|
1311
|
+
const depthLevels = [0.01, 0.02, 0.05, 0.1];
|
|
1312
|
+
const bidDepthByLevel = {};
|
|
1313
|
+
const askDepthByLevel = {};
|
|
1314
|
+
for (const level of depthLevels) {
|
|
1315
|
+
bidDepthByLevel[`${level * 100}%`] = bids.filter((b) => b.price >= bestBid * (1 - level)).reduce((s, b) => s + b.size, 0);
|
|
1316
|
+
askDepthByLevel[`${level * 100}%`] = asks.filter((a) => a.price <= bestAsk * (1 + level)).reduce((s, a) => s + a.size, 0);
|
|
1317
|
+
}
|
|
1318
|
+
const avgBidSize = bids.length ? bids.reduce((s, b) => s + b.size, 0) / bids.length : 0;
|
|
1319
|
+
const avgAskSize = asks.length ? asks.reduce((s, a) => s + a.size, 0) / asks.length : 0;
|
|
1320
|
+
const bidWalls = bids.filter((b) => b.size > avgBidSize * 3).map((b) => ({ price: b.price, size: b.size, multiple: +(b.size / avgBidSize).toFixed(1) }));
|
|
1321
|
+
const askWalls = asks.filter((a) => a.size > avgAskSize * 3).map((a) => ({ price: a.price, size: a.size, multiple: +(a.size / avgAskSize).toFixed(1) }));
|
|
1322
|
+
const totalDepth = totalBidLiquidity + totalAskLiquidity;
|
|
1323
|
+
const imbalance = totalDepth > 0 ? +((totalBidLiquidity - totalAskLiquidity) / totalDepth).toFixed(3) : 0;
|
|
1324
|
+
const spoofIndicators = [];
|
|
1325
|
+
if (bidWalls.length > 2) spoofIndicators.push("Multiple bid walls detected \u2014 possible layering");
|
|
1326
|
+
if (askWalls.length > 2) spoofIndicators.push("Multiple ask walls detected \u2014 possible layering");
|
|
1327
|
+
if (spreadPct > 5) spoofIndicators.push("Wide spread may indicate thin/manipulated market");
|
|
1328
|
+
const recommendation = spreadPct > 3 ? "Wide spread \u2014 use limit orders only, do NOT market buy/sell" : imbalance > 0.65 ? "Strong bid pressure \u2014 consider buying before price moves up" : imbalance < 0.35 ? "Strong sell pressure \u2014 wait for lower prices or go short" : "Balanced book \u2014 safe to trade at market";
|
|
1329
|
+
return {
|
|
1330
|
+
bestBid,
|
|
1331
|
+
bestAsk,
|
|
1332
|
+
spread: +spread.toFixed(4),
|
|
1333
|
+
spreadPct: +spreadPct.toFixed(2),
|
|
1334
|
+
midpoint: midPrice,
|
|
1335
|
+
bidDepth: +totalBidLiquidity.toFixed(2),
|
|
1336
|
+
askDepth: +totalAskLiquidity.toFixed(2),
|
|
1337
|
+
imbalance,
|
|
1338
|
+
topBidSize: bids[0]?.size || 0,
|
|
1339
|
+
topAskSize: asks[0]?.size || 0,
|
|
1340
|
+
levels: Math.max(bids.length, asks.length),
|
|
1341
|
+
totalBidLiquidity: +totalBidLiquidity.toFixed(2),
|
|
1342
|
+
totalAskLiquidity: +totalAskLiquidity.toFixed(2),
|
|
1343
|
+
bidWalls,
|
|
1344
|
+
askWalls,
|
|
1345
|
+
bidDepthByLevel,
|
|
1346
|
+
askDepthByLevel,
|
|
1347
|
+
spoofIndicators,
|
|
1348
|
+
recommendation
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
async function scanWhaleTrades(tokenId, minSize = 1e3) {
|
|
1352
|
+
let trades = null;
|
|
1353
|
+
try {
|
|
1354
|
+
trades = await cachedFetchJSON(`${CLOB_API}/trades?token_id=${tokenId}&limit=100`, 15e3);
|
|
1355
|
+
} catch {
|
|
1356
|
+
return {
|
|
1357
|
+
token_id: tokenId,
|
|
1358
|
+
total_trades: 0,
|
|
1359
|
+
whale_trades: 0,
|
|
1360
|
+
min_size_filter: minSize,
|
|
1361
|
+
trades: [],
|
|
1362
|
+
summary: { total_whale_volume: 0, avg_whale_size: 0, unique_wallets: 0 }
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
const largeTrades = (trades || []).map((t) => ({
|
|
1366
|
+
id: t.id,
|
|
1367
|
+
maker: t.maker_address,
|
|
1368
|
+
taker: t.taker_address,
|
|
1369
|
+
side: t.side,
|
|
1370
|
+
size: parseFloat(t.size || "0"),
|
|
1371
|
+
price: parseFloat(t.price || "0"),
|
|
1372
|
+
timestamp: t.match_time || t.created_at,
|
|
1373
|
+
value: parseFloat(t.size || "0") * parseFloat(t.price || "0")
|
|
1374
|
+
})).filter((t) => t.value >= minSize).sort((a, b) => b.value - a.value);
|
|
1375
|
+
const wallets = /* @__PURE__ */ new Set();
|
|
1376
|
+
for (const t of largeTrades) {
|
|
1377
|
+
if (t.maker) wallets.add(t.maker.toLowerCase());
|
|
1378
|
+
if (t.taker) wallets.add(t.taker.toLowerCase());
|
|
1379
|
+
}
|
|
1380
|
+
return {
|
|
1381
|
+
token_id: tokenId,
|
|
1382
|
+
total_trades: (trades || []).length,
|
|
1383
|
+
whale_trades: largeTrades.length,
|
|
1384
|
+
min_size_filter: minSize,
|
|
1385
|
+
trades: largeTrades.slice(0, 20),
|
|
1386
|
+
summary: {
|
|
1387
|
+
total_whale_volume: +largeTrades.reduce((s, t) => s + t.value, 0).toFixed(2),
|
|
1388
|
+
avg_whale_size: largeTrades.length ? +(largeTrades.reduce((s, t) => s + t.value, 0) / largeTrades.length).toFixed(2) : 0,
|
|
1389
|
+
unique_wallets: wallets.size
|
|
1390
|
+
}
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
async function analyzeFlow(tokenId, windows) {
|
|
1394
|
+
let trades = null;
|
|
1395
|
+
try {
|
|
1396
|
+
trades = await cachedFetchJSON(`${CLOB_API}/trades?token_id=${tokenId}&limit=500`);
|
|
1397
|
+
} catch {
|
|
1398
|
+
return { token_id: tokenId, flows: {} };
|
|
1399
|
+
}
|
|
1400
|
+
if (!trades?.length) return { token_id: tokenId, flows: {} };
|
|
1401
|
+
const now = Date.now();
|
|
1402
|
+
const windowMs = { "5m": 5 * 6e4, "15m": 15 * 6e4, "1h": 60 * 6e4, "4h": 4 * 60 * 6e4, "24h": 24 * 60 * 6e4 };
|
|
1403
|
+
const requestedWindows = windows || ["1h", "4h", "24h"];
|
|
1404
|
+
const flows = {};
|
|
1405
|
+
for (const window of requestedWindows) {
|
|
1406
|
+
const ms = windowMs[window] || 60 * 6e4;
|
|
1407
|
+
const cutoff = now - ms;
|
|
1408
|
+
const windowTrades = trades.filter((t) => {
|
|
1409
|
+
const ts = new Date(t.match_time || t.created_at).getTime();
|
|
1410
|
+
return ts >= cutoff;
|
|
1411
|
+
});
|
|
1412
|
+
let buyVolume = 0, sellVolume = 0, buyCount = 0, sellCount = 0;
|
|
1413
|
+
for (const t of windowTrades) {
|
|
1414
|
+
const value = parseFloat(t.size || "0") * parseFloat(t.price || "0");
|
|
1415
|
+
if (t.side === "BUY") {
|
|
1416
|
+
buyVolume += value;
|
|
1417
|
+
buyCount++;
|
|
1418
|
+
} else {
|
|
1419
|
+
sellVolume += value;
|
|
1420
|
+
sellCount++;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
const netFlow = buyVolume - sellVolume;
|
|
1424
|
+
flows[window] = {
|
|
1425
|
+
buy_volume: +buyVolume.toFixed(2),
|
|
1426
|
+
sell_volume: +sellVolume.toFixed(2),
|
|
1427
|
+
net_flow: +netFlow.toFixed(2),
|
|
1428
|
+
buy_count: buyCount,
|
|
1429
|
+
sell_count: sellCount,
|
|
1430
|
+
total_trades: windowTrades.length,
|
|
1431
|
+
signal: netFlow > 0 ? "bullish" : netFlow < 0 ? "bearish" : "neutral"
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1434
|
+
return { token_id: tokenId, flows };
|
|
1435
|
+
}
|
|
1436
|
+
async function profileWallet(wallet, tokenId) {
|
|
1437
|
+
const addr = wallet.toLowerCase();
|
|
1438
|
+
const [makerTrades, takerTrades] = await Promise.all([
|
|
1439
|
+
cachedFetchJSON(`${CLOB_API}/trades?maker_address=${addr}&limit=200`).catch(() => []),
|
|
1440
|
+
cachedFetchJSON(`${CLOB_API}/trades?taker_address=${addr}&limit=200`).catch(() => [])
|
|
1441
|
+
]);
|
|
1442
|
+
const allTrades = [...makerTrades || [], ...takerTrades || []];
|
|
1443
|
+
const filtered = tokenId ? allTrades.filter((t) => t.asset_id === tokenId || t.token_id === tokenId) : allTrades;
|
|
1444
|
+
let totalVolume = 0, buyVolume = 0, sellVolume = 0;
|
|
1445
|
+
const markets = /* @__PURE__ */ new Set();
|
|
1446
|
+
const tokens = /* @__PURE__ */ new Set();
|
|
1447
|
+
for (const t of filtered) {
|
|
1448
|
+
const value = parseFloat(t.size || "0") * parseFloat(t.price || "0");
|
|
1449
|
+
totalVolume += value;
|
|
1450
|
+
if (t.side === "BUY") buyVolume += value;
|
|
1451
|
+
else sellVolume += value;
|
|
1452
|
+
if (t.market) markets.add(t.market);
|
|
1453
|
+
if (t.asset_id || t.token_id) tokens.add(t.asset_id || t.token_id);
|
|
1454
|
+
}
|
|
1455
|
+
return {
|
|
1456
|
+
wallet: addr,
|
|
1457
|
+
total_trades: filtered.length,
|
|
1458
|
+
total_volume: +totalVolume.toFixed(2),
|
|
1459
|
+
buy_volume: +buyVolume.toFixed(2),
|
|
1460
|
+
sell_volume: +sellVolume.toFixed(2),
|
|
1461
|
+
unique_markets: markets.size,
|
|
1462
|
+
unique_tokens: tokens.size,
|
|
1463
|
+
avg_trade_size: filtered.length ? +(totalVolume / filtered.length).toFixed(2) : 0,
|
|
1464
|
+
buy_sell_ratio: sellVolume > 0 ? +(buyVolume / sellVolume).toFixed(2) : buyVolume > 0 ? Infinity : 0,
|
|
1465
|
+
first_trade: filtered.length ? filtered[filtered.length - 1]?.match_time : null,
|
|
1466
|
+
last_trade: filtered.length ? filtered[0]?.match_time : null,
|
|
1467
|
+
recent_trades: filtered.slice(0, 10).map((t) => ({
|
|
1468
|
+
side: t.side,
|
|
1469
|
+
size: parseFloat(t.size || "0"),
|
|
1470
|
+
price: parseFloat(t.price || "0"),
|
|
1471
|
+
value: +(parseFloat(t.size || "0") * parseFloat(t.price || "0")).toFixed(2),
|
|
1472
|
+
time: t.match_time || t.created_at
|
|
1473
|
+
}))
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1476
|
+
async function mapLiquidity(tokenIds) {
|
|
1477
|
+
if (!tokenIds.length) throw new Error("Provide token_ids");
|
|
1478
|
+
const books = await Promise.all(tokenIds.map(async (tid) => {
|
|
1479
|
+
try {
|
|
1480
|
+
const book = await cachedFetchJSON(`${CLOB_API}/book?token_id=${tid}`, 8e3);
|
|
1481
|
+
const bids = (book?.bids || []).map((b) => ({ price: parseFloat(b.price), size: parseFloat(b.size) }));
|
|
1482
|
+
const asks = (book?.asks || []).map((a) => ({ price: parseFloat(a.price), size: parseFloat(a.size) }));
|
|
1483
|
+
const totalBid = bids.reduce((s, b) => s + b.size * b.price, 0);
|
|
1484
|
+
const totalAsk = asks.reduce((s, a) => s + a.size * a.price, 0);
|
|
1485
|
+
const bestBid = bids.length ? Math.max(...bids.map((b) => b.price)) : 0;
|
|
1486
|
+
const bestAsk = asks.length ? Math.min(...asks.map((a) => a.price)) : 1;
|
|
1487
|
+
const spread = bestAsk - bestBid;
|
|
1488
|
+
const midPrice = (bestBid + bestAsk) / 2;
|
|
1489
|
+
const spreadPct = midPrice > 0 ? +(spread / midPrice * 100).toFixed(2) : 999;
|
|
1490
|
+
const totalDepth = totalBid + totalAsk;
|
|
1491
|
+
const imbalance = totalDepth > 0 ? +((totalBid - totalAsk) / totalDepth).toFixed(3) : 0;
|
|
1492
|
+
return { token_id: tid, bestBid, bestAsk, midPrice: +midPrice.toFixed(4), spreadPct, totalBidLiquidity: +totalBid.toFixed(2), totalAskLiquidity: +totalAsk.toFixed(2), imbalance, bidLevels: bids.length, askLevels: asks.length, signal: imbalance > 0.3 ? "bullish" : imbalance < -0.3 ? "bearish" : "neutral" };
|
|
1493
|
+
} catch {
|
|
1494
|
+
return { token_id: tid, error: "Failed to fetch" };
|
|
1495
|
+
}
|
|
1496
|
+
}));
|
|
1497
|
+
const ranked = books.filter((b) => !b.error).sort((a, b) => (a.spreadPct || 999) - (b.spreadPct || 999));
|
|
1498
|
+
return {
|
|
1499
|
+
tokens_analyzed: tokenIds.length,
|
|
1500
|
+
liquidity_map: ranked,
|
|
1501
|
+
best_to_trade: ranked[0]?.token_id || null,
|
|
1502
|
+
worst_to_trade: ranked[ranked.length - 1]?.token_id || null,
|
|
1503
|
+
total_liquidity: +ranked.reduce((s, b) => s + (b.totalBidLiquidity || 0) + (b.totalAskLiquidity || 0), 0).toFixed(2),
|
|
1504
|
+
warnings: ranked.filter((b) => b.spreadPct > 5).map((b) => `${b.token_id}: spread ${b.spreadPct}% \u2014 AVOID`)
|
|
1505
|
+
};
|
|
1506
|
+
}
|
|
1507
|
+
async function decodeTransaction(txHash) {
|
|
1508
|
+
const scanUrl = `https://api.polygonscan.com/api?module=proxy&action=eth_getTransactionByHash&txhash=${txHash}`;
|
|
1509
|
+
const txData = await cachedFetchJSON(scanUrl).catch(() => null);
|
|
1510
|
+
if (!txData?.result) throw new Error("Transaction not found");
|
|
1511
|
+
const result = txData.result;
|
|
1512
|
+
const to = (result.to || "").toLowerCase();
|
|
1513
|
+
const isCtf = to === CTF_ADDRESS.toLowerCase();
|
|
1514
|
+
const methodSig = (result.input || "").slice(0, 10);
|
|
1515
|
+
const methods = {
|
|
1516
|
+
"0x": "ETH Transfer",
|
|
1517
|
+
"0xa9059cbb": "ERC20 Transfer",
|
|
1518
|
+
"0x23b872dd": "ERC20 TransferFrom",
|
|
1519
|
+
"0x2eb2c2d6": "safeBatchTransferFrom (CTF position transfer)",
|
|
1520
|
+
"0xf242432a": "safeTransferFrom (CTF single transfer)",
|
|
1521
|
+
"0xfb16a595": "splitPosition (mint conditional tokens)",
|
|
1522
|
+
"0x7e7e4b47": "mergePositions (redeem conditional tokens)",
|
|
1523
|
+
"0x3b2bcbf1": "redeemPositions (claim winnings)"
|
|
1524
|
+
};
|
|
1525
|
+
return {
|
|
1526
|
+
tx_hash: txHash,
|
|
1527
|
+
from: result.from,
|
|
1528
|
+
to: result.to,
|
|
1529
|
+
value: parseInt(result.value || "0", 16) / 1e18,
|
|
1530
|
+
is_polymarket_ctf: isCtf,
|
|
1531
|
+
method: methods[methodSig] || `Unknown (${methodSig})`,
|
|
1532
|
+
gas_price_gwei: parseInt(result.gasPrice || "0", 16) / 1e9,
|
|
1533
|
+
block: parseInt(result.blockNumber || "0", 16)
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
async function getWalletTransactions(wallet, limit = 20) {
|
|
1537
|
+
const url = `https://api.polygonscan.com/api?module=account&action=txlist&address=${wallet}&startblock=0&endblock=99999999&page=1&offset=${limit}&sort=desc`;
|
|
1538
|
+
const data = await cachedFetchJSON(url);
|
|
1539
|
+
if (!data?.result) return { wallet, total_transactions: 0, polymarket_related: 0, transactions: [] };
|
|
1540
|
+
const txs = (data.result || []).map((tx) => ({
|
|
1541
|
+
hash: tx.hash,
|
|
1542
|
+
to: tx.to,
|
|
1543
|
+
from: tx.from,
|
|
1544
|
+
value_matic: +(parseInt(tx.value || "0") / 1e18).toFixed(4),
|
|
1545
|
+
is_ctf: (tx.to || "").toLowerCase() === CTF_ADDRESS.toLowerCase(),
|
|
1546
|
+
is_usdc: (tx.to || "").toLowerCase() === USDC_ADDRESS.toLowerCase(),
|
|
1547
|
+
method: tx.functionName || tx.methodId,
|
|
1548
|
+
timestamp: new Date(parseInt(tx.timeStamp) * 1e3).toISOString(),
|
|
1549
|
+
status: tx.isError === "0" ? "success" : "failed"
|
|
1550
|
+
}));
|
|
1551
|
+
const polyTxs = txs.filter((t) => t.is_ctf || t.is_usdc);
|
|
1552
|
+
return {
|
|
1553
|
+
wallet,
|
|
1554
|
+
total_transactions: txs.length,
|
|
1555
|
+
polymarket_related: polyTxs.length,
|
|
1556
|
+
transactions: polyTxs.length > 0 ? polyTxs : txs.slice(0, 10)
|
|
1557
|
+
};
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
// src/polymarket-engines/social.ts
|
|
1561
|
+
async function analyzeTwitterSentiment(query, includePolymarket = true) {
|
|
1562
|
+
const searches = [query];
|
|
1563
|
+
if (includePolymarket) searches.push(`${query} polymarket`);
|
|
1564
|
+
const results = [];
|
|
1565
|
+
for (const q of searches) {
|
|
1566
|
+
try {
|
|
1567
|
+
const rssUrl = `https://news.google.com/rss/search?q=${encodeURIComponent(q)}&hl=en-US&gl=US&ceid=US:en`;
|
|
1568
|
+
const xml = await cachedFetchText(rssUrl);
|
|
1569
|
+
const items = [];
|
|
1570
|
+
const itemRegex = /<item>([\s\S]*?)<\/item>/g;
|
|
1571
|
+
let match;
|
|
1572
|
+
while ((match = itemRegex.exec(xml)) !== null) {
|
|
1573
|
+
const get = (tag) => {
|
|
1574
|
+
const m2 = match[1].match(new RegExp(`<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`));
|
|
1575
|
+
return m2 ? m2[1].replace(/<!\[CDATA\[|\]\]>/g, "").trim() : "";
|
|
1576
|
+
};
|
|
1577
|
+
const title = get("title");
|
|
1578
|
+
if (title) {
|
|
1579
|
+
items.push({ title, link: get("link"), date: get("pubDate"), source: get("source") || "Google News", sentiment: scoreSentiment(title) });
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
results.push({ query: q, items: items.slice(0, 15), avg_sentiment: items.length ? +(items.reduce((s, i) => s + i.sentiment, 0) / items.length).toFixed(3) : 0, volume: items.length });
|
|
1583
|
+
} catch (e) {
|
|
1584
|
+
results.push({ query: q, error: e.message, items: [], avg_sentiment: 0, volume: 0 });
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
const allItems = results.flatMap((r) => r.items || []);
|
|
1588
|
+
const avgSentiment = allItems.length ? +(allItems.reduce((s, i) => s + i.sentiment, 0) / allItems.length).toFixed(3) : 0;
|
|
1589
|
+
return {
|
|
1590
|
+
query,
|
|
1591
|
+
overall_sentiment: avgSentiment,
|
|
1592
|
+
sentiment_label: avgSentiment > 0.2 ? "BULLISH" : avgSentiment < -0.2 ? "BEARISH" : "NEUTRAL",
|
|
1593
|
+
total_mentions: allItems.length,
|
|
1594
|
+
results,
|
|
1595
|
+
topics_detected: extractTopics(allItems.map((i) => i.title).join(" ")),
|
|
1596
|
+
signal: avgSentiment > 0.3 ? "BUY_SIGNAL" : avgSentiment < -0.3 ? "SELL_SIGNAL" : "NO_SIGNAL"
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
async function analyzePolymarketComments(marketSlug) {
|
|
1600
|
+
const marketData = await cachedFetchJSON(`${GAMMA_API}/markets?slug=${marketSlug}`).catch(() => null);
|
|
1601
|
+
const market = marketData?.[0];
|
|
1602
|
+
if (!market) throw new Error("Market not found \u2014 provide valid slug");
|
|
1603
|
+
const conditionId = market.condition_id || market.conditionId;
|
|
1604
|
+
let comments = [];
|
|
1605
|
+
try {
|
|
1606
|
+
const commentsUrl = `${GAMMA_API}/comments?market=${conditionId}&limit=50&sort=recent`;
|
|
1607
|
+
const commentsData = await cachedFetchJSON(commentsUrl);
|
|
1608
|
+
comments = (commentsData || []).map((c) => ({
|
|
1609
|
+
author: c.author || c.user_id || "anon",
|
|
1610
|
+
text: c.text || c.body || c.content || "",
|
|
1611
|
+
likes: c.likes || c.upvotes || 0,
|
|
1612
|
+
timestamp: c.created_at || c.timestamp,
|
|
1613
|
+
sentiment: scoreSentiment(c.text || c.body || c.content || "")
|
|
1614
|
+
}));
|
|
1615
|
+
} catch {
|
|
1616
|
+
}
|
|
1617
|
+
if (comments.length === 0) {
|
|
1618
|
+
return {
|
|
1619
|
+
market: market.question,
|
|
1620
|
+
slug: marketSlug,
|
|
1621
|
+
total_comments: 0,
|
|
1622
|
+
avg_sentiment: 0,
|
|
1623
|
+
sentiment_label: "NO_DATA",
|
|
1624
|
+
top_comments: [],
|
|
1625
|
+
bullish_comments: 0,
|
|
1626
|
+
bearish_comments: 0,
|
|
1627
|
+
browser_url: `https://polymarket.com/event/${marketSlug}`
|
|
1628
|
+
};
|
|
1629
|
+
}
|
|
1630
|
+
const avgSentiment = +(comments.reduce((s, c) => s + c.sentiment, 0) / comments.length).toFixed(3);
|
|
1631
|
+
return {
|
|
1632
|
+
market: market.question,
|
|
1633
|
+
slug: marketSlug,
|
|
1634
|
+
total_comments: comments.length,
|
|
1635
|
+
avg_sentiment: avgSentiment,
|
|
1636
|
+
sentiment_label: avgSentiment > 0.15 ? "BULLISH" : avgSentiment < -0.15 ? "BEARISH" : "MIXED",
|
|
1637
|
+
top_comments: comments.slice(0, 20),
|
|
1638
|
+
bullish_comments: comments.filter((c) => c.sentiment > 0.2).length,
|
|
1639
|
+
bearish_comments: comments.filter((c) => c.sentiment < -0.2).length
|
|
1640
|
+
};
|
|
1641
|
+
}
|
|
1642
|
+
async function monitorRedditPulse(query, subreddits, timeframe = "day") {
|
|
1643
|
+
const subs = subreddits || ["polymarket", "politics", "sports", "worldnews"];
|
|
1644
|
+
const results = [];
|
|
1645
|
+
for (const sub of subs) {
|
|
1646
|
+
try {
|
|
1647
|
+
const url = `https://www.reddit.com/r/${sub}/search.json?q=${encodeURIComponent(query)}&restrict_sr=1&sort=relevance&t=${timeframe}&limit=25`;
|
|
1648
|
+
const data = await cachedFetchJSON(url, 3e4, 15e3);
|
|
1649
|
+
const posts = (data?.data?.children || []).map((child) => {
|
|
1650
|
+
const p = child.data;
|
|
1651
|
+
return {
|
|
1652
|
+
title: p.title,
|
|
1653
|
+
subreddit: p.subreddit,
|
|
1654
|
+
score: p.score,
|
|
1655
|
+
num_comments: p.num_comments,
|
|
1656
|
+
url: `https://reddit.com${p.permalink}`,
|
|
1657
|
+
created: new Date(p.created_utc * 1e3).toISOString(),
|
|
1658
|
+
selftext_preview: (p.selftext || "").slice(0, 200),
|
|
1659
|
+
sentiment: scoreSentiment(p.title + " " + (p.selftext || "")),
|
|
1660
|
+
upvote_ratio: p.upvote_ratio
|
|
1661
|
+
};
|
|
1662
|
+
});
|
|
1663
|
+
results.push({
|
|
1664
|
+
subreddit: sub,
|
|
1665
|
+
posts,
|
|
1666
|
+
avg_sentiment: posts.length ? +(posts.reduce((s, p) => s + p.sentiment, 0) / posts.length).toFixed(3) : 0,
|
|
1667
|
+
total_engagement: posts.reduce((s, p) => s + p.score + p.num_comments, 0)
|
|
1668
|
+
});
|
|
1669
|
+
} catch (e) {
|
|
1670
|
+
results.push({ subreddit: sub, error: e.message, posts: [] });
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
const allPosts = results.flatMap((r) => r.posts || []);
|
|
1674
|
+
const avgSentiment = allPosts.length ? +(allPosts.reduce((s, p) => s + p.sentiment, 0) / allPosts.length).toFixed(3) : 0;
|
|
1675
|
+
return {
|
|
1676
|
+
query,
|
|
1677
|
+
subreddits_searched: subs,
|
|
1678
|
+
total_posts: allPosts.length,
|
|
1679
|
+
overall_sentiment: avgSentiment,
|
|
1680
|
+
sentiment_label: avgSentiment > 0.15 ? "BULLISH" : avgSentiment < -0.15 ? "BEARISH" : "NEUTRAL",
|
|
1681
|
+
results,
|
|
1682
|
+
trending: allPosts.sort((a, b) => b.score - a.score).slice(0, 5).map((p) => ({ title: p.title, score: p.score, subreddit: p.subreddit }))
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1685
|
+
async function monitorTelegram(channels, query, limit = 20) {
|
|
1686
|
+
const results = [];
|
|
1687
|
+
for (const channel of channels) {
|
|
1688
|
+
try {
|
|
1689
|
+
const url = `https://t.me/s/${channel.replace("@", "")}`;
|
|
1690
|
+
const html = await cachedFetchText(url, 15e3);
|
|
1691
|
+
const messageRegex = /<div class="tgme_widget_message_text[^"]*"[^>]*>([\s\S]*?)<\/div>/g;
|
|
1692
|
+
const messages = [];
|
|
1693
|
+
let match;
|
|
1694
|
+
while ((match = messageRegex.exec(html)) !== null) {
|
|
1695
|
+
const text = match[1].replace(/<[^>]+>/g, "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').trim();
|
|
1696
|
+
if (text.length > 10) {
|
|
1697
|
+
messages.push({ text: text.slice(0, 500), sentiment: scoreSentiment(text) });
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
const filtered = query ? messages.filter((m) => m.text.toLowerCase().includes(query.toLowerCase())) : messages;
|
|
1701
|
+
results.push({
|
|
1702
|
+
channel: channel.replace("@", ""),
|
|
1703
|
+
messages: filtered.slice(0, limit),
|
|
1704
|
+
total: filtered.length,
|
|
1705
|
+
avg_sentiment: filtered.length ? +(filtered.reduce((s, m) => s + m.sentiment, 0) / filtered.length).toFixed(3) : 0
|
|
1706
|
+
});
|
|
1707
|
+
} catch (e) {
|
|
1708
|
+
results.push({ channel: channel.replace("@", ""), error: e.message, messages: [], total: 0, avg_sentiment: 0 });
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
const allMessages = results.flatMap((r) => r.messages || []);
|
|
1712
|
+
return {
|
|
1713
|
+
channels_monitored: channels.length,
|
|
1714
|
+
total_messages: allMessages.length,
|
|
1715
|
+
overall_sentiment: allMessages.length ? +(allMessages.reduce((s, m) => s + m.sentiment, 0) / allMessages.length).toFixed(3) : 0,
|
|
1716
|
+
results
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1719
|
+
async function measureSocialVelocity(topic, compareTopic) {
|
|
1720
|
+
async function measure(t) {
|
|
1721
|
+
const sources = [];
|
|
1722
|
+
try {
|
|
1723
|
+
const encoded = encodeURIComponent(t);
|
|
1724
|
+
const [recent, older] = await Promise.all([
|
|
1725
|
+
cachedFetchText(`https://news.google.com/rss/search?q=${encoded}+when:1h&hl=en-US&gl=US&ceid=US:en`).catch(() => ""),
|
|
1726
|
+
cachedFetchText(`https://news.google.com/rss/search?q=${encoded}+when:24h&hl=en-US&gl=US&ceid=US:en`).catch(() => "")
|
|
1727
|
+
]);
|
|
1728
|
+
const recentCount = (recent.match(/<item>/g) || []).length;
|
|
1729
|
+
const olderCount = (older.match(/<item>/g) || []).length;
|
|
1730
|
+
const hourlyRate = olderCount > 0 ? olderCount / 24 : 0;
|
|
1731
|
+
const acceleration = hourlyRate > 0 ? recentCount / hourlyRate : recentCount;
|
|
1732
|
+
sources.push({
|
|
1733
|
+
source: "google_news",
|
|
1734
|
+
last_hour: recentCount,
|
|
1735
|
+
last_24h: olderCount,
|
|
1736
|
+
hourly_rate: +hourlyRate.toFixed(1),
|
|
1737
|
+
acceleration: +acceleration.toFixed(2),
|
|
1738
|
+
status: acceleration > 3 ? "SPIKE" : acceleration > 1.5 ? "RISING" : "NORMAL"
|
|
1739
|
+
});
|
|
1740
|
+
} catch {
|
|
1741
|
+
}
|
|
1742
|
+
try {
|
|
1743
|
+
const url = `https://www.reddit.com/search.json?q=${encodeURIComponent(t)}&sort=new&t=day&limit=100`;
|
|
1744
|
+
const data = await cachedFetchJSON(url, 3e4, 15e3);
|
|
1745
|
+
const posts = data?.data?.children || [];
|
|
1746
|
+
const now = Date.now() / 1e3;
|
|
1747
|
+
const lastHour = posts.filter((p) => now - p.data.created_utc < 3600).length;
|
|
1748
|
+
const last24h = posts.length;
|
|
1749
|
+
const hourlyRate = last24h / 24;
|
|
1750
|
+
const acceleration = hourlyRate > 0 ? lastHour / hourlyRate : lastHour;
|
|
1751
|
+
sources.push({
|
|
1752
|
+
source: "reddit",
|
|
1753
|
+
last_hour: lastHour,
|
|
1754
|
+
last_24h: last24h,
|
|
1755
|
+
hourly_rate: +hourlyRate.toFixed(1),
|
|
1756
|
+
acceleration: +acceleration.toFixed(2),
|
|
1757
|
+
status: acceleration > 3 ? "SPIKE" : acceleration > 1.5 ? "RISING" : "NORMAL"
|
|
1758
|
+
});
|
|
1759
|
+
} catch {
|
|
1760
|
+
}
|
|
1761
|
+
const totalAcceleration = sources.length ? +(sources.reduce((s, src) => s + (src.acceleration || 0), 0) / sources.length).toFixed(2) : 0;
|
|
1762
|
+
return {
|
|
1763
|
+
topic: t,
|
|
1764
|
+
sources,
|
|
1765
|
+
composite_acceleration: +totalAcceleration,
|
|
1766
|
+
alert_level: +totalAcceleration > 5 ? "CRITICAL" : +totalAcceleration > 3 ? "HIGH" : +totalAcceleration > 1.5 ? "ELEVATED" : "NORMAL"
|
|
1767
|
+
};
|
|
1768
|
+
}
|
|
1769
|
+
const primary = await measure(topic);
|
|
1770
|
+
const comparison = compareTopic ? await measure(compareTopic) : void 0;
|
|
1771
|
+
return {
|
|
1772
|
+
...primary,
|
|
1773
|
+
comparison,
|
|
1774
|
+
recommendation: primary.alert_level === "CRITICAL" ? "URGENT: Massive discussion spike detected \u2014 check news immediately and consider trading before the market reprices" : primary.alert_level === "HIGH" ? "Discussion accelerating rapidly \u2014 monitor closely and prepare to trade" : "Normal discussion levels \u2014 no urgency"
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
// src/polymarket-engines/feeds.ts
|
|
1779
|
+
var SOURCE_URLS = {
|
|
1780
|
+
whitehouse: "https://www.whitehouse.gov/feed/",
|
|
1781
|
+
scotus: "https://www.supremecourt.gov/rss/cases/opinions.xml",
|
|
1782
|
+
sec: "https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&type=8-K&dateb=&owner=include&count=20&search_text=&action=getcompany&output=atom",
|
|
1783
|
+
fed: "https://www.federalreserve.gov/feeds/press_all.xml",
|
|
1784
|
+
espn: "https://www.espn.com/espn/rss/news",
|
|
1785
|
+
noaa: "https://alerts.weather.gov/cap/us.php?x=0",
|
|
1786
|
+
congress: "https://www.congress.gov/rss/most-viewed-bills.xml"
|
|
1787
|
+
};
|
|
1788
|
+
async function fetchOfficialSource(source, query, customUrl) {
|
|
1789
|
+
const url = source === "custom" ? customUrl : SOURCE_URLS[source];
|
|
1790
|
+
if (!url) throw new Error(`Unknown source: ${source}`);
|
|
1791
|
+
const text = await cachedFetchText(url, 15e3);
|
|
1792
|
+
let items = parseRSSItems(text);
|
|
1793
|
+
if (items.length === 0) {
|
|
1794
|
+
const entryRegex = /<entry>([\s\S]*?)<\/entry>/g;
|
|
1795
|
+
let match;
|
|
1796
|
+
while ((match = entryRegex.exec(text)) !== null) {
|
|
1797
|
+
const get = (tag) => {
|
|
1798
|
+
const m2 = match[1].match(new RegExp(`<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`));
|
|
1799
|
+
return m2 ? m2[1].replace(/<!\[CDATA\[|\]\]>/g, "").trim() : "";
|
|
1800
|
+
};
|
|
1801
|
+
const linkMatch = match[1].match(/<link[^>]*href="([^"]+)"/);
|
|
1802
|
+
items.push({
|
|
1803
|
+
title: get("title"),
|
|
1804
|
+
link: linkMatch?.[1] || "",
|
|
1805
|
+
pubDate: get("updated") || get("published"),
|
|
1806
|
+
description: get("summary").replace(/<[^>]+>/g, "").slice(0, 300)
|
|
1807
|
+
});
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
const filtered = query ? items.filter((i) => (i.title + " " + i.description).toLowerCase().includes(query.toLowerCase())) : items;
|
|
1811
|
+
return {
|
|
1812
|
+
source,
|
|
1813
|
+
url,
|
|
1814
|
+
total_items: items.length,
|
|
1815
|
+
filtered: filtered.length,
|
|
1816
|
+
items: filtered.slice(0, 25),
|
|
1817
|
+
latest: filtered[0] || null
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
async function compareOdds(marketQuestion, polymarketPrice) {
|
|
1821
|
+
const searches = [`${marketQuestion} odds betting`, `${marketQuestion} prediction probability`];
|
|
1822
|
+
const references = [];
|
|
1823
|
+
for (const q of searches) {
|
|
1824
|
+
try {
|
|
1825
|
+
const xml = await cachedFetchText(`https://news.google.com/rss/search?q=${encodeURIComponent(q)}&hl=en-US&gl=US&ceid=US:en`);
|
|
1826
|
+
const items = parseRSSItems(xml);
|
|
1827
|
+
for (const item of items.slice(0, 5)) {
|
|
1828
|
+
const oddsMatch = item.title.match(/(\d+)%/);
|
|
1829
|
+
const ratioMatch = item.title.match(/(\d+)[/-](\d+)/);
|
|
1830
|
+
references.push({
|
|
1831
|
+
source: item.source || "News",
|
|
1832
|
+
headline: item.title,
|
|
1833
|
+
link: item.link,
|
|
1834
|
+
extracted_probability: oddsMatch ? parseInt(oddsMatch[1]) / 100 : null,
|
|
1835
|
+
extracted_odds: ratioMatch ? `${ratioMatch[1]}/${ratioMatch[2]}` : null,
|
|
1836
|
+
date: item.pubDate
|
|
1837
|
+
});
|
|
1838
|
+
}
|
|
1839
|
+
} catch {
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
const oddsSites = [
|
|
1843
|
+
{ name: "Betfair", note: "UK-based exchange \u2014 check betfair.com for odds" },
|
|
1844
|
+
{ name: "Pinnacle", note: "Sharp book \u2014 their odds are considered most efficient" },
|
|
1845
|
+
{ name: "Metaculus", note: "Forecasting platform \u2014 check metaculus.com" },
|
|
1846
|
+
{ name: "Manifold Markets", note: "Play-money market \u2014 check manifold.markets" }
|
|
1847
|
+
];
|
|
1848
|
+
let divergence = null;
|
|
1849
|
+
if (polymarketPrice) {
|
|
1850
|
+
const externalOdds = references.filter((r) => r.extracted_probability).map((r) => r.extracted_probability);
|
|
1851
|
+
if (externalOdds.length > 0) {
|
|
1852
|
+
const avgExternal = externalOdds.reduce((s, o) => s + o, 0) / externalOdds.length;
|
|
1853
|
+
divergence = {
|
|
1854
|
+
polymarket: polymarketPrice,
|
|
1855
|
+
external_avg: +avgExternal.toFixed(3),
|
|
1856
|
+
difference: +(polymarketPrice - avgExternal).toFixed(3),
|
|
1857
|
+
pct_difference: +((polymarketPrice - avgExternal) / avgExternal * 100).toFixed(1),
|
|
1858
|
+
signal: Math.abs(polymarketPrice - avgExternal) > 0.05 ? polymarketPrice > avgExternal ? "POLYMARKET_OVERPRICED" : "POLYMARKET_UNDERPRICED" : "ALIGNED"
|
|
1859
|
+
};
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
return {
|
|
1863
|
+
market_question: marketQuestion,
|
|
1864
|
+
polymarket_price: polymarketPrice || "not provided",
|
|
1865
|
+
external_references: references,
|
|
1866
|
+
odds_comparison_sites: oddsSites,
|
|
1867
|
+
divergence,
|
|
1868
|
+
recommendation: divergence?.signal === "POLYMARKET_UNDERPRICED" ? "Polymarket is cheaper than external odds \u2014 potential BUY opportunity" : divergence?.signal === "POLYMARKET_OVERPRICED" ? "Polymarket is more expensive than external odds \u2014 consider SELL/SHORT" : "Prices roughly aligned across platforms"
|
|
1869
|
+
};
|
|
1870
|
+
}
|
|
1871
|
+
async function trackResolution(marketSlug, conditionId) {
|
|
1872
|
+
let market = null;
|
|
1873
|
+
if (marketSlug) {
|
|
1874
|
+
const data = await cachedFetchJSON(`${GAMMA_API}/markets?slug=${marketSlug}`);
|
|
1875
|
+
market = data?.[0];
|
|
1876
|
+
} else if (conditionId) {
|
|
1877
|
+
const data = await cachedFetchJSON(`${GAMMA_API}/markets?condition_id=${conditionId}`);
|
|
1878
|
+
market = data?.[0];
|
|
1879
|
+
}
|
|
1880
|
+
if (!market) throw new Error("Market not found");
|
|
1881
|
+
const description = market.description || "";
|
|
1882
|
+
const resolutionSource = market.resolution_source || market.resolutionSource || "";
|
|
1883
|
+
const urlRegex = /https?:\/\/[^\s<>"']+/g;
|
|
1884
|
+
const urls = Array.from(/* @__PURE__ */ new Set([...description.match(urlRegex) || [], ...resolutionSource.match(urlRegex) || []]));
|
|
1885
|
+
const descLower = description.toLowerCase();
|
|
1886
|
+
let resolutionType = "unknown";
|
|
1887
|
+
if (descLower.includes("associated press") || descLower.includes(" ap ")) resolutionType = "AP call";
|
|
1888
|
+
else if (descLower.includes("espn") || descLower.includes("sports")) resolutionType = "sports result";
|
|
1889
|
+
else if (descLower.includes("government") || descLower.includes("official")) resolutionType = "government source";
|
|
1890
|
+
else if (descLower.includes("court") || descLower.includes("ruling")) resolutionType = "court decision";
|
|
1891
|
+
else if (descLower.includes("sec") || descLower.includes("filing")) resolutionType = "regulatory filing";
|
|
1892
|
+
else if (descLower.includes("price") || descLower.includes("close")) resolutionType = "market price";
|
|
1893
|
+
else if (urls.length > 0) resolutionType = "external source";
|
|
1894
|
+
return {
|
|
1895
|
+
market: market.question,
|
|
1896
|
+
slug: market.slug,
|
|
1897
|
+
condition_id: market.condition_id || market.conditionId,
|
|
1898
|
+
end_date: market.end_date_iso || market.endDate,
|
|
1899
|
+
resolution_source: resolutionSource,
|
|
1900
|
+
resolution_type: resolutionType,
|
|
1901
|
+
resolution_description: description.slice(0, 1e3),
|
|
1902
|
+
source_urls: urls,
|
|
1903
|
+
current_price: market.outcomePrices ? JSON.parse(market.outcomePrices) : null,
|
|
1904
|
+
recommendation: `Monitor these sources directly: ${urls.join(", ") || "Check market description for resolution criteria"}`,
|
|
1905
|
+
risk_factors: [
|
|
1906
|
+
market.active === false ? "MARKET INACTIVE" : "",
|
|
1907
|
+
descLower.includes("discretion") ? "Resolution may involve discretionary judgment" : "",
|
|
1908
|
+
descLower.includes("ambiguous") ? "Resolution criteria may be ambiguous" : "",
|
|
1909
|
+
!resolutionSource ? "No explicit resolution source specified" : ""
|
|
1910
|
+
].filter(Boolean)
|
|
1911
|
+
};
|
|
1912
|
+
}
|
|
1913
|
+
async function fetchBreakingNews(params) {
|
|
1914
|
+
const rssFeeds = {
|
|
1915
|
+
ap: "https://rsshub.app/apnews/topics/apf-topnews",
|
|
1916
|
+
reuters: "https://news.google.com/rss/search?q=site:reuters.com&hl=en-US&gl=US&ceid=US:en",
|
|
1917
|
+
bbc: "https://feeds.bbci.co.uk/news/rss.xml",
|
|
1918
|
+
cnn: "https://rss.cnn.com/rss/cnn_topstories.rss"
|
|
1919
|
+
};
|
|
1920
|
+
const requestedSources = params?.sources || Object.keys(rssFeeds);
|
|
1921
|
+
const topics = params?.topics || null;
|
|
1922
|
+
const sinceMs = (params?.since_minutes || 60) * 60 * 1e3;
|
|
1923
|
+
const cutoff = new Date(Date.now() - sinceMs);
|
|
1924
|
+
const allItems = [];
|
|
1925
|
+
await Promise.all(requestedSources.map(async (source) => {
|
|
1926
|
+
const url = rssFeeds[source];
|
|
1927
|
+
if (!url) return;
|
|
1928
|
+
try {
|
|
1929
|
+
const xml = await cachedFetchText(url, 15e3);
|
|
1930
|
+
const items = parseRSSItems(xml);
|
|
1931
|
+
for (const item of items) {
|
|
1932
|
+
const pubDate = item.pubDate ? new Date(item.pubDate) : /* @__PURE__ */ new Date();
|
|
1933
|
+
if (pubDate < cutoff) continue;
|
|
1934
|
+
if (topics) {
|
|
1935
|
+
const lower = (item.title + " " + item.description).toLowerCase();
|
|
1936
|
+
if (!topics.some((t) => lower.includes(t.toLowerCase()))) continue;
|
|
1937
|
+
}
|
|
1938
|
+
const title = item.title.toLowerCase();
|
|
1939
|
+
let relevance = 0;
|
|
1940
|
+
const marketKeywords = [
|
|
1941
|
+
"election",
|
|
1942
|
+
"trump",
|
|
1943
|
+
"biden",
|
|
1944
|
+
"fed",
|
|
1945
|
+
"rate",
|
|
1946
|
+
"court",
|
|
1947
|
+
"ruling",
|
|
1948
|
+
"war",
|
|
1949
|
+
"peace",
|
|
1950
|
+
"crypto",
|
|
1951
|
+
"bitcoin",
|
|
1952
|
+
"ai",
|
|
1953
|
+
"congress",
|
|
1954
|
+
"bill",
|
|
1955
|
+
"vote",
|
|
1956
|
+
"poll",
|
|
1957
|
+
"indictment",
|
|
1958
|
+
"verdict",
|
|
1959
|
+
"championship",
|
|
1960
|
+
"spacex",
|
|
1961
|
+
"launch",
|
|
1962
|
+
"earthquake",
|
|
1963
|
+
"hurricane"
|
|
1964
|
+
];
|
|
1965
|
+
for (const kw of marketKeywords) {
|
|
1966
|
+
if (title.includes(kw)) relevance += 0.2;
|
|
1967
|
+
}
|
|
1968
|
+
relevance = Math.min(1, relevance);
|
|
1969
|
+
allItems.push({ ...item, source, relevance: +relevance.toFixed(2), minutes_ago: Math.round((Date.now() - pubDate.getTime()) / 6e4) });
|
|
1970
|
+
}
|
|
1971
|
+
} catch {
|
|
1972
|
+
}
|
|
1973
|
+
}));
|
|
1974
|
+
allItems.sort((a, b) => (a.minutes_ago || 999) - (b.minutes_ago || 999));
|
|
1975
|
+
const highRelevance = allItems.filter((i) => i.relevance >= 0.4);
|
|
1976
|
+
return {
|
|
1977
|
+
sources_checked: requestedSources,
|
|
1978
|
+
total_items: allItems.length,
|
|
1979
|
+
high_relevance: highRelevance.length,
|
|
1980
|
+
items: allItems.slice(0, 30),
|
|
1981
|
+
market_moving: highRelevance.slice(0, 10),
|
|
1982
|
+
alert: highRelevance.length > 0 ? `${highRelevance.length} potentially market-moving headlines detected \u2014 review immediately` : "No high-relevance breaking news in the last " + (params?.since_minutes || 60) + " minutes"
|
|
1983
|
+
};
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
// src/polymarket-engines/counterintel.ts
|
|
1987
|
+
async function detectManipulation(tokenId) {
|
|
1988
|
+
const trades = await cachedFetchJSON(`${CLOB_API}/trades?token_id=${tokenId}&limit=500`);
|
|
1989
|
+
if (!trades?.length) return { token_id: tokenId, trades_analyzed: 0, risk_level: "LOW", risk_score: 0, alerts: [], top_wallets: [], recommendation: "No trades to analyze" };
|
|
1990
|
+
const alerts = [];
|
|
1991
|
+
let riskScore = 0;
|
|
1992
|
+
const selfDeals = trades.filter((t) => t.maker_address && t.taker_address && t.maker_address.toLowerCase() === t.taker_address.toLowerCase());
|
|
1993
|
+
if (selfDeals.length > 0) {
|
|
1994
|
+
const pct = selfDeals.length / trades.length * 100;
|
|
1995
|
+
alerts.push({
|
|
1996
|
+
type: "WASH_TRADING",
|
|
1997
|
+
severity: pct > 10 ? "HIGH" : "MEDIUM",
|
|
1998
|
+
detail: `${selfDeals.length} self-deals (${pct.toFixed(1)}% of trades). Same wallet on both sides.`,
|
|
1999
|
+
wallets: Array.from(new Set(selfDeals.map((t) => t.maker_address))).slice(0, 5)
|
|
2000
|
+
});
|
|
2001
|
+
riskScore += pct > 10 ? 3 : 1;
|
|
2002
|
+
}
|
|
2003
|
+
const walletVolume = /* @__PURE__ */ new Map();
|
|
2004
|
+
for (const t of trades) {
|
|
2005
|
+
for (const addr of [t.maker_address, t.taker_address].filter(Boolean)) {
|
|
2006
|
+
const key = addr.toLowerCase();
|
|
2007
|
+
const val = parseFloat(t.size || "0") * parseFloat(t.price || "0");
|
|
2008
|
+
walletVolume.set(key, (walletVolume.get(key) || 0) + val);
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
const totalVolume = Array.from(walletVolume.values()).reduce((s, v) => s + v, 0);
|
|
2012
|
+
const topWallets = Array.from(walletVolume.entries()).sort((a, b) => b[1] - a[1]).slice(0, 5);
|
|
2013
|
+
const topPct = totalVolume > 0 ? (topWallets[0]?.[1] || 0) / totalVolume * 100 : 0;
|
|
2014
|
+
if (topPct > 50) {
|
|
2015
|
+
alerts.push({ type: "VOLUME_CONCENTRATION", severity: "HIGH", detail: `Top wallet controls ${topPct.toFixed(1)}% of volume \u2014 market may be easily manipulated.` });
|
|
2016
|
+
riskScore += 3;
|
|
2017
|
+
} else if (topPct > 30) {
|
|
2018
|
+
alerts.push({ type: "VOLUME_CONCENTRATION", severity: "MEDIUM", detail: `Top wallet controls ${topPct.toFixed(1)}% of volume.` });
|
|
2019
|
+
riskScore += 1;
|
|
2020
|
+
}
|
|
2021
|
+
const tradesByTime = trades.map((t) => ({ ...t, ts: new Date(t.match_time || t.created_at).getTime() })).sort((a, b) => a.ts - b.ts);
|
|
2022
|
+
let rapidBursts = 0;
|
|
2023
|
+
for (let i = 1; i < tradesByTime.length; i++) {
|
|
2024
|
+
if (tradesByTime[i].ts - tradesByTime[i - 1].ts < 1e3) rapidBursts++;
|
|
2025
|
+
}
|
|
2026
|
+
const rapidPct = trades.length > 0 ? rapidBursts / trades.length * 100 : 0;
|
|
2027
|
+
if (rapidPct > 20) {
|
|
2028
|
+
alerts.push({ type: "RAPID_FIRE_TRADING", severity: "MEDIUM", detail: `${rapidPct.toFixed(1)}% of trades are <1 second apart \u2014 possible bot activity or spoofing.` });
|
|
2029
|
+
riskScore += 2;
|
|
2030
|
+
}
|
|
2031
|
+
const priceSeries = tradesByTime.map((t) => parseFloat(t.price || "0")).filter((p) => p > 0);
|
|
2032
|
+
let maxSwing = 0;
|
|
2033
|
+
for (let i = 10; i < priceSeries.length; i++) {
|
|
2034
|
+
const window = priceSeries.slice(i - 10, i);
|
|
2035
|
+
const swing = (Math.max(...window) - Math.min(...window)) / Math.min(...window) * 100;
|
|
2036
|
+
if (swing > maxSwing) maxSwing = swing;
|
|
2037
|
+
}
|
|
2038
|
+
if (maxSwing > 20) {
|
|
2039
|
+
alerts.push({ type: "PRICE_MANIPULATION", severity: "HIGH", detail: `${maxSwing.toFixed(1)}% price swing in 10-trade window \u2014 possible pump/dump.` });
|
|
2040
|
+
riskScore += 3;
|
|
2041
|
+
}
|
|
2042
|
+
try {
|
|
2043
|
+
const book = await cachedFetchJSON(`${CLOB_API}/book?token_id=${tokenId}`);
|
|
2044
|
+
const bidSizes = (book?.bids || []).map((b) => Math.round(parseFloat(b.size)));
|
|
2045
|
+
const sameSizeBids = bidSizes.filter((s, i) => i > 0 && s === bidSizes[i - 1]).length;
|
|
2046
|
+
if (sameSizeBids > 5) {
|
|
2047
|
+
alerts.push({ type: "ORDERBOOK_LAYERING", severity: "MEDIUM", detail: `${sameSizeBids} consecutive same-sized bid orders \u2014 possible layering to create fake support.` });
|
|
2048
|
+
riskScore += 2;
|
|
2049
|
+
}
|
|
2050
|
+
} catch {
|
|
2051
|
+
}
|
|
2052
|
+
const risk = riskScore >= 6 ? "HIGH" : riskScore >= 3 ? "MEDIUM" : "LOW";
|
|
2053
|
+
return {
|
|
2054
|
+
token_id: tokenId,
|
|
2055
|
+
trades_analyzed: trades.length,
|
|
2056
|
+
risk_level: risk,
|
|
2057
|
+
risk_score: riskScore,
|
|
2058
|
+
alerts,
|
|
2059
|
+
top_wallets: topWallets.slice(0, 5).map(([addr, vol]) => ({ address: addr, volume: +vol.toFixed(2), pct: +(vol / totalVolume * 100).toFixed(1) })),
|
|
2060
|
+
recommendation: risk === "HIGH" ? "DANGER: Multiple manipulation indicators detected. Do NOT place large orders. Consider avoiding this market entirely." : risk === "MEDIUM" ? "CAUTION: Some suspicious patterns detected. Use limit orders only, keep positions small." : "No significant manipulation detected. Normal trading activity."
|
|
2061
|
+
};
|
|
2062
|
+
}
|
|
2063
|
+
async function assessResolutionRisk(marketSlug, conditionId) {
|
|
2064
|
+
let market = null;
|
|
2065
|
+
if (marketSlug?.startsWith("0x")) {
|
|
2066
|
+
conditionId = marketSlug;
|
|
2067
|
+
marketSlug = void 0;
|
|
2068
|
+
}
|
|
2069
|
+
if (marketSlug) {
|
|
2070
|
+
let data = await cachedFetchJSON(`${GAMMA_API}/markets?slug=${marketSlug}`).catch(() => null);
|
|
2071
|
+
market = data?.[0];
|
|
2072
|
+
if (!market) {
|
|
2073
|
+
data = await cachedFetchJSON(`${GAMMA_API}/markets?search=${encodeURIComponent(marketSlug)}&limit=1`).catch(() => null);
|
|
2074
|
+
market = data?.[0];
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
if (!market && conditionId) {
|
|
2078
|
+
const data = await cachedFetchJSON(`${GAMMA_API}/markets?condition_id=${conditionId}`).catch(() => null);
|
|
2079
|
+
market = data?.[0];
|
|
2080
|
+
}
|
|
2081
|
+
if (!market) throw new Error("Market not found \u2014 try passing a valid slug or condition_id (0x...)");
|
|
2082
|
+
const question = (market.question || "").toLowerCase();
|
|
2083
|
+
const description = (market.description || "").toLowerCase();
|
|
2084
|
+
const combined = question + " " + description;
|
|
2085
|
+
const risks = [];
|
|
2086
|
+
let riskScore = 0;
|
|
2087
|
+
const ambiguousTerms = ["might", "could", "may", "approximately", "around", "roughly", "about", "substantially", "significantly", "effectively", "practically", "essentially"];
|
|
2088
|
+
const foundAmbiguous = ambiguousTerms.filter((t) => combined.includes(t));
|
|
2089
|
+
if (foundAmbiguous.length > 0) {
|
|
2090
|
+
risks.push({ type: "AMBIGUOUS_LANGUAGE", terms: foundAmbiguous, severity: "MEDIUM", detail: `Resolution criteria contain vague terms: ${foundAmbiguous.join(", ")}` });
|
|
2091
|
+
riskScore += foundAmbiguous.length;
|
|
2092
|
+
}
|
|
2093
|
+
const subjectiveTerms = ["in the opinion", "at discretion", "reasonably", "judgment", "interpret", "determine", "decide", "deem"];
|
|
2094
|
+
const foundSubjective = subjectiveTerms.filter((t) => combined.includes(t));
|
|
2095
|
+
if (foundSubjective.length > 0) {
|
|
2096
|
+
risks.push({ type: "SUBJECTIVE_RESOLUTION", terms: foundSubjective, severity: "HIGH", detail: `Resolution may depend on subjective judgment: ${foundSubjective.join(", ")}` });
|
|
2097
|
+
riskScore += foundSubjective.length * 2;
|
|
2098
|
+
}
|
|
2099
|
+
const hasUrl = /https?:\/\//.test(description);
|
|
2100
|
+
const hasSource = /resolution source|resolv.*based on|determined by|according to/.test(combined);
|
|
2101
|
+
if (!hasUrl && !hasSource) {
|
|
2102
|
+
risks.push({ type: "NO_RESOLUTION_SOURCE", severity: "HIGH", detail: "No explicit resolution source or URL found in market description" });
|
|
2103
|
+
riskScore += 3;
|
|
2104
|
+
}
|
|
2105
|
+
const hasDeadline = market.end_date_iso || market.endDate;
|
|
2106
|
+
if (!hasDeadline) {
|
|
2107
|
+
risks.push({ type: "NO_END_DATE", severity: "MEDIUM", detail: "No clear end date \u2014 market could remain open indefinitely" });
|
|
2108
|
+
riskScore += 2;
|
|
2109
|
+
}
|
|
2110
|
+
const volume = parseFloat(market.volume || "0");
|
|
2111
|
+
const liquidity = parseFloat(market.liquidity || "0");
|
|
2112
|
+
if (volume < 1e4) {
|
|
2113
|
+
risks.push({ type: "LOW_VOLUME", severity: "MEDIUM", detail: `Volume only $${volume.toFixed(0)} \u2014 thin market, hard to exit` });
|
|
2114
|
+
riskScore += 1;
|
|
2115
|
+
}
|
|
2116
|
+
if (liquidity < 5e3) {
|
|
2117
|
+
risks.push({ type: "LOW_LIQUIDITY", severity: "MEDIUM", detail: `Liquidity only $${liquidity.toFixed(0)} \u2014 high slippage risk` });
|
|
2118
|
+
riskScore += 1;
|
|
2119
|
+
}
|
|
2120
|
+
const andCount = (question.match(/\band\b/g) || []).length;
|
|
2121
|
+
const orCount = (question.match(/\bor\b/g) || []).length;
|
|
2122
|
+
if (andCount + orCount >= 3) {
|
|
2123
|
+
risks.push({ type: "COMPLEX_CONDITIONS", severity: "MEDIUM", detail: `Question has ${andCount} AND + ${orCount} OR clauses \u2014 complex resolution criteria` });
|
|
2124
|
+
riskScore += 2;
|
|
2125
|
+
}
|
|
2126
|
+
const risk = riskScore >= 8 ? "HIGH" : riskScore >= 4 ? "MEDIUM" : "LOW";
|
|
2127
|
+
return {
|
|
2128
|
+
market: market.question,
|
|
2129
|
+
slug: market.slug,
|
|
2130
|
+
risk_level: risk,
|
|
2131
|
+
risk_score: riskScore,
|
|
2132
|
+
risks,
|
|
2133
|
+
end_date: hasDeadline || "not specified",
|
|
2134
|
+
volume,
|
|
2135
|
+
liquidity,
|
|
2136
|
+
recommendation: risk === "HIGH" ? "AVOID this market \u2014 high probability of resolution disputes, ambiguity, or void." : risk === "MEDIUM" ? "Proceed with caution. Review resolution criteria carefully. Keep position small." : "Resolution criteria appear clear and well-defined. Proceed normally."
|
|
2137
|
+
};
|
|
2138
|
+
}
|
|
2139
|
+
async function analyzeCounterparties(tokenId, side) {
|
|
2140
|
+
const trades = await cachedFetchJSON(`${CLOB_API}/trades?token_id=${tokenId}&limit=500`);
|
|
2141
|
+
if (!trades?.length) return { token_id: tokenId, total_traders: 0, breakdown: { whales: { count: 0, volume: 0, pct: 0 }, mid_size: { count: 0, volume: 0 }, retail: { count: 0, volume: 0 } }, top_whales: [], opposite_side_analysis: null, risk_assessment: "No trades to analyze" };
|
|
2142
|
+
const walletStats = /* @__PURE__ */ new Map();
|
|
2143
|
+
for (const t of trades) {
|
|
2144
|
+
const value = parseFloat(t.size || "0") * parseFloat(t.price || "0");
|
|
2145
|
+
for (const [addr] of [[t.maker_address, "maker"], [t.taker_address, "taker"]]) {
|
|
2146
|
+
if (!addr) continue;
|
|
2147
|
+
const key = addr.toLowerCase();
|
|
2148
|
+
if (!walletStats.has(key)) walletStats.set(key, { volume: 0, tradeCount: 0, sides: /* @__PURE__ */ new Set() });
|
|
2149
|
+
const stats = walletStats.get(key);
|
|
2150
|
+
stats.volume += value;
|
|
2151
|
+
stats.tradeCount++;
|
|
2152
|
+
stats.sides.add(t.side);
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
const wallets = Array.from(walletStats.entries()).map(([addr, stats]) => ({
|
|
2156
|
+
address: addr,
|
|
2157
|
+
...stats,
|
|
2158
|
+
sides: Array.from(stats.sides),
|
|
2159
|
+
category: stats.volume > 1e4 ? "WHALE" : stats.volume > 1e3 ? "MID" : "RETAIL"
|
|
2160
|
+
}));
|
|
2161
|
+
const whales = wallets.filter((w) => w.category === "WHALE");
|
|
2162
|
+
const mids = wallets.filter((w) => w.category === "MID");
|
|
2163
|
+
const retail = wallets.filter((w) => w.category === "RETAIL");
|
|
2164
|
+
const whaleVolume = whales.reduce((s, w) => s + w.volume, 0);
|
|
2165
|
+
const totalVolume = wallets.reduce((s, w) => s + w.volume, 0);
|
|
2166
|
+
const whalePct = totalVolume > 0 ? whaleVolume / totalVolume * 100 : 0;
|
|
2167
|
+
let oppositeAnalysis = null;
|
|
2168
|
+
if (side) {
|
|
2169
|
+
const oppositeSide = side === "BUY" ? "SELL" : "BUY";
|
|
2170
|
+
const oppTrades = trades.filter((t) => t.side === oppositeSide);
|
|
2171
|
+
const oppWallets = /* @__PURE__ */ new Map();
|
|
2172
|
+
for (const t of oppTrades) {
|
|
2173
|
+
const addr = (t.maker_address || t.taker_address || "").toLowerCase();
|
|
2174
|
+
const val = parseFloat(t.size || "0") * parseFloat(t.price || "0");
|
|
2175
|
+
oppWallets.set(addr, (oppWallets.get(addr) || 0) + val);
|
|
2176
|
+
}
|
|
2177
|
+
const oppBySize = Array.from(oppWallets.entries()).sort((a, b) => b[1] - a[1]);
|
|
2178
|
+
const topOpp = oppBySize[0];
|
|
2179
|
+
oppositeAnalysis = {
|
|
2180
|
+
your_side: side,
|
|
2181
|
+
opposite_side: oppositeSide,
|
|
2182
|
+
unique_counterparties: oppWallets.size,
|
|
2183
|
+
top_counterparty_volume: topOpp ? +topOpp[1].toFixed(2) : 0,
|
|
2184
|
+
concentration: oppWallets.size > 0 && topOpp ? +(topOpp[1] / Array.from(oppWallets.values()).reduce((s, v) => s + v, 0) * 100).toFixed(1) : 0
|
|
2185
|
+
};
|
|
2186
|
+
}
|
|
2187
|
+
return {
|
|
2188
|
+
token_id: tokenId,
|
|
2189
|
+
total_traders: wallets.length,
|
|
2190
|
+
breakdown: {
|
|
2191
|
+
whales: { count: whales.length, volume: +whaleVolume.toFixed(2), pct: +whalePct.toFixed(1) },
|
|
2192
|
+
mid_size: { count: mids.length, volume: +mids.reduce((s, w) => s + w.volume, 0).toFixed(2) },
|
|
2193
|
+
retail: { count: retail.length, volume: +retail.reduce((s, w) => s + w.volume, 0).toFixed(2) }
|
|
2194
|
+
},
|
|
2195
|
+
top_whales: whales.sort((a, b) => b.volume - a.volume).slice(0, 5).map((w) => ({ address: w.address, volume: +w.volume.toFixed(2), trades: w.tradeCount, sides: w.sides })),
|
|
2196
|
+
opposite_side_analysis: oppositeAnalysis,
|
|
2197
|
+
risk_assessment: whalePct > 70 ? "HIGH RISK: Whales dominate this market. They likely have better information." : whalePct > 40 ? "MODERATE: Mix of whale and retail activity." : "FAVORABLE: Mostly retail counterparties. Your informational edge is likely stronger."
|
|
2198
|
+
};
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
// src/polymarket-engines/portfolio.ts
|
|
2202
|
+
async function analyzePortfolio(positions) {
|
|
2203
|
+
const enriched = await Promise.all(positions.map(async (pos) => {
|
|
2204
|
+
let currentPrice = pos.avg_price;
|
|
2205
|
+
try {
|
|
2206
|
+
const book = await cachedFetchJSON(`${CLOB_API}/book?token_id=${pos.token_id}`, 8e3);
|
|
2207
|
+
const bestBid = Math.max(...(book?.bids || []).map((b) => parseFloat(b.price)), 0);
|
|
2208
|
+
const bestAsk = Math.min(...(book?.asks || []).map((a) => parseFloat(a.price)), 1);
|
|
2209
|
+
currentPrice = (bestBid + bestAsk) / 2;
|
|
2210
|
+
} catch {
|
|
2211
|
+
}
|
|
2212
|
+
const value = pos.size * currentPrice;
|
|
2213
|
+
const costBasis = pos.size * pos.avg_price;
|
|
2214
|
+
const pnl = value - costBasis;
|
|
2215
|
+
return {
|
|
2216
|
+
...pos,
|
|
2217
|
+
current_price: +currentPrice.toFixed(4),
|
|
2218
|
+
value: +value.toFixed(2),
|
|
2219
|
+
cost_basis: +costBasis.toFixed(2),
|
|
2220
|
+
pnl: +pnl.toFixed(2),
|
|
2221
|
+
pnl_pct: costBasis > 0 ? +(pnl / costBasis * 100).toFixed(2) : 0,
|
|
2222
|
+
weight: 0
|
|
2223
|
+
// filled below
|
|
2224
|
+
};
|
|
2225
|
+
}));
|
|
2226
|
+
const totalValue = enriched.reduce((s, p) => s + p.value, 0);
|
|
2227
|
+
const totalCost = enriched.reduce((s, p) => s + p.cost_basis, 0);
|
|
2228
|
+
for (const p of enriched) p.weight = totalValue > 0 ? +(p.value / totalValue * 100).toFixed(1) : 0;
|
|
2229
|
+
const weights = enriched.map((p) => p.value / (totalValue || 1));
|
|
2230
|
+
const hhi = +(weights.reduce((s, w) => s + w * w, 0) * 1e4).toFixed(0);
|
|
2231
|
+
const topPct = Math.max(...weights) * 100;
|
|
2232
|
+
let portfolioVol = 0;
|
|
2233
|
+
try {
|
|
2234
|
+
const vols = await Promise.all(enriched.slice(0, 5).map(async (p) => {
|
|
2235
|
+
const prices = await fetchPriceHistory(p.token_id);
|
|
2236
|
+
return prices.length >= 5 ? calculateVolatility(prices) : 0.5;
|
|
2237
|
+
}));
|
|
2238
|
+
portfolioVol = +Math.sqrt(vols.reduce((s, v, i) => s + (weights[i] || 0) ** 2 * v ** 2, 0)).toFixed(4);
|
|
2239
|
+
} catch {
|
|
2240
|
+
portfolioVol = 0;
|
|
2241
|
+
}
|
|
2242
|
+
return {
|
|
2243
|
+
total_value: +totalValue.toFixed(2),
|
|
2244
|
+
total_cost: +totalCost.toFixed(2),
|
|
2245
|
+
total_pnl: +(totalValue - totalCost).toFixed(2),
|
|
2246
|
+
total_pnl_pct: totalCost > 0 ? +((totalValue - totalCost) / totalCost * 100).toFixed(2) : 0,
|
|
2247
|
+
positions: enriched.sort((a, b) => b.value - a.value),
|
|
2248
|
+
concentration: {
|
|
2249
|
+
hhi,
|
|
2250
|
+
top_position_pct: +topPct.toFixed(1),
|
|
2251
|
+
diversified: hhi < 2500 && topPct < 40
|
|
2252
|
+
},
|
|
2253
|
+
risk_metrics: {
|
|
2254
|
+
portfolio_volatility: portfolioVol,
|
|
2255
|
+
max_single_loss: +Math.max(...enriched.map((p) => p.value)).toFixed(2),
|
|
2256
|
+
correlation_risk: enriched.length > 5 ? "Check correlation matrix" : enriched.length <= 2 ? "HIGH \u2014 too few positions" : "MODERATE"
|
|
2257
|
+
}
|
|
2258
|
+
};
|
|
2259
|
+
}
|
|
2260
|
+
async function buildCorrelationMatrix(tokenIds) {
|
|
2261
|
+
const tokens = tokenIds.slice(0, 10);
|
|
2262
|
+
const histories = await Promise.all(tokens.map((tid) => fetchPriceHistory(tid)));
|
|
2263
|
+
const n = tokens.length;
|
|
2264
|
+
const matrix = Array.from({ length: n }, () => Array(n).fill(0));
|
|
2265
|
+
const highCorr = [];
|
|
2266
|
+
for (let i = 0; i < n; i++) {
|
|
2267
|
+
matrix[i][i] = 1;
|
|
2268
|
+
for (let j = i + 1; j < n; j++) {
|
|
2269
|
+
const corr = pearsonCorrelation(histories[i], histories[j]);
|
|
2270
|
+
matrix[i][j] = +corr.toFixed(3);
|
|
2271
|
+
matrix[j][i] = +corr.toFixed(3);
|
|
2272
|
+
if (Math.abs(corr) > 0.5) {
|
|
2273
|
+
highCorr.push({ pair: [tokens[i], tokens[j]], correlation: +corr.toFixed(3) });
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
let sumAbsCorr = 0, count = 0;
|
|
2278
|
+
for (let i = 0; i < n; i++) for (let j = i + 1; j < n; j++) {
|
|
2279
|
+
sumAbsCorr += Math.abs(matrix[i][j]);
|
|
2280
|
+
count++;
|
|
2281
|
+
}
|
|
2282
|
+
const avgCorr = count > 0 ? sumAbsCorr / count : 0;
|
|
2283
|
+
const divScore = +((1 - avgCorr) * 100).toFixed(1);
|
|
2284
|
+
return { tokens, matrix, high_correlations: highCorr.sort((a, b) => Math.abs(b.correlation) - Math.abs(a.correlation)), diversification_score: divScore };
|
|
2285
|
+
}
|
|
2286
|
+
function calculatePortfolioKelly(positions, bankroll) {
|
|
2287
|
+
return positions.map((pos) => {
|
|
2288
|
+
const p = pos.estimated_true_prob;
|
|
2289
|
+
const b = 1 / pos.current_price - 1;
|
|
2290
|
+
const q = 1 - p;
|
|
2291
|
+
const kelly = b > 0 ? Math.max(0, (p * b - q) / b) : 0;
|
|
2292
|
+
const halfKelly = kelly / 2;
|
|
2293
|
+
return {
|
|
2294
|
+
token_id: pos.token_id,
|
|
2295
|
+
market: pos.market,
|
|
2296
|
+
estimated_edge: +((p - pos.current_price) * 100).toFixed(2),
|
|
2297
|
+
kelly_fraction: +kelly.toFixed(4),
|
|
2298
|
+
half_kelly: +halfKelly.toFixed(4),
|
|
2299
|
+
recommended_allocation_pct: +(halfKelly * 100).toFixed(2),
|
|
2300
|
+
recommended_size_usd: +(bankroll * halfKelly).toFixed(2)
|
|
2301
|
+
};
|
|
2302
|
+
}).sort((a, b) => b.recommended_size_usd - a.recommended_size_usd);
|
|
2303
|
+
}
|
|
2304
|
+
function attributePnL(trades) {
|
|
2305
|
+
const totalPnl = trades.reduce((s, t) => s + t.pnl, 0);
|
|
2306
|
+
const winners = trades.filter((t) => t.pnl > 0);
|
|
2307
|
+
const losers = trades.filter((t) => t.pnl < 0);
|
|
2308
|
+
const byMarket = /* @__PURE__ */ new Map();
|
|
2309
|
+
for (const t of trades) byMarket.set(t.market, (byMarket.get(t.market) || 0) + t.pnl);
|
|
2310
|
+
const byPosition = Array.from(byMarket.entries()).map(([market, pnl]) => ({ market, pnl: +pnl.toFixed(2), contribution_pct: totalPnl !== 0 ? +(pnl / Math.abs(totalPnl) * 100).toFixed(1) : 0 })).sort((a, b) => b.pnl - a.pnl);
|
|
2311
|
+
const sorted = [...trades].sort((a, b) => b.pnl - a.pnl);
|
|
2312
|
+
const totalWins = winners.reduce((s, t) => s + t.pnl, 0);
|
|
2313
|
+
const totalLosses = Math.abs(losers.reduce((s, t) => s + t.pnl, 0));
|
|
2314
|
+
return {
|
|
2315
|
+
total_pnl: +totalPnl.toFixed(2),
|
|
2316
|
+
by_position: byPosition,
|
|
2317
|
+
best_trade: sorted[0] ? { market: sorted[0].market, pnl: +sorted[0].pnl.toFixed(2) } : null,
|
|
2318
|
+
worst_trade: sorted[sorted.length - 1] ? { market: sorted[sorted.length - 1].market, pnl: +sorted[sorted.length - 1].pnl.toFixed(2) } : null,
|
|
2319
|
+
win_rate: trades.length > 0 ? +(winners.length / trades.length * 100).toFixed(1) : 0,
|
|
2320
|
+
avg_winner: winners.length > 0 ? +(totalWins / winners.length).toFixed(2) : 0,
|
|
2321
|
+
avg_loser: losers.length > 0 ? +(totalLosses / losers.length).toFixed(2) : 0,
|
|
2322
|
+
profit_factor: totalLosses > 0 ? +(totalWins / totalLosses).toFixed(2) : totalWins > 0 ? Infinity : 0
|
|
2323
|
+
};
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
// src/polymarket-engines/pipeline.ts
|
|
2327
|
+
async function fullAnalysis(params) {
|
|
2328
|
+
const { tokenId, marketSlug, conditionId, marketQuestion, bankroll = 100, side } = params;
|
|
2329
|
+
let market = { question: marketQuestion || "", slug: marketSlug || "", token_id: tokenId, condition_id: conditionId || "" };
|
|
2330
|
+
if (marketSlug) {
|
|
2331
|
+
try {
|
|
2332
|
+
const data = await cachedFetchJSON(`${GAMMA_API}/markets?slug=${marketSlug}`);
|
|
2333
|
+
if (data?.[0]) {
|
|
2334
|
+
market = { question: data[0].question, slug: data[0].slug, token_id: tokenId, condition_id: data[0].condition_id || data[0].conditionId || conditionId || "" };
|
|
2335
|
+
}
|
|
2336
|
+
} catch {
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
let currentPrice = params.currentPrice || 0.5;
|
|
2340
|
+
let bestBid = 0, bestAsk = 1;
|
|
2341
|
+
let clobAvailable = true;
|
|
2342
|
+
try {
|
|
2343
|
+
const book = await cachedFetchJSON(`${CLOB_API}/book?token_id=${tokenId}`);
|
|
2344
|
+
bestBid = Math.max(...(book?.bids || []).map((b) => parseFloat(b.price)), 0);
|
|
2345
|
+
bestAsk = Math.min(...(book?.asks || []).map((a) => parseFloat(a.price)), 1);
|
|
2346
|
+
currentPrice = (bestBid + bestAsk) / 2;
|
|
2347
|
+
} catch {
|
|
2348
|
+
clobAvailable = false;
|
|
2349
|
+
try {
|
|
2350
|
+
const gammaData = await cachedFetchJSON(`${GAMMA_API}/markets?clob_token_ids=${tokenId}&limit=1`);
|
|
2351
|
+
if (gammaData?.[0]?.outcomePrices) {
|
|
2352
|
+
const prices = JSON.parse(gammaData[0].outcomePrices);
|
|
2353
|
+
currentPrice = parseFloat(prices[0]) || 0.5;
|
|
2354
|
+
}
|
|
2355
|
+
} catch {
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
const estimatedProb = params.estimatedTrueProb || currentPrice;
|
|
2359
|
+
const priceHistory = await fetchPriceHistory(tokenId).catch(() => []);
|
|
2360
|
+
const [
|
|
2361
|
+
regimeResult,
|
|
2362
|
+
smartMoneyResult,
|
|
2363
|
+
microResult,
|
|
2364
|
+
orderbookResult,
|
|
2365
|
+
whaleResult,
|
|
2366
|
+
flowResult,
|
|
2367
|
+
manipulationResult
|
|
2368
|
+
] = await Promise.all([
|
|
2369
|
+
detectRegime(tokenId).catch(() => null),
|
|
2370
|
+
calculateSmartMoneyIndex(tokenId, market.question).catch(() => null),
|
|
2371
|
+
analyzeMicrostructure(tokenId).catch(() => null),
|
|
2372
|
+
analyzeOrderbookDepth(tokenId).catch(() => null),
|
|
2373
|
+
scanWhaleTrades(tokenId, 500).catch(() => null),
|
|
2374
|
+
analyzeFlow(tokenId).catch(() => null),
|
|
2375
|
+
detectManipulation(tokenId).catch(() => null)
|
|
2376
|
+
]);
|
|
2377
|
+
let kellyResult = null, technicals = null, volatilityResult = null;
|
|
2378
|
+
let monteCarloResult = null, varResult = null, compositeSignal = null;
|
|
2379
|
+
if (priceHistory.length >= 10) {
|
|
2380
|
+
[kellyResult, technicals, volatilityResult, monteCarloResult, varResult, compositeSignal] = await Promise.all([
|
|
2381
|
+
Promise.resolve(calculateKelly({ true_probability: estimatedProb, market_price: currentPrice, bankroll })),
|
|
2382
|
+
calculateTechnicalIndicators({ token_id: tokenId, prices: priceHistory }).catch(() => null),
|
|
2383
|
+
analyzeVolatility({ token_id: tokenId, prices: priceHistory }).catch(() => null),
|
|
2384
|
+
runMonteCarlo({ positions: [{ token_id: tokenId, current_price: currentPrice, entry_price: currentPrice, size: bankroll }], volatility: priceHistory.length >= 20 ? calculateVolFromPrices(priceHistory) : 0.5, time_horizon_hours: 24 }).catch(() => null),
|
|
2385
|
+
calculateVaR({ positions: [{ token_id: tokenId, size: bankroll, entry_price: currentPrice }] }).catch(() => null),
|
|
2386
|
+
generateCompositeSignal({ token_id: tokenId }).catch(() => null)
|
|
2387
|
+
]);
|
|
2388
|
+
} else {
|
|
2389
|
+
kellyResult = calculateKelly({ true_probability: estimatedProb, market_price: currentPrice, bankroll });
|
|
2390
|
+
}
|
|
2391
|
+
let twitterResult = null, redditResult = null, velocityResult = null;
|
|
2392
|
+
let oddsResult = null, resolutionResult = null;
|
|
2393
|
+
let resRiskResult = null, counterpartyResult = null;
|
|
2394
|
+
if (!params.skipSlow) {
|
|
2395
|
+
const slowResults = await Promise.all([
|
|
2396
|
+
market.question ? analyzeTwitterSentiment(market.question).catch(() => null) : null,
|
|
2397
|
+
market.question ? monitorRedditPulse(market.question).catch(() => null) : null,
|
|
2398
|
+
market.question ? measureSocialVelocity(market.question).catch(() => null) : null,
|
|
2399
|
+
market.question ? compareOdds(market.question, currentPrice).catch(() => null) : null,
|
|
2400
|
+
marketSlug ? trackResolution(marketSlug).catch(() => null) : null,
|
|
2401
|
+
marketSlug ? assessResolutionRisk(marketSlug).catch(() => null) : null,
|
|
2402
|
+
analyzeCounterparties(tokenId, side).catch(() => null)
|
|
2403
|
+
]);
|
|
2404
|
+
[twitterResult, redditResult, velocityResult, oddsResult, resolutionResult, resRiskResult, counterpartyResult] = slowResults;
|
|
2405
|
+
}
|
|
2406
|
+
const synthesis = synthesize({
|
|
2407
|
+
currentPrice,
|
|
2408
|
+
estimatedProb,
|
|
2409
|
+
bankroll,
|
|
2410
|
+
kelly: kellyResult,
|
|
2411
|
+
technicals,
|
|
2412
|
+
regime: regimeResult,
|
|
2413
|
+
smartMoney: smartMoneyResult,
|
|
2414
|
+
manipulation: manipulationResult,
|
|
2415
|
+
resRisk: resRiskResult,
|
|
2416
|
+
microstructure: microResult,
|
|
2417
|
+
twitter: twitterResult,
|
|
2418
|
+
reddit: redditResult,
|
|
2419
|
+
compositeSignal,
|
|
2420
|
+
flow: flowResult,
|
|
2421
|
+
whales: whaleResult,
|
|
2422
|
+
orderbook: orderbookResult
|
|
2423
|
+
});
|
|
2424
|
+
return {
|
|
2425
|
+
market,
|
|
2426
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2427
|
+
screener: null,
|
|
2428
|
+
// screener works on market lists, not single tokens
|
|
2429
|
+
quant: { kelly: kellyResult, technicals, volatility: volatilityResult, monteCarlo: monteCarloResult, var: varResult, compositeSignal },
|
|
2430
|
+
analytics: { regime: regimeResult, smartMoney: smartMoneyResult, microstructure: microResult },
|
|
2431
|
+
onchain: { orderbook: orderbookResult, whales: whaleResult, flow: flowResult },
|
|
2432
|
+
social: { twitter: twitterResult, reddit: redditResult, velocity: velocityResult },
|
|
2433
|
+
feeds: { odds: oddsResult, resolution: resolutionResult },
|
|
2434
|
+
counterintel: { manipulation: manipulationResult, resolutionRisk: resRiskResult, counterparties: counterpartyResult },
|
|
2435
|
+
synthesis
|
|
2436
|
+
};
|
|
2437
|
+
}
|
|
2438
|
+
function calculateVolFromPrices(prices) {
|
|
2439
|
+
if (prices.length < 2) return 0.5;
|
|
2440
|
+
const returns = [];
|
|
2441
|
+
for (let i = 1; i < prices.length; i++) {
|
|
2442
|
+
if (prices[i - 1] > 0) returns.push(Math.log(prices[i] / prices[i - 1]));
|
|
2443
|
+
}
|
|
2444
|
+
if (returns.length < 2) return 0.5;
|
|
2445
|
+
const mean = returns.reduce((s, r) => s + r, 0) / returns.length;
|
|
2446
|
+
const variance = returns.reduce((s, r) => s + (r - mean) ** 2, 0) / (returns.length - 1);
|
|
2447
|
+
return Math.sqrt(variance * 365);
|
|
2448
|
+
}
|
|
2449
|
+
function synthesize(data) {
|
|
2450
|
+
const { currentPrice, estimatedProb, bankroll, kelly, technicals, regime, smartMoney, manipulation, resRisk, microstructure, twitter, reddit, compositeSignal, flow, whales, orderbook } = data;
|
|
2451
|
+
let score = 50;
|
|
2452
|
+
const factors = [];
|
|
2453
|
+
const warnings = [];
|
|
2454
|
+
if (kelly?.edge) {
|
|
2455
|
+
const edge = kelly.edge;
|
|
2456
|
+
if (edge > 10) {
|
|
2457
|
+
score += 15;
|
|
2458
|
+
factors.push(`Strong edge: ${edge.toFixed(1)}%`);
|
|
2459
|
+
} else if (edge > 5) {
|
|
2460
|
+
score += 8;
|
|
2461
|
+
factors.push(`Moderate edge: ${edge.toFixed(1)}%`);
|
|
2462
|
+
} else if (edge < -5) {
|
|
2463
|
+
score -= 10;
|
|
2464
|
+
warnings.push(`Negative edge: ${edge.toFixed(1)}%`);
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
if (regime) {
|
|
2468
|
+
if (regime.regime === "TRENDING" && regime.trend_direction === "UP") {
|
|
2469
|
+
score += 8;
|
|
2470
|
+
factors.push("Uptrend detected");
|
|
2471
|
+
} else if (regime.regime === "TRENDING" && regime.trend_direction === "DOWN") {
|
|
2472
|
+
score -= 8;
|
|
2473
|
+
warnings.push("Downtrend detected");
|
|
2474
|
+
} else if (regime.regime === "MEAN_REVERTING") {
|
|
2475
|
+
factors.push("Mean-reverting regime \u2014 trade contrarian");
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
if (smartMoney) {
|
|
2479
|
+
if (smartMoney.smart_money_index > 0.2) {
|
|
2480
|
+
score += 10;
|
|
2481
|
+
factors.push(`Smart money buying (${smartMoney.smart_money_index.toFixed(2)})`);
|
|
2482
|
+
} else if (smartMoney.smart_money_index < -0.2) {
|
|
2483
|
+
score -= 10;
|
|
2484
|
+
warnings.push(`Smart money selling (${smartMoney.smart_money_index.toFixed(2)})`);
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
if (manipulation?.risk_level === "HIGH") {
|
|
2488
|
+
score -= 20;
|
|
2489
|
+
warnings.push("HIGH manipulation risk detected");
|
|
2490
|
+
} else if (manipulation?.risk_level === "MEDIUM") {
|
|
2491
|
+
score -= 8;
|
|
2492
|
+
warnings.push("Moderate manipulation risk");
|
|
2493
|
+
}
|
|
2494
|
+
if (resRisk?.risk_level === "HIGH") {
|
|
2495
|
+
score -= 15;
|
|
2496
|
+
warnings.push("HIGH resolution risk \u2014 potential disputes");
|
|
2497
|
+
}
|
|
2498
|
+
if (twitter?.overall_sentiment > 0.3) {
|
|
2499
|
+
score += 5;
|
|
2500
|
+
factors.push("Positive social sentiment");
|
|
2501
|
+
} else if (twitter?.overall_sentiment < -0.3) {
|
|
2502
|
+
score -= 5;
|
|
2503
|
+
warnings.push("Negative social sentiment");
|
|
2504
|
+
}
|
|
2505
|
+
if (compositeSignal?.signal === "BUY") {
|
|
2506
|
+
score += 8;
|
|
2507
|
+
factors.push("Composite signal: BUY");
|
|
2508
|
+
} else if (compositeSignal?.signal === "SELL") {
|
|
2509
|
+
score -= 8;
|
|
2510
|
+
warnings.push("Composite signal: SELL");
|
|
2511
|
+
}
|
|
2512
|
+
if (flow?.flows?.["1h"]?.signal === "bullish") {
|
|
2513
|
+
score += 5;
|
|
2514
|
+
factors.push("Bullish order flow (1h)");
|
|
2515
|
+
} else if (flow?.flows?.["1h"]?.signal === "bearish") {
|
|
2516
|
+
score -= 5;
|
|
2517
|
+
warnings.push("Bearish order flow (1h)");
|
|
2518
|
+
}
|
|
2519
|
+
if (microstructure?.market_quality === "POOR") {
|
|
2520
|
+
score -= 10;
|
|
2521
|
+
warnings.push("Poor market quality (wide spread)");
|
|
2522
|
+
}
|
|
2523
|
+
score = Math.max(0, Math.min(100, score));
|
|
2524
|
+
const action = score >= 70 ? "STRONG_BUY" : score >= 58 ? "BUY" : score <= 30 ? "STRONG_SELL" : score <= 42 ? "SELL" : "HOLD";
|
|
2525
|
+
const recommended = kelly?.half_kelly_size || 0;
|
|
2526
|
+
const target = Math.min(0.99, currentPrice * 1.15);
|
|
2527
|
+
const stopLoss = Math.max(0.01, currentPrice * 0.8);
|
|
2528
|
+
const riskReward = (target - currentPrice) / (currentPrice - stopLoss) || 0;
|
|
2529
|
+
return {
|
|
2530
|
+
overall_score: score,
|
|
2531
|
+
confidence: factors.length > 3 ? 0.8 : factors.length > 1 ? 0.6 : 0.4,
|
|
2532
|
+
action,
|
|
2533
|
+
recommended_size: +recommended.toFixed(2),
|
|
2534
|
+
entry_price: +currentPrice.toFixed(4),
|
|
2535
|
+
target_price: +target.toFixed(4),
|
|
2536
|
+
stop_loss: +stopLoss.toFixed(4),
|
|
2537
|
+
risk_reward: +riskReward.toFixed(2),
|
|
2538
|
+
key_factors: factors,
|
|
2539
|
+
warnings,
|
|
2540
|
+
thesis: `${action} at ${currentPrice.toFixed(3)} (score ${score}/100). ${factors.slice(0, 3).join(". ")}. ${warnings.length ? "Risks: " + warnings.slice(0, 2).join(", ") : "No major risks."}`
|
|
2541
|
+
};
|
|
2542
|
+
}
|
|
2543
|
+
async function quickAnalysis(tokenId, marketQuestion, bankroll = 100) {
|
|
2544
|
+
const result = await fullAnalysis({
|
|
2545
|
+
tokenId,
|
|
2546
|
+
marketQuestion,
|
|
2547
|
+
bankroll,
|
|
2548
|
+
skipSlow: true
|
|
2549
|
+
});
|
|
2550
|
+
const orderbook = result.onchain.orderbook ? {
|
|
2551
|
+
spread: result.onchain.orderbook.spread,
|
|
2552
|
+
imbalance: result.onchain.orderbook.imbalance,
|
|
2553
|
+
quality: result.analytics.microstructure?.market_quality || "UNKNOWN"
|
|
2554
|
+
} : { spread: null, imbalance: null, quality: "UNAVAILABLE \u2014 CLOB rate limited, use poly_orderbook_depth later" };
|
|
2555
|
+
return {
|
|
2556
|
+
token_id: tokenId,
|
|
2557
|
+
market: result.market.question,
|
|
2558
|
+
current_price: result.synthesis.entry_price,
|
|
2559
|
+
score: result.synthesis.overall_score,
|
|
2560
|
+
action: result.synthesis.action,
|
|
2561
|
+
kelly: result.quant.kelly || { note: "Insufficient price history for Kelly calculation", recommendation: "Use poly_kelly_criterion directly with your estimated probability" },
|
|
2562
|
+
orderbook,
|
|
2563
|
+
regime: result.analytics.regime?.regime || "UNAVAILABLE",
|
|
2564
|
+
smart_money: result.analytics.smartMoney?.smart_money_index ?? null,
|
|
2565
|
+
manipulation_risk: result.counterintel.manipulation?.risk_level || "UNCHECKED",
|
|
2566
|
+
thesis: result.synthesis.thesis
|
|
2567
|
+
};
|
|
2568
|
+
}
|
|
2569
|
+
async function batchScreen(params) {
|
|
2570
|
+
return screenMarkets(params);
|
|
2571
|
+
}
|
|
2572
|
+
async function portfolioReview(params) {
|
|
2573
|
+
const overview = await analyzePortfolio(params.positions);
|
|
2574
|
+
const tokenIds = params.positions.map((p) => p.token_id).slice(0, 10);
|
|
2575
|
+
const correlations = tokenIds.length >= 2 ? await buildCorrelationMatrix(tokenIds).catch(() => null) : null;
|
|
2576
|
+
const kellySizing = calculatePortfolioKelly(
|
|
2577
|
+
params.positions.map((p) => {
|
|
2578
|
+
const pos = overview.positions.find((op) => op.token_id === p.token_id);
|
|
2579
|
+
return { token_id: p.token_id, market: p.market, current_price: pos?.current_price || p.avg_price, estimated_true_prob: pos?.current_price || p.avg_price };
|
|
2580
|
+
}),
|
|
2581
|
+
params.bankroll
|
|
2582
|
+
);
|
|
2583
|
+
const pnlAttribution = params.closedTrades?.length ? attributePnL(params.closedTrades) : null;
|
|
2584
|
+
const recommendations = [];
|
|
2585
|
+
if (!overview.concentration.diversified) recommendations.push("Portfolio is concentrated \u2014 consider diversifying across more markets");
|
|
2586
|
+
if (correlations && correlations.diversification_score < 50) recommendations.push("High correlation between positions \u2014 diversification is poor");
|
|
2587
|
+
if (overview.total_pnl_pct < -15) recommendations.push("Portfolio down >15% \u2014 review thesis for each position");
|
|
2588
|
+
for (const pos of overview.positions) {
|
|
2589
|
+
if (pos.pnl_pct < -25) recommendations.push(`${pos.market}: Down ${Math.abs(pos.pnl_pct)}% \u2014 consider cutting losses`);
|
|
2590
|
+
if (pos.pnl_pct > 50) recommendations.push(`${pos.market}: Up ${pos.pnl_pct}% \u2014 consider taking partial profits`);
|
|
2591
|
+
}
|
|
2592
|
+
return { overview, correlations, kellySizing, pnlAttribution, recommendations };
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
export {
|
|
2596
|
+
analyzeTwitterSentiment,
|
|
2597
|
+
analyzePolymarketComments,
|
|
2598
|
+
monitorRedditPulse,
|
|
2599
|
+
monitorTelegram,
|
|
2600
|
+
measureSocialVelocity,
|
|
2601
|
+
fetchOfficialSource,
|
|
2602
|
+
compareOdds,
|
|
2603
|
+
trackResolution,
|
|
2604
|
+
fetchBreakingNews,
|
|
2605
|
+
analyzeOrderbookDepth,
|
|
2606
|
+
scanWhaleTrades,
|
|
2607
|
+
analyzeFlow,
|
|
2608
|
+
profileWallet,
|
|
2609
|
+
mapLiquidity,
|
|
2610
|
+
decodeTransaction,
|
|
2611
|
+
getWalletTransactions,
|
|
2612
|
+
findCorrelations,
|
|
2613
|
+
scanArbitrage,
|
|
2614
|
+
detectRegime,
|
|
2615
|
+
calculateSmartMoneyIndex,
|
|
2616
|
+
analyzeMicrostructure,
|
|
2617
|
+
analyzePortfolio,
|
|
2618
|
+
calculatePortfolioKelly,
|
|
2619
|
+
attributePnL,
|
|
2620
|
+
calculateKelly,
|
|
2621
|
+
priceBinaryOption,
|
|
2622
|
+
bayesianUpdate,
|
|
2623
|
+
runMonteCarlo,
|
|
2624
|
+
calculateTechnicalIndicators,
|
|
2625
|
+
analyzeVolatility,
|
|
2626
|
+
analyzeStatArb,
|
|
2627
|
+
calculateVaR,
|
|
2628
|
+
calculateEntropy,
|
|
2629
|
+
analyzeSentiment,
|
|
2630
|
+
fetchNewsFeed,
|
|
2631
|
+
generateCompositeSignal,
|
|
2632
|
+
calculateCorrelationMatrix,
|
|
2633
|
+
testMarketEfficiency,
|
|
2634
|
+
detectManipulation,
|
|
2635
|
+
assessResolutionRisk,
|
|
2636
|
+
analyzeCounterparties,
|
|
2637
|
+
fullAnalysis,
|
|
2638
|
+
quickAnalysis,
|
|
2639
|
+
batchScreen,
|
|
2640
|
+
portfolioReview
|
|
2641
|
+
};
|