| 151 | if (out.riv !== null && out.basis === 'transactions' && out.confidence >= ASK_MIN_CONFIDENCE && out.sampleSize >= ASK_MIN_SAMPLE) { |
151 | if (out.riv !== null && out.basis === 'transactions' && out.confidence >= ASK_MIN_CONFIDENCE && out.sampleSize >= ASK_MIN_SAMPLE) { |
| 152 | const lo = out.riv * ASK_ANOMALY_LOW_RATIO; |
152 | const lo = out.riv * ASK_ANOMALY_LOW_RATIO; |
| 153 | const hi = out.riv * ASK_ANOMALY_HIGH_RATIO; |
153 | const hi = out.riv * ASK_ANOMALY_HIGH_RATIO; |
| 154 | − const scope = and(eq(listings.variantId, v.id), eq(listings.availability, 'available'), sql`${listings.priceUsd} > 0`); |
154 | + // Auction lots are excluded: a current/opening bid (often ¥1 on Yahoo! Auctions) is not an asking price. |
| |
155 | + // Bid-vs-RIV belongs to auction intelligence with fees (§33–§35), not to the deal rails. |
| |
156 | + const scope = and(eq(listings.variantId, v.id), eq(listings.availability, 'available'), sql`${listings.priceUsd} > 0`, sql`${listings.listingType} <> 'auction'`); |
| 155 | await db() |
157 | await db() |
| 156 | .update(listings) |
158 | .update(listings) |
| 157 | .set({ discountToRiv: sql`round((${listings.priceUsd} - ${out.riv}) / ${out.riv}, 4)`, flags: sql`array_remove(${listings.flags}, 'riv_anomaly')` }) |
159 | .set({ discountToRiv: sql`round((${listings.priceUsd} - ${out.riv}) / ${out.riv}, 4)`, flags: sql`array_remove(${listings.flags}, 'riv_anomaly')` }) |
| 268 | |
270 | |
| 269 | async function activeListingStats(assetId: string, variantId: string | null): Promise<{ count: number; minAsk: number | null }> { |
271 | async function activeListingStats(assetId: string, variantId: string | null): Promise<{ count: number; minAsk: number | null }> { |
| 270 | const [row] = await db() |
272 | const [row] = await db() |
| 271 | − .select({ n: sql<number>`count(*)::int`, min: sql<number | null>`min(${listings.priceUsd})` }) |
273 | + // count = every live listing; min ask = fixed-price / best-offer / ask only (an auction bid is not an ask) |
| |
274 | + .select({ n: sql<number>`count(*)::int`, min: sql<number | null>`min(${listings.priceUsd}) filter (where ${listings.listingType} <> 'auction' and ${listings.priceUsd} > 0)` }) |
| 272 | .from(listings) |
275 | .from(listings) |
| 273 | .where(and(eq(listings.assetId, assetId), eq(listings.availability, 'available'), variantId ? eq(listings.variantId, variantId) : undefined)); |
276 | .where(and(eq(listings.assetId, assetId), eq(listings.availability, 'available'), variantId ? eq(listings.variantId, variantId) : undefined)); |
| 274 | return { count: row?.n ?? 0, minAsk: row?.min === null || row?.min === undefined ? null : Number(row.min) }; |
277 | return { count: row?.n ?? 0, minAsk: row?.min === null || row?.min === undefined ? null : Number(row.min) }; |
| 275 | |
278 | |