← back to Omega Watches 2
collectors/antiquorum/collector.mjs
203 lines
/**
* Antiquorum Auction Collector
*
* Fetches realized auction prices from antiquorum.swiss for Omega watches.
* Writes to the market_event ledger with event_type = 'auction_realized'.
*
* Antiquorum publishes lot results after auctions complete. Their results
* pages include hammer prices, buyer's premiums, and condition grades.
*
* Rate limit: 10 RPM (per source config)
*/
import { BaseCollector, pool } from '../base-collector.mjs';
import https from 'https';
import crypto from 'crypto';
const ANTIQUORUM_BASE = 'https://www.antiquorum.swiss';
const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36';
export default class AntiquorumCollector extends BaseCollector {
constructor() {
super('antiquorum');
}
getJobType() { return 'incremental'; }
fetch(url) {
return new Promise((resolve, reject) => {
const parsedUrl = new URL(url);
const req = https.get({
hostname: parsedUrl.hostname,
path: parsedUrl.pathname + parsedUrl.search,
headers: {
'User-Agent': USER_AGENT,
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.9',
},
timeout: 30000,
}, (res) => {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
const redir = res.headers.location.startsWith('http')
? res.headers.location
: `${ANTIQUORUM_BASE}${res.headers.location}`;
return this.fetch(redir).then(resolve).catch(reject);
}
let body = '';
res.on('data', chunk => body += chunk);
res.on('end', () => resolve({ statusCode: res.statusCode, body }));
});
req.on('error', reject);
req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); });
});
}
/**
* Parse Antiquorum lot results page for auction results
*/
parseLotResults(html) {
const results = [];
// Antiquorum lot pages use structured lot entries
// Pattern: each lot has a lot number, description, estimate, and result
const lotPattern = /<div[^>]*class="[^"]*lot-item[^"]*"[^>]*>([\s\S]*?)<\/div>\s*<\/div>/gi;
let match;
while ((match = lotPattern.exec(html)) !== null) {
const content = match[1];
// Extract lot number
const lotNumMatch = content.match(/lot\s*#?\s*(\d+)/i);
const lotNum = lotNumMatch ? lotNumMatch[1] : null;
// Look for Omega references
const refMatch = content.match(/\b(\d{3}\.\d{2}\.\d{2}\.\d{2}\.\d{2}\.\d{3})\b/);
if (!refMatch) continue; // Skip non-Omega lots
const result = {
source_listing_id: `antiq-lot-${lotNum || crypto.randomBytes(4).toString('hex')}`,
reference_number_raw: refMatch[1],
event_type: 'auction_realized',
source_name: 'antiquorum',
};
// Extract hammer price
const hammerMatch = content.match(/(?:hammer|sold|realized)[^:]*:\s*(?:CHF|USD|EUR|GBP)?\s*([\d,]+)/i);
if (hammerMatch) {
result.hammer_price = parseFloat(hammerMatch[1].replace(/,/g, ''));
}
// Extract currency
const currMatch = content.match(/\b(CHF|USD|EUR|GBP)\b/);
result.currency = currMatch ? currMatch[1] : 'CHF'; // Antiquorum defaults to CHF
// Extract estimate range
const estMatch = content.match(/estimate[^:]*:\s*([\d,]+)\s*[-–]\s*([\d,]+)/i);
if (estMatch) {
result.estimate_low = parseFloat(estMatch[1].replace(/,/g, ''));
result.estimate_high = parseFloat(estMatch[2].replace(/,/g, ''));
}
// Calculate buyer's premium (Antiquorum: typically 25% buyer's premium)
if (result.hammer_price) {
result.buyers_premium = Math.round(result.hammer_price * 0.25);
result.total_to_buyer = result.hammer_price + result.buyers_premium;
result.price_amount = result.total_to_buyer;
result.fee_semantics = 'hammer_plus_premium';
}
// Extract condition
const condMatch = content.match(/condition[^:]*:\s*([^<\n]+)/i);
if (condMatch) {
result.condition_raw = condMatch[1].trim();
result.condition_normalized = this.normalizeCondition(condMatch[1]);
}
// Extract date
const dateMatch = content.match(/(\d{1,2})\s+(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\s+(\d{4})/i);
if (dateMatch) {
const months = { jan: '01', feb: '02', mar: '03', apr: '04', may: '05', jun: '06', jul: '07', aug: '08', sep: '09', oct: '10', nov: '11', dec: '12' };
result.sale_date = `${dateMatch[3]}-${months[dateMatch[2].toLowerCase()]}-${dateMatch[1].padStart(2, '0')}`;
}
// Extract serial/movement numbers
const serialMatch = content.match(/(?:serial|no\.|number)\s*[:#]?\s*(\d{4,})/i);
if (serialMatch) result.serial_number = serialMatch[1];
result.seller_type = 'auction_house';
if (result.price_amount > 0) {
results.push(result);
}
}
return results;
}
normalizeCondition(raw) {
const lower = (raw || '').toLowerCase();
if (lower.includes('mint') || lower.includes('excellent') || lower.includes('pristine')) return 'mint';
if (lower.includes('very good') || lower.includes('fine')) return 'very_good';
if (lower.includes('good')) return 'good';
if (lower.includes('fair')) return 'fair';
return 'unknown';
}
async collect() {
// Load references to know which lots to look for
const { rows: refs } = await pool.query(
'SELECT reference_number FROM watch_reference'
);
const refSet = new Set(refs.map(r => r.reference_number));
console.log(`[antiquorum] Tracking ${refSet.size} reference numbers`);
// Fetch recent auction results
// Antiquorum organizes by auction sales: /en/results/
const resultsUrl = `${ANTIQUORUM_BASE}/en/results/?brand=omega&sort=date_desc`;
try {
await this.rateLimit();
const response = await this.fetch(resultsUrl);
this.stats.fetched++;
if (response.statusCode !== 200) {
console.log(`[antiquorum] Results page returned HTTP ${response.statusCode}`);
return;
}
const pageHash = crypto.createHash('sha256').update(response.body).digest('hex');
await this.archiveSnapshot(resultsUrl, response.body);
const lots = this.parseLotResults(response.body);
this.stats.parsed = lots.length;
console.log(`[antiquorum] Found ${lots.length} Omega lots`);
for (const lot of lots) {
lot.source_url = resultsUrl;
lot.page_hash = pageHash;
const result = await this.insertMarketEvent(lot);
if (result.action === 'inserted') {
console.log(` Lot ${lot.source_listing_id}: ${lot.reference_number_raw} — ${lot.currency} ${lot.total_to_buyer?.toLocaleString()}`);
}
}
console.log(`[antiquorum] Complete: ${this.stats.inserted} new, ${this.stats.deduplicated} dupes, ${this.stats.quarantined} quarantined`);
} catch (err) {
console.error(`[antiquorum] Collection error:`, err.message);
throw err;
}
}
validate(event) {
const base = super.validate(event);
if (base) return base;
// Auction-specific: extremely high prices need manual review
if (event.total_to_buyer > 1000000) return 'auction_price_exceptional';
// Lots with no condition data flagged
if (!event.condition_raw) return 'missing_condition';
return null;
}
}