Fix mock tests
This commit is contained in:
@@ -20,7 +20,7 @@ set -e # Exit on error
|
||||
# Configuration
|
||||
REMOTE_HOST="tour@athena.lan"
|
||||
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"
|
||||
REMOTE_TMP="/tmp"
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.jboss.logging.Logger;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
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
|
||||
* Manually trigger scraper import workflow
|
||||
|
||||
@@ -544,42 +544,19 @@ public class DatabaseService {
|
||||
*/
|
||||
synchronized List<Lot> getActiveLots() throws SQLException {
|
||||
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";
|
||||
|
||||
try (var conn = DriverManager.getConnection(url); var stmt = conn.createStatement()) {
|
||||
var rs = stmt.executeQuery(sql);
|
||||
while (rs.next()) {
|
||||
var closingStr = rs.getString("closing_time");
|
||||
LocalDateTime closing = null;
|
||||
if (closingStr != null && !closingStr.isBlank()) {
|
||||
try {
|
||||
closing = LocalDateTime.parse(closingStr);
|
||||
} catch (Exception e) {
|
||||
log.debug("Invalid closing_time format for lot {}: {}", rs.getLong("lot_id"), closingStr);
|
||||
}
|
||||
try {
|
||||
// Use ScraperDataAdapter to handle TEXT parsing from legacy database
|
||||
var lot = ScraperDataAdapter.fromScraperLot(rs);
|
||||
list.add(lot);
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to parse lot {}: {}", rs.getString("lot_id"), e.getMessage());
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -629,6 +606,44 @@ public class DatabaseService {
|
||||
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.
|
||||
|
||||
@@ -4,6 +4,19 @@ import lombok.With;
|
||||
import java.time.Duration;
|
||||
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.
|
||||
/// Data typically populated by the external scraper process.
|
||||
/// This project enriches the data with image analysis and monitoring.
|
||||
|
||||
@@ -37,6 +37,8 @@ public class ObjectDetectionService {
|
||||
private final Net net;
|
||||
private final List<String> classNames;
|
||||
private final boolean enabled;
|
||||
private int warnCount = 0;
|
||||
private static final int MAX_WARNINGS = 5;
|
||||
|
||||
ObjectDetectionService(String cfgPath, String weightsPath, String classNamesPath) throws IOException {
|
||||
// Check if model files exist
|
||||
@@ -101,23 +103,44 @@ public class ObjectDetectionService {
|
||||
// Post‑process: for each detection compute score and choose class
|
||||
var confThreshold = 0.5f;
|
||||
for (var out : outs) {
|
||||
for (var i = 0; i < out.rows(); i++) {
|
||||
var data = out.get(i, 0);
|
||||
if (data == null) continue;
|
||||
// YOLO output shape: [num_detections, 85] where 85 = 4 (bbox) + 1 (objectness) + 80 (classes)
|
||||
int numDetections = out.rows();
|
||||
int numElements = out.cols();
|
||||
int expectedLength = 5 + classNames.size();
|
||||
|
||||
// The first 5 numbers are bounding box, then class scores
|
||||
// Check if data has enough elements before copying
|
||||
int expectedLength = 5 + classNames.size();
|
||||
if (data.length < expectedLength) {
|
||||
log.warn("Detection data too short: expected {} elements, got {}. Skipping detection.",
|
||||
expectedLength, data.length);
|
||||
continue;
|
||||
if (numElements < expectedLength) {
|
||||
// Rate-limit warnings to prevent thread blocking from excessive logging
|
||||
if (warnCount < MAX_WARNINGS) {
|
||||
log.warn("Output matrix has wrong dimensions: expected {} columns, got {}. Output shape: [{}, {}]",
|
||||
expectedLength, numElements, numDetections, numElements);
|
||||
warnCount++;
|
||||
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()];
|
||||
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 confidence = scores[classId];
|
||||
var confidence = scores[classId] * objectness; // Combine objectness with class confidence
|
||||
|
||||
if (confidence > confThreshold) {
|
||||
var label = classNames.get(classId);
|
||||
if (!labels.contains(label)) {
|
||||
|
||||
@@ -26,6 +26,20 @@ public class ValuationAnalyticsResource {
|
||||
@Inject
|
||||
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
|
||||
* Main valuation endpoint that calculates FMV, undervaluation score,
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
<title>Auctiora - Intelligence Dashboard</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/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">
|
||||
<style>
|
||||
:root {
|
||||
@@ -757,6 +756,12 @@ function showToast(message, type = 'info', duration = 4000) {
|
||||
|
||||
// Initialize dashboard
|
||||
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');
|
||||
startUptimeCounter();
|
||||
fetchAllData();
|
||||
@@ -1029,7 +1034,13 @@ function updateCountryChart(data) {
|
||||
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
|
||||
document.getElementById('top-country').textContent = countries[0] || '--';
|
||||
@@ -1061,7 +1072,13 @@ function updateCategoryChart(data) {
|
||||
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
|
||||
document.getElementById('top-category').textContent = categories[0] || '--';
|
||||
@@ -1105,7 +1122,13 @@ function updateTrendChart(data) {
|
||||
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
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
<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 {
|
||||
@@ -639,6 +638,93 @@ async function recalculate() {
|
||||
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)
|
||||
async function calculateMockFallback(params) {
|
||||
addLog('Using fallback calculation...', 'info');
|
||||
@@ -654,9 +740,9 @@ async function calculateMockFallback(params) {
|
||||
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 wc = Math.exp(-CONSTANTS.lambda_c * Math.abs(params.conditionScore - comp.condition));
|
||||
const wt = Math.exp(-CONSTANTS.lambda_t * Math.abs(params.manufacturingYear - comp.year));
|
||||
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 weight = wc * wt * wp * wh;
|
||||
@@ -664,12 +750,12 @@ async function calculateMockFallback(params) {
|
||||
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);
|
||||
let fmv = weightSum > 0 ? weightedSum / weightSum : (params.estimatedMin + params.estimatedMax) / 2;
|
||||
const mCond = Math.exp(CONSTANTS.alpha_c * Math.sqrt(params.conditionScore) - CONSTANTS.beta_c);
|
||||
fmv *= mCond;
|
||||
|
||||
if (params.n_docs > 0) {
|
||||
const provPremium = CONSTANTS.delta_p + 0.05 * Math.log(1 + params.n_docs);
|
||||
if (params.provenanceDocs > 0) {
|
||||
const provPremium = CONSTANTS.delta_p + 0.05 * Math.log(1 + params.provenanceDocs);
|
||||
fmv *= (1 + provPremium);
|
||||
}
|
||||
|
||||
@@ -678,7 +764,7 @@ async function calculateMockFallback(params) {
|
||||
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,
|
||||
provenancePremium: params.provenanceDocs > 0 ? CONSTANTS.delta_p + 0.05 * Math.log(1 + params.provenanceDocs) : 0,
|
||||
comparablesUsed: comparables.length,
|
||||
confidence: 0.85,
|
||||
weightedComparables: comparables.map(comp => ({
|
||||
@@ -686,9 +772,9 @@ async function calculateMockFallback(params) {
|
||||
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,
|
||||
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.manufacturingYear - comp.year)) * 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
|
||||
}
|
||||
}))
|
||||
@@ -803,47 +889,57 @@ function generateSensitivityCharts() {
|
||||
const conditionRange = Array.from({length: 21}, (_, i) => i * 0.5);
|
||||
const conditionValues = conditionRange.map(c => {
|
||||
const params = getInputParams();
|
||||
params.c_target = c;
|
||||
params.conditionScore = 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});
|
||||
// Check if Plotly is available before using it
|
||||
if (typeof Plotly !== 'undefined') {
|
||||
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});
|
||||
} 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
|
||||
const timeRange = Array.from({length: 20}, (_, i) => i * 5);
|
||||
const timeValues = timeRange.map(t => {
|
||||
const params = getInputParams();
|
||||
params.t_close = t;
|
||||
params.minutesUntilClose = 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});
|
||||
// Check if Plotly is available before using it
|
||||
if (typeof Plotly !== 'undefined') {
|
||||
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});
|
||||
} 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
|
||||
@@ -857,22 +953,22 @@ function calculateMockFMV(params) {
|
||||
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 wc = Math.exp(-CONSTANTS.lambda_c * Math.abs(params.conditionScore - comp.condition));
|
||||
const wt = Math.exp(-CONSTANTS.lambda_t * Math.abs(params.manufacturingYear - 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);
|
||||
const mCond = Math.exp(CONSTANTS.alpha_c * Math.sqrt(params.conditionScore) - 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);
|
||||
const epsilon_bid = Math.tanh(CONSTANTS.phi_1 * params.bidVelocity);
|
||||
const epsilon_time = CONSTANTS.psi * Math.exp(-params.minutesUntilClose / 30);
|
||||
return fmv * (1 + epsilon_bid + epsilon_time);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user