feat: Add comprehensive data validation system

- Add --validate command for detecting data quality issues
- Implement adaptive price change monitoring with 3-month learning scope
- Configurable threshold (default 1%) with --change-threshold option
- Detect potential data corruption when price changes exceed thresholds
- Support for validating specific currencies or all currencies
- JSON and text output formats for validation results
- Severity classification: minor, moderate, severe violations
- Adaptive threshold calculation based on currency volatility
- Data quality scoring system
- Comprehensive CLI argument parsing with --no-adaptive option

Core validation features:
- Price change anomaly detection between consecutive dates
- Adaptive threshold learning from 3-month historical data
- Corruption risk assessment for extreme changes
- Structured reporting with violation details and recommendations
- Multi-currency validation support
- Configurable sensitivity levels

Technical implementation:
- New data_validator.py module with validation algorithms
- Integrated CLI support with argument parsing
- JSON schema for programmatic consumption
- Backward compatible with existing functionality

Usage examples:
  python src/cli.py --validate --currency USD --year 2025
  python src/cli.py --validate --all-currencies --change-threshold 0.5 --json
  python src/cli.py --validate --currency EUR --no-adaptive
This commit is contained in:
kdusek
2026-01-12 23:05:47 +01:00
parent ed5d126d77
commit 7d9dfa309c
2 changed files with 534 additions and 20 deletions

View File

@@ -9,11 +9,12 @@ from datetime import datetime
# Přidání adresáře src do sys.path, aby bylo možné importovat moduly
sys.path.insert(0, os.path.join(os.path.dirname(__file__)))
import data_fetcher
import database
import data_fetcher
import holidays
import rate_finder
import rate_reporter
import data_validator
# Global debug flag
DEBUG = False
@@ -36,6 +37,7 @@ def set_debug_mode(debug):
holidays.set_debug_mode(DEBUG)
rate_finder.set_debug_mode(DEBUG)
rate_reporter.set_debug_mode(DEBUG)
data_validator.set_debug_mode(DEBUG)
def format_single_rate_json(
@@ -195,6 +197,46 @@ def main():
"Pokud je zadán rok, vytvoří kurz pro konkrétní rok. "
"Pokud není rok zadán, vytvoří kurzy pro všechny roky s dostupnými daty.",
)
parser.add_argument(
"--validate",
action="store_true",
help="Validuje data pro měnu nebo všechny měny. Zkontroluje konzistenci kurzů a detekuje možné chyby.",
)
parser.add_argument(
"--change-threshold",
type=float,
default=1.0,
help="Práh pro detekci změn kurzů v procentech (výchozí: 1.0).",
)
parser.add_argument(
"--no-adaptive",
action="store_true",
help="Vypne adaptivní učení prahů na základě historických dat.",
)
parser.add_argument(
"--debug", action="store_true", help="Zobrazí podrobné ladicí informace."
)
parser.add_argument(
"--json",
action="store_true",
help="Výstup ve formátu JSON místo prostého textu pro programové zpracování.",
)
parser.add_argument(
"--validate",
action="store_true",
help="Validuje data pro měnu nebo všechny měny. Zkontroluje konzistenci kurzů a detekuje možné chyby.",
)
parser.add_argument(
"--change-threshold",
type=float,
default=1.0,
help="Práh pro detekci změn kurzů v procentech (výchozí: 1.0).",
)
parser.add_argument(
"--no-adaptive",
action="store_true",
help="Vypne adaptivní učení prahů na základě historických dat.",
)
parser.add_argument(
"--debug", action="store_true", help="Zobrazí podrobné ladicí informace."
)
@@ -206,17 +248,6 @@ def main():
args = parser.parse_args()
# Pokud nebyly zadány žádné argumenty, vytiskneme nápovědu a seznam dostupných měn
if len(sys.argv) == 1:
parser.print_help()
print("\nDostupné měny:")
currencies = database.get_available_currencies()
if currencies:
print(", ".join(currencies))
else:
print("Žádné měny nejsou v databázi k dispozici.")
sys.exit(0)
# Nastavíme debug mód
DEBUG = args.debug
set_debug_mode(DEBUG)
@@ -245,14 +276,69 @@ def main():
pass
# Zde bude logika pro zpracování argumentů
if args.year:
debug_print(f"Stahuji roční data pro rok {args.year}...")
# Ujistěme se, že adresář data existuje
os.makedirs("data", exist_ok=True)
# Volání funkce pro stažení ročních dat
data_fetcher.download_yearly_data(args.year, output_dir="data")
elif args.currency and args.start_date and args.end_date and not args.report_period:
# Zde bude logika pro zpracování argumentů
if args.validate:
# Validation command
base_threshold = args.change_threshold
adaptive = not args.no_adaptive
if args.currency:
# Validate specific currency
debug_print(f"Validuji data pro měnu {args.currency}...")
results = data_validator.validate_currency_data(
args.currency, args.year, base_threshold, adaptive
)
if args.json:
output_json(results)
else:
text_output = data_validator.format_validation_text(results)
print(text_output)
else:
# Validate all currencies
debug_print("Validuji data pro všechny měny...")
results = data_validator.validate_all_currencies(
args.year, base_threshold, adaptive
)
if args.json:
output_json(results)
else:
text_output = data_validator.format_validation_text(results)
print(text_output)
elif args.year:
# Validation command
base_threshold = args.change_threshold
adaptive = not args.no_adaptive
if args.currency:
# Validate specific currency
debug_print(f"Validuji data pro měnu {args.currency}...")
results = data_validator.validate_currency_data(
args.currency, args.year, base_threshold, adaptive
)
if args.json:
output_json(results)
else:
text_output = data_validator.format_validation_text(results)
print(text_output)
else:
# Validate all currencies
debug_print("Validuji data pro všechny měny...")
results = data_validator.validate_all_currencies(
args.year, base_threshold, adaptive
)
if args.json:
output_json(results)
else:
text_output = data_validator.format_validation_text(results)
print(text_output)
return
# elif args.currency and args.start_date and args.end_date and not args.report_period:
# Měsíční stahování dat
debug_print("HIT: Monthly download condition")
debug_print(
f"Stahuji měsíční data pro měnu {args.currency} od {args.start_date} do {args.end_date}..."
)
@@ -264,6 +350,7 @@ def main():
)
elif args.report_period and args.currency:
start_date, end_date = args.report_period
debug_print("HIT: Report period condition")
debug_print(
f"Generuji report pro měnu {args.currency} od {start_date} do {end_date}..."
)
@@ -271,12 +358,14 @@ def main():
start_date, end_date, args.currency, output_dir="data"
)
elif args.date:
debug_print("HIT: Daily data condition")
debug_print(f"Stahuji denní data pro datum {args.date}...")
# Ujistěme se, že adresář data existuje
os.makedirs("data", exist_ok=True)
# Volání funkce pro stažení denních dat
data_fetcher.download_daily_data(args.date, output_dir="data")
elif args.get_rate and args.currency:
debug_print("HIT: Get rate condition")
date_str = args.get_rate
currency_code = args.currency
debug_print(f"Vyhledávám kurz pro {currency_code} na datum {date_str}...")
@@ -309,6 +398,7 @@ def main():
f"Kurz {currency_code} na datum {date_str} (ani v předchozích dnech) nebyl nalezen."
)
elif args.get_rate is not None and not args.currency:
debug_print("HIT: Get rate without currency condition")
# Pokud je zadán --get-rate bez data a bez měny
if DEBUG:
print(
@@ -318,7 +408,7 @@ def main():
# DŮLEŽITÉ: Pořadí následujících elif podmínek je důležité!
# Nejprve zpracujeme --stats, pak teprve "poslední dostupný kurz"
elif args.stats is not None and args.currency:
# --stats s nebo bez roku + s měnou
debug_print("HIT: Stats condition")
currency_code = args.currency
if args.stats is True:
# Pokud je --stats zadán bez roku, vytvoříme kurzy pro všechny roky s dostupnými daty
@@ -417,6 +507,36 @@ def main():
print(
f"'Jednotný kurz' pro daňové účely podle metodiky ČNB pro {currency_code} za rok {year} nebyl nalezen."
)
debug_print("HIT: Validation condition")
print("VALIDATION: Condition matched!")
# Validation command
base_threshold = args.change_threshold
adaptive = not args.no_adaptive
if args.currency:
# Validate specific currency
debug_print(f"Validuji data pro měnu {args.currency}...")
results = data_validator.validate_currency_data(
args.currency, args.year, base_threshold, adaptive
)
if args.json:
output_json(results)
else:
text_output = data_validator.format_validation_text(results)
print(text_output)
else:
# Validate all currencies
debug_print("Validuji data pro všechny měny...")
results = data_validator.validate_all_currencies(
args.year, base_threshold, adaptive
)
if args.json:
output_json(results)
else:
text_output = data_validator.format_validation_text(results)
print(text_output)
elif args.currency and not args.get_rate:
# Pokud je zadána měna, ale není zadán --get-rate, vytiskneme poslední dostupný kurz
# Toto musí být až po --stats, jinak by se --stats nikdy nevykonalo