@sfranalytics/mcp 0.6.0
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/LICENSE +24 -0
- package/README.md +147 -0
- package/dist/config.d.ts +28 -0
- package/dist/config.js +101 -0
- package/dist/config.js.map +1 -0
- package/dist/http.d.ts +2 -0
- package/dist/http.js +252 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +213 -0
- package/dist/server.js.map +1 -0
- package/dist/services/httpClient.d.ts +19 -0
- package/dist/services/httpClient.js +73 -0
- package/dist/services/httpClient.js.map +1 -0
- package/dist/services/plr.d.ts +26 -0
- package/dist/services/plr.js +75 -0
- package/dist/services/plr.js.map +1 -0
- package/dist/services/sfr.d.ts +33 -0
- package/dist/services/sfr.js +124 -0
- package/dist/services/sfr.js.map +1 -0
- package/dist/services/snowflake.d.ts +12 -0
- package/dist/services/snowflake.js +71 -0
- package/dist/services/snowflake.js.map +1 -0
- package/dist/tools/dateHelper.d.ts +26 -0
- package/dist/tools/dateHelper.js +86 -0
- package/dist/tools/dateHelper.js.map +1 -0
- package/dist/tools/formatters.d.ts +66 -0
- package/dist/tools/formatters.js +219 -0
- package/dist/tools/formatters.js.map +1 -0
- package/dist/tools/health.d.ts +5 -0
- package/dist/tools/health.js +38 -0
- package/dist/tools/health.js.map +1 -0
- package/dist/tools/nextActions.d.ts +10 -0
- package/dist/tools/nextActions.js +23 -0
- package/dist/tools/nextActions.js.map +1 -0
- package/dist/tools/plr/borrowerContacts.d.ts +3 -0
- package/dist/tools/plr/borrowerContacts.js +73 -0
- package/dist/tools/plr/borrowerContacts.js.map +1 -0
- package/dist/tools/plr/borrowerLoans.d.ts +3 -0
- package/dist/tools/plr/borrowerLoans.js +115 -0
- package/dist/tools/plr/borrowerLoans.js.map +1 -0
- package/dist/tools/plr/borrowerProfile.d.ts +3 -0
- package/dist/tools/plr/borrowerProfile.js +201 -0
- package/dist/tools/plr/borrowerProfile.js.map +1 -0
- package/dist/tools/plr/borrowerRankings.d.ts +3 -0
- package/dist/tools/plr/borrowerRankings.js +123 -0
- package/dist/tools/plr/borrowerRankings.js.map +1 -0
- package/dist/tools/plr/borrowerSearch.d.ts +3 -0
- package/dist/tools/plr/borrowerSearch.js +128 -0
- package/dist/tools/plr/borrowerSearch.js.map +1 -0
- package/dist/tools/plr/churnedBorrowers.d.ts +3 -0
- package/dist/tools/plr/churnedBorrowers.js +86 -0
- package/dist/tools/plr/churnedBorrowers.js.map +1 -0
- package/dist/tools/plr/lenderBorrowers.d.ts +3 -0
- package/dist/tools/plr/lenderBorrowers.js +71 -0
- package/dist/tools/plr/lenderBorrowers.js.map +1 -0
- package/dist/tools/plr/lenderRankings.d.ts +3 -0
- package/dist/tools/plr/lenderRankings.js +74 -0
- package/dist/tools/plr/lenderRankings.js.map +1 -0
- package/dist/tools/plr/loansNearby.d.ts +3 -0
- package/dist/tools/plr/loansNearby.js +113 -0
- package/dist/tools/plr/loansNearby.js.map +1 -0
- package/dist/tools/plr/marketTrends.d.ts +3 -0
- package/dist/tools/plr/marketTrends.js +95 -0
- package/dist/tools/plr/marketTrends.js.map +1 -0
- package/dist/tools/plr/msaRankings.d.ts +3 -0
- package/dist/tools/plr/msaRankings.js +74 -0
- package/dist/tools/plr/msaRankings.js.map +1 -0
- package/dist/tools/plr/negativeRemarks.d.ts +3 -0
- package/dist/tools/plr/negativeRemarks.js +94 -0
- package/dist/tools/plr/negativeRemarks.js.map +1 -0
- package/dist/tools/plr/ownerSearch.d.ts +3 -0
- package/dist/tools/plr/ownerSearch.js +57 -0
- package/dist/tools/plr/ownerSearch.js.map +1 -0
- package/dist/tools/plr/portfolioSummary.d.ts +3 -0
- package/dist/tools/plr/portfolioSummary.js +99 -0
- package/dist/tools/plr/portfolioSummary.js.map +1 -0
- package/dist/tools/plr/topBorrowers.d.ts +3 -0
- package/dist/tools/plr/topBorrowers.js +69 -0
- package/dist/tools/plr/topBorrowers.js.map +1 -0
- package/dist/tools/plr/topLenders.d.ts +3 -0
- package/dist/tools/plr/topLenders.js +75 -0
- package/dist/tools/plr/topLenders.js.map +1 -0
- package/dist/tools/plr/transactionHistory.d.ts +3 -0
- package/dist/tools/plr/transactionHistory.js +74 -0
- package/dist/tools/plr/transactionHistory.js.map +1 -0
- package/dist/tools/prompts.d.ts +7 -0
- package/dist/tools/prompts.js +157 -0
- package/dist/tools/prompts.js.map +1 -0
- package/dist/tools/registerToolSafe.d.ts +29 -0
- package/dist/tools/registerToolSafe.js +36 -0
- package/dist/tools/registerToolSafe.js.map +1 -0
- package/dist/tools/sfr/activityHighlights.d.ts +3 -0
- package/dist/tools/sfr/activityHighlights.js +70 -0
- package/dist/tools/sfr/activityHighlights.js.map +1 -0
- package/dist/tools/sfr/bestBuyers.d.ts +3 -0
- package/dist/tools/sfr/bestBuyers.js +60 -0
- package/dist/tools/sfr/bestBuyers.js.map +1 -0
- package/dist/tools/sfr/buyerGrowth.d.ts +3 -0
- package/dist/tools/sfr/buyerGrowth.js +68 -0
- package/dist/tools/sfr/buyerGrowth.js.map +1 -0
- package/dist/tools/sfr/buyerProfile.d.ts +3 -0
- package/dist/tools/sfr/buyerProfile.js +162 -0
- package/dist/tools/sfr/buyerProfile.js.map +1 -0
- package/dist/tools/sfr/distressSearch.d.ts +3 -0
- package/dist/tools/sfr/distressSearch.js +173 -0
- package/dist/tools/sfr/distressSearch.js.map +1 -0
- package/dist/tools/sfr/flipActivity.d.ts +3 -0
- package/dist/tools/sfr/flipActivity.js +110 -0
- package/dist/tools/sfr/flipActivity.js.map +1 -0
- package/dist/tools/sfr/flipStats.d.ts +3 -0
- package/dist/tools/sfr/flipStats.js +98 -0
- package/dist/tools/sfr/flipStats.js.map +1 -0
- package/dist/tools/sfr/getProperty.d.ts +3 -0
- package/dist/tools/sfr/getProperty.js +142 -0
- package/dist/tools/sfr/getProperty.js.map +1 -0
- package/dist/tools/sfr/institutionalOwners.d.ts +3 -0
- package/dist/tools/sfr/institutionalOwners.js +88 -0
- package/dist/tools/sfr/institutionalOwners.js.map +1 -0
- package/dist/tools/sfr/investorActivity.d.ts +3 -0
- package/dist/tools/sfr/investorActivity.js +130 -0
- package/dist/tools/sfr/investorActivity.js.map +1 -0
- package/dist/tools/sfr/marketHighlights.d.ts +3 -0
- package/dist/tools/sfr/marketHighlights.js +100 -0
- package/dist/tools/sfr/marketHighlights.js.map +1 -0
- package/dist/tools/sfr/msaResolver.d.ts +15 -0
- package/dist/tools/sfr/msaResolver.js +109 -0
- package/dist/tools/sfr/msaResolver.js.map +1 -0
- package/dist/tools/sfr/propertyBatch.d.ts +3 -0
- package/dist/tools/sfr/propertyBatch.js +73 -0
- package/dist/tools/sfr/propertyBatch.js.map +1 -0
- package/dist/tools/sfr/propertyComps.d.ts +3 -0
- package/dist/tools/sfr/propertyComps.js +56 -0
- package/dist/tools/sfr/propertyComps.js.map +1 -0
- package/dist/tools/sfr/propertyTransactions.d.ts +3 -0
- package/dist/tools/sfr/propertyTransactions.js +50 -0
- package/dist/tools/sfr/propertyTransactions.js.map +1 -0
- package/dist/tools/sfr/rentalComparables.d.ts +3 -0
- package/dist/tools/sfr/rentalComparables.js +91 -0
- package/dist/tools/sfr/rentalComparables.js.map +1 -0
- package/dist/tools/sfr/rentalMarketAnalysis.d.ts +3 -0
- package/dist/tools/sfr/rentalMarketAnalysis.js +134 -0
- package/dist/tools/sfr/rentalMarketAnalysis.js.map +1 -0
- package/dist/tools/sfr/rentalStats.d.ts +3 -0
- package/dist/tools/sfr/rentalStats.js +118 -0
- package/dist/tools/sfr/rentalStats.js.map +1 -0
- package/dist/tools/sfr/searchProperties.d.ts +3 -0
- package/dist/tools/sfr/searchProperties.js +157 -0
- package/dist/tools/sfr/searchProperties.js.map +1 -0
- package/dist/tools/sfr/topBuyers.d.ts +3 -0
- package/dist/tools/sfr/topBuyers.js +91 -0
- package/dist/tools/sfr/topBuyers.js.map +1 -0
- package/dist/tools/sfr/zipDetail.d.ts +3 -0
- package/dist/tools/sfr/zipDetail.js +85 -0
- package/dist/tools/sfr/zipDetail.js.map +1 -0
- package/dist/tools/sfr/zipFinder.d.ts +3 -0
- package/dist/tools/sfr/zipFinder.js +79 -0
- package/dist/tools/sfr/zipFinder.js.map +1 -0
- package/dist/tools/slugHelper.d.ts +2 -0
- package/dist/tools/slugHelper.js +5 -0
- package/dist/tools/slugHelper.js.map +1 -0
- package/dist/tools/snowflake/compareMarkets.d.ts +2 -0
- package/dist/tools/snowflake/compareMarkets.js +95 -0
- package/dist/tools/snowflake/compareMarkets.js.map +1 -0
- package/dist/tools/snowflake/hviTrend.d.ts +2 -0
- package/dist/tools/snowflake/hviTrend.js +77 -0
- package/dist/tools/snowflake/hviTrend.js.map +1 -0
- package/dist/tools/snowflake/investorActivity.d.ts +2 -0
- package/dist/tools/snowflake/investorActivity.js +138 -0
- package/dist/tools/snowflake/investorActivity.js.map +1 -0
- package/dist/tools/snowflake/marketSnapshot.d.ts +2 -0
- package/dist/tools/snowflake/marketSnapshot.js +185 -0
- package/dist/tools/snowflake/marketSnapshot.js.map +1 -0
- package/dist/tools/snowflake/marketTrends.d.ts +2 -0
- package/dist/tools/snowflake/marketTrends.js +81 -0
- package/dist/tools/snowflake/marketTrends.js.map +1 -0
- package/dist/tools/snowflake/rankRentalZips.d.ts +2 -0
- package/dist/tools/snowflake/rankRentalZips.js +94 -0
- package/dist/tools/snowflake/rankRentalZips.js.map +1 -0
- package/dist/tools/snowflake/rankZips.d.ts +2 -0
- package/dist/tools/snowflake/rankZips.js +86 -0
- package/dist/tools/snowflake/rankZips.js.map +1 -0
- package/dist/tools/snowflake/rentalMarket.d.ts +2 -0
- package/dist/tools/snowflake/rentalMarket.js +111 -0
- package/dist/tools/snowflake/rentalMarket.js.map +1 -0
- package/dist/tools/snowflake/rentalYield.d.ts +2 -0
- package/dist/tools/snowflake/rentalYield.js +143 -0
- package/dist/tools/snowflake/rentalYield.js.map +1 -0
- package/dist/tools/snowflake/zipProfile.d.ts +2 -0
- package/dist/tools/snowflake/zipProfile.js +110 -0
- package/dist/tools/snowflake/zipProfile.js.map +1 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/package.json +53 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { sfQuery } from "../../services/snowflake.js";
|
|
3
|
+
import { structuredResult, markdownTable } from "../formatters.js";
|
|
4
|
+
import { action } from "../nextActions.js";
|
|
5
|
+
import { registerToolSafe } from "../registerToolSafe.js";
|
|
6
|
+
const PROPERTY_TYPES = [
|
|
7
|
+
"All Residential", "Single Family Residential", "Condo/Co-op",
|
|
8
|
+
"Townhouse", "Multi-Family (2-4 Unit)",
|
|
9
|
+
];
|
|
10
|
+
const Input = z.object({
|
|
11
|
+
zipCode: z.string().describe("5-digit ZIP code"),
|
|
12
|
+
state: z.string().describe("2-letter state code"),
|
|
13
|
+
months: z.coerce.number().optional().describe("Months of history (default 12, max 60)"),
|
|
14
|
+
propertyType: z.enum(PROPERTY_TYPES).optional().describe("Property type filter"),
|
|
15
|
+
});
|
|
16
|
+
function fmt$(v) {
|
|
17
|
+
if (v === null)
|
|
18
|
+
return "—";
|
|
19
|
+
return "$" + Math.round(v).toLocaleString("en-US");
|
|
20
|
+
}
|
|
21
|
+
export function registerMarketTrendsTool(server) {
|
|
22
|
+
registerToolSafe(server, "sfr_market_trends", {
|
|
23
|
+
title: "Redfin price/DOM/inventory trend for a ZIP (Snowflake)",
|
|
24
|
+
description: "Get historical Redfin market data for a ZIP code: median sale price, days on market, " +
|
|
25
|
+
"and inventory over time. Detects appreciation/depreciation/flat trend. Up to 60 months.",
|
|
26
|
+
inputSchema: Input,
|
|
27
|
+
}, async (args) => {
|
|
28
|
+
const st = args.state.toUpperCase();
|
|
29
|
+
const pt = args.propertyType ?? "All Residential";
|
|
30
|
+
const months = Math.min(args.months ?? 12, 60);
|
|
31
|
+
const rows = await sfQuery(`SELECT PERIOD_END, MEDIAN_SALE_PRICE, MEDIAN_DOM, INVENTORY
|
|
32
|
+
FROM ANALYTICS.PUBLIC.REDFIN_ZIP_MARKET
|
|
33
|
+
WHERE REGION = ? AND STATE_CODE = ? AND PROPERTY_TYPE = ?
|
|
34
|
+
AND PERIOD_END >= DATEADD(month, -?, CURRENT_DATE) ORDER BY PERIOD_END ASC`, [args.zipCode, st, pt, months]);
|
|
35
|
+
if (rows.length === 0) {
|
|
36
|
+
return structuredResult(`## Market Trends — ${args.zipCode}, ${st}\n\n_No data found._`, {});
|
|
37
|
+
}
|
|
38
|
+
const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
39
|
+
const first = rows[0].MEDIAN_SALE_PRICE;
|
|
40
|
+
const last = rows[rows.length - 1].MEDIAN_SALE_PRICE;
|
|
41
|
+
let direction = "flat";
|
|
42
|
+
let changePct = null;
|
|
43
|
+
if (first !== null && last !== null && first > 0) {
|
|
44
|
+
changePct = (last - first) / first;
|
|
45
|
+
if (changePct > 0.02)
|
|
46
|
+
direction = "appreciating";
|
|
47
|
+
else if (changePct < -0.02)
|
|
48
|
+
direction = "depreciating";
|
|
49
|
+
}
|
|
50
|
+
const tableRows = rows.map((r) => {
|
|
51
|
+
const d = new Date(r.PERIOD_END);
|
|
52
|
+
return [
|
|
53
|
+
`${monthNames[d.getMonth()]} ${String(d.getFullYear()).slice(2)}`,
|
|
54
|
+
fmt$(r.MEDIAN_SALE_PRICE),
|
|
55
|
+
r.MEDIAN_DOM !== null ? `${r.MEDIAN_DOM}d` : "—",
|
|
56
|
+
r.INVENTORY !== null ? r.INVENTORY.toLocaleString() : "—",
|
|
57
|
+
];
|
|
58
|
+
});
|
|
59
|
+
const changeLbl = changePct !== null
|
|
60
|
+
? `${changePct >= 0 ? "+" : ""}${(changePct * 100).toFixed(1)}%`
|
|
61
|
+
: "N/A";
|
|
62
|
+
const lines = [
|
|
63
|
+
`## Market Trends — ${args.zipCode}, ${st} (${months} months)`,
|
|
64
|
+
"",
|
|
65
|
+
`**Trend:** ${direction.toUpperCase()} (${changeLbl})`,
|
|
66
|
+
`**Start:** ${fmt$(first)} | **Latest:** ${fmt$(last)}`,
|
|
67
|
+
"",
|
|
68
|
+
markdownTable(["Date", "Med. Price", "DOM", "Inventory"], tableRows),
|
|
69
|
+
];
|
|
70
|
+
const actions = [
|
|
71
|
+
action("sfr_market_snapshot", "Current snapshot", { zipCode: args.zipCode, state: st }),
|
|
72
|
+
action("sfr_hvi_trend", "Home Value Index trend", { zipCode: args.zipCode }),
|
|
73
|
+
action("sfr_rank_zips", "Rank ZIPs by growth", { state: st, sortBy: "price_growth", metro: "" }),
|
|
74
|
+
];
|
|
75
|
+
return structuredResult(lines.join("\n"), {
|
|
76
|
+
zipCode: args.zipCode, state: st, months, direction, changePct,
|
|
77
|
+
startPrice: first, endPrice: last, dataPoints: rows.length,
|
|
78
|
+
}, actions);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=marketTrends.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"marketTrends.js","sourceRoot":"","sources":["../../../src/tools/snowflake/marketTrends.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { sfQuery } from "../../services/snowflake.js";
|
|
3
|
+
import { structuredResult, markdownTable } from "../formatters.js";
|
|
4
|
+
import { action } from "../nextActions.js";
|
|
5
|
+
import { registerToolSafe } from "../registerToolSafe.js";
|
|
6
|
+
const RENT_TABLE = "SFRA_PROD.PUBLIC.RENT_VIEW";
|
|
7
|
+
const SORT_OPTIONS = ["median_rent", "gross_yield", "rent_per_sqft", "total_listings"];
|
|
8
|
+
const SORT_COLUMN = {
|
|
9
|
+
median_rent: "MEDIAN_RENT",
|
|
10
|
+
gross_yield: "MARKET_YIELD",
|
|
11
|
+
rent_per_sqft: "AVG_RENT_PER_SQFT",
|
|
12
|
+
total_listings: "TOTAL_LISTINGS",
|
|
13
|
+
};
|
|
14
|
+
const Input = z.object({
|
|
15
|
+
state: z.string().describe("2-letter state code"),
|
|
16
|
+
metro: z.string().optional().describe("Metro/MSA name filter (fuzzy)"),
|
|
17
|
+
sortBy: z.enum(SORT_OPTIONS).describe("Metric to rank by"),
|
|
18
|
+
order: z.enum(["asc", "desc"]).optional().describe("Sort order (default: desc)"),
|
|
19
|
+
limit: z.coerce.number().optional().describe("Max results (default 15, max 50)"),
|
|
20
|
+
bedrooms: z.coerce.number().optional().describe("Filter by bedroom count"),
|
|
21
|
+
propertyType: z.string().optional().describe("Filter by home type"),
|
|
22
|
+
minListings: z.coerce.number().optional().describe("Minimum listings per ZIP (default 10)"),
|
|
23
|
+
});
|
|
24
|
+
function fmt$(v) { return v !== null ? "$" + Math.round(v).toLocaleString() : "—"; }
|
|
25
|
+
function fmtPct(v, d = 1) { return v !== null ? (v * 100).toFixed(d) + "%" : "—"; }
|
|
26
|
+
export function registerRankRentalZipsTool(server) {
|
|
27
|
+
registerToolSafe(server, "sfr_rank_rental_zips", {
|
|
28
|
+
title: "Rank ZIPs by rental metrics (Snowflake)",
|
|
29
|
+
description: "Rank ZIP codes by median rent, gross yield, rent/sqft, or listing count. " +
|
|
30
|
+
"Uses RENT_VIEW (5.9M listings) for real rental data with market yield calculation.",
|
|
31
|
+
inputSchema: Input,
|
|
32
|
+
}, async (args) => {
|
|
33
|
+
const st = args.state.toUpperCase();
|
|
34
|
+
const limit = Math.min(args.limit ?? 15, 50);
|
|
35
|
+
const order = args.order ?? "desc";
|
|
36
|
+
const col = SORT_COLUMN[args.sortBy];
|
|
37
|
+
const minList = args.minListings ?? 10;
|
|
38
|
+
const clauses = [
|
|
39
|
+
"r.LOCATION_STATE_ABBR = ?", "r.PRICE IS NOT NULL", "r.PRICE > 0", "r.PRICE < 25000",
|
|
40
|
+
"r.EFFECTIVE_DATE >= DATEADD(month, -12, CURRENT_DATE)", "r.ADDRESS_ZIPCODE IS NOT NULL",
|
|
41
|
+
];
|
|
42
|
+
const binds = [st];
|
|
43
|
+
if (args.metro) {
|
|
44
|
+
clauses.push("UPPER(r.LOCATION_MSA) LIKE UPPER(?)");
|
|
45
|
+
binds.push(`%${args.metro}%`);
|
|
46
|
+
}
|
|
47
|
+
if (args.bedrooms !== undefined) {
|
|
48
|
+
clauses.push("r.BEDROOMS = ?");
|
|
49
|
+
binds.push(args.bedrooms);
|
|
50
|
+
}
|
|
51
|
+
if (args.propertyType) {
|
|
52
|
+
clauses.push("UPPER(r.HOMETYPE) = UPPER(?)");
|
|
53
|
+
binds.push(args.propertyType);
|
|
54
|
+
}
|
|
55
|
+
const rows = await sfQuery(`SELECT r.ADDRESS_ZIPCODE AS ZIP, MAX(r.LOCATION_CITY) AS CITY, COUNT(*) AS TOTAL_LISTINGS,
|
|
56
|
+
MEDIAN(r.PRICE) AS MEDIAN_RENT, AVG(r.PRICE) AS AVG_RENT,
|
|
57
|
+
AVG(CASE WHEN r.LIVINGAREA > 0 THEN r.PRICE / r.LIVINGAREA ELSE NULL END) AS AVG_RENT_PER_SQFT,
|
|
58
|
+
CASE WHEN MEDIAN(COALESCE(r.CURRENT_AVM_VALUE, r.ZESTIMATE)) > 0
|
|
59
|
+
THEN (MEDIAN(r.PRICE) * 12) / MEDIAN(COALESCE(r.CURRENT_AVM_VALUE, r.ZESTIMATE))
|
|
60
|
+
ELSE NULL END AS MARKET_YIELD,
|
|
61
|
+
MEDIAN(COALESCE(r.CURRENT_AVM_VALUE, r.ZESTIMATE)) AS MEDIAN_AVM
|
|
62
|
+
FROM ${RENT_TABLE} r WHERE ${clauses.join(" AND ")}
|
|
63
|
+
GROUP BY r.ADDRESS_ZIPCODE HAVING COUNT(*) >= ?
|
|
64
|
+
ORDER BY ${col} ${order === "asc" ? "ASC" : "DESC"} NULLS LAST LIMIT ?`, [...binds, minList, limit]);
|
|
65
|
+
if (rows.length === 0) {
|
|
66
|
+
return structuredResult(`## Rental ZIP Rankings\n\n_No data for ${st}${args.metro ? ` (${args.metro})` : ""}_`, {});
|
|
67
|
+
}
|
|
68
|
+
const tableRows = rows.map((r, i) => [
|
|
69
|
+
i + 1, r.ZIP, (r.CITY ?? "").slice(0, 16), fmt$(r.MEDIAN_RENT),
|
|
70
|
+
r.TOTAL_LISTINGS.toLocaleString(),
|
|
71
|
+
r.MARKET_YIELD !== null ? fmtPct(r.MARKET_YIELD) : "—",
|
|
72
|
+
fmt$(r.MEDIAN_AVM),
|
|
73
|
+
]);
|
|
74
|
+
const metroLbl = args.metro ? ` (${args.metro})` : "";
|
|
75
|
+
const lines = [
|
|
76
|
+
`## Top ${rows.length} Rental ZIPs — ${st}${metroLbl} (by ${args.sortBy}, ${order})`,
|
|
77
|
+
"",
|
|
78
|
+
markdownTable(["#", "ZIP", "City", "Med. Rent", "Listings", "Mkt Yield", "Home Value"], tableRows),
|
|
79
|
+
];
|
|
80
|
+
const topZip = rows[0].ZIP;
|
|
81
|
+
const actions = [
|
|
82
|
+
action("sfr_rental_yield", `Yield for ${topZip}`, { zipCode: topZip, state: st }),
|
|
83
|
+
action("sfr_rental_market_sf", `Rental stats for ${topZip}`, { zipCode: topZip, state: st }),
|
|
84
|
+
];
|
|
85
|
+
return structuredResult(lines.join("\n"), {
|
|
86
|
+
state: st, sortBy: args.sortBy, count: rows.length,
|
|
87
|
+
zips: rows.map((r) => ({
|
|
88
|
+
zip: r.ZIP, city: r.CITY, medianRent: r.MEDIAN_RENT,
|
|
89
|
+
listings: r.TOTAL_LISTINGS, yield: r.MARKET_YIELD, homeValue: r.MEDIAN_AVM,
|
|
90
|
+
})),
|
|
91
|
+
}, actions);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=rankRentalZips.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rankRentalZips.js","sourceRoot":"","sources":["../../../src/tools/snowflake/rankRentalZips.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { sfQuery } from "../../services/snowflake.js";
|
|
3
|
+
import { structuredResult, markdownTable } from "../formatters.js";
|
|
4
|
+
import { action } from "../nextActions.js";
|
|
5
|
+
import { registerToolSafe } from "../registerToolSafe.js";
|
|
6
|
+
const PROPERTY_TYPES = [
|
|
7
|
+
"All Residential", "Single Family Residential", "Condo/Co-op",
|
|
8
|
+
"Townhouse", "Multi-Family (2-4 Unit)",
|
|
9
|
+
];
|
|
10
|
+
const SORT_OPTIONS = [
|
|
11
|
+
"median_price", "dom", "inventory", "price_growth", "homes_sold", "sale_to_list",
|
|
12
|
+
];
|
|
13
|
+
const SORT_COLUMN = {
|
|
14
|
+
median_price: "MEDIAN_SALE_PRICE",
|
|
15
|
+
dom: "MEDIAN_DOM",
|
|
16
|
+
inventory: "INVENTORY",
|
|
17
|
+
price_growth: "MEDIAN_SALE_PRICE_YOY",
|
|
18
|
+
homes_sold: "HOMES_SOLD",
|
|
19
|
+
sale_to_list: "AVG_SALE_TO_LIST",
|
|
20
|
+
};
|
|
21
|
+
const ZIP_COLUMNS = `
|
|
22
|
+
REGION, CITY, PARENT_METRO_REGION, MEDIAN_SALE_PRICE,
|
|
23
|
+
MEDIAN_DOM, INVENTORY, HOMES_SOLD, NEW_LISTINGS,
|
|
24
|
+
AVG_SALE_TO_LIST, SOLD_ABOVE_LIST, MEDIAN_SALE_PRICE_YOY, PRICE_DROPS`;
|
|
25
|
+
const Input = z.object({
|
|
26
|
+
metro: z.string().describe("Metro area name (fuzzy match), e.g. 'Austin' or 'Dallas'"),
|
|
27
|
+
state: z.string().describe("2-letter state code"),
|
|
28
|
+
sortBy: z.enum(SORT_OPTIONS).describe("Metric to sort by"),
|
|
29
|
+
order: z.enum(["asc", "desc"]).optional().describe("Sort order (default: desc)"),
|
|
30
|
+
limit: z.coerce.number().optional().describe("Max results (default 10, max 50)"),
|
|
31
|
+
propertyType: z.enum(PROPERTY_TYPES).optional(),
|
|
32
|
+
});
|
|
33
|
+
function fmt$(v) { return v !== null ? "$" + Math.round(v).toLocaleString() : "—"; }
|
|
34
|
+
function fmtPct(v, d = 1) { return v !== null ? (v * 100).toFixed(d) + "%" : "—"; }
|
|
35
|
+
export function registerRankZipsTool(server) {
|
|
36
|
+
registerToolSafe(server, "sfr_rank_zips", {
|
|
37
|
+
title: "Rank ZIP codes by Redfin metrics (Snowflake)",
|
|
38
|
+
description: "Rank ZIPs in a metro area by median price, DOM, inventory, price growth, " +
|
|
39
|
+
"homes sold, or sale-to-list ratio. Uses latest Redfin data from Snowflake.",
|
|
40
|
+
inputSchema: Input,
|
|
41
|
+
}, async (args) => {
|
|
42
|
+
const st = args.state.toUpperCase();
|
|
43
|
+
const pt = args.propertyType ?? "All Residential";
|
|
44
|
+
const limit = Math.min(args.limit ?? 10, 50);
|
|
45
|
+
const order = args.order ?? "desc";
|
|
46
|
+
const col = SORT_COLUMN[args.sortBy];
|
|
47
|
+
const rows = await sfQuery(`SELECT ${ZIP_COLUMNS} FROM ANALYTICS.PUBLIC.REDFIN_ZIP_MARKET
|
|
48
|
+
WHERE UPPER(PARENT_METRO_REGION) LIKE UPPER(?)
|
|
49
|
+
AND STATE_CODE = ? AND PROPERTY_TYPE = ?
|
|
50
|
+
AND PERIOD_END = (SELECT MAX(PERIOD_END) FROM ANALYTICS.PUBLIC.REDFIN_ZIP_MARKET WHERE STATE_CODE = ?)
|
|
51
|
+
ORDER BY ${col} ${order === "asc" ? "ASC" : "DESC"} NULLS LAST
|
|
52
|
+
LIMIT ?`, [`%${args.metro}%`, st, pt, st, limit]);
|
|
53
|
+
if (rows.length === 0) {
|
|
54
|
+
return structuredResult(`## ZIP Rankings\n\n_No data for "${args.metro}" in ${st}_`, {});
|
|
55
|
+
}
|
|
56
|
+
const tableRows = rows.map((r, i) => [
|
|
57
|
+
i + 1,
|
|
58
|
+
r.REGION,
|
|
59
|
+
(r.CITY ?? "").slice(0, 16),
|
|
60
|
+
fmt$(r.MEDIAN_SALE_PRICE),
|
|
61
|
+
r.MEDIAN_DOM !== null ? `${r.MEDIAN_DOM}d` : "—",
|
|
62
|
+
r.INVENTORY?.toLocaleString() ?? "—",
|
|
63
|
+
fmtPct(r.MEDIAN_SALE_PRICE_YOY),
|
|
64
|
+
fmtPct(r.AVG_SALE_TO_LIST, 2),
|
|
65
|
+
]);
|
|
66
|
+
const lines = [
|
|
67
|
+
`## Top ${rows.length} ZIPs — ${args.metro}, ${st} (by ${args.sortBy}, ${order})`,
|
|
68
|
+
"",
|
|
69
|
+
markdownTable(["#", "ZIP", "City", "Med. Price", "DOM", "Inventory", "Price YoY", "Sale/List"], tableRows),
|
|
70
|
+
];
|
|
71
|
+
const topZip = rows[0].REGION;
|
|
72
|
+
const actions = [
|
|
73
|
+
action("sfr_market_snapshot", `Snapshot for top ZIP ${topZip}`, { zipCode: topZip, state: st }),
|
|
74
|
+
action("sfr_rental_yield", `Yield for ${topZip}`, { zipCode: topZip, state: st }),
|
|
75
|
+
action("sfr_zip_profile", `Demographics for ${topZip}`, { zipCode: topZip }),
|
|
76
|
+
];
|
|
77
|
+
return structuredResult(lines.join("\n"), {
|
|
78
|
+
metro: args.metro, state: st, sortBy: args.sortBy, order, count: rows.length,
|
|
79
|
+
zips: rows.map((r) => ({
|
|
80
|
+
zip: r.REGION, city: r.CITY, medianPrice: r.MEDIAN_SALE_PRICE,
|
|
81
|
+
dom: r.MEDIAN_DOM, inventory: r.INVENTORY, priceYoy: r.MEDIAN_SALE_PRICE_YOY,
|
|
82
|
+
})),
|
|
83
|
+
}, actions);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=rankZips.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rankZips.js","sourceRoot":"","sources":["../../../src/tools/snowflake/rankZips.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { sfQuery } from "../../services/snowflake.js";
|
|
3
|
+
import { structuredResult, kvSummary, markdownTable } from "../formatters.js";
|
|
4
|
+
import { action } from "../nextActions.js";
|
|
5
|
+
import { registerToolSafe } from "../registerToolSafe.js";
|
|
6
|
+
const RENT_TABLE = "SFRA_PROD.PUBLIC.RENT_VIEW";
|
|
7
|
+
const Input = z.object({
|
|
8
|
+
zipCode: z.string().optional().describe("5-digit ZIP code"),
|
|
9
|
+
city: z.string().optional().describe("City name (if no ZIP)"),
|
|
10
|
+
state: z.string().describe("2-letter state code"),
|
|
11
|
+
bedrooms: z.coerce.number().optional().describe("Filter by bedroom count"),
|
|
12
|
+
propertyType: z.string().optional().describe("Filter by home type, e.g. 'SINGLE_FAMILY'"),
|
|
13
|
+
lookbackMonths: z.coerce.number().optional().describe("Months of data (default 12)"),
|
|
14
|
+
});
|
|
15
|
+
function fmt$(v) { return v !== null ? "$" + Math.round(v).toLocaleString() : "—"; }
|
|
16
|
+
function buildWhere(args, alias = "r") {
|
|
17
|
+
const c = [];
|
|
18
|
+
const b = [];
|
|
19
|
+
if (args.zipCode) {
|
|
20
|
+
c.push(`${alias}.ADDRESS_ZIPCODE = ?`);
|
|
21
|
+
b.push(args.zipCode);
|
|
22
|
+
}
|
|
23
|
+
else if (args.city) {
|
|
24
|
+
c.push(`UPPER(${alias}.LOCATION_CITY) = UPPER(?)`);
|
|
25
|
+
b.push(args.city);
|
|
26
|
+
}
|
|
27
|
+
c.push(`${alias}.LOCATION_STATE_ABBR = ?`);
|
|
28
|
+
b.push(args.state.toUpperCase());
|
|
29
|
+
c.push(`${alias}.PRICE IS NOT NULL`, `${alias}.PRICE > 0`, `${alias}.PRICE < 25000`);
|
|
30
|
+
if (args.bedrooms !== undefined) {
|
|
31
|
+
c.push(`${alias}.BEDROOMS = ?`);
|
|
32
|
+
b.push(args.bedrooms);
|
|
33
|
+
}
|
|
34
|
+
if (args.propertyType) {
|
|
35
|
+
c.push(`UPPER(${alias}.HOMETYPE) = UPPER(?)`);
|
|
36
|
+
b.push(args.propertyType);
|
|
37
|
+
}
|
|
38
|
+
return { where: c.join(" AND "), binds: b };
|
|
39
|
+
}
|
|
40
|
+
export function registerRentalMarketTool(server) {
|
|
41
|
+
registerToolSafe(server, "sfr_rental_market_sf", {
|
|
42
|
+
title: "Rental pricing stats from Snowflake (percentiles, type breakdown)",
|
|
43
|
+
description: "Get rental market stats from 5.9M listings: median/avg rent, rent/sqft, " +
|
|
44
|
+
"percentile distribution (p10-p90), property type breakdown. " +
|
|
45
|
+
"Queries RENT_VIEW in Snowflake for richer data than the HTTP API.",
|
|
46
|
+
inputSchema: Input,
|
|
47
|
+
}, async (args) => {
|
|
48
|
+
const lookback = args.lookbackMonths ?? 12;
|
|
49
|
+
const { where, binds } = buildWhere(args);
|
|
50
|
+
const [statsRows, typeRows] = await Promise.all([
|
|
51
|
+
sfQuery(`SELECT COUNT(*) AS TOTAL, MEDIAN(r.PRICE) AS MED_RENT, AVG(r.PRICE) AS AVG_RENT,
|
|
52
|
+
AVG(CASE WHEN r.LIVINGAREA > 0 THEN r.PRICE / r.LIVINGAREA ELSE NULL END) AS AVG_RPSF,
|
|
53
|
+
PERCENTILE_CONT(0.10) WITHIN GROUP (ORDER BY r.PRICE) AS P10,
|
|
54
|
+
PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY r.PRICE) AS P25,
|
|
55
|
+
PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY r.PRICE) AS P50,
|
|
56
|
+
PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY r.PRICE) AS P75,
|
|
57
|
+
PERCENTILE_CONT(0.90) WITHIN GROUP (ORDER BY r.PRICE) AS P90,
|
|
58
|
+
AVG(r.BEDROOMS) AS AVG_BEDS, AVG(r.LIVINGAREA) AS AVG_SQFT
|
|
59
|
+
FROM ${RENT_TABLE} r WHERE ${where} AND r.EFFECTIVE_DATE >= DATEADD(month, -?, CURRENT_DATE)`, [...binds, lookback]),
|
|
60
|
+
sfQuery(`SELECT r.HOMETYPE, COUNT(*) AS CNT, MEDIAN(r.PRICE) AS MED_RENT
|
|
61
|
+
FROM ${RENT_TABLE} r WHERE ${where} AND r.EFFECTIVE_DATE >= DATEADD(month, -?, CURRENT_DATE)
|
|
62
|
+
AND r.HOMETYPE IS NOT NULL GROUP BY r.HOMETYPE ORDER BY CNT DESC LIMIT 8`, [...binds, lookback]),
|
|
63
|
+
]);
|
|
64
|
+
const s = statsRows[0];
|
|
65
|
+
if (!s || s.TOTAL === 0) {
|
|
66
|
+
return structuredResult(`## Rental Market\n\n_No data for ${args.zipCode ?? args.city}, ${args.state}_`, {});
|
|
67
|
+
}
|
|
68
|
+
const loc = args.zipCode ?? args.city ?? "Unknown";
|
|
69
|
+
const lines = [
|
|
70
|
+
`## Rental Market — ${loc}, ${args.state.toUpperCase()} (last ${lookback} months)`,
|
|
71
|
+
"",
|
|
72
|
+
"### Pricing",
|
|
73
|
+
kvSummary([
|
|
74
|
+
["Total Listings", s.TOTAL.toLocaleString()],
|
|
75
|
+
["Median Rent", fmt$(s.MED_RENT)],
|
|
76
|
+
["Average Rent", fmt$(s.AVG_RENT)],
|
|
77
|
+
["Avg Rent/SqFt", s.AVG_RPSF !== null ? `$${s.AVG_RPSF.toFixed(2)}` : undefined],
|
|
78
|
+
]),
|
|
79
|
+
"",
|
|
80
|
+
"### Price Distribution",
|
|
81
|
+
kvSummary([
|
|
82
|
+
["10th %ile", fmt$(s.P10)],
|
|
83
|
+
["25th %ile", fmt$(s.P25)],
|
|
84
|
+
["Median", fmt$(s.P50)],
|
|
85
|
+
["75th %ile", fmt$(s.P75)],
|
|
86
|
+
["90th %ile", fmt$(s.P90)],
|
|
87
|
+
]),
|
|
88
|
+
"",
|
|
89
|
+
"### Market Characteristics",
|
|
90
|
+
kvSummary([
|
|
91
|
+
["Avg Bedrooms", s.AVG_BEDS !== null ? s.AVG_BEDS.toFixed(1) : undefined],
|
|
92
|
+
["Avg SqFt", s.AVG_SQFT !== null ? Math.round(s.AVG_SQFT).toLocaleString() : undefined],
|
|
93
|
+
]),
|
|
94
|
+
];
|
|
95
|
+
if (typeRows.length > 0) {
|
|
96
|
+
lines.push("", "### Property Type Breakdown");
|
|
97
|
+
lines.push(markdownTable(["Type", "Listings", "Median Rent"], typeRows.map((t) => [t.HOMETYPE, t.CNT.toLocaleString(), fmt$(t.MED_RENT)])));
|
|
98
|
+
}
|
|
99
|
+
const zip = args.zipCode ?? "";
|
|
100
|
+
const actions = [
|
|
101
|
+
action("sfr_rental_yield", "Yield analysis", { zipCode: zip, state: args.state }),
|
|
102
|
+
action("sfr_market_snapshot", "Sale market data", { zipCode: zip, state: args.state }),
|
|
103
|
+
];
|
|
104
|
+
return structuredResult(lines.join("\n"), {
|
|
105
|
+
location: loc, state: args.state.toUpperCase(), totalListings: s.TOTAL,
|
|
106
|
+
medianRent: s.MED_RENT, avgRent: s.AVG_RENT, avgRentPerSqft: s.AVG_RPSF,
|
|
107
|
+
distribution: { p10: s.P10, p25: s.P25, p50: s.P50, p75: s.P75, p90: s.P90 },
|
|
108
|
+
}, actions);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=rentalMarket.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rentalMarket.js","sourceRoot":"","sources":["../../../src/tools/snowflake/rentalMarket.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { sfQuery } from "../../services/snowflake.js";
|
|
3
|
+
import { structuredResult, kvSummary } from "../formatters.js";
|
|
4
|
+
import { action } from "../nextActions.js";
|
|
5
|
+
import { registerToolSafe } from "../registerToolSafe.js";
|
|
6
|
+
const RENT_TABLE = "SFRA_PROD.PUBLIC.RENT_VIEW";
|
|
7
|
+
const GEO_FLIPS_TABLE = "SFRA_PROD.PUBLIC.GEO_FLIPS";
|
|
8
|
+
const Input = z.object({
|
|
9
|
+
zipCode: z.string().optional().describe("5-digit ZIP code (required for acquisition yield)"),
|
|
10
|
+
city: z.string().optional().describe("City name (market yield only, no acquisition data)"),
|
|
11
|
+
state: z.string().describe("2-letter state code"),
|
|
12
|
+
bedrooms: z.coerce.number().optional().describe("Filter by bedroom count"),
|
|
13
|
+
propertyType: z.string().optional().describe("Filter by home type"),
|
|
14
|
+
});
|
|
15
|
+
function fmt$(v) { return v !== null ? "$" + Math.round(v).toLocaleString() : "—"; }
|
|
16
|
+
function fmtPct(v, d = 1) { return v !== null ? (v * 100).toFixed(d) + "%" : "—"; }
|
|
17
|
+
function buildWhere(args, alias = "r") {
|
|
18
|
+
const c = [];
|
|
19
|
+
const b = [];
|
|
20
|
+
if (args.zipCode) {
|
|
21
|
+
c.push(`${alias}.ADDRESS_ZIPCODE = ?`);
|
|
22
|
+
b.push(args.zipCode);
|
|
23
|
+
}
|
|
24
|
+
else if (args.city) {
|
|
25
|
+
c.push(`UPPER(${alias}.LOCATION_CITY) = UPPER(?)`);
|
|
26
|
+
b.push(args.city);
|
|
27
|
+
}
|
|
28
|
+
c.push(`${alias}.LOCATION_STATE_ABBR = ?`);
|
|
29
|
+
b.push(args.state.toUpperCase());
|
|
30
|
+
c.push(`${alias}.PRICE IS NOT NULL`, `${alias}.PRICE > 0`, `${alias}.PRICE < 25000`);
|
|
31
|
+
if (args.bedrooms !== undefined) {
|
|
32
|
+
c.push(`${alias}.BEDROOMS = ?`);
|
|
33
|
+
b.push(args.bedrooms);
|
|
34
|
+
}
|
|
35
|
+
if (args.propertyType) {
|
|
36
|
+
c.push(`UPPER(${alias}.HOMETYPE) = UPPER(?)`);
|
|
37
|
+
b.push(args.propertyType);
|
|
38
|
+
}
|
|
39
|
+
return { where: c.join(" AND "), binds: b };
|
|
40
|
+
}
|
|
41
|
+
export function registerRentalYieldTool(server) {
|
|
42
|
+
registerToolSafe(server, "sfr_rental_yield", {
|
|
43
|
+
title: "Rental yield analysis: market yield + acquisition yield (Snowflake)",
|
|
44
|
+
description: "Two-part yield analysis: (1) MARKET YIELD — rent vs current home values (Zestimate/AVM), " +
|
|
45
|
+
"(2) ACQUISITION YIELD — rent vs recent investor purchase prices from deed records. " +
|
|
46
|
+
"Includes rent-to-price ratio, 1% rule check, cap rate rating, and discount/premium signal.",
|
|
47
|
+
inputSchema: Input,
|
|
48
|
+
}, async (args) => {
|
|
49
|
+
const st = args.state.toUpperCase();
|
|
50
|
+
const { where, binds } = buildWhere(args);
|
|
51
|
+
const [rentRows, acqRows] = await Promise.all([
|
|
52
|
+
sfQuery(`SELECT COUNT(*) AS TOTAL, MEDIAN(r.PRICE) AS MED_RENT, AVG(r.PRICE) AS AVG_RENT,
|
|
53
|
+
MEDIAN(r.ZESTIMATE) AS MED_ZEST, MEDIAN(r.CURRENT_AVM_VALUE) AS MED_AVM,
|
|
54
|
+
MEDIAN(CASE WHEN r.LIVINGAREA > 0 THEN r.PRICE / r.LIVINGAREA ELSE NULL END) AS MED_RPSF
|
|
55
|
+
FROM ${RENT_TABLE} r WHERE ${where} AND r.EFFECTIVE_DATE >= DATEADD(month, -12, CURRENT_DATE)`, binds),
|
|
56
|
+
args.zipCode
|
|
57
|
+
? sfQuery(`SELECT COUNT(*) AS TX_CNT,
|
|
58
|
+
MEDIAN(CASE WHEN SALE_AMT > 10000 THEN SALE_AMT ELSE NULL END) AS MED_PRICE
|
|
59
|
+
FROM ${GEO_FLIPS_TABLE}
|
|
60
|
+
WHERE ZIP_CODE = ? AND STATE = ? AND RECORDING_DATE >= DATEADD(year, -2, CURRENT_DATE)`, [args.zipCode, st])
|
|
61
|
+
: Promise.resolve([{ TX_CNT: 0, MED_PRICE: null }]),
|
|
62
|
+
]);
|
|
63
|
+
const r = rentRows[0];
|
|
64
|
+
if (!r || r.TOTAL === 0) {
|
|
65
|
+
return structuredResult(`## Rental Yield\n\n_No data for ${args.zipCode ?? args.city}, ${st}_`, {});
|
|
66
|
+
}
|
|
67
|
+
const acq = acqRows[0] ?? { TX_CNT: 0, MED_PRICE: null };
|
|
68
|
+
const annualRent = r.MED_RENT !== null ? r.MED_RENT * 12 : null;
|
|
69
|
+
const mktVal = r.MED_AVM ?? r.MED_ZEST;
|
|
70
|
+
const mktYield = annualRent !== null && mktVal && mktVal > 0 ? annualRent / mktVal : null;
|
|
71
|
+
const acqYield = annualRent !== null && acq.MED_PRICE && acq.MED_PRICE > 0 ? annualRent / acq.MED_PRICE : null;
|
|
72
|
+
const rtp = r.MED_RENT !== null && mktVal && mktVal > 0 ? r.MED_RENT / mktVal : null;
|
|
73
|
+
let rating = "—";
|
|
74
|
+
if (mktYield !== null) {
|
|
75
|
+
if (mktYield >= 0.08)
|
|
76
|
+
rating = "STRONG CASH FLOW (>8%)";
|
|
77
|
+
else if (mktYield >= 0.06)
|
|
78
|
+
rating = "GOOD CASH FLOW (6-8%)";
|
|
79
|
+
else if (mktYield >= 0.04)
|
|
80
|
+
rating = "MODERATE (4-6%)";
|
|
81
|
+
else
|
|
82
|
+
rating = "APPRECIATION PLAY (<4%)";
|
|
83
|
+
}
|
|
84
|
+
let rtpNote = "";
|
|
85
|
+
if (rtp !== null) {
|
|
86
|
+
if (rtp >= 0.01)
|
|
87
|
+
rtpNote = " (meets 1% rule)";
|
|
88
|
+
else if (rtp >= 0.008)
|
|
89
|
+
rtpNote = " (close to 1% rule)";
|
|
90
|
+
else
|
|
91
|
+
rtpNote = " (below 1% rule)";
|
|
92
|
+
}
|
|
93
|
+
let discountNote = "";
|
|
94
|
+
if (acqYield !== null && mktYield !== null) {
|
|
95
|
+
if (acqYield > mktYield * 1.1)
|
|
96
|
+
discountNote = `Investors buying at discount — acq yield ${fmtPct(acqYield)} > market ${fmtPct(mktYield)}`;
|
|
97
|
+
else if (acqYield < mktYield * 0.9)
|
|
98
|
+
discountNote = `Investors paying premium — acq yield ${fmtPct(acqYield)} < market ${fmtPct(mktYield)}`;
|
|
99
|
+
}
|
|
100
|
+
const loc = args.zipCode ?? args.city ?? "Unknown";
|
|
101
|
+
const lines = [
|
|
102
|
+
`## Rental Yield — ${loc}, ${st}`,
|
|
103
|
+
"",
|
|
104
|
+
"### Rental Income",
|
|
105
|
+
kvSummary([
|
|
106
|
+
["Median Rent", `${fmt$(r.MED_RENT)}/mo (${fmt$(annualRent)}/yr)`],
|
|
107
|
+
["Rent/SqFt", r.MED_RPSF !== null ? `$${r.MED_RPSF.toFixed(2)}` : undefined],
|
|
108
|
+
]),
|
|
109
|
+
"",
|
|
110
|
+
"### Market Yield (rent vs current values)",
|
|
111
|
+
kvSummary([
|
|
112
|
+
["Median Zestimate", fmt$(r.MED_ZEST)],
|
|
113
|
+
["Median AVM", fmt$(r.MED_AVM)],
|
|
114
|
+
["Market Cap Rate", fmtPct(mktYield)],
|
|
115
|
+
]),
|
|
116
|
+
];
|
|
117
|
+
if (args.zipCode) {
|
|
118
|
+
lines.push("", "### Acquisition Yield (rent vs recent purchases, 2yr)", kvSummary([
|
|
119
|
+
["Recent Investor Transactions", acq.TX_CNT.toLocaleString()],
|
|
120
|
+
["Median Purchase Price", fmt$(acq.MED_PRICE)],
|
|
121
|
+
["Acquisition Cap Rate", fmtPct(acqYield)],
|
|
122
|
+
]));
|
|
123
|
+
}
|
|
124
|
+
lines.push("", "### Investment Context", kvSummary([
|
|
125
|
+
["Rent-to-Price Ratio", rtp !== null ? `${(rtp * 100).toFixed(3)}%${rtpNote}` : undefined],
|
|
126
|
+
["Rating", rating],
|
|
127
|
+
...(discountNote ? [["Note", discountNote]] : []),
|
|
128
|
+
]));
|
|
129
|
+
const zip = args.zipCode ?? "";
|
|
130
|
+
const actions = [
|
|
131
|
+
action("sfr_rental_market_sf", "Full rental stats", { zipCode: zip, state: st }),
|
|
132
|
+
action("sfr_zip_investor_activity", "Investor transactions", { zipCode: zip, state: st }),
|
|
133
|
+
action("sfr_market_snapshot", "Sale market data", { zipCode: zip, state: st }),
|
|
134
|
+
];
|
|
135
|
+
return structuredResult(lines.join("\n"), {
|
|
136
|
+
location: loc, state: st, medianRent: r.MED_RENT,
|
|
137
|
+
medianZestimate: r.MED_ZEST, medianAvm: r.MED_AVM, marketYield: mktYield,
|
|
138
|
+
recentTxCount: acq.TX_CNT, medianPurchasePrice: acq.MED_PRICE, acquisitionYield: acqYield,
|
|
139
|
+
rentToPrice: rtp, rating,
|
|
140
|
+
}, actions);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=rentalYield.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rentalYield.js","sourceRoot":"","sources":["../../../src/tools/snowflake/rentalYield.ts"],"names":[],"mappings":"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"}
|