@lendwise/mcp 0.1.2 → 0.1.3
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/dist/core/graphql/queries.d.ts +24 -0
- package/dist/core/graphql/queries.js +100 -0
- package/dist/core/tools/find-best-markets.d.ts +16 -3
- package/dist/core/tools/find-best-markets.js +42 -13
- package/dist/core/tools/get-market-details.d.ts +2 -0
- package/dist/core/tools/get-market-history.d.ts +11 -3
- package/dist/core/tools/get-market-history.js +28 -11
- package/dist/core/tools/shared.d.ts +4 -0
- package/dist/core/tools/shared.js +9 -0
- package/package.json +1 -1
|
@@ -28,6 +28,8 @@ export interface RewardItem {
|
|
|
28
28
|
export interface MarketState {
|
|
29
29
|
supplyAssetsUsd: number | null;
|
|
30
30
|
utilizationRate: number | null;
|
|
31
|
+
/** Borrow snapshots only — supply documents don't select it, so it's absent there. */
|
|
32
|
+
borrowAssetsUsd?: number | null;
|
|
31
33
|
}
|
|
32
34
|
export interface Quality {
|
|
33
35
|
status: string;
|
|
@@ -42,6 +44,10 @@ export interface SnapshotRow {
|
|
|
42
44
|
apy: ApyBreakdown;
|
|
43
45
|
market: MarketState;
|
|
44
46
|
quality: Quality;
|
|
47
|
+
/** Accepted collateral assets — borrow snapshots only, absent on supply rows. */
|
|
48
|
+
collaterals?: {
|
|
49
|
+
symbol: string;
|
|
50
|
+
}[];
|
|
45
51
|
product: {
|
|
46
52
|
protocol: {
|
|
47
53
|
provider: string;
|
|
@@ -116,6 +122,17 @@ export interface ProductRow {
|
|
|
116
122
|
}
|
|
117
123
|
export declare const PRODUCT_FACETS = "\n query ProductFacets($filters: ProductFilters) {\n productFacets(filters: $filters) {\n assets {\n symbol\n count\n }\n chains {\n id\n name\n count\n }\n protocols {\n name\n count\n }\n }\n }\n";
|
|
118
124
|
export declare const LATEST_SUPPLY_APY = "\n query LatestSupplyApy($filters: LatestFilters, $first: Int) {\n latestSupplyApy(\n filters: $filters\n first: $first\n orderBy: apyNet\n orderDirection: desc\n ) {\n \n items {\n hour\n productId\n asset\n chainId\n apy {\n base\n rewards\n fees\n net\n }\n market {\n supplyAssetsUsd\n utilizationRate\n }\n quality {\n status\n count\n expectedCount\n }\n product {\n protocol {\n provider\n name\n chain {\n id\n name\n }\n }\n }\n }\n pagination {\n count\n countTotal\n limit\n skip\n }\n\n }\n }\n";
|
|
125
|
+
/**
|
|
126
|
+
* Current best borrow markets. Borrow net APY is a COST (base + fees − rewards),
|
|
127
|
+
* so the cheapest market has the LOWEST net — the opposite of supply.
|
|
128
|
+
*
|
|
129
|
+
* `orderDirection: asc` is set explicitly as defense in depth. Upstream now
|
|
130
|
+
* defaults `latestBorrowApy` to asc too (best-first, kind-aware), so this only
|
|
131
|
+
* restates that default — but pinning it here keeps the ranking correct even if
|
|
132
|
+
* that default is ever changed back, and makes the intent legible at the call
|
|
133
|
+
* site rather than depending on a server-side convention.
|
|
134
|
+
*/
|
|
135
|
+
export declare const LATEST_BORROW_APY = "\n query LatestBorrowApy($filters: LatestBorrowFilters, $first: Int) {\n latestBorrowApy(\n filters: $filters\n first: $first\n orderBy: apyNet\n orderDirection: asc\n ) {\n \n items {\n hour\n productId\n asset\n chainId\n apy {\n base\n rewards\n fees\n net\n }\n market {\n supplyAssetsUsd\n borrowAssetsUsd\n utilizationRate\n }\n quality {\n status\n count\n expectedCount\n }\n collaterals {\n symbol\n }\n product {\n protocol {\n provider\n name\n chain {\n id\n name\n }\n }\n }\n }\n pagination {\n count\n countTotal\n limit\n skip\n }\n\n }\n }\n";
|
|
119
136
|
/**
|
|
120
137
|
* Latest snapshot for an explicit set of products — the optimizer's APY source.
|
|
121
138
|
*
|
|
@@ -131,3 +148,10 @@ export declare const LATEST_SUPPLY_BY_PRODUCTS = "\n query LatestSupplyByProduc
|
|
|
131
148
|
*/
|
|
132
149
|
export declare const MARKET_DETAILS = "\n query MarketDetails($productId: String!) {\n products(filters: { productId: $productId }, first: 1) {\n items {\n id\n kind\n asset {\n symbol\n name\n address\n decimals\n }\n protocol {\n provider\n type\n version\n name\n chain {\n id\n name\n }\n address\n meta\n }\n collaterals {\n symbol\n name\n ltv\n lltv\n canBeCollateral\n }\n }\n }\n latestSupplyApy(filters: { productId: $productId }, first: 1) {\n \n items {\n hour\n productId\n asset\n chainId\n apy {\n base\n rewards\n fees\n net\n rewardItems {\n token {\n symbol\n }\n apy\n source\n program\n }\n }\n market {\n supplyAssetsUsd\n utilizationRate\n }\n quality {\n status\n count\n expectedCount\n }\n product {\n protocol {\n provider\n name\n chain {\n id\n name\n }\n }\n }\n }\n\n }\n latestBorrowApy(filters: { productId: $productId }, first: 1) {\n \n items {\n hour\n productId\n asset\n chainId\n apy {\n base\n rewards\n fees\n net\n rewardItems {\n token {\n symbol\n }\n apy\n source\n program\n }\n }\n market {\n supplyAssetsUsd\n utilizationRate\n }\n quality {\n status\n count\n expectedCount\n }\n product {\n protocol {\n provider\n name\n chain {\n id\n name\n }\n }\n }\n }\n\n }\n }\n";
|
|
133
150
|
export declare const MARKET_HISTORY = "\n query MarketHistory($productId: String!, $range: String!) {\n supplyApyDaily(\n filters: { productId: $productId, range: $range }\n first: 500\n orderBy: time\n orderDirection: asc\n ) {\n items {\n date\n apy {\n net\n base\n rewards\n }\n }\n pagination {\n count\n countTotal\n }\n }\n }\n";
|
|
151
|
+
/**
|
|
152
|
+
* The borrow-side twin of MARKET_HISTORY. Same daily shape and the same
|
|
153
|
+
* mean/stddev question — but for borrow the stat measures how much the *cost*
|
|
154
|
+
* moves, not the yield. A stable borrow rate is the signal a fixed-horizon
|
|
155
|
+
* borrower needs.
|
|
156
|
+
*/
|
|
157
|
+
export declare const BORROW_HISTORY = "\n query BorrowHistory($productId: String!, $range: String!) {\n borrowApyDaily(\n filters: { productId: $productId, range: $range }\n first: 500\n orderBy: time\n orderDirection: asc\n ) {\n items {\n date\n apy {\n net\n base\n rewards\n }\n }\n pagination {\n count\n countTotal\n }\n }\n }\n";
|
|
@@ -81,6 +81,77 @@ export const LATEST_SUPPLY_APY = /* GraphQL */ `
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
`;
|
|
84
|
+
/**
|
|
85
|
+
* Borrow rows carry two fields supply rows don't: `borrowAssetsUsd` (how much of
|
|
86
|
+
* the pool is drawn) and the `collaterals` accepted against the loan. Selecting
|
|
87
|
+
* them here — not on the shared supply fragment — is deliberate: a supply market
|
|
88
|
+
* state has no `borrowAssetsUsd`, so asking for it there is a schema error.
|
|
89
|
+
*/
|
|
90
|
+
const BORROW_SNAPSHOT_FIELDS = /* GraphQL */ `
|
|
91
|
+
items {
|
|
92
|
+
hour
|
|
93
|
+
productId
|
|
94
|
+
asset
|
|
95
|
+
chainId
|
|
96
|
+
apy {
|
|
97
|
+
base
|
|
98
|
+
rewards
|
|
99
|
+
fees
|
|
100
|
+
net
|
|
101
|
+
}
|
|
102
|
+
market {
|
|
103
|
+
supplyAssetsUsd
|
|
104
|
+
borrowAssetsUsd
|
|
105
|
+
utilizationRate
|
|
106
|
+
}
|
|
107
|
+
quality {
|
|
108
|
+
status
|
|
109
|
+
count
|
|
110
|
+
expectedCount
|
|
111
|
+
}
|
|
112
|
+
collaterals {
|
|
113
|
+
symbol
|
|
114
|
+
}
|
|
115
|
+
product {
|
|
116
|
+
protocol {
|
|
117
|
+
provider
|
|
118
|
+
name
|
|
119
|
+
chain {
|
|
120
|
+
id
|
|
121
|
+
name
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
pagination {
|
|
127
|
+
count
|
|
128
|
+
countTotal
|
|
129
|
+
limit
|
|
130
|
+
skip
|
|
131
|
+
}
|
|
132
|
+
`;
|
|
133
|
+
/**
|
|
134
|
+
* Current best borrow markets. Borrow net APY is a COST (base + fees − rewards),
|
|
135
|
+
* so the cheapest market has the LOWEST net — the opposite of supply.
|
|
136
|
+
*
|
|
137
|
+
* `orderDirection: asc` is set explicitly as defense in depth. Upstream now
|
|
138
|
+
* defaults `latestBorrowApy` to asc too (best-first, kind-aware), so this only
|
|
139
|
+
* restates that default — but pinning it here keeps the ranking correct even if
|
|
140
|
+
* that default is ever changed back, and makes the intent legible at the call
|
|
141
|
+
* site rather than depending on a server-side convention.
|
|
142
|
+
*/
|
|
143
|
+
export const LATEST_BORROW_APY = /* GraphQL */ `
|
|
144
|
+
query LatestBorrowApy($filters: LatestBorrowFilters, $first: Int) {
|
|
145
|
+
latestBorrowApy(
|
|
146
|
+
filters: $filters
|
|
147
|
+
first: $first
|
|
148
|
+
orderBy: apyNet
|
|
149
|
+
orderDirection: asc
|
|
150
|
+
) {
|
|
151
|
+
${BORROW_SNAPSHOT_FIELDS}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
`;
|
|
84
155
|
/**
|
|
85
156
|
* Latest snapshot for an explicit set of products — the optimizer's APY source.
|
|
86
157
|
*
|
|
@@ -205,3 +276,32 @@ export const MARKET_HISTORY = /* GraphQL */ `
|
|
|
205
276
|
}
|
|
206
277
|
}
|
|
207
278
|
`;
|
|
279
|
+
/**
|
|
280
|
+
* The borrow-side twin of MARKET_HISTORY. Same daily shape and the same
|
|
281
|
+
* mean/stddev question — but for borrow the stat measures how much the *cost*
|
|
282
|
+
* moves, not the yield. A stable borrow rate is the signal a fixed-horizon
|
|
283
|
+
* borrower needs.
|
|
284
|
+
*/
|
|
285
|
+
export const BORROW_HISTORY = /* GraphQL */ `
|
|
286
|
+
query BorrowHistory($productId: String!, $range: String!) {
|
|
287
|
+
borrowApyDaily(
|
|
288
|
+
filters: { productId: $productId, range: $range }
|
|
289
|
+
first: 500
|
|
290
|
+
orderBy: time
|
|
291
|
+
orderDirection: asc
|
|
292
|
+
) {
|
|
293
|
+
items {
|
|
294
|
+
date
|
|
295
|
+
apy {
|
|
296
|
+
net
|
|
297
|
+
base
|
|
298
|
+
rewards
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
pagination {
|
|
302
|
+
count
|
|
303
|
+
countTotal
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
`;
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
export declare const findBestMarketsArgs: {
|
|
3
|
+
kind: z.ZodDefault<z.ZodEnum<{
|
|
4
|
+
supply: "supply";
|
|
5
|
+
borrow: "borrow";
|
|
6
|
+
}>>;
|
|
3
7
|
asset: z.ZodOptional<z.ZodString>;
|
|
4
8
|
chainId: z.ZodOptional<z.ZodNumber>;
|
|
5
9
|
protocol: z.ZodOptional<z.ZodEnum<{
|
|
@@ -7,25 +11,34 @@ export declare const findBestMarketsArgs: {
|
|
|
7
11
|
morpho: "morpho";
|
|
8
12
|
compound: "compound";
|
|
9
13
|
}>>;
|
|
14
|
+
collateral: z.ZodOptional<z.ZodString>;
|
|
10
15
|
minTvlUsd: z.ZodDefault<z.ZodNumber>;
|
|
11
16
|
limit: z.ZodDefault<z.ZodNumber>;
|
|
12
17
|
};
|
|
13
18
|
/**
|
|
14
|
-
* Current best
|
|
15
|
-
*
|
|
16
|
-
*
|
|
19
|
+
* Current best markets, either side of the book.
|
|
20
|
+
*
|
|
21
|
+
* Both kinds read a server-side latest-snapshot query, so the ranking and the
|
|
22
|
+
* TVL floor are applied in Postgres — this never pages the catalogue. The two
|
|
23
|
+
* sides rank in opposite directions: supply by highest net APY (most earned),
|
|
24
|
+
* borrow by lowest (cheapest cost). That inversion lives in the GraphQL
|
|
25
|
+
* documents (`LATEST_BORROW_APY` sends `orderDirection: asc`), so here we only
|
|
26
|
+
* pick the document.
|
|
17
27
|
*/
|
|
18
28
|
export declare function findBestMarkets(raw: unknown): Promise<{
|
|
19
29
|
disclaimer: string;
|
|
20
30
|
warning?: string | undefined;
|
|
21
31
|
hint?: string | undefined;
|
|
22
32
|
query: {
|
|
33
|
+
kind: "supply" | "borrow";
|
|
23
34
|
asset: string | undefined;
|
|
24
35
|
chainId: number | undefined;
|
|
25
36
|
protocol: "aave" | "morpho" | "compound" | undefined;
|
|
37
|
+
collateral: string | undefined;
|
|
26
38
|
minTvlUsd: number;
|
|
27
39
|
limit: number;
|
|
28
40
|
};
|
|
41
|
+
ranking: string;
|
|
29
42
|
matched: number;
|
|
30
43
|
returned: number;
|
|
31
44
|
markets: import("./shared.js").DescribedMarket[];
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { DEFAULT_MIN_TVL_USD, NOT_ADVICE } from '../config.js';
|
|
3
3
|
import { gql } from '../graphql/client.js';
|
|
4
|
-
import { LATEST_SUPPLY_APY, } from '../graphql/queries.js';
|
|
4
|
+
import { LATEST_BORROW_APY, LATEST_SUPPLY_APY, } from '../graphql/queries.js';
|
|
5
5
|
import { describeRow, isUnreliable } from './shared.js';
|
|
6
6
|
export const findBestMarketsArgs = {
|
|
7
|
+
kind: z
|
|
8
|
+
.enum(['supply', 'borrow'])
|
|
9
|
+
.default('supply')
|
|
10
|
+
.describe('supply = earn yield (best = highest net APY). borrow = take a loan ' +
|
|
11
|
+
'(best = LOWEST net APY, i.e. cheapest cost). Defaults to supply.'),
|
|
7
12
|
asset: z
|
|
8
13
|
.string()
|
|
9
14
|
.optional()
|
|
@@ -13,30 +18,54 @@ export const findBestMarketsArgs = {
|
|
|
13
18
|
.enum(['aave', 'morpho', 'compound'])
|
|
14
19
|
.optional()
|
|
15
20
|
.describe('Protocol provider.'),
|
|
21
|
+
collateral: z
|
|
22
|
+
.string()
|
|
23
|
+
.optional()
|
|
24
|
+
.describe('Borrow only: filter by collateral asset symbol (e.g. WETH). Valid values ' +
|
|
25
|
+
'come from the `collaterals` on returned borrow rows, not guessed. ' +
|
|
26
|
+
'Ignored when kind is supply.'),
|
|
16
27
|
minTvlUsd: z
|
|
17
28
|
.number()
|
|
18
29
|
.nonnegative()
|
|
19
30
|
.default(DEFAULT_MIN_TVL_USD)
|
|
20
|
-
.describe('Minimum supplied
|
|
21
|
-
'headline
|
|
31
|
+
.describe('Minimum supplied pool depth in USD. Defaults to $1M — in a thinner ' +
|
|
32
|
+
'market the headline rate is mostly noise (and, for borrow, the pool ' +
|
|
33
|
+
'may be too shallow to draw from). Lower it deliberately, not by accident.'),
|
|
22
34
|
limit: z.number().int().min(1).max(50).default(10),
|
|
23
35
|
};
|
|
24
36
|
const Args = z.object(findBestMarketsArgs);
|
|
25
37
|
/**
|
|
26
|
-
* Current best
|
|
27
|
-
*
|
|
28
|
-
*
|
|
38
|
+
* Current best markets, either side of the book.
|
|
39
|
+
*
|
|
40
|
+
* Both kinds read a server-side latest-snapshot query, so the ranking and the
|
|
41
|
+
* TVL floor are applied in Postgres — this never pages the catalogue. The two
|
|
42
|
+
* sides rank in opposite directions: supply by highest net APY (most earned),
|
|
43
|
+
* borrow by lowest (cheapest cost). That inversion lives in the GraphQL
|
|
44
|
+
* documents (`LATEST_BORROW_APY` sends `orderDirection: asc`), so here we only
|
|
45
|
+
* pick the document.
|
|
29
46
|
*/
|
|
30
47
|
export async function findBestMarkets(raw) {
|
|
31
|
-
const { asset, chainId, protocol, minTvlUsd, limit } = Args.parse(raw);
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
48
|
+
const { kind, asset, chainId, protocol, collateral, minTvlUsd, limit } = Args.parse(raw);
|
|
49
|
+
const isBorrow = kind === 'borrow';
|
|
50
|
+
const document = isBorrow ? LATEST_BORROW_APY : LATEST_SUPPLY_APY;
|
|
51
|
+
// `collateral` is a borrow-only filter; sending it on the supply side would be
|
|
52
|
+
// rejected by the schema, so it's dropped there rather than passed through.
|
|
53
|
+
const filters = {
|
|
54
|
+
asset,
|
|
55
|
+
chainId,
|
|
56
|
+
protocol,
|
|
57
|
+
minTvlUsd,
|
|
58
|
+
...(isBorrow ? { collateral } : {}),
|
|
59
|
+
};
|
|
60
|
+
const data = await gql(document, { filters, first: limit });
|
|
61
|
+
// Exactly one key is present — the one for the document we sent.
|
|
62
|
+
const { items, pagination } = (isBorrow ? data.latestBorrowApy : data.latestSupplyApy);
|
|
37
63
|
const markets = items.map(describeRow);
|
|
38
64
|
return {
|
|
39
|
-
query: { asset, chainId, protocol, minTvlUsd, limit },
|
|
65
|
+
query: { kind, asset, chainId, protocol, collateral, minTvlUsd, limit },
|
|
66
|
+
ranking: isBorrow
|
|
67
|
+
? 'By net borrow cost APY, cheapest first — lower is better.'
|
|
68
|
+
: 'By net supply APY, highest first.',
|
|
40
69
|
matched: pagination.countTotal,
|
|
41
70
|
returned: markets.length,
|
|
42
71
|
markets,
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
export declare const getMarketHistoryArgs: {
|
|
3
3
|
productId: z.ZodString;
|
|
4
|
+
kind: z.ZodDefault<z.ZodEnum<{
|
|
5
|
+
supply: "supply";
|
|
6
|
+
borrow: "borrow";
|
|
7
|
+
}>>;
|
|
4
8
|
range: z.ZodDefault<z.ZodEnum<{
|
|
5
9
|
"7d": "7d";
|
|
6
10
|
"30d": "30d";
|
|
@@ -9,14 +13,17 @@ export declare const getMarketHistoryArgs: {
|
|
|
9
13
|
}>>;
|
|
10
14
|
};
|
|
11
15
|
/**
|
|
12
|
-
* Daily net-APY series plus its mean / stddev / min / max.
|
|
16
|
+
* Daily net-APY series plus its mean / stddev / min / max, for either side.
|
|
13
17
|
*
|
|
14
18
|
* The stats are the deliverable, not the series. A snapshot cannot distinguish a
|
|
15
|
-
* durable
|
|
16
|
-
* deviation can.
|
|
19
|
+
* durable rate from a reward spike that ends next week; a 180-day standard
|
|
20
|
+
* deviation can. For supply that stability is the yield you can count on; for
|
|
21
|
+
* borrow it is the cost you can count on. Same maths, opposite reading — hence
|
|
22
|
+
* the kind-dependent interpretation copy.
|
|
17
23
|
*/
|
|
18
24
|
export declare function getMarketHistory(raw: unknown): Promise<{
|
|
19
25
|
productId: string;
|
|
26
|
+
kind: "supply" | "borrow";
|
|
20
27
|
range: "7d" | "30d" | "90d" | "180d";
|
|
21
28
|
observations: number;
|
|
22
29
|
hint: string;
|
|
@@ -26,6 +33,7 @@ export declare function getMarketHistory(raw: unknown): Promise<{
|
|
|
26
33
|
series?: undefined;
|
|
27
34
|
} | {
|
|
28
35
|
productId: string;
|
|
36
|
+
kind: "supply" | "borrow";
|
|
29
37
|
range: "7d" | "30d" | "90d" | "180d";
|
|
30
38
|
observations: number;
|
|
31
39
|
netApy: {
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { NOT_ADVICE } from '../config.js';
|
|
3
3
|
import { gql } from '../graphql/client.js';
|
|
4
|
-
import { MARKET_HISTORY } from '../graphql/queries.js';
|
|
4
|
+
import { BORROW_HISTORY, MARKET_HISTORY } from '../graphql/queries.js';
|
|
5
5
|
import { stats } from '../stats.js';
|
|
6
6
|
export const getMarketHistoryArgs = {
|
|
7
7
|
productId: z.string().min(1).describe('Exact productId from find_best_markets.'),
|
|
8
|
+
kind: z
|
|
9
|
+
.enum(['supply', 'borrow'])
|
|
10
|
+
.default('supply')
|
|
11
|
+
.describe('Which side to chart. Must match the productId — a borrow productId ends ' +
|
|
12
|
+
'in :borrow. Defaults to supply.'),
|
|
8
13
|
range: z
|
|
9
14
|
.enum(['7d', '30d', '90d', '180d'])
|
|
10
15
|
.default('90d')
|
|
@@ -12,31 +17,40 @@ export const getMarketHistoryArgs = {
|
|
|
12
17
|
};
|
|
13
18
|
const Args = z.object(getMarketHistoryArgs);
|
|
14
19
|
/**
|
|
15
|
-
* Daily net-APY series plus its mean / stddev / min / max.
|
|
20
|
+
* Daily net-APY series plus its mean / stddev / min / max, for either side.
|
|
16
21
|
*
|
|
17
22
|
* The stats are the deliverable, not the series. A snapshot cannot distinguish a
|
|
18
|
-
* durable
|
|
19
|
-
* deviation can.
|
|
23
|
+
* durable rate from a reward spike that ends next week; a 180-day standard
|
|
24
|
+
* deviation can. For supply that stability is the yield you can count on; for
|
|
25
|
+
* borrow it is the cost you can count on. Same maths, opposite reading — hence
|
|
26
|
+
* the kind-dependent interpretation copy.
|
|
20
27
|
*/
|
|
21
28
|
export async function getMarketHistory(raw) {
|
|
22
|
-
const { productId, range } = Args.parse(raw);
|
|
23
|
-
const
|
|
24
|
-
const
|
|
29
|
+
const { productId, kind, range } = Args.parse(raw);
|
|
30
|
+
const isBorrow = kind === 'borrow';
|
|
31
|
+
const document = isBorrow ? BORROW_HISTORY : MARKET_HISTORY;
|
|
32
|
+
const data = await gql(document, { productId, range });
|
|
33
|
+
// Exactly one key is present — the one for the document we sent.
|
|
34
|
+
const items = (isBorrow ? data.borrowApyDaily : data.supplyApyDaily).items;
|
|
25
35
|
// Non-finite APYs (Postgres double precision can hold NaN) are dropped, not
|
|
26
36
|
// returned — a NaN in a series poisons every stat computed from it.
|
|
27
37
|
const netStats = stats(items.map((d) => d.apy.net));
|
|
28
38
|
if (!netStats) {
|
|
29
39
|
return {
|
|
30
40
|
productId,
|
|
41
|
+
kind,
|
|
31
42
|
range,
|
|
32
43
|
observations: 0,
|
|
33
44
|
hint: 'No usable daily history for this product in that range. It may be ' +
|
|
34
|
-
'newly tracked
|
|
45
|
+
'newly tracked, or the productId may not match the requested kind ' +
|
|
46
|
+
'(a borrow productId ends in :borrow). Try a shorter range.',
|
|
35
47
|
disclaimer: NOT_ADVICE,
|
|
36
48
|
};
|
|
37
49
|
}
|
|
50
|
+
const volatile = netStats.mean > 0 && netStats.stddev / Math.abs(netStats.mean) > 0.5;
|
|
38
51
|
return {
|
|
39
52
|
productId,
|
|
53
|
+
kind,
|
|
40
54
|
range,
|
|
41
55
|
observations: netStats.count,
|
|
42
56
|
netApy: {
|
|
@@ -45,9 +59,12 @@ export async function getMarketHistory(raw) {
|
|
|
45
59
|
min: netStats.min,
|
|
46
60
|
max: netStats.max,
|
|
47
61
|
},
|
|
48
|
-
interpretation:
|
|
49
|
-
?
|
|
50
|
-
'
|
|
62
|
+
interpretation: volatile
|
|
63
|
+
? isBorrow
|
|
64
|
+
? 'Volatile: the spread is large relative to the mean, so the current ' +
|
|
65
|
+
'rate is a poor predictor of what you would actually pay.'
|
|
66
|
+
: 'Volatile: the spread is large relative to the mean, so the current ' +
|
|
67
|
+
'APY is a poor predictor of what you would actually earn.'
|
|
51
68
|
: 'Relatively stable over this window.',
|
|
52
69
|
series: items.map((d) => ({
|
|
53
70
|
date: d.date,
|
|
@@ -16,6 +16,10 @@ export interface DescribedMarket {
|
|
|
16
16
|
};
|
|
17
17
|
tvlUsd: number | null;
|
|
18
18
|
utilizationRate: number | null;
|
|
19
|
+
/** Borrow rows only: value currently drawn from the pool. Absent for supply. */
|
|
20
|
+
borrowedUsd?: number | null;
|
|
21
|
+
/** Borrow rows only: accepted collateral symbols. Absent for supply. */
|
|
22
|
+
collaterals?: string[];
|
|
19
23
|
/** Snapshot time — every response says how fresh it is. */
|
|
20
24
|
asOf: string;
|
|
21
25
|
quality: {
|
|
@@ -32,6 +32,15 @@ export function describeRow(row) {
|
|
|
32
32
|
},
|
|
33
33
|
tvlUsd: finite(row.market.supplyAssetsUsd),
|
|
34
34
|
utilizationRate: finite(row.market.utilizationRate),
|
|
35
|
+
// Borrow-only fields, keyed off what the document actually selected rather
|
|
36
|
+
// than a passed-in kind: a supply document omits both, a borrow one carries
|
|
37
|
+
// them, so their presence on the row is the ground truth.
|
|
38
|
+
...(row.market.borrowAssetsUsd !== undefined
|
|
39
|
+
? { borrowedUsd: finite(row.market.borrowAssetsUsd) }
|
|
40
|
+
: {}),
|
|
41
|
+
...(row.collaterals !== undefined
|
|
42
|
+
? { collaterals: row.collaterals.map((c) => c.symbol) }
|
|
43
|
+
: {}),
|
|
35
44
|
asOf: row.hour,
|
|
36
45
|
quality: reliability(row),
|
|
37
46
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lendwise/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Unified view for lending markets, one standard — MCP server to compare and optimize DeFi supply/borrow across Aave, Morpho and Compound.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|