@oracle-agent/oracle 0.4.1 → 0.4.2
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/README.md +13 -8
- package/SETUP.md +2 -2
- package/docs/oracle-pack-standard.md +29 -0
- package/docs/profiles.md +1 -1
- package/examples/oracle-pack-template.mjs +38 -0
- package/package.json +7 -2
- package/profiles/oracle/SOUL.md +3 -1
- package/public/agent-connect/app.js +81 -0
- package/public/agent-connect/index.html +83 -0
- package/public/agent-connect/styles.css +70 -0
- package/scripts/adversarial-bench.mjs +114 -0
- package/skills/oracle-action-semantics/SKILL.md +1 -1
- package/src/action-receipts.mjs +168 -0
- package/src/address-book.mjs +53 -5
- package/src/exec-policy.mjs +6 -0
- package/src/index.mjs +24 -0
- package/src/onboarding/harness-configs.mjs +102 -0
- package/src/oracle-env.mjs +11 -2
- package/src/portfolio-risk.mjs +170 -0
- package/src/signals/engine.mjs +146 -0
- package/src/signals/index.mjs +1 -0
- package/src/watch-preferences.mjs +85 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
const SIGNAL_TYPES = Object.freeze([
|
|
2
|
+
"wallet",
|
|
3
|
+
"pool",
|
|
4
|
+
"nft_floor",
|
|
5
|
+
"hl_flow",
|
|
6
|
+
"prediction_market",
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
const TYPE_ALIASES = Object.freeze({
|
|
10
|
+
wallets: "wallet",
|
|
11
|
+
smart_wallet: "wallet",
|
|
12
|
+
pools: "pool",
|
|
13
|
+
new_pool: "pool",
|
|
14
|
+
nft: "nft_floor",
|
|
15
|
+
nft_floors: "nft_floor",
|
|
16
|
+
hyperliquid: "hl_flow",
|
|
17
|
+
prediction: "prediction_market",
|
|
18
|
+
prediction_markets: "prediction_market",
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const clamp = (value, min = 0, max = 1) => Math.min(max, Math.max(min, value));
|
|
22
|
+
const finite = (value) => (Number.isFinite(Number(value)) ? Number(value) : null);
|
|
23
|
+
const round = (value) => Math.round(value * 10_000) / 10_000;
|
|
24
|
+
|
|
25
|
+
function typeOf(event) {
|
|
26
|
+
const raw = String(event?.type ?? event?.surface ?? event?.kind ?? "").toLowerCase();
|
|
27
|
+
return TYPE_ALIASES[raw] ?? raw;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function idOf(event, index) {
|
|
31
|
+
return String(event?.id ?? event?.signalId ?? event?.asset ?? event?.market ?? event?.pool ?? event?.collection ?? `${typeOf(event)}:${index}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function add(contributions, feature, value, observed = true) {
|
|
35
|
+
if (!observed || value === null || !Number.isFinite(value)) return;
|
|
36
|
+
contributions.push({ feature, value: round(value) });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function walletFeatures(event, out) {
|
|
40
|
+
const buys = finite(event.repeatBuys ?? event.repeat_buys ?? event.buyCount);
|
|
41
|
+
const winRate = finite(event.walletWinRate ?? event.winRate);
|
|
42
|
+
add(out, "repeat_buys", buys === null ? null : clamp((buys - 1) / 4) * 0.38);
|
|
43
|
+
add(out, "wallet_win_rate", winRate === null ? null : (clamp(winRate) - 0.5) * 0.5);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function poolFeatures(event, out) {
|
|
47
|
+
const age = finite(event.ageHours ?? event.poolAgeHours);
|
|
48
|
+
const liquidity = finite(event.liquidityUsd ?? event.liquidity);
|
|
49
|
+
const locked = event.liquidityLocked ?? event.locked;
|
|
50
|
+
add(out, "pool_age", age === null ? null : age < 24 ? -0.42 : clamp(age / 720) * 0.12);
|
|
51
|
+
add(out, "liquidity", liquidity === null ? null : (clamp(Math.log10(Math.max(1, liquidity)) / 7) - 0.5) * 0.28);
|
|
52
|
+
add(out, "liquidity_lock", locked == null ? null : locked ? 0.12 : -0.28);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function nftFeatures(event, out) {
|
|
56
|
+
const change = finite(event.floorChange ?? event.floorChangePct);
|
|
57
|
+
const sales = finite(event.sales ?? event.saleCount);
|
|
58
|
+
add(out, "floor_change", change === null ? null : clamp(change, -1, 1) * 0.35);
|
|
59
|
+
add(out, "sales_depth", sales === null ? null : clamp(sales / 50) * 0.18);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function hlFeatures(event, out) {
|
|
63
|
+
const imbalance = finite(event.flowImbalance ?? event.imbalance);
|
|
64
|
+
const funding = finite(event.fundingRate ?? event.funding);
|
|
65
|
+
add(out, "flow_imbalance", imbalance === null ? null : clamp(imbalance, -1, 1) * 0.42);
|
|
66
|
+
add(out, "funding", funding === null ? null : -clamp(funding * 100, -1, 1) * 0.12);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function predictionFeatures(event, out) {
|
|
70
|
+
const market = finite(event.marketProbability ?? event.marketPrice ?? event.probability);
|
|
71
|
+
const fair = finite(event.fairProbability ?? event.referenceProbability ?? event.fairValue);
|
|
72
|
+
add(out, "probability_mispricing", market === null || fair === null ? null : clamp(fair - market, -1, 1) * 0.65);
|
|
73
|
+
const liquidity = finite(event.liquidityUsd ?? event.liquidity);
|
|
74
|
+
add(out, "market_liquidity", liquidity === null ? null : clamp(Math.log10(Math.max(1, liquidity)) / 6) * 0.12);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const FEATURE_BUILDERS = Object.freeze({
|
|
78
|
+
wallet: walletFeatures,
|
|
79
|
+
pool: poolFeatures,
|
|
80
|
+
nft_floor: nftFeatures,
|
|
81
|
+
hl_flow: hlFeatures,
|
|
82
|
+
prediction_market: predictionFeatures,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
function markoutFor(event, id, markouts) {
|
|
86
|
+
const rows = markouts.filter((row) => {
|
|
87
|
+
const target = row?.signalId ?? row?.eventId ?? row?.id ?? row?.asset ?? row?.market;
|
|
88
|
+
return target != null && String(target) === id;
|
|
89
|
+
});
|
|
90
|
+
if (rows.length === 0) return null;
|
|
91
|
+
const values = rows.map((row) => finite(row.return ?? row.markout ?? row.pnl ?? row.value)).filter((value) => value !== null);
|
|
92
|
+
if (values.length === 0) return null;
|
|
93
|
+
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Pure, deterministic scoring of observed onchain events. It never fetches data or executes. */
|
|
97
|
+
export function scoreSignals(events = [], markouts = [], options = {}) {
|
|
98
|
+
if (!Array.isArray(events) || !Array.isArray(markouts)) throw new TypeError("events and markouts must be arrays");
|
|
99
|
+
const minimumCoverage = clamp(finite(options.minimumCoverage) ?? 0.5);
|
|
100
|
+
|
|
101
|
+
return events.map((event, index) => {
|
|
102
|
+
const type = typeOf(event);
|
|
103
|
+
const id = idOf(event, index);
|
|
104
|
+
const builder = FEATURE_BUILDERS[type];
|
|
105
|
+
const contributions = [];
|
|
106
|
+
if (builder) builder(event, contributions);
|
|
107
|
+
|
|
108
|
+
const expected = type === "wallet" ? 2 : type === "pool" ? 3 : 2;
|
|
109
|
+
const coverage = round(clamp(contributions.length / expected));
|
|
110
|
+
const rawFeatureScore = contributions.reduce((sum, item) => sum + item.value, 0);
|
|
111
|
+
const markout = markoutFor(event, id, markouts);
|
|
112
|
+
const markoutContribution = markout === null ? 0 : clamp(markout, -1, 1) * 0.35;
|
|
113
|
+
if (markout !== null) add(contributions, "shadow_markout", markoutContribution);
|
|
114
|
+
const score = round(clamp(0.5 + rawFeatureScore + markoutContribution));
|
|
115
|
+
const markoutFactor = markout === null ? 0.8 : clamp(0.85 + markout * 0.3, 0.35, 1);
|
|
116
|
+
const confidence = round(clamp(coverage * markoutFactor));
|
|
117
|
+
const flags = ["COLD_INTELLIGENCE_ONLY", "NO_HOT_EXECUTION"];
|
|
118
|
+
if (!builder) flags.push("UNSUPPORTED_SIGNAL_TYPE");
|
|
119
|
+
if (coverage < minimumCoverage) flags.push("LOW_COVERAGE", "NO_TRADE");
|
|
120
|
+
if (confidence < 0.5) flags.push("LOW_CONFIDENCE", "NO_TRADE");
|
|
121
|
+
if (type === "pool" && (finite(event.ageHours ?? event.poolAgeHours) ?? Infinity) < 24) flags.push("NEW_POOL_RISK", "NO_TRADE");
|
|
122
|
+
if (markout !== null && markout < 0) flags.push("NEGATIVE_MARKOUT");
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
id,
|
|
126
|
+
type,
|
|
127
|
+
score,
|
|
128
|
+
confidence,
|
|
129
|
+
coverage,
|
|
130
|
+
contributions,
|
|
131
|
+
flags: [...new Set(flags)],
|
|
132
|
+
noTrade: flags.includes("NO_TRADE"),
|
|
133
|
+
executionAllowed: false,
|
|
134
|
+
};
|
|
135
|
+
}).sort((a, b) => b.score - a.score || b.confidence - a.confidence || a.id.localeCompare(b.id));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function createSignalsEngine(defaults = {}) {
|
|
139
|
+
return Object.freeze({
|
|
140
|
+
score(events, markouts = [], options = {}) {
|
|
141
|
+
return scoreSignals(events, markouts, { ...defaults, ...options });
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export { SIGNAL_TYPES };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SIGNAL_TYPES, createSignalsEngine, scoreSignals } from "./engine.mjs";
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/** Notification classes supported by Oracle watches. */
|
|
2
|
+
export const WATCH_CATEGORIES = Object.freeze([
|
|
3
|
+
'price',
|
|
4
|
+
'wallet',
|
|
5
|
+
'risk',
|
|
6
|
+
'execution',
|
|
7
|
+
'security',
|
|
8
|
+
'nft',
|
|
9
|
+
'governance',
|
|
10
|
+
'system',
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
const CATEGORY_SET = new Set(WATCH_CATEGORIES);
|
|
14
|
+
|
|
15
|
+
function assertCategory(category) {
|
|
16
|
+
if (!CATEGORY_SET.has(category)) {
|
|
17
|
+
throw new TypeError(`Unknown watch category: ${String(category)}`);
|
|
18
|
+
}
|
|
19
|
+
return category;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function subscribedCategories(preferences = {}) {
|
|
23
|
+
const categories = preferences.subscribedCategories ?? [];
|
|
24
|
+
if (!Array.isArray(categories)) {
|
|
25
|
+
throw new TypeError('subscribedCategories must be an array');
|
|
26
|
+
}
|
|
27
|
+
return categories;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** New users receive no category notifications until they explicitly opt in. */
|
|
31
|
+
export function defaultPreferences() {
|
|
32
|
+
return { subscribedCategories: [] };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Return a new preference value with exactly one notification class enabled. */
|
|
36
|
+
export function subscribe(preferences, category) {
|
|
37
|
+
assertCategory(category);
|
|
38
|
+
const current = subscribedCategories(preferences);
|
|
39
|
+
if (current.includes(category)) return { ...preferences, subscribedCategories: [...current] };
|
|
40
|
+
return { ...preferences, subscribedCategories: [...current, category] };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Return a new preference value with the requested notification class disabled. */
|
|
44
|
+
export function unsubscribe(preferences, category) {
|
|
45
|
+
assertCategory(category);
|
|
46
|
+
const current = subscribedCategories(preferences);
|
|
47
|
+
return {
|
|
48
|
+
...preferences,
|
|
49
|
+
subscribedCategories: current.filter((candidate) => candidate !== category),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Create a direct watch. Notifications are opt-in independently for every watch. */
|
|
54
|
+
export function createWatch({ category, notify = false, ...watch } = {}) {
|
|
55
|
+
assertCategory(category);
|
|
56
|
+
if (typeof notify !== 'boolean') throw new TypeError('watch notify must be a boolean');
|
|
57
|
+
return { ...watch, category, notify };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isMatchingNotifyingWatch(alert, watch) {
|
|
61
|
+
if (!watch || watch.notify !== true || watch.category !== alert.category) return false;
|
|
62
|
+
if (alert.watchId !== undefined && watch.id !== alert.watchId) return false;
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Decide whether an alert may be delivered. This function only describes policy;
|
|
68
|
+
* it performs no scheduling or delivery.
|
|
69
|
+
*/
|
|
70
|
+
export function evaluateAlert(alert, preferences = defaultPreferences(), watch) {
|
|
71
|
+
if (!alert || typeof alert !== 'object') throw new TypeError('alert must be an object');
|
|
72
|
+
assertCategory(alert.category);
|
|
73
|
+
|
|
74
|
+
if (subscribedCategories(preferences).includes(alert.category)) {
|
|
75
|
+
return { deliver: true, reason: 'category-subscribed' };
|
|
76
|
+
}
|
|
77
|
+
if (isMatchingNotifyingWatch(alert, watch)) {
|
|
78
|
+
return { deliver: true, reason: 'watch-opt-in' };
|
|
79
|
+
}
|
|
80
|
+
return { deliver: false, reason: 'not-subscribed' };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function shouldDeliverAlert(alert, preferences, watch) {
|
|
84
|
+
return evaluateAlert(alert, preferences, watch).deliver;
|
|
85
|
+
}
|