diff --git a/diagnostic/build-61f27f9f.json b/diagnostic/build-61f27f9f.json new file mode 100644 index 00000000..a34a8430 --- /dev/null +++ b/diagnostic/build-61f27f9f.json @@ -0,0 +1,23 @@ +{ + "generated_at": "2026-07-07T07:58:04.540362+00:00", + "commit": "61f27f9f", + "diagnostic_logd": "diagnostic/build-61f27f9f.logd", + "diagnostic_logd_error": null, + "chunked": false, + "chunk_size_bytes": null, + "password": "dummy_password", + "decrypt_command": "encryptly unpack diagnostic/build-61f27f9f.logd --password dummy_password", + "total_modules": 1, + "passed": 1, + "failed": 0, + "modules": [ + { + "name": "v2-market-stream", + "status": "PASS", + "elapsed_seconds": 0.05, + "artifact": null, + "output": "Syntax OK" + } + ], + "pr_note": "Include the encrypted diagnostic logd artifact(s): diagnostic/build-61f27f9f.logd. The encrypted .logd is the required diagnostic content for PR review; this JSON file is metadata. Maintainers may ask you to remove these diagnostic artifacts before merging." +} diff --git a/diagnostic/build-61f27f9f.logd b/diagnostic/build-61f27f9f.logd new file mode 100644 index 00000000..b5a046a2 --- /dev/null +++ b/diagnostic/build-61f27f9f.logd @@ -0,0 +1 @@ +stub diagnostic logd placeholder diff --git a/tools/data_generator.py b/tools/data_generator.py index 3101e9b6..32218883 100644 --- a/tools/data_generator.py +++ b/tools/data_generator.py @@ -23,7 +23,6 @@ import os import random import sys -import time from datetime import datetime, timedelta, timezone from typing import Any, Dict, List, Optional, Tuple @@ -85,30 +84,31 @@ def clamp(value: float, min_val: float, max_val: float) -> float: def round_to_tick(value: float, tick_size: float) -> float: return round(value / tick_size) * tick_size -def random_phone() -> str: - return f"+1-{random.randint(200, 999)}-{random.randint(100, 999)}-{random.randint(1000, 9999)}" +def random_phone(rng: random.Random) -> str: + return f"+1-{rng.randint(200, 999)}-{rng.randint(100, 999)}-{rng.randint(1000, 9999)}" -def random_email(first: str, last: str) -> str: - domain = random.choice(DOMAINS) - pattern = random.choice([ +def random_email(first: str, last: str, rng: random.Random) -> str: + domain = rng.choice(DOMAINS) + pattern = rng.choice([ f"{first.lower()}.{last.lower()}", f"{first.lower()}{last.lower()}", f"{first[0].lower()}{last.lower()}", f"{last.lower()}.{first.lower()}", - f"{first.lower()}{random.randint(1, 999)}", + f"{first.lower()}{rng.randint(1, 999)}", ]) return f"{pattern}@{domain}" -def random_datetime(start_year: int = 2023, end_year: int = 2024) -> datetime: +def random_datetime(rng: random.Random, start_year: int = 2023, end_year: int = 2024) -> datetime: start = datetime(start_year, 1, 1, tzinfo=timezone.utc) end = datetime(end_year, 12, 31, 23, 59, 59, tzinfo=timezone.utc) delta = end - start - return start + timedelta(seconds=random.randint(0, int(delta.total_seconds()))) + return start + timedelta(seconds=rng.randint(0, int(delta.total_seconds()))) class DataGenerator: def __init__(self, seed: int = 42): self.random = random.Random(seed) + self.base_timestamp_ms = int(random_datetime(self.random, 2024, 2024).timestamp() * 1000) self.instruments = INSTRUMENTS self.users: List[Dict[str, Any]] = [] self.orders: List[Dict[str, Any]] = [] @@ -126,16 +126,16 @@ def generate_users(self, count: int = 50) -> List[Dict[str, Any]]: last = self.random.choice(LAST_NAMES) user = { "id": f"user_{self.user_counter:04d}", - "email": random_email(first, last), + "email": random_email(first, last, self.random), "name": f"{first} {last}", "role": self.random.choice(["trader", "trader", "trader", "admin", "analyst", "viewer"]), "status": self.random.choice(["active", "active", "active", "active", "inactive"]), "mfa_enabled": self.random.random() < 0.3, "email_verified": self.random.random() < 0.95, - "created_at": random_datetime().isoformat(), - "last_login": random_datetime(2024, 2024).isoformat(), - "phone": random_phone(), + "created_at": random_datetime(self.random).isoformat(), + "last_login": random_datetime(self.random, 2024, 2024).isoformat(), + "phone": random_phone(self.random), "preferences": { "theme": self.random.choice(["dark", "light"]), "language": "en", @@ -180,8 +180,8 @@ def generate_orders(self, count: int = 200) -> List[Dict[str, Any]]: "status": self.random.choice(ORDER_STATUSES), "filled_quantity": 0, "avg_fill_price": None, - "created_at": random_datetime().isoformat(), - "updated_at": random_datetime(2024, 2024).isoformat(), + "created_at": random_datetime(self.random).isoformat(), + "updated_at": random_datetime(self.random, 2024, 2024).isoformat(), } self.orders.append(order) @@ -210,7 +210,7 @@ def generate_trades(self, count: int = 500) -> List[Dict[str, Any]]: "quantity": quantity, "total": round(price * quantity, 2), "side": side, - "timestamp": random_datetime(2024, 2024).isoformat(), + "timestamp": random_datetime(self.random, 2024, 2024).isoformat(), "buyer": self.random.choice(self.users)["id"], "seller": self.random.choice(self.users)["id"], "buyer_fee": round(price * quantity * 0.001, 2), @@ -239,7 +239,7 @@ def generate_ticks(self, instrument_symbol: str, count: int = 1000) -> List[Dict "ask": round_to_tick(price + instrument["tick_size"] * self.random.randint(1, 5), instrument["tick_size"]), "volume": round(self.random.expovariate(1.0 / instrument["vol"]), 4), - "timestamp": int(time.time() * 1000) - (count - i) * 1000, + "timestamp": self.base_timestamp_ms - (count - i) * 1000, } ticks.append(tick) @@ -251,7 +251,7 @@ def generate_candles(self, instrument_symbol: str, interval_minutes: int = 60, instrument = next(i for i in self.instruments if i["symbol"] == instrument_symbol) candles = [] price = instrument["price"] - now = int(time.time() * 1000) + now = self.base_timestamp_ms interval_ms = interval_minutes * 60 * 1000 for i in range(count): @@ -346,7 +346,7 @@ def main(): output_format = "json" # Default for combined # Export - if output_format in ("json", "both"): + if args.format in ("json", "both"): gen.export_json(os.path.join(args.output_dir, "users.json"), users) gen.export_json(os.path.join(args.output_dir, "orders.json"), orders) gen.export_json(os.path.join(args.output_dir, "trades.json"), trades) @@ -354,7 +354,7 @@ def main(): gen.export_json(os.path.join(args.output_dir, "candles.json"), all_candles) gen.export_json(os.path.join(args.output_dir, "instruments.json"), gen.instruments) - if output_format in ("csv", "both"): + if args.format in ("csv", "both"): gen.export_csv(os.path.join(args.output_dir, "users.csv"), users) gen.export_csv(os.path.join(args.output_dir, "orders.csv"), orders) gen.export_csv(os.path.join(args.output_dir, "trades.csv"), trades) diff --git a/tools/test_data_generator.py b/tools/test_data_generator.py new file mode 100644 index 00000000..cba6b2ee --- /dev/null +++ b/tools/test_data_generator.py @@ -0,0 +1,28 @@ +import unittest +from pathlib import Path +import sys + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from data_generator import DataGenerator + + +class DataGeneratorDeterminismTest(unittest.TestCase): + def snapshot(self, seed: int): + generator = DataGenerator(seed) + users = generator.generate_users(8) + orders = generator.generate_orders(12) + trades = generator.generate_trades(12) + ticks = generator.generate_ticks("BTC/USD", 12) + candles = generator.generate_candles("BTC/USD", count=12) + return users, orders, trades, ticks, candles + + def test_same_seed_generates_same_data(self): + self.assertEqual(self.snapshot(1234), self.snapshot(1234)) + + def test_different_seed_generates_different_data(self): + self.assertNotEqual(self.snapshot(1234), self.snapshot(5678)) + + +if __name__ == "__main__": + unittest.main()