Fix mock tests
This commit is contained in:
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -630,6 +607,44 @@ public class DatabaseService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
* Since the scraper doesn't populate a separate auctions table,
|
* Since the scraper doesn't populate a separate auctions table,
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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 {
|
|||||||
// Post‑process: for each detection compute score and choose class
|
// Post‑process: 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)) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user