Fix mock tests

Former-commit-id: 43b5fc03fd
This commit is contained in:
Tour
2025-12-07 11:08:59 +01:00
parent 89969b8234
commit fb31915b39
8 changed files with 322 additions and 94 deletions

View File

@@ -20,7 +20,7 @@ set -e # Exit on error
# Configuration # Configuration
REMOTE_HOST="tour@athena.lan" REMOTE_HOST="tour@athena.lan"
REMOTE_VOLUME="shared-auction-data" REMOTE_VOLUME="shared-auction-data"
LOCAL_DB_PATH="c:/mnt/okcomputer/cache.db" LOCAL_DB_PATH="c:/mnt/okcomputer/output/cache.db"
LOCAL_IMAGES_PATH="c:/mnt/okcomputer/images" LOCAL_IMAGES_PATH="c:/mnt/okcomputer/images"
REMOTE_TMP="/tmp" REMOTE_TMP="/tmp"

View File

@@ -5,6 +5,7 @@ import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import java.time.LocalDateTime;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
@@ -121,6 +122,49 @@ public class AuctionMonitorResource {
} }
} }
/**
* GET /api/monitor/closing-soon
* Returns lots closing within the next specified hours (default: 24 hours)
*/
@GET
@Path("/closing-soon")
public Response getClosingSoon(@QueryParam("hours") @DefaultValue("24") int hours) {
try {
var lots = db.getAllLots();
var closingSoon = lots.stream()
.filter(lot -> lot.closingTime() != null)
.filter(lot -> lot.minutesUntilClose() > 0 && lot.minutesUntilClose() <= hours * 60)
.sorted((a, b) -> Long.compare(a.minutesUntilClose(), b.minutesUntilClose()))
.limit(100)
.toList();
return Response.ok(closingSoon).build();
} catch (Exception e) {
LOG.error("Failed to get closing soon lots", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", e.getMessage()))
.build();
}
}
/**
* GET /api/monitor/lots/{lotId}/bid-history
* Returns bid history for a specific lot
*/
@GET
@Path("/lots/{lotId}/bid-history")
public Response getBidHistory(@PathParam("lotId") String lotId) {
try {
var history = db.getBidHistory(lotId);
return Response.ok(history).build();
} catch (Exception e) {
LOG.error("Failed to get bid history for lot {}", lotId, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", e.getMessage()))
.build();
}
}
/** /**
* POST /api/monitor/trigger/scraper-import * POST /api/monitor/trigger/scraper-import
* Manually trigger scraper import workflow * Manually trigger scraper import workflow

View File

@@ -544,42 +544,19 @@ public class DatabaseService {
*/ */
synchronized List<Lot> getActiveLots() throws SQLException { synchronized List<Lot> getActiveLots() throws SQLException {
List<Lot> list = new ArrayList<>(); List<Lot> list = new ArrayList<>();
var sql = "SELECT lot_id, sale_id, title, description, manufacturer, type, year, category, " + var sql = "SELECT lot_id, sale_id as auction_id, title, description, manufacturer, type, year, category, " +
"current_bid, currency, url, closing_time, closing_notified FROM lots"; "current_bid, currency, url, closing_time, closing_notified FROM lots";
try (var conn = DriverManager.getConnection(url); var stmt = conn.createStatement()) { try (var conn = DriverManager.getConnection(url); var stmt = conn.createStatement()) {
var rs = stmt.executeQuery(sql); var rs = stmt.executeQuery(sql);
while (rs.next()) { while (rs.next()) {
var closingStr = rs.getString("closing_time"); try {
LocalDateTime closing = null; // Use ScraperDataAdapter to handle TEXT parsing from legacy database
if (closingStr != null && !closingStr.isBlank()) { var lot = ScraperDataAdapter.fromScraperLot(rs);
try { list.add(lot);
closing = LocalDateTime.parse(closingStr); } catch (Exception e) {
} catch (Exception e) { log.warn("Failed to parse lot {}: {}", rs.getString("lot_id"), e.getMessage());
log.debug("Invalid closing_time format for lot {}: {}", rs.getLong("lot_id"), closingStr);
}
} }
list.add(new Lot(
rs.getLong("sale_id"),
rs.getLong("lot_id"),
rs.getString("title"),
rs.getString("description"),
rs.getString("manufacturer"),
rs.getString("type"),
rs.getInt("year"),
rs.getString("category"),
rs.getDouble("current_bid"),
rs.getString("currency"),
rs.getString("url"),
closing,
rs.getInt("closing_notified") != 0,
// New intelligence fields - set to null for now
null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null,
null, null
));
} }
} }
return list; return list;
@@ -629,6 +606,44 @@ public class DatabaseService {
ps.executeUpdate(); ps.executeUpdate();
} }
} }
/**
* Retrieves bid history for a specific lot
*/
synchronized List<BidHistory> getBidHistory(String lotId) throws SQLException {
List<BidHistory> history = new ArrayList<>();
var sql = "SELECT id, lot_id, bid_amount, bid_time, is_autobid, bidder_id, bidder_number " +
"FROM bid_history WHERE lot_id = ? ORDER BY bid_time DESC LIMIT 100";
try (var conn = DriverManager.getConnection(url);
var ps = conn.prepareStatement(sql)) {
ps.setString(1, lotId);
var rs = ps.executeQuery();
while (rs.next()) {
LocalDateTime bidTime = null;
var bidTimeStr = rs.getString("bid_time");
if (bidTimeStr != null && !bidTimeStr.isBlank()) {
try {
bidTime = LocalDateTime.parse(bidTimeStr);
} catch (Exception e) {
log.debug("Invalid bid_time format: {}", bidTimeStr);
}
}
history.add(new BidHistory(
rs.getInt("id"),
rs.getString("lot_id"),
rs.getDouble("bid_amount"),
bidTime,
rs.getInt("is_autobid") != 0,
rs.getString("bidder_id"),
rs.getInt("bidder_number")
));
}
}
return history;
}
/** /**
* Imports auctions from scraper's schema format. * Imports auctions from scraper's schema format.

View File

@@ -4,6 +4,19 @@ import lombok.With;
import java.time.Duration; import java.time.Duration;
import java.time.LocalDateTime; import java.time.LocalDateTime;
/**
* Represents a bid in the bid history
*/
record BidHistory(
int id,
String lotId,
double bidAmount,
LocalDateTime bidTime,
boolean isAutobid,
String bidderId,
Integer bidderNumber
) {}
/// Represents a lot (kavel) in an auction. /// Represents a lot (kavel) in an auction.
/// Data typically populated by the external scraper process. /// Data typically populated by the external scraper process.
/// This project enriches the data with image analysis and monitoring. /// This project enriches the data with image analysis and monitoring.

View File

@@ -37,6 +37,8 @@ public class ObjectDetectionService {
private final Net net; private final Net net;
private final List<String> classNames; private final List<String> classNames;
private final boolean enabled; private final boolean enabled;
private int warnCount = 0;
private static final int MAX_WARNINGS = 5;
ObjectDetectionService(String cfgPath, String weightsPath, String classNamesPath) throws IOException { ObjectDetectionService(String cfgPath, String weightsPath, String classNamesPath) throws IOException {
// Check if model files exist // Check if model files exist
@@ -101,23 +103,44 @@ public class ObjectDetectionService {
// Postprocess: for each detection compute score and choose class // Postprocess: for each detection compute score and choose class
var confThreshold = 0.5f; var confThreshold = 0.5f;
for (var out : outs) { for (var out : outs) {
for (var i = 0; i < out.rows(); i++) { // YOLO output shape: [num_detections, 85] where 85 = 4 (bbox) + 1 (objectness) + 80 (classes)
var data = out.get(i, 0); int numDetections = out.rows();
if (data == null) continue; int numElements = out.cols();
int expectedLength = 5 + classNames.size();
// The first 5 numbers are bounding box, then class scores if (numElements < expectedLength) {
// Check if data has enough elements before copying // Rate-limit warnings to prevent thread blocking from excessive logging
int expectedLength = 5 + classNames.size(); if (warnCount < MAX_WARNINGS) {
if (data.length < expectedLength) { log.warn("Output matrix has wrong dimensions: expected {} columns, got {}. Output shape: [{}, {}]",
log.warn("Detection data too short: expected {} elements, got {}. Skipping detection.", expectedLength, numElements, numDetections, numElements);
expectedLength, data.length); warnCount++;
continue; if (warnCount == MAX_WARNINGS) {
log.warn("Suppressing further dimension warnings (reached {} warnings)", MAX_WARNINGS);
}
}
continue;
}
for (var i = 0; i < numDetections; i++) {
// Get entire row (all 85 elements)
var data = new double[numElements];
for (int j = 0; j < numElements; j++) {
data[j] = out.get(i, j)[0];
} }
// Extract objectness score (index 4) and class scores (index 5+)
double objectness = data[4];
if (objectness < confThreshold) {
continue; // Skip low-confidence detections
}
// Extract class scores
var scores = new double[classNames.size()]; var scores = new double[classNames.size()];
System.arraycopy(data, 5, scores, 0, scores.length); System.arraycopy(data, 5, scores, 0, Math.min(scores.length, data.length - 5));
var classId = argMax(scores); var classId = argMax(scores);
var confidence = scores[classId]; var confidence = scores[classId] * objectness; // Combine objectness with class confidence
if (confidence > confThreshold) { if (confidence > confThreshold) {
var label = classNames.get(classId); var label = classNames.get(classId);
if (!labels.contains(label)) { if (!labels.contains(label)) {

View File

@@ -26,6 +26,20 @@ public class ValuationAnalyticsResource {
@Inject @Inject
DatabaseService db; DatabaseService db;
/**
* GET /api/analytics/valuation/health
* Health check endpoint to verify API availability
*/
@GET
@Path("/valuation/health")
public Response healthCheck() {
return Response.ok(Map.of(
"status", "healthy",
"service", "valuation-analytics",
"timestamp", java.time.LocalDateTime.now().toString()
)).build();
}
/** /**
* POST /api/analytics/valuation * POST /api/analytics/valuation
* Main valuation endpoint that calculates FMV, undervaluation score, * Main valuation endpoint that calculates FMV, undervaluation score,

View File

@@ -6,7 +6,6 @@
<title>Auctiora - Intelligence Dashboard</title> <title>Auctiora - Intelligence Dashboard</title>
<script src="https://cdn.tailwindcss.com"></script> <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://cdnjs.cloudflare.com/ajax/libs/plotly.js/3.0.3/plotly.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet"> <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style> <style>
:root { :root {
@@ -757,6 +756,12 @@ function showToast(message, type = 'info', duration = 4000) {
// Initialize dashboard // Initialize dashboard
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Check for missing CDN dependencies
if (typeof Plotly === 'undefined') {
console.warn('Plotly.js not loaded - charts will be disabled');
addLog('Running in offline mode - charts disabled', 'warning');
}
addLog('Dashboard v2.1 initialized with predictive analytics'); addLog('Dashboard v2.1 initialized with predictive analytics');
startUptimeCounter(); startUptimeCounter();
fetchAllData(); fetchAllData();
@@ -1029,7 +1034,13 @@ function updateCountryChart(data) {
font: { size: 12 } font: { size: 12 }
}; };
Plotly.newPlot('country-chart', [chartData], layout, {responsive: true}); // Check if Plotly is available before using it
if (typeof Plotly !== 'undefined') {
Plotly.newPlot('country-chart', [chartData], layout, {responsive: true});
} else {
console.warn('Plotly not loaded - chart rendering skipped');
document.getElementById('country-chart').innerHTML = '<div class="text-gray-500 text-sm p-4">Chart library not available (offline mode)</div>';
}
// Update summary stats // Update summary stats
document.getElementById('top-country').textContent = countries[0] || '--'; document.getElementById('top-country').textContent = countries[0] || '--';
@@ -1061,7 +1072,13 @@ function updateCategoryChart(data) {
font: { size: 12 } font: { size: 12 }
}; };
Plotly.newPlot('category-chart', [chartData], layout, {responsive: true}); // Check if Plotly is available before using it
if (typeof Plotly !== 'undefined') {
Plotly.newPlot('category-chart', [chartData], layout, {responsive: true});
} else {
console.warn('Plotly not loaded - chart rendering skipped');
document.getElementById('category-chart').innerHTML = '<div class="text-gray-500 text-sm p-4">Chart library not available (offline mode)</div>';
}
// Update summary stats // Update summary stats
document.getElementById('top-category').textContent = categories[0] || '--'; document.getElementById('top-category').textContent = categories[0] || '--';
@@ -1105,7 +1122,13 @@ function updateTrendChart(data) {
font: { size: 12 } font: { size: 12 }
}; };
Plotly.newPlot('trend-chart', [trace1, trace2], layout, {responsive: true}); // Check if Plotly is available before using it
if (typeof Plotly !== 'undefined') {
Plotly.newPlot('trend-chart', [trace1, trace2], layout, {responsive: true});
} else {
console.warn('Plotly not loaded - chart rendering skipped');
document.getElementById('trend-chart').innerHTML = '<div class="text-gray-500 text-sm p-4">Chart library not available (offline mode)</div>';
}
} }
// Update insights // Update insights

View File

@@ -10,7 +10,6 @@
<title>Auctiora - Valuation Analytics</title> <title>Auctiora - Valuation Analytics</title>
<script src="https://cdn.tailwindcss.com"></script> <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://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"> <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style> <style>
:root { :root {
@@ -639,6 +638,93 @@ async function recalculate() {
showLoadingState(false); showLoadingState(false);
} }
// Helper function: Calculate undervaluation score
// Formula: U_score = (FMV - P_current)/FMV · σ_market · (1 + B_velocity/10) · ln(1 + W_watch/W_bid)
function calculateUndervaluationScore(params, fmv) {
if (fmv <= 0) return 0.0;
const priceGap = (fmv - params.currentBid) / fmv;
const velocityFactor = 1 + params.bidVelocity / 10.0;
const watchRatio = Math.log(1 + params.watchCount / Math.max(params.bidCount, 1));
const uScore = priceGap * params.marketVolatility * velocityFactor * watchRatio;
return Math.max(0.0, Math.round(uScore * 1000) / 1000);
}
// Helper function: Predict final price
// Formula: P̂_final = FMV · (1 + ε_bid + ε_time + ε_comp)
function calculateFinalPrice(params, fmv) {
// Bid momentum error: ε_bid = tanh(φ_1 · Λ_b - φ_2 · P_current/FMV)
const epsilonBid = Math.tanh(0.15 * params.bidVelocity - 0.10 * (params.currentBid / fmv));
// Time pressure error: ε_time = ψ · exp(-t_close/30)
const epsilonTime = 0.20 * Math.exp(-params.minutesUntilClose / 30.0);
// Competition error: ε_comp = ρ · ln(1 + W_watch/50)
const epsilonComp = 0.08 * Math.log(1 + params.watchCount / 50.0);
const predictedPrice = fmv * (1 + epsilonBid + epsilonTime + epsilonComp);
// 95% confidence interval: ± 1.96 · σ_residual
const residualStdDev = fmv * 0.08;
const ciLower = predictedPrice - 1.96 * residualStdDev;
const ciUpper = predictedPrice + 1.96 * residualStdDev;
return {
predictedPrice: Math.round(predictedPrice * 100) / 100,
confidenceIntervalLower: Math.round(ciLower * 100) / 100,
confidenceIntervalUpper: Math.round(ciUpper * 100) / 100,
components: {
bidMomentum: Math.round(epsilonBid * 1000) / 1000,
timePressure: Math.round(epsilonTime * 1000) / 1000,
competition: Math.round(epsilonComp * 1000) / 1000
}
};
}
// Helper function: Generate bidding strategy
function generateBiddingStrategy(params, fmv, prediction) {
const strategy = {
competitionLevel: 'MEDIUM',
recommendedTiming: 'FINAL_10_MINUTES',
recommendedTimingText: 'FINAL 10 MINUTES',
maxBid: 0,
analysis: '',
riskFactors: []
};
// Determine competition level
if (params.bidVelocity > 5.0) {
strategy.competitionLevel = 'HIGH';
strategy.recommendedTiming = 'FINAL_30_SECONDS';
strategy.recommendedTimingText = 'FINAL 30 SECONDS';
strategy.maxBid = prediction.predictedPrice + 50;
strategy.riskFactors = ['Bidding war likely', 'Sniping detected'];
} else if (params.minutesUntilClose < 10) {
strategy.competitionLevel = 'EXTREME';
strategy.recommendedTiming = 'FINAL_10_SECONDS';
strategy.recommendedTimingText = 'FINAL 10 SECONDS';
strategy.maxBid = prediction.predictedPrice * 1.02;
strategy.riskFactors = ['Last-minute sniping', 'Price volatility'];
} else {
const undervaluationScore = calculateUndervaluationScore(params, fmv.value);
if (undervaluationScore > 0.25) {
strategy.maxBid = fmv.value * 1.05;
strategy.analysis = 'Significant undervaluation detected. Consider aggressive bidding.';
} else {
strategy.maxBid = fmv.value * 1.03;
}
strategy.riskFactors = ['Standard competition level'];
}
// Generate analysis
strategy.analysis = `Bid velocity is ${params.bidVelocity.toFixed(1)} bids/min with ${params.watchCount} watchers. ${strategy.competitionLevel} competition detected. Predicted final: €${prediction.predictedPrice.toFixed(2)}.`;
strategy.maxBid = Math.round(strategy.maxBid * 100) / 100;
return strategy;
}
// Fallback mock calculation (original logic) // Fallback mock calculation (original logic)
async function calculateMockFallback(params) { async function calculateMockFallback(params) {
addLog('Using fallback calculation...', 'info'); addLog('Using fallback calculation...', 'info');
@@ -654,9 +740,9 @@ async function calculateMockFallback(params) {
let weightSum = 0; let weightSum = 0;
comparables.forEach(comp => { comparables.forEach(comp => {
const wc = Math.exp(-CONSTANTS.lambda_c * Math.abs(params.c_target - comp.condition)); const wc = Math.exp(-CONSTANTS.lambda_c * Math.abs(params.conditionScore - comp.condition));
const wt = Math.exp(-CONSTANTS.lambda_t * Math.abs(params.t_target - comp.year)); const wt = Math.exp(-CONSTANTS.lambda_t * Math.abs(params.manufacturingYear - comp.year));
const wp = 1 + CONSTANTS.delta_p * (params.n_docs > 0 ? 1 : 0 - comp.provenance); const wp = 1 + CONSTANTS.delta_p * (params.provenanceDocs > 0 ? 1 : 0 - comp.provenance);
const wh = 1 / (1 + Math.exp(-CONSTANTS.kh * (comp.days_ago - 45))); const wh = 1 / (1 + Math.exp(-CONSTANTS.kh * (comp.days_ago - 45)));
const weight = wc * wt * wp * wh; const weight = wc * wt * wp * wh;
@@ -664,12 +750,12 @@ async function calculateMockFallback(params) {
weightSum += weight; weightSum += weight;
}); });
let fmv = weightSum > 0 ? weightedSum / weightSum : (params.est_min + params.est_max) / 2; let fmv = weightSum > 0 ? weightedSum / weightSum : (params.estimatedMin + params.estimatedMax) / 2;
const mCond = Math.exp(CONSTANTS.alpha_c * Math.sqrt(params.c_target) - CONSTANTS.beta_c); const mCond = Math.exp(CONSTANTS.alpha_c * Math.sqrt(params.conditionScore) - CONSTANTS.beta_c);
fmv *= mCond; fmv *= mCond;
if (params.n_docs > 0) { if (params.provenanceDocs > 0) {
const provPremium = CONSTANTS.delta_p + 0.05 * Math.log(1 + params.n_docs); const provPremium = CONSTANTS.delta_p + 0.05 * Math.log(1 + params.provenanceDocs);
fmv *= (1 + provPremium); fmv *= (1 + provPremium);
} }
@@ -678,7 +764,7 @@ async function calculateMockFallback(params) {
fairMarketValue: { fairMarketValue: {
value: Math.round(fmv * 100) / 100, value: Math.round(fmv * 100) / 100,
conditionMultiplier: Math.round(mCond * 1000) / 1000, conditionMultiplier: Math.round(mCond * 1000) / 1000,
provenancePremium: params.n_docs > 0 ? CONSTANTS.delta_p + 0.05 * Math.log(1 + params.n_docs) : 0, provenancePremium: params.provenanceDocs > 0 ? CONSTANTS.delta_p + 0.05 * Math.log(1 + params.provenanceDocs) : 0,
comparablesUsed: comparables.length, comparablesUsed: comparables.length,
confidence: 0.85, confidence: 0.85,
weightedComparables: comparables.map(comp => ({ weightedComparables: comparables.map(comp => ({
@@ -686,9 +772,9 @@ async function calculateMockFallback(params) {
finalPrice: comp.price, finalPrice: comp.price,
totalWeight: 0.25, totalWeight: 0.25,
components: { components: {
conditionWeight: Math.round(Math.exp(-CONSTANTS.lambda_c * Math.abs(params.c_target - comp.condition)) * 1000) / 1000, conditionWeight: Math.round(Math.exp(-CONSTANTS.lambda_c * Math.abs(params.conditionScore - comp.condition)) * 1000) / 1000,
timeWeight: Math.round(Math.exp(-CONSTANTS.lambda_t * Math.abs(params.t_target - comp.year)) * 1000) / 1000, timeWeight: Math.round(Math.exp(-CONSTANTS.lambda_t * Math.abs(params.manufacturingYear - comp.year)) * 1000) / 1000,
provenanceWeight: Math.round((1 + CONSTANTS.delta_p * (params.n_docs > 0 ? 1 : 0 - comp.provenance)) * 1000) / 1000, provenanceWeight: Math.round((1 + CONSTANTS.delta_p * (params.provenanceDocs > 0 ? 1 : 0 - comp.provenance)) * 1000) / 1000,
historicalWeight: Math.round((1 / (1 + Math.exp(-CONSTANTS.kh * (comp.days_ago - 45)))) * 1000) / 1000 historicalWeight: Math.round((1 / (1 + Math.exp(-CONSTANTS.kh * (comp.days_ago - 45)))) * 1000) / 1000
} }
})) }))
@@ -803,47 +889,57 @@ function generateSensitivityCharts() {
const conditionRange = Array.from({length: 21}, (_, i) => i * 0.5); const conditionRange = Array.from({length: 21}, (_, i) => i * 0.5);
const conditionValues = conditionRange.map(c => { const conditionValues = conditionRange.map(c => {
const params = getInputParams(); const params = getInputParams();
params.c_target = c; params.conditionScore = c;
return calculateMockFMV(params); // Use simplified mock for chart return calculateMockFMV(params); // Use simplified mock for chart
}); });
Plotly.newPlot('condition-chart', [{ // Check if Plotly is available before using it
x: conditionRange, if (typeof Plotly !== 'undefined') {
y: conditionValues, Plotly.newPlot('condition-chart', [{
type: 'scatter', x: conditionRange,
mode: 'lines+markers', y: conditionValues,
line: { color: '#3b82f6', width: 3 }, type: 'scatter',
marker: { size: 6 }, mode: 'lines+markers',
name: 'FMV vs Condition' line: { color: '#3b82f6', width: 3 },
}], { marker: { size: 6 },
xaxis: { title: 'Condition Score (C)' }, name: 'FMV vs Condition'
yaxis: { title: 'FMV (€)' }, }], {
margin: { t: 20, b: 40, l: 60, r: 20 }, xaxis: { title: 'Condition Score (C)' },
font: { size: 12 } yaxis: { title: 'FMV (€)' },
}, {responsive: true}); margin: { t: 20, b: 40, l: 60, r: 20 },
font: { size: 12 }
}, {responsive: true});
} else {
document.getElementById('condition-chart').innerHTML = '<div class="text-gray-500 text-sm p-4">Chart library not available (offline mode)</div>';
}
// Time sensitivity chart // Time sensitivity chart
const timeRange = Array.from({length: 20}, (_, i) => i * 5); const timeRange = Array.from({length: 20}, (_, i) => i * 5);
const timeValues = timeRange.map(t => { const timeValues = timeRange.map(t => {
const params = getInputParams(); const params = getInputParams();
params.t_close = t; params.minutesUntilClose = t;
return calculateMockFinalPrice(params, calculateMockFMV(params)); return calculateMockFinalPrice(params, calculateMockFMV(params));
}); });
Plotly.newPlot('time-chart', [{ // Check if Plotly is available before using it
x: timeRange, if (typeof Plotly !== 'undefined') {
y: timeValues, Plotly.newPlot('time-chart', [{
type: 'scatter', x: timeRange,
mode: 'lines+markers', y: timeValues,
line: { color: '#10b981', width: 3 }, type: 'scatter',
marker: { size: 6 }, mode: 'lines+markers',
name: 'Predicted Final vs Time' line: { color: '#10b981', width: 3 },
}], { marker: { size: 6 },
xaxis: { title: 'Time to Close (minutes)' }, name: 'Predicted Final vs Time'
yaxis: { title: 'Predicted Final Price (€)' }, }], {
margin: { t: 20, b: 40, l: 60, r: 20 }, xaxis: { title: 'Time to Close (minutes)' },
font: { size: 12 } yaxis: { title: 'Predicted Final Price (€)' },
}, {responsive: true}); margin: { t: 20, b: 40, l: 60, r: 20 },
font: { size: 12 }
}, {responsive: true});
} else {
document.getElementById('time-chart').innerHTML = '<div class="text-gray-500 text-sm p-4">Chart library not available (offline mode)</div>';
}
} }
// Simplified mock FMV for chart generation // Simplified mock FMV for chart generation
@@ -857,22 +953,22 @@ function calculateMockFMV(params) {
let weightSum = 0; let weightSum = 0;
comparables.forEach(comp => { comparables.forEach(comp => {
const wc = Math.exp(-CONSTANTS.lambda_c * Math.abs(params.c_target - comp.condition)); const wc = Math.exp(-CONSTANTS.lambda_c * Math.abs(params.conditionScore - comp.condition));
const wt = Math.exp(-CONSTANTS.lambda_t * Math.abs(params.t_target - comp.year)); const wt = Math.exp(-CONSTANTS.lambda_t * Math.abs(params.manufacturingYear - comp.year));
const weight = wc * wt; const weight = wc * wt;
weightedSum += comp.price * weight; weightedSum += comp.price * weight;
weightSum += weight; weightSum += weight;
}); });
let fmv = weightSum > 0 ? weightedSum / weightSum : 8000; let fmv = weightSum > 0 ? weightedSum / weightSum : 8000;
const mCond = Math.exp(CONSTANTS.alpha_c * Math.sqrt(params.c_target) - CONSTANTS.beta_c); const mCond = Math.exp(CONSTANTS.alpha_c * Math.sqrt(params.conditionScore) - CONSTANTS.beta_c);
return fmv * mCond; return fmv * mCond;
} }
// Simplified mock final price for chart generation // Simplified mock final price for chart generation
function calculateMockFinalPrice(params, fmv) { function calculateMockFinalPrice(params, fmv) {
const epsilon_bid = Math.tanh(CONSTANTS.phi_1 * params.lambda_b); const epsilon_bid = Math.tanh(CONSTANTS.phi_1 * params.bidVelocity);
const epsilon_time = CONSTANTS.psi * Math.exp(-params.t_close / 30); const epsilon_time = CONSTANTS.psi * Math.exp(-params.minutesUntilClose / 30);
return fmv * (1 + epsilon_bid + epsilon_time); return fmv * (1 + epsilon_bid + epsilon_time);
} }