Fix mock tests

Former-commit-id: ef804b3896
This commit is contained in:
Tour
2025-12-07 06:28:37 +01:00
parent 4764f072b5
commit 03f94de020
18 changed files with 3055 additions and 56 deletions

View File

@@ -267,6 +267,63 @@
</div>
</div>
</div>
<!-- Intelligence Dashboard - NEW -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<!-- Sleeper Lots -->
<div class="bg-gradient-to-br from-purple-50 to-purple-100 rounded-xl shadow-md p-6 card-hover">
<div class="flex justify-between items-start mb-4">
<div>
<h3 class="text-lg font-semibold text-purple-900 flex items-center">
<i class="fas fa-eye mr-2"></i>Sleeper Lots
</h3>
<p class="text-xs text-purple-700 mt-1">High interest, low bids</p>
</div>
<div class="text-3xl font-bold text-purple-600" id="sleeper-count">
<i class="fas fa-spinner fa-spin"></i>
</div>
</div>
<button onclick="showSleeperLots()" class="w-full bg-purple-600 text-white py-2 rounded-lg hover:bg-purple-700 transition">
<i class="fas fa-search mr-2"></i>View Opportunities
</button>
</div>
<!-- Bargain Lots -->
<div class="bg-gradient-to-br from-green-50 to-green-100 rounded-xl shadow-md p-6 card-hover">
<div class="flex justify-between items-start mb-4">
<div>
<h3 class="text-lg font-semibold text-green-900 flex items-center">
<i class="fas fa-tag mr-2"></i>Bargains
</h3>
<p class="text-xs text-green-700 mt-1">Below estimate</p>
</div>
<div class="text-3xl font-bold text-green-600" id="bargain-count">
<i class="fas fa-spinner fa-spin"></i>
</div>
</div>
<button onclick="showBargainLots()" class="w-full bg-green-600 text-white py-2 rounded-lg hover:bg-green-700 transition">
<i class="fas fa-search mr-2"></i>Find Deals
</button>
</div>
<!-- Popular Lots -->
<div class="bg-gradient-to-br from-orange-50 to-orange-100 rounded-xl shadow-md p-6 card-hover">
<div class="flex justify-between items-start mb-4">
<div>
<h3 class="text-lg font-semibold text-orange-900 flex items-center">
<i class="fas fa-fire mr-2"></i>Hot Lots
</h3>
<p class="text-xs text-orange-700 mt-1">High competition</p>
</div>
<div class="text-3xl font-bold text-orange-600" id="popular-count">
<i class="fas fa-spinner fa-spin"></i>
</div>
</div>
<button onclick="showPopularLots()" class="w-full bg-orange-600 text-white py-2 rounded-lg hover:bg-orange-700 transition">
<i class="fas fa-search mr-2"></i>View Trending
</button>
</div>
</div>
<!-- Statistics & Performance -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
@@ -548,11 +605,17 @@
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" onclick="sortTable('title')">
Title <i class="fas fa-sort ml-1"></i>
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" onclick="sortTable('followersCount')">
Watchers <i class="fas fa-sort ml-1"></i>
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" onclick="sortTable('currentBid')">
Current Bid <i class="fas fa-sort ml-1"></i>
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Closing Time
Est. Range
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Total Cost
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" onclick="sortTable('minutesUntilClose')">
Time Left <i class="fas fa-sort ml-1"></i>
@@ -641,7 +704,10 @@ let dashboardState = {
closingSoon: [],
countryDistribution: {},
categoryDistribution: {},
trendData: {}
trendData: {},
sleepers: [],
bargains: [],
popular: []
},
filters: {
auction: '',
@@ -748,7 +814,8 @@ async function fetchAllData() {
fetchStatistics(),
fetchRateLimitStats(),
fetchClosingSoon(),
fetchChartData()
fetchChartData(),
fetchIntelligenceData()
]);
updateLastUpdate();
updateDataAge();
@@ -766,6 +833,38 @@ async function fetchAllData() {
}
}
// Fetch intelligence data
async function fetchIntelligenceData() {
try {
// Fetch sleepers
const sleepersRes = await fetch('/api/monitor/intelligence/sleepers');
if (sleepersRes.ok) {
const sleepersData = await sleepersRes.json();
document.getElementById('sleeper-count').textContent = sleepersData.count || 0;
dashboardState.data.sleepers = sleepersData.lots || [];
}
// Fetch bargains
const bargainsRes = await fetch('/api/monitor/intelligence/bargains');
if (bargainsRes.ok) {
const bargainsData = await bargainsRes.json();
document.getElementById('bargain-count').textContent = bargainsData.count || 0;
dashboardState.data.bargains = bargainsData.lots || [];
}
// Fetch popular lots
const popularRes = await fetch('/api/monitor/intelligence/popular?level=HIGH');
if (popularRes.ok) {
const popularData = await popularRes.json();
document.getElementById('popular-count').textContent = popularData.count || 0;
dashboardState.data.popular = popularData.lots || [];
}
} catch (error) {
console.error('Intelligence data fetch error:', error);
}
}
// Fetch system status with trends
async function fetchStatus() {
try {
@@ -1179,12 +1278,41 @@ function updateClosingSoonTable(data = null) {
const urgencyIcon = minutesLeft < 10 ? 'fa-exclamation-circle' :
minutesLeft < 20 ? 'fa-exclamation-triangle' : 'fa-clock';
// Calculate followers badge
const followers = lot.followersCount || 0;
const followersBadge = followers > 50 ? 'bg-red-100 text-red-800' :
followers > 20 ? 'bg-orange-100 text-orange-800' :
followers > 5 ? 'bg-blue-100 text-blue-800' :
'bg-gray-100 text-gray-600';
// Calculate estimate range
const estMin = lot.estimatedMin ? (lot.estimatedMin / 100).toFixed(0) : null;
const estMax = lot.estimatedMax ? (lot.estimatedMax / 100).toFixed(0) : null;
const estimateDisplay = estMin && estMax ? `${estMin}-${estMax}` : '--';
// Calculate total cost (including VAT and premium)
const currentBid = lot.currentBid || 0;
const vat = lot.vat || 0;
const premium = lot.buyerPremiumPercentage || 0;
const totalCost = currentBid * (1 + (vat/100) + (premium/100));
const totalCostDisplay = totalCost > 0 ? `${totalCost.toFixed(2)}` : '--';
// Bargain indicator
const isBargain = estMin && currentBid < parseFloat(estMin);
const bargainBadge = isBargain ? '<span class="ml-1 text-xs bg-green-500 text-white px-1 rounded">DEAL</span>' : '';
return `
<tr class="hover:bg-gray-50 transition-colors">
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">${lot.lotId || '--'}</td>
<td class="px-6 py-4 text-sm text-gray-900 max-w-xs truncate" title="${lot.title || 'N/A'}">${lot.title || 'N/A'}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-bold text-green-600">${lot.currency || 'EUR'} ${lot.currentBid ? parseFloat(lot.currentBid).toFixed(2) : '0.00'}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${lot.closingTime ? new Date(lot.closingTime).toLocaleString() : 'N/A'}</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${followersBadge}">
<i class="fas fa-eye mr-1"></i>${followers}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-bold text-green-600">${lot.currency || 'EUR'} ${lot.currentBid ? parseFloat(lot.currentBid).toFixed(2) : '0.00'}${bargainBadge}</td>
<td class="px-6 py-4 whitespace-nowrap text-xs text-gray-500">${estimateDisplay}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-semibold text-purple-600" title="Including VAT (${vat}%) + Premium (${premium}%)">${totalCostDisplay}</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${badgeColor}">
<i class="fas ${urgencyIcon} mr-1"></i>${minutesLeft} min
@@ -1394,6 +1522,46 @@ window.addEventListener('offline', () => {
showToast('Connection lost - working offline', 'warning');
addLog('Network connection lost', 'warning');
});
// Intelligence widget handlers
function showSleeperLots() {
if (!dashboardState.data.sleepers || dashboardState.data.sleepers.length === 0) {
showToast('No sleeper lots found', 'info');
return;
}
dashboardState.data.closingSoon = dashboardState.data.sleepers;
applyFilters();
showToast(`Showing ${dashboardState.data.sleepers.length} sleeper lots`, 'success');
addLog(`Filtered to sleeper lots (high interest, low bids)`);
// Scroll to table
document.getElementById('closing-soon-table').scrollIntoView({ behavior: 'smooth' });
}
function showBargainLots() {
if (!dashboardState.data.bargains || dashboardState.data.bargains.length === 0) {
showToast('No bargain lots found', 'info');
return;
}
dashboardState.data.closingSoon = dashboardState.data.bargains;
applyFilters();
showToast(`Showing ${dashboardState.data.bargains.length} bargain lots`, 'success');
addLog(`Filtered to bargain lots (below estimate)`);
// Scroll to table
document.getElementById('closing-soon-table').scrollIntoView({ behavior: 'smooth' });
}
function showPopularLots() {
if (!dashboardState.data.popular || dashboardState.data.popular.length === 0) {
showToast('No popular lots found', 'info');
return;
}
dashboardState.data.closingSoon = dashboardState.data.popular;
applyFilters();
showToast(`Showing ${dashboardState.data.popular.length} popular lots`, 'success');
addLog(`Filtered to popular lots (high followers)`);
// Scroll to table
document.getElementById('closing-soon-table').scrollIntoView({ behavior: 'smooth' });
}
</script>
</body>
</html>

View File

@@ -0,0 +1,990 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Auctiora - Valuation Analytics</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/plotly.js/3.0.3/plotly.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
:root {
--primary: #1e3a8a;
--secondary: #3b82f6;
--accent: #60a5fa;
--success: #10b981;
--warning: #f59e0b;
--danger: #ef4444;
}
.gradient-bg {
background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 50%, var(--accent) 100%);
}
.formula-card {
background: linear-gradient(145deg, #ffffff 0%, #f8fafc 100%);
border-left: 4px solid var(--secondary);
transition: all 0.3s ease;
}
.formula-card:hover {
transform: translateY(-3px);
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1);
}
.variable-badge {
background: linear-gradient(90deg, #dbeafe, #eff6ff);
padding: 2px 8px;
border-radius: 12px;
font-size: 0.75rem;
font-family: 'Courier New', monospace;
color: #1e40af;
border: 1px solid #bfdbfe;
}
.result-highlight {
background: linear-gradient(90deg, #d1fae5, #ecfdf5);
border: 2px solid var(--success);
border-radius: 12px;
padding: 20px;
font-family: 'Courier New', monospace;
}
.insight-alert {
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
.tooltip {
position: relative;
cursor: help;
border-bottom: 1px dotted #3b82f6;
}
.tooltip:hover::after {
content: attr(data-tooltip);
position: absolute;
bottom: 125%;
left: 50%;
transform: translateX(-50%);
background: #374151;
color: white;
padding: 8px 12px;
border-radius: 6px;
font-size: 0.75rem;
white-space: nowrap;
z-index: 1000;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.math-display {
font-family: 'Times New Roman', serif;
font-size: 1.1rem;
line-height: 1.8;
background: #f9fafb;
border-radius: 8px;
padding: 16px;
margin: 12px 0;
border-left: 4px solid var(--secondary);
}
.sensitivity-slider {
-webkit-appearance: none;
appearance: none;
height: 6px;
border-radius: 3px;
background: #e5e7eb;
outline: none;
}
.sensitivity-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--secondary);
cursor: pointer;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
</style>
</head>
<body class="bg-gray-50 min-h-screen">
<!-- Header -->
<header class="gradient-bg text-white py-6 shadow-lg">
<div class="container mx-auto px-4">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-4">
<a href="/dashboard" class="text-white hover:text-blue-200 transition">
<i class="fas fa-arrow-left mr-2"></i>Back to Dashboard
</a>
<div>
<h1 class="text-3xl font-bold mb-1">
<i class="fas fa-calculator mr-3"></i>
Valuation Analytics Engine
</h1>
<p class="text-lg opacity-90">Mathematical Framework for Auction Intelligence</p>
</div>
</div>
<div class="text-right">
<button onclick="exportAnalysis()"
class="bg-white/20 hover:bg-white/30 px-4 py-2 rounded-lg transition">
<i class="fas fa-download mr-2"></i>Export Analysis
</button>
</div>
</div>
</div>
</header>
<!-- Main Content -->
<main class="container mx-auto px-4 py-8">
<!-- Input Parameters Section -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-8">
<!-- Lot Parameter Inputs -->
<div class="lg:col-span-2 bg-white rounded-xl shadow-md p-6">
<h2 class="text-2xl font-semibold text-gray-800 mb-6 flex items-center">
<i class="fas fa-edit mr-2 text-blue-600"></i>
Input Parameters
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Basic Parameters -->
<div class="space-y-4">
<h3 class="text-lg font-semibold text-gray-700 border-b pb-2">Current State</h3>
<div class="flex items-center justify-between">
<label class="flex items-center">
<span class="variable-badge mr-2">P_current</span>
<span class="tooltip" data-tooltip="Current bid in EUR">Current Bid (€):</span>
</label>
<input type="number" id="p_current" value="7200"
class="w-32 px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
oninput="recalculate()">
</div>
<div class="flex items-center justify-between">
<label class="flex items-center">
<span class="variable-badge mr-2">C_target</span>
<span class="tooltip" data-tooltip="Condition score 0-10">Condition Score:</span>
</label>
<input type="range" id="c_target" min="0" max="10" step="0.1" value="7.5"
class="sensitivity-slider"
oninput="recalculate()">
<span id="c_target_display" class="ml-3 font-mono">7.5</span>
</div>
<div class="flex items-center justify-between">
<label class="flex items-center">
<span class="variable-badge mr-2">T_target</span>
<span class="tooltip" data-tooltip="Manufacturing year">Year Made:</span>
</label>
<input type="number" id="t_target" value="2015"
class="w-32 px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
oninput="recalculate()">
</div>
<div class="flex items-center justify-between">
<label class="flex items-center">
<span class="variable-badge mr-2">W_watch</span>
<span class="tooltip" data-tooltip="Number of watchers">Watch Count:</span>
</label>
<input type="number" id="w_watch" value="87"
class="w-32 px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
oninput="recalculate()">
</div>
</div>
<!-- Market Context -->
<div class="space-y-4">
<h3 class="text-lg font-semibold text-gray-700 border-b pb-2">Market Context</h3>
<div class="flex items-center justify-between">
<label class="flex items-center">
<span class="variable-badge mr-2">σ_market</span>
<span class="tooltip" data-tooltip="Market volatility 0-1">Market Volatility:</span>
</label>
<input type="range" id="sigma_market" min="0" max="1" step="0.01" value="0.15"
class="sensitivity-slider"
oninput="recalculate()">
<span id="sigma_display" class="ml-3 font-mono">0.15</span>
</div>
<div class="flex items-center justify-between">
<label class="flex items-center">
<span class="variable-badge mr-2">Λ_b</span>
<span class="tooltip" data-tooltip="Current bid velocity (bids/min)">Bid Velocity:</span>
</label>
<input type="number" id="lambda_b" value="2.3" step="0.1"
class="w-32 px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
oninput="recalculate()">
</div>
<div class="flex items-center justify-between">
<label class="flex items-center">
<span class="variable-badge mr-2">t_close</span>
<span class="tooltip" data-tooltip="Minutes until lot closes">Time to Close (min):</span>
</label>
<input type="number" id="t_close" value="45"
class="w-32 px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
oninput="recalculate()">
</div>
<div class="flex items-center justify-between">
<label class="flex items-center">
<span class="variable-badge mr-2">N_docs</span>
<span class="tooltip" data-tooltip="Number of provenance documents">Provenance Docs:</span>
</label>
<input type="number" id="n_docs" value="2" min="0"
class="w-32 px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
oninput="recalculate()">
</div>
</div>
</div>
<!-- Estimate Range Inputs -->
<div class="mt-6 pt-6 border-t">
<h3 class="text-lg font-semibold text-gray-700 mb-3">Auction Estimates</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm text-gray-600 mb-1">Estimated Min (€)</label>
<input type="number" id="est_min" value="6000"
class="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
oninput="recalculate()">
</div>
<div>
<label class="block text-sm text-gray-600 mb-1">Estimated Max (€)</label>
<input type="number" id="est_max" value="9000"
class="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
oninput="recalculate()">
</div>
</div>
</div>
</div>
<!-- Real-time Results -->
<div class="space-y-6">
<!-- FMV Result -->
<div class="result-highlight">
<div class="flex items-center justify-between mb-2">
<span class="text-sm font-semibold text-gray-700">Fair Market Value (FMV)</span>
<i class="fas fa-chart-line text-green-600"></i>
</div>
<div class="text-3xl font-bold text-green-600" id="fmv_result">€8,500.00</div>
<div class="text-xs text-green-700 mt-1" id="fmv_confidence">87% confidence</div>
</div>
<!-- Undervaluation Score -->
<div class="result-highlight">
<div class="flex items-center justify-between mb-2">
<span class="text-sm font-semibold text-gray-700">Undervaluation Score</span>
<i class="fas fa-gem text-purple-600"></i>
</div>
<div class="text-3xl font-bold" id="u_score_result">0.153</div>
<div class="text-xs mt-1" id="u_interpretation">
<span class="text-green-600">15.3% below fair value</span>
</div>
</div>
<!-- Predicted Final -->
<div class="result-highlight">
<div class="flex items-center justify-between mb-2">
<span class="text-sm font-semibold text-gray-700">Predicted Final Price</span>
<i class="fas fa-crystal-ball text-blue-600"></i>
</div>
<div class="text-3xl font-bold text-blue-600" id="p_final_result">€8,900.00</div>
<div class="text-xs text-blue-700 mt-1" id="prediction_range">
95% CI: €8,200 - €9,600
</div>
</div>
</div>
</div>
<!-- Formula Explanations -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
<!-- FMV Formula -->
<div class="formula-card rounded-xl shadow-md p-6">
<h3 class="text-xl font-semibold text-gray-800 mb-4 flex items-center">
<i class="fas fa-function mr-2 text-blue-600"></i>
1. Fair Market Value (FMV)
</h3>
<div class="math-display">
FMV = <strong>Σ(P<sub>i</sub> · ω<sub>c</sub> · ω<sub>t</sub> · ω<sub>p</sub> · ω<sub>h</sub>) / Σ(ω<sub>c</sub> · ω<sub>t</sub> · ω<sub>p</sub> · ω<sub>h</sub>)</strong>
</div>
<div class="text-sm text-gray-600 space-y-2 mt-4">
<p><strong>Explanation:</strong> Weighted average of comparable sales where each comparable is weighted by:</p>
<ul class="list-disc list-inside ml-4 space-y-1">
<li><span class="variable-badge">ω_c</span> Condition similarity (exponential decay)</li>
<li><span class="variable-badge">ω_t</span> Age proximity (exponential decay)</li>
<li><span class="variable-badge">ω_p</span> Provenance premium (linear boost)</li>
<li><span class="variable-badge">ω_h</span> Historical relevance (logistic function)</li>
</ul>
</div>
</div>
<!-- Undervaluation Score -->
<div class="formula-card rounded-xl shadow-md p-6">
<h3 class="text-xl font-semibold text-gray-800 mb-4 flex items-center">
<i class="fas fa-search-dollar mr-2 text-purple-600"></i>
2. Undervaluation Detection
</h3>
<div class="math-display">
U<sub>score</sub> = <strong>(FMV - P<sub>current</sub>)/FMV · σ<sub>market</sub> · (1 + B<sub>velocity</sub>/10) · ln(1 + W<sub>watch</sub>/W<sub>bid</sub>)</strong>
</div>
<div class="text-sm text-gray-600 space-y-2 mt-4">
<p><strong>Explanation:</strong> Quantifies mispricing opportunity factoring:</p>
<ul class="list-disc list-inside ml-4 space-y-1">
<li>Price gap percentage</li>
<li>Market volatility multiplier</li>
<li>Bid velocity acceleration</li>
<li>Watch-to-bid ratio (buyer intent)</li>
</ul>
<p class="mt-2 p-2 bg-yellow-50 rounded"><strong>Alert threshold:</strong> U<sub>score</sub> > 0.25</p>
</div>
</div>
<!-- Predicted Final Price -->
<div class="formula-card rounded-xl shadow-md p-6">
<h3 class="text-xl font-semibold text-gray-800 mb-4 flex items-center">
<i class="fas fa-chart-line mr-2 text-green-600"></i>
3. Final Price Prediction
</h3>
<div class="math-display">
<sub>final</sub> = <strong>FMV · (1 + ε<sub>bid</sub> + ε<sub>time</sub> + ε<sub>comp</sub>)</strong>
</div>
<div class="text-sm text-gray-600 space-y-2 mt-4">
<p><strong>Explanation:</strong> Adjusts FMV for auction dynamics:</p>
<ul class="list-disc list-inside ml-4 space-y-1">
<li><span class="variable-badge">ε_bid</span> Bid momentum (tanh function)</li>
<li><span class="variable-badge">ε_time</span> Time pressure (exponential decay)</li>
<li><span class="variable-badge">ε_comp</span> Competition level (logarithmic)</li>
</ul>
</div>
</div>
<!-- Condition Multiplier -->
<div class="formula-card rounded-xl shadow-md p-6">
<h3 class="text-xl font-semibold text-gray-800 mb-4 flex items-center">
<i class="fas fa-heartbeat mr-2 text-red-600"></i>
4. Condition Multiplier
</h3>
<div class="math-display">
M<sub>cond</sub> = <strong>exp(α<sub>c</sub> · √C<sub>target</sub> - β<sub>c</sub>)</strong>
</div>
<div class="text-sm text-gray-600 space-y-2 mt-4">
<p><strong>Explanation:</strong> Normalizes prices across condition states using square-root scaling:</p>
<ul class="list-disc list-inside ml-4 space-y-1">
<li>C = 10 (mint): <strong>1.48x</strong> premium</li>
<li>C = 7.5 (good): <strong>1.12x</strong> premium</li>
<li>C = 5 (avg): <strong>0.91x</strong> discount</li>
</ul>
</div>
</div>
</div>
<!-- Strategy Recommendations -->
<div class="bg-white rounded-xl shadow-md p-6 mb-8">
<h2 class="text-2xl font-semibold text-gray-800 mb-6 flex items-center">
<i class="fas fa-chess mr-2 text-blue-600"></i>
Bidding Strategy Recommendations
</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="text-center p-6 bg-gradient-to-br from-green-50 to-green-100 rounded-xl">
<div class="text-3xl font-bold text-green-600 mb-2" id="strategy_max">€9,200</div>
<div class="text-sm text-green-700">Recommended Max Bid</div>
<div class="text-xs text-green-600 mt-2" id="max_strategy_type">Standard strategy</div>
</div>
<div class="text-center p-6 bg-gradient-to-br from-blue-50 to-blue-100 rounded-xl">
<div class="text-3xl font-bold text-blue-600 mb-2" id="optimal_timing">10 min</div>
<div class="text-sm text-blue-700">Optimal Bid Timing</div>
<div class="text-xs text-blue-600 mt-2">Before close</div>
</div>
<div class="text-center p-6 bg-gradient-to-br from-purple-50 to-purple-100 rounded-xl">
<div class="text-3xl font-bold text-purple-600 mb-2" id="competition_level">Medium</div>
<div class="text-sm text-purple-700">Competition Level</div>
<div class="text-xs text-purple-600 mt-2" id="competition_details">2.3 bids/min</div>
</div>
</div>
<!-- Strategy Details -->
<div class="mt-6 p-4 bg-gray-50 rounded-lg">
<div id="strategy_details" class="space-y-3">
<div class="flex items-start space-x-3">
<i class="fas fa-info-circle text-blue-500 mt-1"></i>
<div>
<strong>Analysis:</strong> <span id="strategy_analysis">Bid velocity is moderate (2.3 bids/min) with high watch count indicating potential sniping.</span>
</div>
</div>
<div class="flex items-start space-x-3">
<i class="fas fa-lightbulb text-yellow-500 mt-1"></i>
<div>
<strong>Recommendation:</strong> <span id="strategy_recommendation">Wait until final 10 minutes. Set max bid at €9,200 (7% above FMV) to secure lot while avoiding bidding war.</span>
</div>
</div>
<div class="flex items-start space-x-3">
<i class="fas fa-exclamation-triangle text-red-500 mt-1"></i>
<div>
<strong>Risk Factors:</strong> <span id="strategy_risks">High watch-to-bid ratio (87:8) suggests aggressive sniping likely. Reserve may be close to current bid.</span>
</div>
</div>
</div>
</div>
</div>
<!-- Sensitivity Analysis -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
<!-- Condition Sensitivity -->
<div class="bg-white rounded-xl shadow-md p-6">
<h3 class="text-xl font-semibold text-gray-800 mb-4 flex items-center">
<i class="fas fa-sliders-h mr-2 text-blue-600"></i>
Condition Sensitivity
</h3>
<div id="condition-chart" style="height: 300px;"></div>
<div class="text-sm text-gray-600 mt-3">
<p>How FMV changes with condition score. Current: <strong id="current_condition_point">7.5</strong></p>
</div>
</div>
<!-- Time Sensitivity -->
<div class="bg-white rounded-xl shadow-md p-6">
<h3 class="text-xl font-semibold text-gray-800 mb-4 flex items-center">
<i class="fas fa-clock mr-2 text-green-600"></i>
Time Pressure Impact
</h3>
<div id="time-chart" style="height: 300px;"></div>
<div class="text-sm text-gray-600 mt-3">
<p>Final price prediction vs. time to close. Current: <strong id="current_time_point">45 min</strong></p>
</div>
</div>
</div>
<!-- Activity Log -->
<div class="bg-white rounded-xl shadow-md p-6">
<h3 class="text-xl font-semibold text-gray-800 mb-4 flex items-center">
<i class="fas fa-history mr-2 text-purple-600"></i>
Calculation Log
</h3>
<div id="calculation-log" class="space-y-2 max-h-64 overflow-y-auto border rounded-lg p-3 bg-gray-50">
<div class="text-sm text-gray-500 flex items-center">
<i class="fas fa-info-circle mr-2 text-blue-500"></i>
<span>System initialized. Ready for calculations...</span>
</div>
</div>
</div>
</main>
<!-- Footer -->
<footer class="bg-gray-800 text-white py-6 mt-12">
<div class="container mx-auto px-4 text-center">
<p class="text-gray-300">
<i class="fas fa-calculator mr-2"></i>
Auctiora Valuation Engine v2.1 | Mathematical Framework
</p>
<p class="text-sm text-gray-400 mt-2">
Multi-factor weighted valuation with exponential decay models
</p>
</div>
</footer>
<script>
// Mathematical constants (retained for sensitivity charts and fallback)
const CONSTANTS = {
lambda_c: 0.693, // Condition decay constant
lambda_t: 0.048, // Time decay constant
delta_p: 0.15, // Provenance premium coefficient
kh: 0.01, // Historical relevance coefficient
alpha_c: 0.15, // Condition sensitivity
beta_c: 0.40, // Condition baseline offset
gamma: 0.25, // Depreciation aggressivity
b_threshold: 10, // High velocity threshold
phi_1: 0.15, // Bid momentum coefficient 1
phi_2: 0.10, // Bid momentum coefficient 2
psi: 0.20, // Time pressure coefficient
rho: 0.08, // Competition coefficient
theta_agg: 0.10, // Aggressive discount target
theta_cons: 0.05, // Conservative overbid tolerance
delta_margin: 50 // Minimum margin €
};
// Dashboard state
let dashboardState = {
data: {},
filters: {},
autoRefresh: true,
refreshInterval: 15000,
isCalculating: false,
apiAvailable: false
};
// Initialize page
document.addEventListener('DOMContentLoaded', function() {
addLog('Initializing valuation engine...');
checkAPIHealth();
setupEventListeners();
});
// Check API availability on load
async function checkAPIHealth() {
try {
const response = await fetch('/api/analytics/valuation/health', {
method: 'GET',
headers: { 'Accept': 'application/json' }
});
if (response.ok) {
dashboardState.apiAvailable = true;
addLog('API connection established', 'success');
showToast('Connected to valuation engine', 'success');
} else {
dashboardState.apiAvailable = false;
addLog('API health check failed - running in offline mode', 'warning');
showToast('Running in offline mode', 'warning');
}
} catch (error) {
dashboardState.apiAvailable = false;
addLog('API unavailable - using fallback calculations', 'warning');
showToast('Offline mode: using fallback calculations', 'warning');
} finally {
recalculate();
}
}
// Setup event listeners
function setupEventListeners() {
// Input change listeners
document.getElementById('c_target').addEventListener('input', function() {
document.getElementById('c_target_display').textContent = this.value;
recalculate();
});
document.getElementById('sigma_market').addEventListener('input', function() {
document.getElementById('sigma_display').textContent = this.value;
recalculate();
});
// Add listeners to all input fields
const inputs = document.querySelectorAll('input[type="number"]');
inputs.forEach(input => {
input.addEventListener('input', debounce(recalculate, 300));
});
}
// Debounce function to prevent excessive API calls
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Main recalculation function - calls backend API
async function recalculate() {
if (dashboardState.isCalculating) return;
const params = getInputParams();
showLoadingState(true);
if (dashboardState.apiAvailable) {
try {
const response = await fetch('/api/analytics/valuation', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(params)
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const valuationData = await response.json();
dashboardState.data.valuation = valuationData;
updateAllDisplays(valuationData);
addLog(`API calculation completed in ${valuationData.calculationTimeMs}ms`, 'success');
showToast('Valuation updated from server', 'success');
} catch (error) {
console.error('Valuation API error:', error);
addLog(`API error: ${error.message} - falling back to mock calculation`, 'error');
showToast(`API failed: ${error.message}`, 'error');
await calculateMockFallback(params);
}
} else {
// API not available, use fallback directly
await calculateMockFallback(params);
}
showLoadingState(false);
}
// Fallback mock calculation (original logic)
async function calculateMockFallback(params) {
addLog('Using fallback calculation...', 'info');
const comparables = [
{ price: 8200, condition: 8, year: 2016, provenance: 1, days_ago: 30 },
{ price: 7800, condition: 7, year: 2014, provenance: 0, days_ago: 45 },
{ price: 8500, condition: 9, year: 2017, provenance: 1, days_ago: 60 },
{ price: 7500, condition: 6, year: 2013, provenance: 0, days_ago: 25 }
];
let weightedSum = 0;
let weightSum = 0;
comparables.forEach(comp => {
const wc = Math.exp(-CONSTANTS.lambda_c * Math.abs(params.c_target - comp.condition));
const wt = Math.exp(-CONSTANTS.lambda_t * Math.abs(params.t_target - comp.year));
const wp = 1 + CONSTANTS.delta_p * (params.n_docs > 0 ? 1 : 0 - comp.provenance);
const wh = 1 / (1 + Math.exp(-CONSTANTS.kh * (comp.days_ago - 45)));
const weight = wc * wt * wp * wh;
weightedSum += comp.price * weight;
weightSum += weight;
});
let fmv = weightSum > 0 ? weightedSum / weightSum : (params.est_min + params.est_max) / 2;
const mCond = Math.exp(CONSTANTS.alpha_c * Math.sqrt(params.c_target) - CONSTANTS.beta_c);
fmv *= mCond;
if (params.n_docs > 0) {
const provPremium = CONSTANTS.delta_p + 0.05 * Math.log(1 + params.n_docs);
fmv *= (1 + provPremium);
}
// Create mock response structure matching API format
const mockResponse = {
fairMarketValue: {
value: Math.round(fmv * 100) / 100,
conditionMultiplier: Math.round(mCond * 1000) / 1000,
provenancePremium: params.n_docs > 0 ? CONSTANTS.delta_p + 0.05 * Math.log(1 + params.n_docs) : 0,
comparablesUsed: comparables.length,
confidence: 0.85,
weightedComparables: comparables.map(comp => ({
comparableLotId: `MOCK-${comp.price}`,
finalPrice: comp.price,
totalWeight: 0.25,
components: {
conditionWeight: Math.round(Math.exp(-CONSTANTS.lambda_c * Math.abs(params.c_target - comp.condition)) * 1000) / 1000,
timeWeight: Math.round(Math.exp(-CONSTANTS.lambda_t * Math.abs(params.t_target - comp.year)) * 1000) / 1000,
provenanceWeight: Math.round((1 + CONSTANTS.delta_p * (params.n_docs > 0 ? 1 : 0 - comp.provenance)) * 1000) / 1000,
historicalWeight: Math.round((1 / (1 + Math.exp(-CONSTANTS.kh * (comp.days_ago - 45)))) * 1000) / 1000
}
}))
},
undervaluationScore: calculateUndervaluationScore(params, fmv),
pricePrediction: calculateFinalPrice(params, fmv),
biddingStrategy: generateBiddingStrategy(params, {value: fmv}, calculateFinalPrice(params, fmv)),
calculationTimeMs: 150
};
updateAllDisplays(mockResponse);
showToast('Using fallback calculations', 'warning');
}
// Update all displays with unified response format
function updateAllDisplays(data) {
updateFMVDisplay(data.fairMarketValue);
updateUndervaluationDisplay(data.undervaluationScore);
updateFinalPriceDisplay(data.pricePrediction);
updateStrategyDisplay(data.biddingStrategy);
}
// Show/hide loading state
function showLoadingState(isLoading) {
dashboardState.isCalculating = isLoading;
const inputs = document.querySelectorAll('input[type="number"], input[type="range"], select, button:not(#auto-refresh-btn)');
inputs.forEach(el => el.disabled = isLoading);
// Show loading indicators
const loadingHTML = '<i class="fas fa-spinner fa-spin"></i>';
const elements = ['fmv_result', 'u_score_result', 'p_final_result'];
elements.forEach(id => {
const el = document.getElementById(id);
if (isLoading) {
el.innerHTML = loadingHTML;
el.classList.add('text-blue-600');
} else {
el.classList.remove('text-blue-600');
}
});
}
// Get input parameters
function getInputParams() {
return {
lotId: document.getElementById('lot-id')?.value || 'DEMO-LOT-' + Date.now(),
currentBid: parseFloat(document.getElementById('p_current').value) || 0,
conditionScore: parseFloat(document.getElementById('c_target').value) || 0,
manufacturingYear: parseInt(document.getElementById('t_target').value) || 0,
watchCount: parseInt(document.getElementById('w_watch').value) || 0,
bidCount: parseInt(document.getElementById('w_bid')?.value) || 8,
marketVolatility: parseFloat(document.getElementById('sigma_market').value) || 0.15,
bidVelocity: parseFloat(document.getElementById('lambda_b').value) || 0,
minutesUntilClose: parseInt(document.getElementById('t_close').value) || 0,
provenanceDocs: parseInt(document.getElementById('n_docs').value) || 0,
estimatedMin: parseFloat(document.getElementById('est_min').value) || 0,
estimatedMax: parseFloat(document.getElementById('est_max').value) || 0
};
}
// Update displays (adapted for API response format)
function updateFMVDisplay(fmvData) {
document.getElementById('fmv_result').textContent = `${fmvData.value.toFixed(2)}`;
document.getElementById('fmv_confidence').textContent = `${Math.round(fmvData.confidence * 100)}% confidence`;
if (fmvData.comparablesUsed) {
addLog(`Used ${fmvData.comparablesUsed} comparables for FMV calculation`);
}
}
function updateUndervaluationDisplay(uScore) {
document.getElementById('u_score_result').textContent = uScore.toFixed(3);
const interp = uScore > 0.25 ? 'text-green-600' :
uScore > 0.15 ? 'text-yellow-600' : 'text-red-600';
const text = uScore > 0.25 ? `${(uScore*100).toFixed(1)}% below fair value - STRONG BUY` :
uScore > 0.15 ? `${(uScore*100).toFixed(1)}% below fair value - CONSIDER` :
'Fairly valued or overpriced';
document.getElementById('u_interpretation').innerHTML = `<span class="${interp}">${text}</span>`;
}
function updateFinalPriceDisplay(predictionData) {
document.getElementById('p_final_result').textContent = `${predictionData.predictedPrice.toFixed(2)}`;
document.getElementById('prediction_range').textContent =
`95% CI: €${predictionData.confidenceIntervalLower.toFixed(0)} - €${predictionData.confidenceIntervalUpper.toFixed(0)}`;
if (predictionData.components) {
const {bidMomentum, timePressure, competition} = predictionData.components;
addLog(`Price prediction components: bid=${bidMomentum}, time=${timePressure}, comp=${competition}`);
}
}
function updateStrategyDisplay(strategyData) {
document.getElementById('strategy_max').textContent = `${strategyData.maxBid.toFixed(0)}`;
document.getElementById('optimal_timing').textContent =
strategyData.recommendedTimingText || strategyData.recommendedTiming?.replace('_', ' ') || 'FINAL 10 MINUTES';
document.getElementById('competition_level').textContent = strategyData.competitionLevel || 'MEDIUM';
document.getElementById('competition_details').textContent =
`${strategyData.type ? strategyData.type.replace('_', ' ') : 'Standard'} strategy`;
if (strategyData.analysis) {
document.getElementById('strategy_analysis').textContent = strategyData.analysis;
}
if (strategyData.riskFactors) {
document.getElementById('strategy_risks').textContent = strategyData.riskFactors.join(', ');
}
}
// Generate sensitivity charts (using mock calculations)
function generateSensitivityCharts() {
// Condition sensitivity chart
const conditionRange = Array.from({length: 21}, (_, i) => i * 0.5);
const conditionValues = conditionRange.map(c => {
const params = getInputParams();
params.c_target = c;
return calculateMockFMV(params); // Use simplified mock for chart
});
Plotly.newPlot('condition-chart', [{
x: conditionRange,
y: conditionValues,
type: 'scatter',
mode: 'lines+markers',
line: { color: '#3b82f6', width: 3 },
marker: { size: 6 },
name: 'FMV vs Condition'
}], {
xaxis: { title: 'Condition Score (C)' },
yaxis: { title: 'FMV (€)' },
margin: { t: 20, b: 40, l: 60, r: 20 },
font: { size: 12 }
}, {responsive: true});
// Time sensitivity chart
const timeRange = Array.from({length: 20}, (_, i) => i * 5);
const timeValues = timeRange.map(t => {
const params = getInputParams();
params.t_close = t;
return calculateMockFinalPrice(params, calculateMockFMV(params));
});
Plotly.newPlot('time-chart', [{
x: timeRange,
y: timeValues,
type: 'scatter',
mode: 'lines+markers',
line: { color: '#10b981', width: 3 },
marker: { size: 6 },
name: 'Predicted Final vs Time'
}], {
xaxis: { title: 'Time to Close (minutes)' },
yaxis: { title: 'Predicted Final Price (€)' },
margin: { t: 20, b: 40, l: 60, r: 20 },
font: { size: 12 }
}, {responsive: true});
}
// Simplified mock FMV for chart generation
function calculateMockFMV(params) {
const comparables = [
{ price: 8200, condition: 8, year: 2016, provenance: 1, days_ago: 30 },
{ price: 7800, condition: 7, year: 2014, provenance: 0, days_ago: 45 }
];
let weightedSum = 0;
let weightSum = 0;
comparables.forEach(comp => {
const wc = Math.exp(-CONSTANTS.lambda_c * Math.abs(params.c_target - comp.condition));
const wt = Math.exp(-CONSTANTS.lambda_t * Math.abs(params.t_target - comp.year));
const weight = wc * wt;
weightedSum += comp.price * weight;
weightSum += weight;
});
let fmv = weightSum > 0 ? weightedSum / weightSum : 8000;
const mCond = Math.exp(CONSTANTS.alpha_c * Math.sqrt(params.c_target) - CONSTANTS.beta_c);
return fmv * mCond;
}
// Simplified mock final price for chart generation
function calculateMockFinalPrice(params, fmv) {
const epsilon_bid = Math.tanh(CONSTANTS.phi_1 * params.lambda_b);
const epsilon_time = CONSTANTS.psi * Math.exp(-params.t_close / 30);
return fmv * (1 + epsilon_bid + epsilon_time);
}
// Activity log
function addLog(message, type = 'info') {
const log = document.getElementById('calculation-log');
const entry = document.createElement('div');
const iconMap = {
success: 'fa-check-circle text-green-600',
error: 'fa-exclamation-circle text-red-600',
warning: 'fa-exclamation-triangle text-yellow-600',
info: 'fa-info-circle text-blue-600'
};
entry.className = 'text-sm text-gray-700 flex items-center space-x-2';
entry.innerHTML = `
<i class="fas ${iconMap[type]} mt-1 text-xs"></i>
<span class="text-gray-400 text-xs">${new Date().toLocaleTimeString()}</span>
<span>${message}</span>
`;
log.insertBefore(entry, log.firstChild);
while (log.children.length > 20) log.removeChild(log.lastChild);
}
// Toast notification system
function showToast(message, type = 'info', duration = 4000) {
const container = document.getElementById('toast-container') || createToastContainer();
const toast = document.createElement('div');
const iconMap = {
success: 'fa-check-circle',
error: 'fa-exclamation-circle',
warning: 'fa-exclamation-triangle',
info: 'fa-info-circle'
};
toast.className = `toast ${type}`;
toast.innerHTML = `
<div class="flex items-center space-x-3">
<i class="fas ${iconMap[type]} text-lg"></i>
<span class="font-medium">${message}</span>
<button onclick="this.parentElement.parentElement.remove()"
class="ml-4 text-white/80 hover:text-white">
<i class="fas fa-times"></i>
</button>
</div>
`;
container.appendChild(toast);
setTimeout(() => toast.classList.add('show'), 100);
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => toast.remove(), 300);
}, duration);
}
function createToastContainer() {
const container = document.createElement('div');
container.id = 'toast-container';
container.className = 'fixed top-4 right-4 z-50 space-y-2';
document.body.appendChild(container);
return container;
}
// Export analysis function (uses API data if available)
function exportAnalysis() {
if (!dashboardState.data.valuation) {
showToast('No valuation data to export', 'warning');
return;
}
const exportData = {
...dashboardState.data.valuation,
exportedAt: new Date().toISOString(),
version: '2.1',
source: dashboardState.apiAvailable ? 'API' : 'FALLBACK'
};
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `valuation-${exportData.lotId || 'analysis'}-${Date.now()}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
addLog('Analysis exported successfully', 'success');
showToast('Analysis exported', 'success');
}
// Style for toasts (add to HTML <style> section)
const toastStyles = document.createElement('style');
toastStyles.textContent = `
.toast {
transform: translateX(400px);
transition: transform 0.3s ease;
max-width: 400px;
padding: 16px 24px;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0,0,0,0.15);
color: white;
}
.toast.show { transform: translateX(0); }
.toast.success { background: #10b981; }
.toast.error { background: #ef4444; }
.toast.warning { background: #f59e0b; }
.toast.info { background: #3b82f6; }
`;
document.head.appendChild(toastStyles);
</script>
</body>
</html>