Dynamic price filter no overlapping deals

These are two different things - similar but different - in fact you can already achieve your setting with the current settings - just calculate the DCA max deviation and set the dynamic price after that %.

What has been discussed it’s slightly different - cause it allows you to make a grid of deal not overlapping them on the same price - then you can add DCA or not up to you.

Hope it’s clear

Keep it simple or it won’t get any buyers

The second bot of those two shows the current issue with over and underquite clearly. Or is there even another issue that results in those stacked deals?

Over and under with overlapping deals.

image

I also tested the bot, and the overlapping seems real. The dynamic price filter uses the deal’s average price or entry price, so if the deal’s average or entry price is matched with dynamic filter condition (regardless of deal having more SO to fill), it opens a new deal.
I think it should calculate only when a deal completes all the SOs, shouldn’t it? Or maybe this is a new feature request.

Another example. here opened overlapped deal, i think it’s better to have new option to check dynamic price filter condition after deal execute it’s last “Safety Order”.
so according to screen shot if we choose average price from dynamic rice filter the condition will check from “Final Breakeven”
Bot
https://app.gainium.io/bot/67b03de537f8df4df7fbe4dd?a=1088&aid=share-bot&share=37c3cca9-1b00-4102-bac2-377a54483ebc



It should work without overlaps whatever we configure as Price source.

1 Like

Update 2025-03-20

  • No overlapping deals in dynamic price filter. Prevents the bot from opening a new deal if the price range of the new deal overlaps with the price range of an existing deal.

Thanks for the implementation of this and also for making it optional to keep the previous behaviour for those who prefer it with overlapping. Please give us some details about which of the above suggestions have now been implemented and which have not (yet). Especially regarding this proposed solution.

Implemented. The system will create an array of forbidden ranges by taking the past deals price and their over and under settings.

1 Like

As we have just learnt from here

and the Telegram chat

“Is not intuitive that no overlapping deals should not include the dcas, that’s the specific way you use your strategy.”

But my point of view was and is, that the dynamic price filter without overlapping was and is meant to help us build a DCA grid. That is the DCA deals start at a certain distance and each of them DCAs separately.

The overlapping caused deals to start at almost the same level, which resulted in several funds stacked at the same level. That’s why this new feature should help to avoid the overlapping.

But it shouldn’t take the DCA of the deals into account to prevent deals in the DCA range of another deal. Only the entry price (respective the average price - where I currently have a use case for) should be taken into account to figure out where the next deal can be started.

Unfortunately

… reading back my own “Proposed solution” wasn’t that clear at this point.

My text was talking about deals where I meant the entry price of previous deals.

That’s why I have to modify my above idea. The changes are small but important.

Proposed Solution (Version 2):

  1. Remember the Last Deal’s Entry Price:

    • Track the entry price of the most recent deal (regardless of direction) as the reference point for the dynamic price filter. (That’s what Gainium already does.)
  2. Check for Existing Deals in an Epsilon Range:

    • Before opening a new deal, check if there’s already an open deal with an entry price in

      • either a small delta-epsilon range (e.g., 0.1% < currently relevant deviation over or under) around the current price. (This delta/epsilon (for under/over) could even be configurable.)

      • or the configured deviations for under and over.

    • If such a deal exists, do not open a new deal. Instead, update the “last deal’s price” reference to this existing deal’s entry price.

  3. Apply the Dynamic Filter:

    • If no deal entry price exists in the epsilon range, proceed with opening the new deal and update the “last deal’s price” reference to this new deal’s price.

If we don’t want deals to overlap including their DCA, we could simply increase the values to do so. Or make this no overlap in DCA range optional for dynamic price filter. With DCA enabled for the dynamic price filter and entry price chosen the whole range could be taken into account. With average price the range between that and the last DCA. Without DCA only entry respectively average price matter.

That’s how it was meant to be.

Second deal blocks

First deal blocks

DCA range blocks

Demo Information

Key configurations and usage information.

This demo simulates a deal checker. It prevents opening new deals if the price is within a certain percentage of an existing deal’s entry price.

We test it by entering a price, checking if it’s valid, and then adding the deal.

Key configurations:

  • Over Deviation: 2%
  • Under Deviation: 3%
  • Max Price Label: Max Price
  • Min Price Percent: 5.00%
  • Allow Min Price Configuration: Yes (Check box)

I didn’t refactor the code yet but it could look like this. I split the code to make it fit here.

import React, { useState, useCallback, useEffect, useMemo } from 'react';
import { Button } from '@/components/ui/button'; // Assuming path is correct
import { Input } from '@/components/ui/input';   // Assuming path is correct
import { cn } from '@/lib/utils';               // Assuming path is correct
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; // Assuming path is correct
import { AlertCircle } from "lucide-react";
import { motion, AnimatePresence } from 'framer-motion';
import { Checkbox } from "@/components/ui/checkbox"; // Assuming path is correct
import { Label } from "@/components/ui/label";     // Assuming path is correct
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; // Assuming path is correct

// --- Constants ---
const MESSAGE_TIMEOUT_MS = 3000;
const PERCENTAGE_FACTOR = 100;

// --- Types ---
interface Deal {
    id: string;
    /** The reference price for deviation checks (e.g., entry price, average price). */
    max_price: number;
}
interface DealCheckerDemoProps {
    /** Initial value for over deviation as a decimal (e.g., 0.02 for 2%). Defaults to 0.02. */
    initialOverDeviation?: number;
    /** Initial value for under deviation as a decimal (e.g., 0.03 for 3%). Defaults to 0.03. */
    initialUnderDeviation?: number;
    /** Initial list of deals. Defaults to an empty array. */
    initialDeals?: Deal[];
    /** Whether adding new deals is allowed. Defaults to true. */
    allowAddDeals?: boolean;
    /** Whether checking deals is allowed. Defaults to true. */
    allowCheckDeals?: boolean;
    /** Default minimum price deviation as a decimal (e.g., 0.05 for 5%). Defaults to 0.05. */
    defaultMinPricePercentDecimal?: number; // Renamed for clarity
    /** Custom validation function for the input price. Return null if valid, or an error string if invalid. */
    customValidation?: (price: number) => string | null;
    /** Label for the main price input field. Defaults to "Max Price (Entry/Avg)". */
    maxPriceLabel?: string;
    /** Label for the minimum price deviation configuration. Defaults to "Min Price Deviation (Last SO/SL)". */
    minPriceLabel?: string;
    /** Whether configuring the minimum price deviation is allowed. Defaults to true. */
    allowMinPriceConfiguration?: boolean;
    /** Label for the over deviation configuration. Defaults to "Over Deviation". */
    overDeviationLabel?: string;
    /** Label for the under deviation configuration. Defaults to "Under Deviation". */
    underDeviationLabel?: string;
    /** Whether configuring over/under deviation is allowed. Defaults to true. */
    allowOverUnderDeviationConfiguration?: boolean;
}

interface CheckResult {
    canOpen: boolean;
    blockingDeal?: Deal;
    /** Lower boundary below which a deal might be acceptable */
    lowerBound?: number;
    /** Upper boundary above which a deal might be acceptable */
    upperBound?: number;
}

/** Configuration for calculations, using percentage numbers (e.g., 5 for 5%) */
interface CalculationConfig {
    overDeviationPercent: number;
    underDeviationPercent: number;
    minPricePercent: number;
    useMinPrice: boolean;
}

interface MessageState {
    text: string | null;
    type: 'success' | 'error' | null;
}

// --- Utility Functions ---

/**
 * Parses a string to a positive number. Used for the main price input.
 * @param value - The string to parse.
 * @returns The parsed positive number, or NaN if invalid or not positive.
 */
const parsePositiveNumber = (value: string): number => {
    const num = parseFloat(value);
    return !isNaN(num) && num > 0 ? num : NaN;
};

/**
 * Parses a percentage string (e.g., "5") into a non-negative number (e.g., 5).
 * Used for configuration inputs.
 * @param value - The percentage string to parse.
 * @returns The parsed non-negative percentage number, or NaN if invalid or negative.
 */
const parsePercentageString = (value: string): number => {
    const num = parseFloat(value);
    return !isNaN(num) && num >= 0 ? num : NaN;
};

/**
 * Converts a decimal value (e.g., 0.05) to its percentage string representation (e.g., "5").
 * Used for initializing input state from decimal props.
 * @param decimal - The decimal number to convert.
 * @returns The percentage value as a string.
 */
const decimalToPercentageString = (decimal: number): string => {
    // Multiply, round to avoid floating point issues like 5.0000000001, then convert to string
    return (Math.round(decimal * PERCENTAGE_FACTOR * 100) / 100).toString();
};

/**
 * Calculates a multiplier factor based on a percentage value.
 * Converts the percentage (e.g., 2 or -3) into a decimal factor (e.g., 1.02 or 0.97).
 * `toChangeFactor(2)` returns `1.02`.
 * `toChangeFactor(-3)` returns `0.97`.
 * @param percentage - The deviation percentage (positive for increase, negative for decrease).
 * @returns The multiplier (1 + percentage / PERCENTAGE_FACTOR). Returns 1 if percentage is not a valid number.
 */
const toChangeFactor = (percentage: number): number => {
    // Basic validation: if percentage is not a valid number, return a neutral factor (1)
    if (isNaN(percentage)) {
        console.warn("toChangeFactor received NaN, returning 1.");
        return 1;
    }
    return 1 + (percentage / PERCENTAGE_FACTOR);
};

/**
 * Calculates the lower and upper price boundaries.
 * Receives configuration with percentage numbers (e.g., 5 for 5%) and uses `toChangeFactor`
 * which handles the conversion to a decimal multiplier internally.
 * @param refPrice - The reference price of the existing deal.
 * @param config - The current calculation configuration containing percentage numbers.
 * @returns An object containing the calculated lower and upper boundaries.
 */
const calculateDealBoundaries = (refPrice: number, config: CalculationConfig): { lowerBound: number; upperBound: number } => {
    const minPriceRefFactor = config.useMinPrice
        ? toChangeFactor(-config.minPricePercent) // Pass percentage (-5) -> returns 0.95
        : 1; // Neutral factor if min price check is disabled
    const minPriceRef = refPrice * minPriceRefFactor;

    // Calculate lower bound based on the (potentially adjusted) reference price and under-deviation
    const lowerBound = minPriceRef * toChangeFactor(-config.underDeviationPercent); // Pass percentage (-3) -> returns 0.97

    // Calculate upper bound based on the original reference price and over-deviation
    const upperBound = refPrice * toChangeFactor(config.overDeviationPercent);     // Pass percentage (2) -> returns 1.02

    return { lowerBound, upperBound };
};

/**
 * Checks if a new deal can be opened based on existing deals and configuration.
 * @param checkPrice - The price to check.
 * @param deals - Array of existing deals.
 * @param config - The current calculation configuration (using percentage numbers).
 * @returns A CheckResult object indicating if the deal can be opened and details if blocked.
 */
const checkIfDealCanBeOpened = (checkPrice: number, deals: Deal[], config: CalculationConfig): CheckResult => {
    // Basic validation for checkPrice
    if (isNaN(checkPrice) || checkPrice <= 0) {
        return { canOpen: false }; // Cannot open if checkPrice is invalid
    }

    for (const deal of deals) {
        // Skip deals with invalid reference price
        if (isNaN(deal.max_price) || deal.max_price <= 0) {
            console.warn(`Skipping deal ${deal.id} due to invalid max_price: ${deal.max_price}`);
            continue;
        }

        // calculateDealBoundaries now expects config with percentages
        const { lowerBound, upperBound } = calculateDealBoundaries(deal.max_price, config);

        // Check if boundaries are valid before comparison
        if (isNaN(lowerBound) || isNaN(upperBound)) {
             console.warn(`Invalid boundaries calculated for deal ${deal.id}. Lower: ${lowerBound}, Upper: ${upperBound}`);
             continue; // Skip this deal if boundaries are invalid
        }

        // Check for overlap
        if (lowerBound < checkPrice && checkPrice < upperBound) {
            return {
                canOpen: false,
                blockingDeal: deal,
                lowerBound,
                upperBound,
            };
        }
    }
    // If no blocking deals found after checking all valid deals
    return { canOpen: true };
};

// --- Sub-Components ---

/** Displays the list of existing deals */
const DealList: React.FC<{
    deals: Deal[];
    onDelete: (id: string) => void;
    maxPriceLabel: string;
}> = React.memo(({ deals, onDelete, maxPriceLabel }) => (
    <Card className="bg-gray-900/90 backdrop-blur-md border border-gray-800">
        <CardHeader>
            <CardTitle className="text-xl sm:text-2xl font-semibold text-gray-200">Existing Deals</CardTitle>
            <CardDescription className="text-gray-400">Deals currently being tracked.</CardDescription>
        </CardHeader>
        <CardContent>
            {deals.length === 0 ? (
                <p className="text-gray-400">No deals added yet.</p>
            ) : (
                <div className="space-y-2">
                    <AnimatePresence initial={false}>
                        {deals.map((deal) => (
                            <motion.div
                                key={deal.id}
                                layout // Animate layout changes (like deletion)
                                initial={{ opacity: 0, x: -20 }}
                                animate={{ opacity: 1, x: 0 }}
                                exit={{ opacity: 0, x: 50, transition: { duration: 0.2 } }} // Exit animation
                                className="flex items-center justify-between bg-gray-800/80 border border-gray-700 rounded-md p-2 sm:p-3"
                            >
                                <span className="text-gray-300 truncate pr-2" title={deal.max_price.toString()}>
                                    {maxPriceLabel}: {deal.max_price.toFixed(2)}
                                </span>
                                <Button
                                    variant="destructive"
                                    size="sm"
                                    onClick={() => onDelete(deal.id)}
                                    className="bg-red-500/20 text-red-400 hover:bg-red-500/30 hover:text-red-300 flex-shrink-0"
                                    aria-label={`Delete deal with price ${deal.max_price.toFixed(2)}`}
                                >
                                    Delete
                                </Button>
                            </motion.div>
                        ))}
                    </AnimatePresence>
                </div>
            )}
        </CardContent>
    </Card>
));
DealList.displayName = "DealList";

/** Handles price input and Check/Add actions */
const DealForm: React.FC<{
    priceInput: string;
    onPriceInputChange: (value: string) => void;
    onCheck: () => void;
    onAdd: () => void;
    canAddDeal: boolean;
    allowCheckDeals: boolean;
    allowAddDeals: boolean;
    maxPriceLabel: string;
    cardTitle: string;
    cardDescription: string;
}> = React.memo(({
    priceInput, onPriceInputChange, onCheck, onAdd, canAddDeal, allowCheckDeals,
    allowAddDeals, maxPriceLabel, cardTitle, cardDescription
}) => (
    <Card className="bg-gray-900/90 backdrop-blur-md border border-gray-800">
        <CardHeader>
            <CardTitle className="text-xl sm:text-2xl font-semibold text-gray-200">{cardTitle}</CardTitle>
            <CardDescription className="text-gray-400">{cardDescription}</CardDescription>
        </CardHeader>
        <CardContent className="space-y-4">
            <Input
                type="number"
                placeholder={maxPriceLabel}
                value={priceInput}
                onChange={(e) => onPriceInputChange(e.target.value)}
                className="bg-gray-800/80 border-gray-700 text-white placeholder:text-gray-400"
                aria-label={maxPriceLabel}
                step="any" // Allow decimals
                min="0" // Prevent negative numbers via browser validation (though we validate further)
            />
            <div className="flex flex-col sm:flex-row gap-2">
                {allowCheckDeals && (
                    <Button
                        onClick={onCheck}
                        className="bg-green-500/20 text-green-400 hover:bg-green-500/30 hover:text-green-300 flex-1"
                    >
                        Check Availability
                    </Button>
                )}
                {allowAddDeals && (
                    <Button
                        onClick={onAdd}
                        className="bg-blue-500/20 text-blue-400 hover:bg-blue-500/30 hover:text-blue-300 flex-1"
                        disabled={!canAddDeal}
                        aria-disabled={!canAddDeal}
                    >
                        Add Deal
                    </Button>
                )}
            </div>
        </CardContent>
    </Card>
));
DealForm.displayName = "DealForm";
/** Displays and manages configuration options */
const ConfigurationPanel: React.FC<{
    minPricePercentInput: string;
    overDeviationInput: string;
    underDeviationInput: string;
    isMinPriceEnabled: boolean;
    isOverDeviationEnabled: boolean;
    isUnderDeviationEnabled: boolean;
    onInputChange: (field: 'minPricePercent' | 'overDeviation' | 'underDeviation', value: string) => void;
    onToggleChange: (field: 'minPrice' | 'overDeviation' | 'underDeviation', checked: boolean) => void;
    allowMinPriceConfiguration: boolean;
    allowOverUnderDeviationConfiguration: boolean;
    minPriceLabel: string;
    overDeviationLabel: string;
    underDeviationLabel: string;
}> = React.memo(({
    minPricePercentInput, overDeviationInput, underDeviationInput,
    isMinPriceEnabled, isOverDeviationEnabled, isUnderDeviationEnabled,
    onInputChange, onToggleChange,
    allowMinPriceConfiguration,
    allowOverUnderDeviationConfiguration, minPriceLabel, overDeviationLabel, underDeviationLabel
}) => {

    // Helper to render each config input row
    const renderConfigInput = (
        fieldKey: keyof typeof configEnabled,
        inputKey: keyof typeof configInputs,
        labelText: string, // The main label text (e.g., "Min Price Deviation")
        inputValue: string,
        isEnabled: boolean,
        allowToggle: boolean
    ) => {
        const inputId = `${fieldKey}-input`;
        const checkboxId = `${fieldKey}-checkbox`;
        // Label now refers to the input, not the checkbox
        const inputLabelId = `${fieldKey}-label`;

        return (
            // Use flex row for the overall layout
            <div className="flex flex-col sm:flex-row items-start sm:items-center gap-3 py-3 border-b border-gray-800 last:border-b-0">

                {/* Checkbox (if applicable) - First element */}
                {allowToggle && (
                    <div className="flex-shrink-0 order-1 mb-2 sm:mb-0">
                        <Checkbox
                            id={checkboxId}
                            checked={isEnabled}
                            onCheckedChange={(checked) => onToggleChange(fieldKey, Boolean(checked))}
                            className="border-gray-700 data-[state=checked]:bg-blue-600 data-[state=checked]:border-blue-500 mt-1" // Added mt-1 for alignment potentially
                            aria-label={`Enable ${labelText} configuration`} // More descriptive aria-label
                        />
                    </div>
                )}

                {/* Input and Label Group - Second element */}
                <div className={cn(
                     "flex flex-grow items-center gap-2 order-2 w-full",
                     // If checkbox isn't shown, remove potential left margin that might be added implicitly by gap
                     !allowToggle && "sm:ml-0"
                 )}>
                    {/* Input Field */}
                    <Input
                        id={inputId}
                        type="number"
                        placeholder="%" // Very simple placeholder
                        value={inputValue}
                        onChange={(e) => onInputChange(inputKey, e.target.value)}
                        className={cn(
                            // Assign specific width, let label take remaining space? Or keep input flexible?
                            "w-24 sm:w-28 bg-gray-800/80 border-gray-700 text-white placeholder:text-gray-400",
                            !isEnabled && "opacity-50 cursor-not-allowed",
                        )}
                        disabled={!isEnabled}
                        aria-labelledby={inputLabelId} // Point aria-labelledby to the input's label
                        step="any"
                        min="0"
                    />
                    {/* Descriptive Label for the Input */}
                    <Label id={inputLabelId} htmlFor={inputId} className="text-sm text-gray-300 select-none">
                        {labelText} (%)
                    </Label>
                </div>
            </div>
        );
    };

    return (
        <Card className="bg-gray-900/90 backdrop-blur-md border border-gray-800">
            <CardHeader>
                <CardTitle className="text-xl sm:text-2xl font-semibold text-gray-200">Configuration</CardTitle>
                <CardDescription className="text-gray-400">Enable and set deviation percentages.</CardDescription>
            </CardHeader>
            <CardContent className="space-y-0"> {/* Remove space-y-4, handled by py-3 */}
                {renderConfigInput(
                    'minPrice',
                    'minPricePercent',
                    minPriceLabel,
                    minPricePercentInput,
                    isMinPriceEnabled,
                    allowMinPriceConfiguration
                )}
                {renderConfigInput(
                    'overDeviation',
                    'overDeviation',
                    overDeviationLabel,
                    overDeviationInput,
                    isOverDeviationEnabled,
                    allowOverUnderDeviationConfiguration
                )}
                {renderConfigInput(
                    'underDeviation',
                    'underDeviation',
                    underDeviationLabel,
                    underDeviationInput,
                    isUnderDeviationEnabled,
                    allowOverUnderDeviationConfiguration
                )}
            </CardContent>
        </Card>
    );
});
ConfigurationPanel.displayName = "ConfigurationPanel";

/** Displays success or error messages with animation */
const NotificationAlert: React.FC<{ message: MessageState | null }> = React.memo(({ message }) => (
    <AnimatePresence>
        {message?.text && (
            <motion.div
                initial={{ opacity: 0, y: -20 }}
                animate={{ opacity: 1, y: 0 }}
                exit={{ opacity: 0, y: 20, transition: { duration: 0.3 } }}
                className="mb-6" // Adjust spacing as needed
                role="alert"
                aria-live="polite" // Announce changes politely
            >
                <Alert variant={message.type === 'error' ? "destructive" : "default"}>
                    {message.type === 'error' && <AlertCircle className="h-4 w-4" />}
                    <AlertTitle>{message.type === 'error' ? "Error" : "Success"}</AlertTitle>
                    <AlertDescription>{message.text}</AlertDescription>
                </Alert>
            </motion.div>
        )}
    </AnimatePresence>
));
NotificationAlert.displayName = "NotificationAlert";


// --- Main Component ---

/**
 * DealCheckerDemo component allows users to check if a new deal can be opened
 * based on its price relative to existing deals and configurable deviation percentages.
 * It also allows adding valid deals to a list. Uses percentage values in configuration inputs.
 */
const DealCheckerDemo: React.FC<DealCheckerDemoProps> = ({
    initialOverDeviation = 0.02,
    initialUnderDeviation = 0.03,
    initialDeals = [],
    allowAddDeals = true,
    allowCheckDeals = true,
    defaultMinPricePercentDecimal = 0.05,
    customValidation = () => null,
    maxPriceLabel = "Max Price (Entry/Avg)",
    minPriceLabel = "Min Price Deviation", // Simplified default label slightly
    allowMinPriceConfiguration = true,
    overDeviationLabel = "Over Deviation",
    underDeviationLabel = "Under Deviation",
    allowOverUnderDeviationConfiguration = true,
}) => {
    // --- State (remains the same) ---
    const [deals, setDeals] = useState<Deal[]>(initialDeals);
    const [priceInput, setPriceInput] = useState<string>('');
    const [canAddDeal, setCanAddDeal] = useState<boolean>(false);
    const [message, setMessage] = useState<MessageState | null>(null);
    const [configInputs, setConfigInputs] = useState({
        minPricePercent: decimalToPercentageString(defaultMinPricePercentDecimal),
        overDeviation: decimalToPercentageString(initialOverDeviation),
        underDeviation: decimalToPercentageString(initialUnderDeviation),
    });
    const [configEnabled, setConfigEnabled] = useState<{
        minPrice: boolean; overDeviation: boolean; underDeviation: boolean;
    }>({
        minPrice: true, overDeviation: true, underDeviation: true,
    });

    // --- Memoized Values (remains the same) ---
    const { cardTitle, cardDescription } = useMemo(() => {
        let title = "Deal Checker"; // ... (rest of logic is same)
        let description = "Check if a new deal can be opened.";
        if (allowAddDeals && allowCheckDeals) {
            title = "Check & Add Deal";
            description = "Enter a price, check validity, then add the deal.";
        } else if (allowAddDeals) {
            title = "Add New Deal";
            description = "Enter price and add a new deal directly (validation recommended).";
        } else if (allowCheckDeals) {
            title = "Check Deal Validity";
            description = "Enter a price to check if a new deal can be opened.";
        }
        return { cardTitle: title, cardDescription: description };
    }, [allowAddDeals, allowCheckDeals]);

    // --- Callbacks (remain the same) ---
    const clearMessage = useCallback(() => setMessage(null), []);
    const showTemporaryMessage = useCallback((text: string, type: 'success' | 'error') => {
        setMessage({ text, type });
        const timer = setTimeout(clearMessage, MESSAGE_TIMEOUT_MS);
    }, [clearMessage]);
    const handleConfigInputChange = useCallback((field: keyof typeof configInputs, value: string) => {
        setConfigInputs(prev => ({ ...prev, [field]: value }));
        setCanAddDeal(false);
    }, []);
    const handleConfigToggleChange = useCallback((field: keyof typeof configEnabled, checked: boolean) => {
        setConfigEnabled(prev => ({ ...prev, [field]: checked }));
        setCanAddDeal(false);
    }, []);
    const handleCheckDeal = useCallback(() => { // ... (logic remains the same)
        setMessage(null);
        const checkPrice = parsePositiveNumber(priceInput);

        if (isNaN(checkPrice)) {
            setMessage({ text: 'Please enter a valid positive price to check.', type: 'error' });
            setCanAddDeal(false); return;
        }
        const customValidationError = customValidation(checkPrice);
        if (customValidationError) {
            setMessage({ text: customValidationError, type: 'error' });
            setCanAddDeal(false); return;
        }
        const parsedMinPricePercent = configEnabled.minPrice ? parsePercentageString(configInputs.minPricePercent) : 0;
        const parsedOverDeviationPercent = configEnabled.overDeviation ? parsePercentageString(configInputs.overDeviation) : 0;
        const parsedUnderDeviationPercent = configEnabled.underDeviation ? parsePercentageString(configInputs.underDeviation) : 0;

        if (isNaN(parsedMinPricePercent) || isNaN(parsedOverDeviationPercent) || isNaN(parsedUnderDeviationPercent)) {
             setMessage({ text: 'Invalid configuration percentage(s). Please enter valid non-negative numbers.', type: 'error' });
             setCanAddDeal(false); return;
        }
        const finalConfig: CalculationConfig = {
            overDeviationPercent: parsedOverDeviationPercent,
            underDeviationPercent: parsedUnderDeviationPercent,
            minPricePercent: parsedMinPricePercent,
            useMinPrice: configEnabled.minPrice,
        };
        const result = checkIfDealCanBeOpened(checkPrice, deals, finalConfig);

        if (result.canOpen) {
            setMessage({ text: `A new deal can be opened at ${checkPrice.toFixed(2)}`, type: 'success' });
        } else {
            const lower = result.lowerBound?.toFixed(2) ?? 'N/A';
            const upper = result.upperBound?.toFixed(2) ?? 'N/A';
            const refPrice = result.blockingDeal?.max_price.toFixed(2) ?? 'N/A';
            setMessage({
                text: `Cannot open deal at ${checkPrice.toFixed(2)}. Conflicts with Deal (Ref Price: ${refPrice}). Consider prices outside ${lower} - ${upper}.`,
                type: 'error'
            });
        }
        setCanAddDeal(result.canOpen);
    }, [priceInput, customValidation, configInputs, configEnabled, deals]);
    const handleAddDeal = useCallback(() => { // ... (logic remains the same)
        if (!canAddDeal) {
            showTemporaryMessage('Price must be checked and available before adding.', 'error'); return;
        }
        const entryPrice = parsePositiveNumber(priceInput);
        if (isNaN(entryPrice)) {
            showTemporaryMessage('Invalid price. Cannot add deal.', 'error'); return;
        }
        const newDeal: Deal = { id: crypto.randomUUID(), max_price: entryPrice };
        setDeals(prevDeals => [...prevDeals, newDeal]);
        setPriceInput('');
        showTemporaryMessage('Deal added successfully.', 'success');
        setCanAddDeal(false);
    }, [canAddDeal, priceInput, showTemporaryMessage]);
    const handleDeleteDeal = useCallback((id: string) => { // ... (logic remains the same)
         setDeals(prevDeals => prevDeals.filter(deal => deal.id !== id));
        showTemporaryMessage('Deal deleted.', 'success');
        setCanAddDeal(false);
    }, [showTemporaryMessage]);
    // --- useEffect (remains the same) ---
    useEffect(() => {
        setConfigInputs(prev => ({
            ...prev,
            overDeviation: decimalToPercentageString(initialOverDeviation),
            underDeviation: decimalToPercentageString(initialUnderDeviation),
             minPricePercent: decimalToPercentageString(defaultMinPricePercentDecimal),
        }));
        setCanAddDeal(false);
    }, [initialOverDeviation, initialUnderDeviation, defaultMinPricePercentDecimal]);

    // --- Prepare display strings (remains the same) ---
    const formatPercentageForDisplay = (value: string) => {
        const num = parseFloat(value);
        return isNaN(num) ? 'Invalid' : num.toFixed(2);
    };
    const minPriceDisplay = configEnabled.minPrice ? formatPercentageForDisplay(configInputs.minPricePercent) : "0.00";
    const overDeviationDisplay = configEnabled.overDeviation ? formatPercentageForDisplay(configInputs.overDeviation) : "0.00";
    const underDeviationDisplay = configEnabled.underDeviation ? formatPercentageForDisplay(configInputs.underDeviation) : "0.00";

    // --- Render ---
    return (
        <div className="min-h-screen bg-gradient-to-br from-gray-900 to-gray-800 p-4 sm:p-8 text-gray-200 font-sans">
            <div className="max-w-3xl mx-auto space-y-6">
                {/* Title */}
                <h1 className="text-3xl sm:text-4xl md:text-5xl font-bold text-white text-center mb-6">
                    Deal Checker Demo
                </h1>

                {/* Info Panel */}
                <InfoPanel
                    maxPriceLabel={maxPriceLabel}
                    minPriceLabel={minPriceLabel}
                    minPricePercentDisplay={minPriceDisplay}
                    allowMinPriceConfiguration={allowMinPriceConfiguration}
                    overDeviationLabel={overDeviationLabel}
                    overDeviationDisplay={overDeviationDisplay}
                    underDeviationLabel={underDeviationLabel}
                    underDeviationDisplay={underDeviationDisplay}
                    allowOverUnderDeviationConfiguration={allowOverUnderDeviationConfiguration}
                 />

                {/* Configuration Panel (Conditional) - Uses the refactored component */}
                {(allowMinPriceConfiguration || allowOverUnderDeviationConfiguration) && (
                    <ConfigurationPanel
                        minPricePercentInput={configInputs.minPricePercent}
                        overDeviationInput={configInputs.overDeviation}
                        underDeviationInput={configInputs.underDeviation}
                        isMinPriceEnabled={configEnabled.minPrice}
                        isOverDeviationEnabled={configEnabled.overDeviation}
                        isUnderDeviationEnabled={configEnabled.underDeviation}
                        onInputChange={handleConfigInputChange}
                        onToggleChange={handleConfigToggleChange} // Pass the correct handler
                        allowMinPriceConfiguration={allowMinPriceConfiguration}
                        allowOverUnderDeviationConfiguration={allowOverUnderDeviationConfiguration}
                        minPriceLabel={minPriceLabel} // These props provide the text for the checkbox labels now
                        overDeviationLabel={overDeviationLabel}
                        underDeviationLabel={underDeviationLabel}
                    />
                 )}

                {/* Check/Add Form */}
                {(allowAddDeals || allowCheckDeals) && (
                     <DealForm
                        priceInput={priceInput}
                        onPriceInputChange={setPriceInput}
                        onCheck={handleCheckDeal}
                        onAdd={handleAddDeal}
                        canAddDeal={canAddDeal}
                        allowCheckDeals={allowCheckDeals}
                        allowAddDeals={allowAddDeals}
                        maxPriceLabel={maxPriceLabel}
                        cardTitle={cardTitle}
                        cardDescription={cardDescription}
                    />
                )}

                {/* Notification */}
                <NotificationAlert message={message} />
 
                {/* Deals List */}
                <DealList deals={deals} onDelete={handleDeleteDeal} maxPriceLabel={maxPriceLabel} />
            </div>
        </div>
    );
};

// --- Export ---
export default DealCheckerDemo;

This is the missing code.

/** Displays informative text about the demo */
const InfoPanel: React.FC<{
    maxPriceLabel: string;
    minPriceLabel: string;
    minPricePercentDisplay: string; // Expecting a string ready for display
    allowMinPriceConfiguration: boolean;
    overDeviationLabel: string;
    overDeviationDisplay: string; // Expecting a string ready for display
    underDeviationLabel: string;
    underDeviationDisplay: string; // Expecting a string ready for display
    allowOverUnderDeviationConfiguration: boolean;
}> = React.memo(({
    maxPriceLabel, minPriceLabel, minPricePercentDisplay, allowMinPriceConfiguration,
    overDeviationLabel, overDeviationDisplay, underDeviationLabel, underDeviationDisplay,
    allowOverUnderDeviationConfiguration
}) => (
    <Card className="bg-gray-900/90 backdrop-blur-md border border-gray-800">
        <CardHeader>
            <CardTitle className="text-xl sm:text-2xl font-semibold text-gray-200">Demo Information</CardTitle>
            <CardDescription className="text-gray-400">Key configurations and usage instructions.</CardDescription>
        </CardHeader>
        <CardContent className='text-gray-400 text-sm space-y-3'>
            <p>
                This demo simulates a deal checker. It prevents opening new deals
                if the input price falls within a calculated deviation range of an existing deal's
                reference price ({maxPriceLabel}).
            </p>
            <p>
                Test it by entering a price, adjusting configurations (in %), checking validity, and then adding the deal if possible.
            </p>
            <div>
                <p className="font-semibold text-gray-300 mb-1">Current Configurations (as %):</p>
                <ul className='list-disc list-inside space-y-1 pl-2'>
                    <li>
                        <b>{minPriceLabel}:</b> {minPricePercentDisplay}% {allowMinPriceConfiguration ? "(Configurable)" : "(Fixed)"}
                    </li>
                    <li>
                        <b>{overDeviationLabel}:</b> {overDeviationDisplay}% {allowOverUnderDeviationConfiguration ? "(Configurable)" : "(Fixed)"}
                    </li>
                    <li>
                        <b>{underDeviationLabel}:</b> {underDeviationDisplay}% {allowOverUnderDeviationConfiguration ? "(Configurable)" : "(Fixed)"}
                    </li>
                </ul>
            </div>
            <p className="pt-2 text-xs text-gray-500">
                Note: Internal calculations derive factors (like 1.05 for +5% or 0.97 for -3%) from these percentage values using the `toChangeFactor` utility before checking price boundaries.
            </p>
        </CardContent>
    </Card>
));
InfoPanel.displayName = "InfoPanel";

What I currently haven’t thought of is what should happen if we have under 2% and over 3%. If the first deal starts at 100 then under would allow the next deal at 98 but the over of 98 wouldn’t allow it. The next price that would allow under of 100 and over of the next deal’s price would be at 100 / 1.03 = 97.087.

Of course, it would also work as it in the above dem, where only the borders of existing deals define where new orders can be placed, but maybe it could be another option that also the new deal checks it’s borders and whether those overlap.

That is, to get a geometric grid in both directions in above demo we would have to use

under = (1 - 1 / (1 + over / 100)) * 100

Example

over  = 5
under = (1 - 1 / 1.05) * 100 = 4.76

over  = 25
under = (1 - 1 / 1.25) * 100 = 20

This is the updated code for

import React, { useState, useCallback, useEffect, useMemo } from 'react';
// Make sure these paths are correct for your project setup
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { cn } from '@/lib/utils';
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { AlertCircle } from "lucide-react";
import { motion, AnimatePresence } from 'framer-motion';
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";

// --- Constants ---
const MESSAGE_TIMEOUT_MS = 3000;
const PERCENTAGE_FACTOR = 100;

// --- Types ---
interface Deal {
    id: string;
    max_price: number;
}

interface DealCheckerDemoProps {
    initialOverDeviation?: number;
    initialUnderDeviation?: number;
    initialDeals?: Deal[];
    allowAddDeals?: boolean;
    allowCheckDeals?: boolean;
    defaultMinPricePercentDecimal?: number;
    customValidation?: (price: number) => string | null;
    maxPriceLabel?: string;
    minPriceLabel?: string;
    allowMinPriceConfiguration?: boolean;
    overDeviationLabel?: string;
    underDeviationLabel?: string;
    allowOverUnderDeviationConfiguration?: boolean;
    initialCheckNewDealBoundaries?: boolean;
}

enum BlockReason {
    PRICE_IN_EXISTING_RANGE = 'PRICE_IN_EXISTING_RANGE',
    NEW_RANGE_OVERLAPS_EXISTING_PRICE = 'NEW_RANGE_OVERLAPS_EXISTING_PRICE',
    CALCULATION_ERROR = 'CALCULATION_ERROR',
}

interface CheckResult {
    canOpen: boolean;
    blockingDeal?: Deal;
    overlappedDeal?: Deal;
    lowerBound?: number; // Range of the *blocking* deal
    upperBound?: number; // Range of the *blocking* deal
    newDealLowerBound?: number; // Range calculated *from* checkPrice
    newDealUpperBound?: number; // Range calculated *from* checkPrice
    blockReason?: BlockReason;
}

interface CalculationConfig {
    overDeviationPercent: number;
    underDeviationPercent: number;
    minPricePercent: number;
    useMinPrice: boolean;
}

interface MessageState {
    text: string | null;
    type: 'success' | 'error' | null;
}

// --- Utility Functions ---
const parsePositiveNumber = (value: string): number => {
    const num = parseFloat(value);
    return !isNaN(num) && num > 0 ? num : NaN;
};
const parsePercentageString = (value: string): number => {
    const num = parseFloat(value);
    return !isNaN(num) && num >= 0 ? num : NaN;
};
const decimalToPercentageString = (decimal: number): string => {
    return (Math.round(decimal * PERCENTAGE_FACTOR * 100) / 100).toString();
};
const toChangeFactor = (percentage: number): number => {
    if (isNaN(percentage)) { console.warn("NaN in toChangeFactor"); return 1; }
    const factor = 1 + (percentage / PERCENTAGE_FACTOR);
    // Prevent division by zero or near-zero issues later
    return factor <= 0 ? 0.000001 : factor; // Return a very small positive number instead of 0 or negative
};
const calculateDealBoundaries = (refPrice: number, config: CalculationConfig): { lowerBound: number; upperBound: number } => {
    const minPriceRefFactor = config.useMinPrice ? toChangeFactor(-config.minPricePercent) : 1;
    const minPriceRef = refPrice * minPriceRefFactor;
    const lowerBound = minPriceRef * toChangeFactor(-config.underDeviationPercent);
    const upperBound = refPrice * toChangeFactor(config.overDeviationPercent);
    return { lowerBound, upperBound };
};
const checkIfDealCanBeOpened = ( checkPrice: number, deals: Deal[], config: CalculationConfig, checkNewDealBoundariesOption: boolean ): CheckResult => {
    if (isNaN(checkPrice) || checkPrice <= 0) return { canOpen: false, blockReason: BlockReason.CALCULATION_ERROR };
    let newDealLowerBound: number | undefined, newDealUpperBound: number | undefined;
    if (checkNewDealBoundariesOption) {
        const bounds = calculateDealBoundaries(checkPrice, config);
        newDealLowerBound = bounds.lowerBound; newDealUpperBound = bounds.upperBound;
        if (isNaN(newDealLowerBound) || isNaN(newDealUpperBound)) { console.warn(`Invalid bounds for new deal`); return { canOpen: false, blockReason: BlockReason.CALCULATION_ERROR }; }
    }
    for (const existingDeal of deals) {
        if (isNaN(existingDeal.max_price) || existingDeal.max_price <= 0) { console.warn(`Skipping invalid existing deal ${existingDeal.id}`); continue; }
        const { lowerBound: existingLb, upperBound: existingUb } = calculateDealBoundaries(existingDeal.max_price, config);
        if (isNaN(existingLb) || isNaN(existingUb)) { console.warn(`Invalid bounds for existing deal ${existingDeal.id}`); continue; }
        if (existingLb < checkPrice && checkPrice < existingUb) {
            return { canOpen: false, blockingDeal: existingDeal, lowerBound: existingLb, upperBound: existingUb, blockReason: BlockReason.PRICE_IN_EXISTING_RANGE };
        }
        if (checkNewDealBoundariesOption && newDealLowerBound !== undefined && newDealUpperBound !== undefined) {
             if (newDealLowerBound < existingDeal.max_price && existingDeal.max_price < newDealUpperBound) {
                return { canOpen: false, overlappedDeal: existingDeal, newDealLowerBound: newDealLowerBound, newDealUpperBound: newDealUpperBound, blockReason: BlockReason.NEW_RANGE_OVERLAPS_EXISTING_PRICE };
            }
        }
    }
    return { canOpen: true };
};

// --- Sub-Components (Assumed Unchanged - Full code included below) ---
const DealList: React.FC<{ deals: Deal[]; onDelete: (id: string) => void; maxPriceLabel: string; }> = React.memo(({ deals, onDelete, maxPriceLabel }) => ( <Card className="bg-gray-900/90 backdrop-blur-md border border-gray-800"><CardHeader><CardTitle className="text-xl sm:text-2xl font-semibold text-gray-200">Existing Deals</CardTitle><CardDescription className="text-gray-400">Deals currently being tracked.</CardDescription></CardHeader><CardContent>{deals.length === 0 ? <p className="text-gray-400">No deals added yet.</p> : <div className="space-y-2"><AnimatePresence initial={false}>{deals.map((deal) => (<motion.div key={deal.id} layout initial={{ opacity: 0, x: -20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: 50, transition: { duration: 0.2 } }} className="flex items-center justify-between bg-gray-800/80 border border-gray-700 rounded-md p-2 sm:p-3"><span className="text-gray-300 truncate pr-2" title={deal.max_price.toString()}>{maxPriceLabel}: {deal.max_price.toFixed(2)}</span><Button variant="destructive" size="sm" onClick={() => onDelete(deal.id)} className="bg-red-500/20 text-red-400 hover:bg-red-500/30 hover:text-red-300 flex-shrink-0" aria-label={`Delete deal with price ${deal.max_price.toFixed(2)}`}>Delete</Button></motion.div>))}</AnimatePresence></div>}</CardContent></Card> ));
DealList.displayName = "DealList";
const DealForm: React.FC<{ priceInput: string; onPriceInputChange: (value: string) => void; checkNewDeal: boolean; onCheckNewDealChange: (checked: boolean) => void; onCheck: () => void; onAdd: () => void; canAddDeal: boolean; allowCheckDeals: boolean; allowAddDeals: boolean; maxPriceLabel: string; cardTitle: string; cardDescription: string; }> = React.memo(({ priceInput, onPriceInputChange, checkNewDeal, onCheckNewDealChange, onCheck, onAdd, canAddDeal, allowCheckDeals, allowAddDeals, maxPriceLabel, cardTitle, cardDescription }) => ( <Card className="bg-gray-900/90 backdrop-blur-md border border-gray-800"><CardHeader><CardTitle className="text-xl sm:text-2xl font-semibold text-gray-200">{cardTitle}</CardTitle><CardDescription className="text-gray-400">{cardDescription}</CardDescription></CardHeader><CardContent className="space-y-4"><Input type="number" placeholder={maxPriceLabel} value={priceInput} onChange={(e) => onPriceInputChange(e.target.value)} className="bg-gray-800/80 border-gray-700 text-white placeholder:text-gray-400 w-full" aria-label={maxPriceLabel} step="any" min="0" /><div className="flex items-center space-x-2"><Checkbox id="check-new-deal-boundaries" checked={checkNewDeal} onCheckedChange={onCheckNewDealChange} className="border-gray-700 data-[state=checked]:bg-blue-600 data-[state=checked]:border-blue-500" /><Label htmlFor="check-new-deal-boundaries" className="text-sm font-medium text-gray-300 select-none cursor-pointer">Check New Deal Boundaries (Two-Way Check)</Label></div><div className="flex flex-col sm:flex-row gap-2">{allowCheckDeals && <Button onClick={onCheck} className="bg-green-500/20 text-green-400 hover:bg-green-500/30 hover:text-green-300 flex-1">Check Availability</Button>}{allowAddDeals && <Button onClick={onAdd} className="bg-blue-500/20 text-blue-400 hover:bg-blue-500/30 hover:text-blue-300 flex-1" disabled={!canAddDeal} aria-disabled={!canAddDeal}>Add Deal</Button>}</div></CardContent></Card> ));
DealForm.displayName = "DealForm";
const ConfigurationPanel: React.FC<{ minPricePercentInput: string; overDeviationInput: string; underDeviationInput: string; isMinPriceEnabled: boolean; isOverDeviationEnabled: boolean; isUnderDeviationEnabled: boolean; onInputChange: (field: 'minPricePercent' | 'overDeviation' | 'underDeviation', value: string) => void; onToggleChange: (field: 'minPrice' | 'overDeviation' | 'underDeviation', checked: boolean) => void; allowMinPriceConfiguration: boolean; allowOverUnderDeviationConfiguration: boolean; minPriceLabel: string; overDeviationLabel: string; underDeviationLabel: string; }> = React.memo(({ minPricePercentInput, overDeviationInput, underDeviationInput, isMinPriceEnabled, isOverDeviationEnabled, isUnderDeviationEnabled, onInputChange, onToggleChange, allowMinPriceConfiguration, allowOverUnderDeviationConfiguration, minPriceLabel, overDeviationLabel, underDeviationLabel }) => { const renderConfigInput = ( fieldKey: keyof typeof configEnabled, inputKey: keyof typeof configInputs, labelText: string, inputValue: string, isEnabled: boolean, allowToggle: boolean ) => { const inputId = `${fieldKey}-input`; const checkboxId = `${fieldKey}-checkbox`; const checkboxLabelId = `${fieldKey}-label`; return ( <div className="flex flex-col sm:flex-row sm:items-center gap-3 py-3 border-b border-gray-800 last:border-b-0"> {allowToggle && (<div className="flex items-center gap-2 flex-shrink-0 w-full sm:w-auto"><Checkbox id={checkboxId} checked={isEnabled} onCheckedChange={(checked) => onToggleChange(fieldKey, Boolean(checked))} className="border-gray-700 data-[state=checked]:bg-blue-600 data-[state=checked]:border-blue-500" aria-labelledby={checkboxLabelId} /><Label id={checkboxLabelId} htmlFor={checkboxId} className="text-sm text-gray-300 cursor-pointer select-none font-medium">{labelText}</Label></div>)} <div className={cn("w-full", allowToggle && "sm:ml-auto")}> <Input id={inputId} type="number" placeholder="Enter %" value={inputValue} onChange={(e) => onInputChange(inputKey, e.target.value)} className={cn("w-full sm:w-32 bg-gray-800/80 border-gray-700 text-white placeholder:text-gray-400", !isEnabled && "opacity-50 cursor-not-allowed")} disabled={!isEnabled} aria-label={`${labelText} Percentage Input`} step="any" min="0" /> </div> </div> ); }; return ( <Card className="bg-gray-900/90 backdrop-blur-md border border-gray-800"><CardHeader><CardTitle className="text-xl sm:text-2xl font-semibold text-gray-200">Configuration</CardTitle><CardDescription className="text-gray-400">Enable and set deviation percentages.</CardDescription></CardHeader><CardContent className="space-y-0">{renderConfigInput('minPrice', 'minPricePercent', minPriceLabel, minPricePercentInput, isMinPriceEnabled, allowMinPriceConfiguration)}{renderConfigInput('overDeviation', 'overDeviation', overDeviationLabel, overDeviationInput, isOverDeviationEnabled, allowOverUnderDeviationConfiguration)}{renderConfigInput('underDeviation', 'underDeviation', underDeviationLabel, underDeviationInput, isUnderDeviationEnabled, allowOverUnderDeviationConfiguration)}</CardContent></Card> ); });
ConfigurationPanel.displayName = "ConfigurationPanel";
const InfoPanel: React.FC<{ maxPriceLabel: string; minPriceLabel: string; minPricePercentDisplay: string; allowMinPriceConfiguration: boolean; overDeviationLabel: string; overDeviationDisplay: string; underDeviationLabel: string; underDeviationDisplay: string; allowOverUnderDeviationConfiguration: boolean; checkNewDealBoundaries: boolean; }> = React.memo(({ maxPriceLabel, minPriceLabel, minPricePercentDisplay, allowMinPriceConfiguration, overDeviationLabel, overDeviationDisplay, underDeviationLabel, underDeviationDisplay, allowOverUnderDeviationConfiguration, checkNewDealBoundaries }) => ( <Card className="bg-gray-900/90 backdrop-blur-md border border-gray-800"><CardHeader><CardTitle className="text-xl sm:text-2xl font-semibold text-gray-200">Demo Information</CardTitle><CardDescription className="text-gray-400">Key configurations and usage instructions.</CardDescription></CardHeader><CardContent className='text-gray-400 text-sm space-y-3'><p>This demo simulates a deal checker. It prevents opening new deals if the input price falls within a calculated deviation range of an existing deal's reference price ({maxPriceLabel}).</p><p>Test it by entering a price, adjusting configurations (in %), checking validity, and then adding the deal if possible.</p><div><p className="font-semibold text-gray-300 mb-1">Current Configurations (as %):</p><ul className='list-disc list-inside space-y-1 pl-2'><li><b>{minPriceLabel}:</b> {minPricePercentDisplay}% {allowMinPriceConfiguration ? "(Configurable)" : "(Fixed)"}</li><li><b>{overDeviationLabel}:</b> {overDeviationDisplay}% {allowOverUnderDeviationConfiguration ? "(Configurable)" : "(Fixed)"}</li><li><b>{underDeviationLabel}:</b> {underDeviationDisplay}% {allowOverUnderDeviationConfiguration ? "(Configurable)" : "(Fixed)"}</li><li className={cn(checkNewDealBoundaries ? "text-green-400" : "text-gray-500")}><b>Two-Way Boundary Check:</b> {checkNewDealBoundaries ? "Enabled" : "Disabled"}</li></ul></div>{checkNewDealBoundaries && (<p className="text-xs text-blue-400">Two-Way Check is ON: A new deal is also blocked if its calculated range would overlap the price of an existing deal.</p>)}{!checkNewDealBoundaries && (<p className="text-xs text-gray-500">Two-Way Check is OFF: Only checks if the new price falls within an existing deal's range.</p>)}<p className="pt-1 text-xs text-gray-500">Note: Internal calculations derive factors (like 1.05 for +5% or 0.97 for -3%) from percentage values using `toChangeFactor`.</p></CardContent></Card> ));
InfoPanel.displayName = "InfoPanel";
const NotificationAlert: React.FC<{ message: MessageState | null }> = React.memo(({ message }) => ( <AnimatePresence>{message?.text && (<motion.div initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: 20, transition: { duration: 0.3 } }} className="mb-6" role="alert" aria-live="polite"><Alert variant={message.type === 'error' ? "destructive" : "default"}>{message.type === 'error' && <AlertCircle className="h-4 w-4" />}<AlertTitle>{message.type === 'error' ? "Error" : "Success"}</AlertTitle><AlertDescription>{message.text}</AlertDescription></Alert></motion.div>)}</AnimatePresence> ));
NotificationAlert.displayName = "NotificationAlert";

// --- Main Component (`DealCheckerDemo`) ---
const DealCheckerDemo: React.FC<DealCheckerDemoProps> = ({
    initialOverDeviation = 0.02, initialUnderDeviation = 0.03, initialDeals = [],
    allowAddDeals = true, allowCheckDeals = true, defaultMinPricePercentDecimal = 0.05,
    customValidation = () => null, maxPriceLabel = "Max Price (Entry/Avg)",
    minPriceLabel = "Min Price Deviation", allowMinPriceConfiguration = true,
    overDeviationLabel = "Over Deviation", underDeviationLabel = "Under Deviation",
    allowOverUnderDeviationConfiguration = true,
    initialCheckNewDealBoundaries = false,
}) => {
    // --- State ---
    const [deals, setDeals] = useState<Deal[]>(initialDeals);
    const [priceInput, setPriceInput] = useState<string>('');
    const [canAddDeal, setCanAddDeal] = useState<boolean>(false);
    const [message, setMessage] = useState<MessageState | null>(null);
    const [configInputs, setConfigInputs] = useState({
        minPricePercent: decimalToPercentageString(defaultMinPricePercentDecimal),
        overDeviation: decimalToPercentageString(initialOverDeviation),
        underDeviation: decimalToPercentageString(initialUnderDeviation),
    });
    const [configEnabled, setConfigEnabled] = useState<{ minPrice: boolean; overDeviation: boolean; underDeviation: boolean; }>({
        minPrice: true, overDeviation: true, underDeviation: true,
    });
    const [checkNewDealBorders, setCheckNewDealBorders] = useState<boolean>(initialCheckNewDealBoundaries);

    // --- Memoized Values ---
    const { cardTitle, cardDescription } = useMemo(() => {
        let title = "Deal Checker"; let description = "Check if a new deal can be opened.";
        if (allowAddDeals && allowCheckDeals) { title = "Check & Add Deal"; description = "Enter a price, check validity, then add the deal."; }
        else if (allowAddDeals) { title = "Add New Deal"; description = "Enter price and add a new deal directly (validation recommended)."; }
        else if (allowCheckDeals) { title = "Check Deal Validity"; description = "Enter a price to check if a new deal can be opened."; }
        return { cardTitle: title, cardDescription: description };
    }, [allowAddDeals, allowCheckDeals]);

    // --- Callbacks ---
    const clearMessage = useCallback(() => setMessage(null), []);
    const showTemporaryMessage = useCallback((text: string, type: 'success' | 'error') => { setMessage({ text, type }); setTimeout(clearMessage, MESSAGE_TIMEOUT_MS); }, [clearMessage]);
    const handleConfigInputChange = useCallback((field: keyof typeof configInputs, value: string) => { setConfigInputs(prev => ({ ...prev, [field]: value })); setCanAddDeal(false); }, []);
    const handleConfigToggleChange = useCallback((field: keyof typeof configEnabled, checked: boolean) => { setConfigEnabled(prev => ({ ...prev, [field]: checked })); setCanAddDeal(false); }, []);
    const handleCheckNewDealChange = useCallback((checked: boolean) => { setCheckNewDealBorders(checked); setCanAddDeal(false); }, []);

    // *** Updated handleCheckDeal with CORRECTED messaging ***
    const handleCheckDeal = useCallback(() => {
        setMessage(null);
        const checkPrice = parsePositiveNumber(priceInput);
        if (isNaN(checkPrice)) { setMessage({ text: 'Please enter a valid positive price to check.', type: 'error' }); setCanAddDeal(false); return; }
        const customValidationError = customValidation(checkPrice);
        if (customValidationError) { setMessage({ text: customValidationError, type: 'error' }); setCanAddDeal(false); return; }

        const parsedMinPricePercent = configEnabled.minPrice ? parsePercentageString(configInputs.minPricePercent) : 0;
        const parsedOverDeviationPercent = configEnabled.overDeviation ? parsePercentageString(configInputs.overDeviation) : 0;
        const parsedUnderDeviationPercent = configEnabled.underDeviation ? parsePercentageString(configInputs.underDeviation) : 0;
        if (isNaN(parsedMinPricePercent) || isNaN(parsedOverDeviationPercent) || isNaN(parsedUnderDeviationPercent)) { setMessage({ text: 'Invalid configuration percentage(s). Please enter valid non-negative numbers.', type: 'error' }); setCanAddDeal(false); return; }

        const finalConfig: CalculationConfig = {
            overDeviationPercent: parsedOverDeviationPercent, underDeviationPercent: parsedUnderDeviationPercent,
            minPricePercent: parsedMinPricePercent, useMinPrice: configEnabled.minPrice
        };

        const result = checkIfDealCanBeOpened(checkPrice, deals, finalConfig, checkNewDealBorders);

        if (result.canOpen) {
            setMessage({ text: `A new deal can be opened at ${checkPrice.toFixed(2)}`, type: 'success' });
        } else {
            let blockMessage = `Cannot open deal at ${checkPrice.toFixed(2)}.`;
            switch (result.blockReason) {
                case BlockReason.PRICE_IN_EXISTING_RANGE:
                    if (result.blockingDeal && result.lowerBound !== undefined && result.upperBound !== undefined) {
                        blockMessage += ` Price is within the forbidden range (${result.lowerBound.toFixed(2)} - ${result.upperBound.toFixed(2)}) of Deal (Ref Price: ${result.blockingDeal.max_price.toFixed(2)}). Consider prices outside this range.`;
                    } else { blockMessage += ` Price is too close to an existing deal.`; }
                    break;

                case BlockReason.NEW_RANGE_OVERLAPS_EXISTING_PRICE:
                    if (result.overlappedDeal && result.newDealLowerBound !== undefined && result.newDealUpperBound !== undefined) {
                        const overlappedPrice = result.overlappedDeal.max_price;
                        const overlappedPriceStr = overlappedPrice.toFixed(2);

                        if (overlappedPrice > checkPrice) {
                            // Existing price is ABOVE checkPrice. Conflict is: overlappedPrice < newDealUpperBound.
                            // User needs to go lower. Boundary = overlappedPrice / (1 + over%)
                            const upperBoundaryFactor = toChangeFactor(finalConfig.overDeviationPercent);
                            // Ensure factor is positive to avoid division issues
                            const boundary = upperBoundaryFactor > 0 ? overlappedPrice / upperBoundaryFactor : 0;
                            blockMessage += ` Existing Deal at ${overlappedPriceStr} is too close ABOVE. It falls within the calculated upper range (${result.newDealUpperBound.toFixed(2)}) based on ${checkPrice.toFixed(2)}. Try prices below ${boundary.toFixed(2)}.`;
                        } else {
                            // Existing price is BELOW checkPrice. Conflict is: newDealLowerBound < overlappedPrice.
                            // User needs to go higher. Boundary = overlappedPrice / [(1 - min%) * (1 - under%)]
                            const lowerBoundaryFactor = (configEnabled.minPrice ? toChangeFactor(-finalConfig.minPricePercent) : 1) * toChangeFactor(-finalConfig.underDeviationPercent);
                             // Ensure factor is positive
                            const boundary = lowerBoundaryFactor > 0 ? overlappedPrice / lowerBoundaryFactor : Infinity; // If factor is 0, effectively no upper limit needed
                            blockMessage += ` Existing Deal at ${overlappedPriceStr} is too close BELOW. It falls within the calculated lower range (${result.newDealLowerBound.toFixed(2)}) based on ${checkPrice.toFixed(2)}. Try prices above ${boundary.toFixed(2)}.`;
                        }
                    } else { blockMessage += ` Potential new deal range overlaps an existing deal.`; }
                    break;

                case BlockReason.CALCULATION_ERROR:
                     blockMessage += ` Calculation error occurred during check (invalid price or configuration).`; break;
                default:
                    blockMessage += ` Conflicts with existing deals or configuration.`; break;
            }
            setMessage({ text: blockMessage, type: 'error' });
        }
        setCanAddDeal(result.canOpen);

    }, [priceInput, customValidation, configInputs, configEnabled, deals, checkNewDealBorders]); // Keep dependencies

    const handleAddDeal = useCallback(() => { if (!canAddDeal) { showTemporaryMessage('Price must be checked and available before adding.', 'error'); return; } const entryPrice = parsePositiveNumber(priceInput); if (isNaN(entryPrice)) { showTemporaryMessage('Invalid price. Cannot add deal.', 'error'); return; } const newDeal: Deal = { id: crypto.randomUUID(), max_price: entryPrice }; setDeals(prevDeals => [...prevDeals, newDeal]); setPriceInput(''); showTemporaryMessage('Deal added successfully.', 'success'); setCanAddDeal(false); }, [canAddDeal, priceInput, showTemporaryMessage]);
    const handleDeleteDeal = useCallback((id: string) => { setDeals(prevDeals => prevDeals.filter(deal => deal.id !== id)); showTemporaryMessage('Deal deleted.', 'success'); setCanAddDeal(false); }, [showTemporaryMessage]);

    // --- useEffect ---
    useEffect(() => { setConfigInputs(prev => ({ ...prev, overDeviation: decimalToPercentageString(initialOverDeviation), underDeviation: decimalToPercentageString(initialUnderDeviation), minPricePercent: decimalToPercentageString(defaultMinPricePercentDecimal) })); setCanAddDeal(false); }, [initialOverDeviation, initialUnderDeviation, defaultMinPricePercentDecimal]);

    // --- Prepare display strings ---
    const formatPercentageForDisplay = (value: string) => { const num = parseFloat(value); return isNaN(num) ? 'Invalid' : num.toFixed(2); };
    const minPriceDisplay = configEnabled.minPrice ? formatPercentageForDisplay(configInputs.minPricePercent) : "Disabled";
    const overDeviationDisplay = configEnabled.overDeviation ? formatPercentageForDisplay(configInputs.overDeviation) : "Disabled";
    const underDeviationDisplay = configEnabled.underDeviation ? formatPercentageForDisplay(configInputs.underDeviation) : "Disabled";

    // --- Render ---
    return (
        <div className="min-h-screen bg-gradient-to-br from-gray-900 to-gray-800 p-4 sm:p-8 text-gray-200 font-sans">
            <div className="max-w-3xl mx-auto space-y-6">
                <h1 className="text-3xl sm:text-4xl md:text-5xl font-bold text-white text-center mb-6">Deal Checker Demo</h1>
                <NotificationAlert message={message} />
                <DealList deals={deals} onDelete={handleDeleteDeal} maxPriceLabel={maxPriceLabel} />
                {(allowMinPriceConfiguration || allowOverUnderDeviationConfiguration) && ( <ConfigurationPanel minPricePercentInput={configInputs.minPricePercent} overDeviationInput={configInputs.overDeviation} underDeviationInput={configInputs.underDeviation} isMinPriceEnabled={configEnabled.minPrice} isOverDeviationEnabled={configEnabled.overDeviation} isUnderDeviationEnabled={configEnabled.underDeviation} onInputChange={handleConfigInputChange} onToggleChange={handleConfigToggleChange} allowMinPriceConfiguration={allowMinPriceConfiguration} allowOverUnderDeviationConfiguration={allowOverUnderDeviationConfiguration} minPriceLabel={minPriceLabel} overDeviationLabel={overDeviationLabel} underDeviationLabel={underDeviationLabel} /> )}
                {(allowAddDeals || allowCheckDeals) && ( <DealForm priceInput={priceInput} onPriceInputChange={setPriceInput} checkNewDeal={checkNewDealBorders} onCheckNewDealChange={handleCheckNewDealChange} onCheck={handleCheckDeal} onAdd={handleAddDeal} canAddDeal={canAddDeal} allowCheckDeals={allowCheckDeals} allowAddDeals={allowAddDeals} maxPriceLabel={maxPriceLabel} cardTitle={cardTitle} cardDescription={cardDescription} /> )}
                <InfoPanel maxPriceLabel={maxPriceLabel} minPriceLabel={minPriceLabel} minPricePercentDisplay={minPriceDisplay} allowMinPriceConfiguration={allowMinPriceConfiguration} overDeviationLabel={overDeviationLabel} overDeviationDisplay={overDeviationDisplay} underDeviationLabel={underDeviationLabel} underDeviationDisplay={underDeviationDisplay} allowOverUnderDeviationConfiguration={allowOverUnderDeviationConfiguration} checkNewDealBoundaries={checkNewDealBorders} />
            </div>
        </div>
    );
};

// --- Export ---
export default DealCheckerDemo;

How this Overlap Check works

The system checks for deal overlaps in two main ways:

1. One-Way Check: Is the New Price Inside an Existing Deal’s Range?

  • For each existing deal, the system calculates a lower and upper bound around its reference price (max_price).

  • These bounds are determined by:

    • Upper Bound: max_price + (over_deviation %)
      (e.g., if max_price = $100 and over_deviation = 5%, upper bound = $105)

    • Lower Bound: max_price - (min_price %) and then - (under_deviation %)
      (e.g., if max_price = $100, min_price = 10%$90, then under_deviation = 3%$87.30)

  • Check: If the new price falls between any existing deal’s lower and upper bounds, it is blocked.

2. Two-Way Check (Optional): Does the New Deal’s Range Overlap an Existing Price?

  • If enabled, the system also calculates a range around the new price using the same deviation rules.

  • It then checks if any existing deal’s max_price falls inside this new range.

  • Why?

    • Prevents a new deal from “encroaching” on an existing price, even if the new price itself is outside existing ranges.

    • Example:

      • New price = $107 (outside an existing deal’s range of $87.30–$105).

      • But if the new deal’s calculated range is $102–$112, and an existing deal is at $100, it still blocks because $100 is inside $102–$112.


Why Checking Existing Deals One-by-One is Enough

  1. Exhaustive Check:

    • Each existing deal is checked independently against the new price (and optionally, the new range).

    • If any single existing deal causes an overlap, the new deal is blocked—no need to check further.

  2. No Need for Cross-Comparison Between Existing Deals:

    • The system doesn’t need to check if existing deals overlap with each other because:

      • The rules only care about new vs. existing, not existing vs. existing.

      • Existing deals are assumed to have been validated when they were added.

  3. Efficiency:

    • The check is O(n) (linear time), where n = number of existing deals.

    • Since each check is a simple numeric comparison (lower < price < upper), it’s fast even with many deals.

  4. Two-Way Check Adds Symmetry:

    • Without it, a new deal could be just outside an existing range but still “too close” to an existing price.

    • The two-way check ensures both directions are protected.


Key Takeaways

  • One-Way Check: Ensures the new price isn’t inside any existing deal’s range.

  • Two-Way Check (Optional): Ensures no existing price is inside the new deal’s range.

  • Why One-by-One Works:

    • Each existing deal is checked independently.

    • Only one overlap is needed to block the new deal.

    • No need for complex pairwise comparisons between existing deals.

This approach ensures accuracy (no overlaps missed) while keeping the logic simple and efficient.

I added a web-version of above code to my homepage: