From 18ff42ac01ed1ce6e645a014b8ec2fede0f7924f Mon Sep 17 00:00:00 2001 From: marcvanduyn Date: Tue, 21 Apr 2026 23:46:25 +0100 Subject: [PATCH 1/5] feat: add opt-in validate_symbol parameter to order creation (#247) Add validate_symbol parameter (default False) to create_limit_order and create_order in Context and TradingStrategy. When enabled, validates the target_symbol against registered data sources and portfolio positions, raising OperationalException for unknown symbols. Closes #247 --- investing_algorithm_framework/app/context.py | 70 ++++++++- investing_algorithm_framework/app/strategy.py | 6 +- tests/app/algorithm/test_validate_symbol.py | 147 ++++++++++++++++++ 3 files changed, 219 insertions(+), 4 deletions(-) create mode 100644 tests/app/algorithm/test_validate_symbol.py diff --git a/investing_algorithm_framework/app/context.py b/investing_algorithm_framework/app/context.py index ef4ebe9b..85054613 100644 --- a/investing_algorithm_framework/app/context.py +++ b/investing_algorithm_framework/app/context.py @@ -51,6 +51,59 @@ def __init__( self.trade_take_profit_service: TradeTakeProfitService = \ trade_take_profit_service + def _validate_target_symbol(self, target_symbol, market=None): + """ + Validate that the target_symbol is a known symbol by checking + against registered data sources and existing portfolio positions. + + Args: + target_symbol: The symbol to validate (e.g., "BTC" or "BTC/EUR") + market: The market to check against + + Raises: + OperationalException: If the symbol is not recognized + """ + known_symbols = set() + + # Collect symbols from registered data sources + if self.data_provider_service.data_provider_index is not None: + for data_source, _ in \ + self.data_provider_service \ + .data_provider_index.get_all(): + if data_source.symbol is not None: + known_symbols.add(data_source.symbol.upper()) + + # Collect symbols from existing portfolio positions + portfolio = self.portfolio_service.find({"market": market}) + + if portfolio is not None: + positions = self.position_service.get_all( + {"portfolio": portfolio.id} + ) + + for position in positions: + if position.symbol is not None: + symbol = f"{position.symbol.upper()}" \ + f"/{portfolio.trading_symbol.upper()}" + known_symbols.add(symbol) + + # Build the full symbol for comparison + full_symbol = target_symbol.upper() + + if "/" not in full_symbol and portfolio is not None: + full_symbol = \ + f"{full_symbol}/{portfolio.trading_symbol.upper()}" + + if full_symbol not in known_symbols: + sorted_symbols = sorted(known_symbols) + raise OperationalException( + f"Symbol '{target_symbol}' is not a known asset. " + f"Known symbols: {sorted_symbols}. " + f"Check for typos or add a DataSource for this symbol. " + f"To skip this check, set validate_symbol=False " + f"or omit the parameter." + ) + @property def config(self): """ @@ -78,7 +131,8 @@ def create_order( market=None, execute=True, validate=True, - sync=True + sync=True, + validate_symbol=False ) -> Order: """ Function to create an order. This function will create an order @@ -96,10 +150,15 @@ def create_order( validate: If set to True, the order will be validated sync: If set to True, the created order will be synced with the portfolio of the algorithm. + validate_symbol: Default False. If set to True, + the target_symbol will be validated against known symbols. Returns: The order created """ + if validate_symbol: + self._validate_target_symbol(target_symbol, market=market) + portfolio = self.portfolio_service.find({"market": market}) order_data = { "target_symbol": target_symbol, @@ -162,7 +221,8 @@ def create_limit_order( execute=True, validate=True, sync=True, - metadata=None + metadata=None, + validate_symbol=False ) -> Order: """ Function to create a limit order. This function will create a limit @@ -193,10 +253,16 @@ def create_limit_order( sync (optional): Default True. If set to True, the created order will be synced with the portfolio of the algorithm + validate_symbol (optional): Default False. If set to True, + the target_symbol will be validated against known symbols + from registered data sources and portfolio positions. Returns: Order: Instance of the order created """ + if validate_symbol: + self._validate_target_symbol(target_symbol, market=market) + portfolio = self.portfolio_service.find({"market": market}) if percentage_of_portfolio is not None: diff --git a/investing_algorithm_framework/app/strategy.py b/investing_algorithm_framework/app/strategy.py index 70cc325f..5d84a3f2 100644 --- a/investing_algorithm_framework/app/strategy.py +++ b/investing_algorithm_framework/app/strategy.py @@ -939,7 +939,8 @@ def create_limit_order( execute=True, validate=True, sync=True, - metadata=None + metadata=None, + validate_symbol=False ) -> Order: """ Function to create a limit order. This function will create @@ -986,7 +987,8 @@ def create_limit_order( execute=execute, validate=validate, sync=sync, - metadata=metadata + metadata=metadata, + validate_symbol=validate_symbol ) def close_position( diff --git a/tests/app/algorithm/test_validate_symbol.py b/tests/app/algorithm/test_validate_symbol.py new file mode 100644 index 00000000..f18165d4 --- /dev/null +++ b/tests/app/algorithm/test_validate_symbol.py @@ -0,0 +1,147 @@ +from investing_algorithm_framework import PortfolioConfiguration, \ + MarketCredential, OrderSide, DataSource +from investing_algorithm_framework.domain import OperationalException +from tests.resources import TestBase +from tests.resources.strategies_for_testing import StrategyOne + + +class TestValidateSymbol(TestBase): + """Tests for opt-in symbol validation on order creation (issue #247).""" + portfolio_configurations = [ + PortfolioConfiguration( + market="BITVAVO", + trading_symbol="EUR" + ) + ] + market_credentials = [ + MarketCredential( + market="BITVAVO", + api_key="api_key", + secret_key="secret_key" + ) + ] + external_balances = { + "EUR": 1000 + } + + def _register_data_source(self, symbol): + """Register a data source symbol in the data provider index.""" + data_source = DataSource( + identifier=f"{symbol}_ohlcv", + symbol=symbol, + data_type="OHLCV", + time_frame="1d", + market="BITVAVO", + ) + data_provider_service = self.app.container.data_provider_service() + data_provider_service.data_provider_index\ + .data_providers_lookup[data_source] = None + + def test_default_allows_unknown_symbol(self): + """Default behavior: no symbol validation, any symbol is accepted.""" + self.app.add_strategy(StrategyOne) + self.app.context.create_limit_order( + target_symbol="UNKNOWN_TOKEN", + amount=1, + price=10, + order_side=OrderSide.BUY, + ) + order_repository = self.app.container.order_repository() + order = order_repository.find({"target_symbol": "UNKNOWN_TOKEN"}) + self.assertIsNotNone(order) + + def test_validate_symbol_false_allows_unknown_symbol(self): + """Explicit validate_symbol=False allows any symbol.""" + self.app.add_strategy(StrategyOne) + self.app.context.create_limit_order( + target_symbol="UNKNOWN_TOKEN", + amount=1, + price=10, + order_side=OrderSide.BUY, + validate_symbol=False, + ) + order_repository = self.app.container.order_repository() + order = order_repository.find({"target_symbol": "UNKNOWN_TOKEN"}) + self.assertIsNotNone(order) + + def test_validate_symbol_true_rejects_unknown_symbol(self): + """validate_symbol=True raises for a symbol not in data sources.""" + self.app.add_strategy(StrategyOne) + self._register_data_source("BTC/EUR") + + with self.assertRaises(OperationalException) as cm: + self.app.context.create_limit_order( + target_symbol="BT/EUR", + amount=1, + price=10, + order_side=OrderSide.BUY, + validate_symbol=True, + ) + + self.assertIn("BT/EUR", str(cm.exception)) + self.assertIn("not a known asset", str(cm.exception)) + self.assertIn("BTC/EUR", str(cm.exception)) + + def test_validate_symbol_true_accepts_known_symbol(self): + """validate_symbol=True passes for a symbol in data sources.""" + self.app.add_strategy(StrategyOne) + self._register_data_source("BTC/EUR") + + self.app.context.create_limit_order( + target_symbol="BTC", + amount=1, + price=10, + order_side=OrderSide.BUY, + validate_symbol=True, + ) + order_repository = self.app.container.order_repository() + order = order_repository.find({"target_symbol": "BTC"}) + self.assertIsNotNone(order) + + def test_validate_symbol_true_multiple_data_sources(self): + """validate_symbol=True checks across all registered data sources.""" + self.app.add_strategy(StrategyOne) + self._register_data_source("BTC/EUR") + self._register_data_source("ETH/EUR") + + # ETH should pass + self.app.context.create_limit_order( + target_symbol="ETH", + amount=1, + price=10, + order_side=OrderSide.BUY, + validate_symbol=True, + ) + order_repository = self.app.container.order_repository() + order = order_repository.find({"target_symbol": "ETH"}) + self.assertIsNotNone(order) + + # SOL should fail + with self.assertRaises(OperationalException): + self.app.context.create_limit_order( + target_symbol="SOL", + amount=1, + price=10, + order_side=OrderSide.BUY, + validate_symbol=True, + ) + + def test_validate_symbol_error_message_contains_known_symbols(self): + """Error message lists known symbols for user reference.""" + self.app.add_strategy(StrategyOne) + self._register_data_source("BTC/EUR") + self._register_data_source("ETH/EUR") + + with self.assertRaises(OperationalException) as cm: + self.app.context.create_limit_order( + target_symbol="TYPO", + amount=1, + price=10, + order_side=OrderSide.BUY, + validate_symbol=True, + ) + + error_msg = str(cm.exception) + self.assertIn("BTC/EUR", error_msg) + self.assertIn("ETH/EUR", error_msg) + self.assertIn("validate_symbol=False", error_msg) From 0b382db235254d45c664168708bd0feaf58722cf Mon Sep 17 00:00:00 2001 From: marcvanduyn Date: Tue, 21 Apr 2026 23:52:02 +0100 Subject: [PATCH 2/5] refactor: simplify validate_symbol to direct comparison only (#247) Remove symbol format construction from _validate_target_symbol. Validation now does a simple direct comparison of target_symbol against registered data source symbols. The framework should not guess or construct symbol formats. Updates tests to use exact data source symbols. --- investing_algorithm_framework/app/context.py | 27 +- scripts/setup_discord.py | 411 +++++++++++++++++++ tests/app/algorithm/test_validate_symbol.py | 14 +- 3 files changed, 422 insertions(+), 30 deletions(-) create mode 100644 scripts/setup_discord.py diff --git a/investing_algorithm_framework/app/context.py b/investing_algorithm_framework/app/context.py index 85054613..ce2249d0 100644 --- a/investing_algorithm_framework/app/context.py +++ b/investing_algorithm_framework/app/context.py @@ -54,7 +54,7 @@ def __init__( def _validate_target_symbol(self, target_symbol, market=None): """ Validate that the target_symbol is a known symbol by checking - against registered data sources and existing portfolio positions. + against registered data source symbols. Args: target_symbol: The symbol to validate (e.g., "BTC" or "BTC/EUR") @@ -73,28 +73,9 @@ def _validate_target_symbol(self, target_symbol, market=None): if data_source.symbol is not None: known_symbols.add(data_source.symbol.upper()) - # Collect symbols from existing portfolio positions - portfolio = self.portfolio_service.find({"market": market}) - - if portfolio is not None: - positions = self.position_service.get_all( - {"portfolio": portfolio.id} - ) - - for position in positions: - if position.symbol is not None: - symbol = f"{position.symbol.upper()}" \ - f"/{portfolio.trading_symbol.upper()}" - known_symbols.add(symbol) - - # Build the full symbol for comparison - full_symbol = target_symbol.upper() - - if "/" not in full_symbol and portfolio is not None: - full_symbol = \ - f"{full_symbol}/{portfolio.trading_symbol.upper()}" + target = target_symbol.upper() - if full_symbol not in known_symbols: + if target not in known_symbols: sorted_symbols = sorted(known_symbols) raise OperationalException( f"Symbol '{target_symbol}' is not a known asset. " @@ -255,7 +236,7 @@ def create_limit_order( portfolio of the algorithm validate_symbol (optional): Default False. If set to True, the target_symbol will be validated against known symbols - from registered data sources and portfolio positions. + from registered data sources. Returns: Order: Instance of the order created diff --git a/scripts/setup_discord.py b/scripts/setup_discord.py new file mode 100644 index 00000000..547fa1d2 --- /dev/null +++ b/scripts/setup_discord.py @@ -0,0 +1,411 @@ +""" +Discord channel layout setup script for the +investing-algorithm-framework community server. + +Usage: + export DISCORD_BOT_TOKEN="your-bot-token" + export DISCORD_GUILD_ID="your-guild-id" + python scripts/setup_discord.py + +Requires: pip install discord.py + +Bot invite URL (replace CLIENT_ID with your Application ID): + https://discord.com/oauth2/authorize?client_id=CLIENT_ID&permissions=268435536&scope=bot +""" + +import os +import asyncio +from dotenv import load_dotenv +import discord +load_dotenv() +BOT_TOKEN = os.environ["DISCORD_BOT_TOKEN"] +GUILD_ID = int(os.environ["DISCORD_GUILD_ID"]) + +# Channel layout: list of (category_name, channels) +# Each channel is (name, type, topic) +CHANNEL_LAYOUT = [ + ( + "━━ INFO ━━", + [ + ( + "👋・welcome", + discord.ChannelType.text, + "Start here! Learn about the community and how to get involved.", + ), + ( + "📜・rules", + discord.ChannelType.text, + "Community guidelines — please read before posting.", + ), + ( + "📢・announcements", + discord.ChannelType.text, + "Official releases, updates, and important news.", + ), + ( + "🗺️・roadmap", + discord.ChannelType.text, + "Framework roadmap and upcoming features.", + ), + ], + ), + ( + "━━ COMMUNITY ━━", + [ + ( + "💬・general", + discord.ChannelType.text, + "Hang out and chat about anything algo trading related.", + ), + ( + "🙋・introductions", + discord.ChannelType.text, + "New here? Introduce yourself and tell us what you're building.", + ), + ( + "🏆・showcase", + discord.ChannelType.text, + "Show off your strategies, bots, dashboards, and results.", + ), + ( + "💡・ideas", + discord.ChannelType.text, + "Brainstorm ideas for strategies, tools, and integrations.", + ), + ], + ), + ( + "━━ HELP & SUPPORT ━━", + [ + ( + "❓・help", + discord.ChannelType.text, + "Ask questions about using the framework. No question is too basic!", + ), + ( + "🐛・bug-reports", + discord.ChannelType.text, + "Found a bug? Report it here with steps to reproduce.", + ), + ( + "✨・feature-requests", + discord.ChannelType.text, + "Suggest new features and improvements for the framework.", + ), + ], + ), + ( + "━━ DEVELOPMENT ━━", + [ + ( + "🔨・contributing", + discord.ChannelType.text, + "Want to contribute? Discuss PRs, issues, and development workflow.", + ), + ( + "📋・pull-requests", + discord.ChannelType.text, + "Discuss open PRs and code reviews.", + ), + ( + "🚀・releases", + discord.ChannelType.text, + "Release notes, changelogs, and migration guides.", + ), + ( + "🤖・github-feed", + discord.ChannelType.text, + "Automated feed of GitHub activity (issues, PRs, releases).", + ), + ], + ), + ( + "━━ STRATEGIES ━━", + [ + ( + "📊・strategy-discussion", + discord.ChannelType.text, + "Discuss trading strategies, signals, indicators, and alpha.", + ), + ( + "🔬・backtesting", + discord.ChannelType.text, + "Share backtesting results, methodology, and pitfalls.", + ), + ( + "⚡・live-trading", + discord.ChannelType.text, + "Running strategies live? Discuss deployment, monitoring, and ops.", + ), + ( + "🪙・crypto", + discord.ChannelType.text, + "Crypto-specific strategies, exchanges (Bitvavo, Coinbase, etc.).", + ), + ], + ), + ( + "━━ RESOURCES ━━", + [ + ( + "📚・tutorials", + discord.ChannelType.text, + "Step-by-step guides and learning resources.", + ), + ( + "📖・documentation", + discord.ChannelType.text, + "Docs feedback, corrections, and improvement suggestions.", + ), + ( + "🔗・external-resources", + discord.ChannelType.text, + "Useful links — papers, books, tools, datasets, and APIs.", + ), + ], + ), + ( + "━━ MARKETPLACES ━━", + [ + ( + "🏪・general-marketplace", + discord.ChannelType.text, + "General discussion about algo trading marketplaces and platforms.", + ), + ( + "🐱・finterion", + discord.ChannelType.text, + "Discussion about Finterion — deploying and monetizing your strategies.", + ), + ], + ), + ( + "━━ VOICE ━━", + [ + ("🎙️・general-voice", discord.ChannelType.voice, None), + ("👥・pair-programming", discord.ChannelType.voice, None), + ("📺・screen-share", discord.ChannelType.voice, None), + ], + ), +] + +# Read-only channels — only admins/mods can post +READ_ONLY_CHANNELS = { + "👋・welcome", + "📜・rules", + "📢・announcements", + "🗺️・roadmap", + "🚀・releases", + "🤖・github-feed", +} + +# Pinned messages per channel (sent and pinned on first setup) +PINNED_MESSAGES = { + "👋・welcome": ( + "# Welcome to the Investing Algorithm Framework community! 🎉\n\n" + "We're a community of algo traders, quants, and developers building " + "trading strategies with the **Investing Algorithm Framework**.\n\n" + "**Quick links:**\n" + "📖 [Documentation](https://investing-algorithm-framework.com)\n" + "💻 [GitHub](https://github.com/coding-kitties/investing-algorithm-framework)\n" + "📦 [PyPI](https://pypi.org/project/investing-algorithm-framework/)\n" + "🐱 [Finterion](https://finterion.com)\n\n" + "**Getting started:**\n" + "1. Read the 📜・rules\n" + "2. Introduce yourself in 🙋・introductions\n" + "3. Ask questions in ❓・help\n" + "4. Share your work in 🏆・showcase\n\n" + "Happy trading! 🚀" + ), + "📜・rules": ( + "# Community Rules 📜\n\n" + "**1. Be respectful** — Treat everyone with kindness. No harassment, " + "hate speech, or personal attacks.\n\n" + "**2. No financial advice** — This is an engineering community. " + "Don't give or ask for specific investment advice.\n\n" + "**3. No spam or self-promotion** — Keep promotional content to " + "🏆・showcase. No unsolicited DMs.\n\n" + "**4. Stay on topic** — Use the right channels. Off-topic chat " + "goes in 💬・general.\n\n" + "**5. No sharing of API keys or credentials** — Never post " + "secrets, tokens, or passwords.\n\n" + "**6. Help others learn** — Share knowledge, explain your " + "reasoning, and be patient with beginners.\n\n" + "**7. Report issues properly** — Use 🐛・bug-reports with " + "reproduction steps, or open a GitHub issue.\n\n" + "Breaking these rules may result in a warning or ban. " + "Moderators have final say." + ), + "🔨・contributing": ( + "# Contributing to the Framework 🔨\n\n" + "Want to contribute? Here's how to get started:\n\n" + "1. **Fork** the repo: " + "[github.com/coding-kitties/investing-algorithm-framework]" + "(https://github.com/coding-kitties/investing-algorithm-framework)\n" + "2. **Check open issues** — Look for `good first issue` or " + "`help wanted` labels\n" + "3. **Create a branch** — `feature/your-feature` or " + "`fix/your-fix`\n" + "4. **Submit a PR** — Reference the issue number\n\n" + "Discuss your ideas here before starting large changes!" + ), + "🐱・finterion": ( + "# Finterion 🐱\n\n" + "**Finterion** is the marketplace for algorithmic trading strategies.\n" + "🔗 [finterion.com](https://finterion.com)\n\n" + "Use this channel to discuss deploying strategies, " + "monetization, and the Finterion platform." + ), +} + + +class SetupBot(discord.Client): + async def on_ready(self): + guild = self.get_guild(GUILD_ID) + + if guild is None: + print(f"Guild {GUILD_ID} not found. Check DISCORD_GUILD_ID.") + await self.close() + return + + print(f"Connected to guild: {guild.name}") + + # Get or create mod role for read-only channel overrides + mod_role = discord.utils.get(guild.roles, name="Moderator") + + if mod_role is None: + try: + mod_role = await guild.create_role( + name="Moderator", + permissions=discord.Permissions( + manage_messages=True, + kick_members=True, + ban_members=True, + ), + color=discord.Color.blue(), + reason="Setup script: moderator role", + ) + print(f" Created role: Moderator") + except discord.Forbidden: + print( + " Warning: Missing 'Manage Roles' permission. " + "Skipping Moderator role creation. " + "Read-only channels will not have permission overrides." + ) + mod_role = None + + position = 0 + + for category_name, channels in CHANNEL_LAYOUT: + # Check if category already exists + category = discord.utils.get( + guild.categories, name=category_name + ) + + if category is None: + category = await guild.create_category( + category_name, + position=position, + reason="Setup script: channel layout", + ) + print(f"Created category: {category_name}") + else: + print(f"Category already exists: {category_name}") + + position += 1 + + for channel_name, channel_type, topic in channels: + # Check if channel already exists in category + existing = discord.utils.get( + category.channels, name=channel_name + ) + + if existing is not None: + # Check if pinned message is missing + if ( + channel_name in PINNED_MESSAGES + and isinstance(existing, discord.TextChannel) + ): + has_pins = False + async for _ in existing.pins(): + has_pins = True + break + + if not has_pins: + try: + msg = await existing.send( + PINNED_MESSAGES[channel_name] + ) + await msg.pin() + print( + f" Channel already exists: " + f"#{channel_name} (added pinned message)" + ) + except discord.Forbidden: + print( + f" Channel already exists: " + f"#{channel_name} (could not pin — " + f"missing Manage Messages permission)" + ) + else: + print( + f" Channel already exists: #{channel_name}" + ) + else: + print( + f" Channel already exists: #{channel_name}" + ) + continue + + overwrites = None + + if channel_name in READ_ONLY_CHANNELS and mod_role is not None: + overwrites = { + guild.default_role: discord.PermissionOverwrite( + send_messages=False + ), + mod_role: discord.PermissionOverwrite( + send_messages=True + ), + } + + if channel_type == discord.ChannelType.voice: + await category.create_voice_channel( + channel_name, + reason="Setup script: channel layout", + ) + else: + kwargs = { + "topic": topic, + "reason": "Setup script: channel layout", + } + if overwrites: + kwargs["overwrites"] = overwrites + + channel = await category.create_text_channel( + channel_name, + **kwargs, + ) + + # Send and pin message if configured + if channel_name in PINNED_MESSAGES: + try: + msg = await channel.send(PINNED_MESSAGES[channel_name]) + await msg.pin() + print(f" Created channel: #{channel_name} (+ pinned message)") + except discord.Forbidden: + print(f" Created channel: #{channel_name} (could not pin — missing Manage Messages permission)") + else: + print(f" Created channel: #{channel_name}") + continue + + print(f" Created channel: #{channel_name}") + + print("\nDone! Channel layout created.") + await self.close() + + +intents = discord.Intents.default() +intents.guilds = True +bot = SetupBot(intents=intents) +bot.run(BOT_TOKEN) diff --git a/tests/app/algorithm/test_validate_symbol.py b/tests/app/algorithm/test_validate_symbol.py index f18165d4..21227a30 100644 --- a/tests/app/algorithm/test_validate_symbol.py +++ b/tests/app/algorithm/test_validate_symbol.py @@ -88,14 +88,14 @@ def test_validate_symbol_true_accepts_known_symbol(self): self._register_data_source("BTC/EUR") self.app.context.create_limit_order( - target_symbol="BTC", + target_symbol="BTC/EUR", amount=1, price=10, order_side=OrderSide.BUY, validate_symbol=True, ) order_repository = self.app.container.order_repository() - order = order_repository.find({"target_symbol": "BTC"}) + order = order_repository.find({"target_symbol": "BTC/EUR"}) self.assertIsNotNone(order) def test_validate_symbol_true_multiple_data_sources(self): @@ -104,22 +104,22 @@ def test_validate_symbol_true_multiple_data_sources(self): self._register_data_source("BTC/EUR") self._register_data_source("ETH/EUR") - # ETH should pass + # ETH/EUR should pass self.app.context.create_limit_order( - target_symbol="ETH", + target_symbol="ETH/EUR", amount=1, price=10, order_side=OrderSide.BUY, validate_symbol=True, ) order_repository = self.app.container.order_repository() - order = order_repository.find({"target_symbol": "ETH"}) + order = order_repository.find({"target_symbol": "ETH/EUR"}) self.assertIsNotNone(order) - # SOL should fail + # SOL/EUR should fail with self.assertRaises(OperationalException): self.app.context.create_limit_order( - target_symbol="SOL", + target_symbol="SOL/EUR", amount=1, price=10, order_side=OrderSide.BUY, From d617e4475046fea6573e07e48816053acaa51c5b Mon Sep 17 00:00:00 2001 From: marcvanduyn Date: Tue, 21 Apr 2026 23:55:39 +0100 Subject: [PATCH 3/5] refactor: simplify validate_symbol to only check target != trading_symbol (#247) validate_symbol now only prevents nonsensical orders where target_symbol equals trading_symbol (e.g. EUR/EUR). No data source lookups or symbol format construction needed. --- investing_algorithm_framework/app/context.py | 39 +++---- tests/app/algorithm/test_validate_symbol.py | 108 +++++++------------ 2 files changed, 57 insertions(+), 90 deletions(-) diff --git a/investing_algorithm_framework/app/context.py b/investing_algorithm_framework/app/context.py index ce2249d0..9d3c5b51 100644 --- a/investing_algorithm_framework/app/context.py +++ b/investing_algorithm_framework/app/context.py @@ -53,34 +53,28 @@ def __init__( def _validate_target_symbol(self, target_symbol, market=None): """ - Validate that the target_symbol is a known symbol by checking - against registered data source symbols. + Validate that the target_symbol combined with the portfolio's + trading_symbol does not result in trading_symbol/trading_symbol + (e.g., EUR/EUR), which is a nonsensical order. Args: - target_symbol: The symbol to validate (e.g., "BTC" or "BTC/EUR") + target_symbol: The symbol of the asset to trade market: The market to check against Raises: - OperationalException: If the symbol is not recognized + OperationalException: If target_symbol equals + the trading_symbol """ - known_symbols = set() - - # Collect symbols from registered data sources - if self.data_provider_service.data_provider_index is not None: - for data_source, _ in \ - self.data_provider_service \ - .data_provider_index.get_all(): - if data_source.symbol is not None: - known_symbols.add(data_source.symbol.upper()) - - target = target_symbol.upper() + portfolio = self.portfolio_service.find({"market": market}) + trading_symbol = portfolio.trading_symbol.upper() - if target not in known_symbols: - sorted_symbols = sorted(known_symbols) + if target_symbol.upper() == trading_symbol: raise OperationalException( - f"Symbol '{target_symbol}' is not a known asset. " - f"Known symbols: {sorted_symbols}. " - f"Check for typos or add a DataSource for this symbol. " + f"target_symbol '{target_symbol}' is the same as " + f"the trading_symbol '{portfolio.trading_symbol}'. " + f"This would result in a " + f"'{portfolio.trading_symbol}/{portfolio.trading_symbol}' " + f"order which is not valid. " f"To skip this check, set validate_symbol=False " f"or omit the parameter." ) @@ -132,7 +126,7 @@ def create_order( sync: If set to True, the created order will be synced with the portfolio of the algorithm. validate_symbol: Default False. If set to True, - the target_symbol will be validated against known symbols. + validates that target_symbol is not the trading_symbol. Returns: The order created @@ -235,8 +229,7 @@ def create_limit_order( the created order will be synced with the portfolio of the algorithm validate_symbol (optional): Default False. If set to True, - the target_symbol will be validated against known symbols - from registered data sources. + validates that target_symbol is not the trading_symbol. Returns: Order: Instance of the order created diff --git a/tests/app/algorithm/test_validate_symbol.py b/tests/app/algorithm/test_validate_symbol.py index 21227a30..f6be2b47 100644 --- a/tests/app/algorithm/test_validate_symbol.py +++ b/tests/app/algorithm/test_validate_symbol.py @@ -1,12 +1,16 @@ from investing_algorithm_framework import PortfolioConfiguration, \ - MarketCredential, OrderSide, DataSource + MarketCredential, OrderSide from investing_algorithm_framework.domain import OperationalException from tests.resources import TestBase from tests.resources.strategies_for_testing import StrategyOne class TestValidateSymbol(TestBase): - """Tests for opt-in symbol validation on order creation (issue #247).""" + """Tests for opt-in symbol validation on order creation (issue #247). + + Validates that target_symbol is not the same as trading_symbol, + which would result in a nonsensical order (e.g. EUR/EUR). + """ portfolio_configurations = [ PortfolioConfiguration( market="BITVAVO", @@ -24,117 +28,88 @@ class TestValidateSymbol(TestBase): "EUR": 1000 } - def _register_data_source(self, symbol): - """Register a data source symbol in the data provider index.""" - data_source = DataSource( - identifier=f"{symbol}_ohlcv", - symbol=symbol, - data_type="OHLCV", - time_frame="1d", - market="BITVAVO", - ) - data_provider_service = self.app.container.data_provider_service() - data_provider_service.data_provider_index\ - .data_providers_lookup[data_source] = None - - def test_default_allows_unknown_symbol(self): - """Default behavior: no symbol validation, any symbol is accepted.""" + def test_default_allows_trading_symbol_as_target(self): + """Default behavior: no validation, even trading_symbol is accepted.""" self.app.add_strategy(StrategyOne) self.app.context.create_limit_order( - target_symbol="UNKNOWN_TOKEN", + target_symbol="EUR", amount=1, price=10, order_side=OrderSide.BUY, ) order_repository = self.app.container.order_repository() - order = order_repository.find({"target_symbol": "UNKNOWN_TOKEN"}) + order = order_repository.find({"target_symbol": "EUR"}) self.assertIsNotNone(order) - def test_validate_symbol_false_allows_unknown_symbol(self): - """Explicit validate_symbol=False allows any symbol.""" + def test_validate_symbol_false_allows_trading_symbol_as_target(self): + """Explicit validate_symbol=False allows trading_symbol as target.""" self.app.add_strategy(StrategyOne) self.app.context.create_limit_order( - target_symbol="UNKNOWN_TOKEN", + target_symbol="EUR", amount=1, price=10, order_side=OrderSide.BUY, validate_symbol=False, ) order_repository = self.app.container.order_repository() - order = order_repository.find({"target_symbol": "UNKNOWN_TOKEN"}) + order = order_repository.find({"target_symbol": "EUR"}) self.assertIsNotNone(order) - def test_validate_symbol_true_rejects_unknown_symbol(self): - """validate_symbol=True raises for a symbol not in data sources.""" + def test_validate_symbol_true_rejects_trading_symbol_as_target(self): + """validate_symbol=True raises when target_symbol equals + trading_symbol (e.g. EUR/EUR).""" self.app.add_strategy(StrategyOne) - self._register_data_source("BTC/EUR") with self.assertRaises(OperationalException) as cm: self.app.context.create_limit_order( - target_symbol="BT/EUR", + target_symbol="EUR", amount=1, price=10, order_side=OrderSide.BUY, validate_symbol=True, ) - self.assertIn("BT/EUR", str(cm.exception)) - self.assertIn("not a known asset", str(cm.exception)) - self.assertIn("BTC/EUR", str(cm.exception)) + error_msg = str(cm.exception) + self.assertIn("EUR", error_msg) + self.assertIn("trading_symbol", error_msg) + self.assertIn("EUR/EUR", error_msg) - def test_validate_symbol_true_accepts_known_symbol(self): - """validate_symbol=True passes for a symbol in data sources.""" + def test_validate_symbol_true_case_insensitive(self): + """validate_symbol=True catches trading_symbol regardless of case.""" self.app.add_strategy(StrategyOne) - self._register_data_source("BTC/EUR") - self.app.context.create_limit_order( - target_symbol="BTC/EUR", - amount=1, - price=10, - order_side=OrderSide.BUY, - validate_symbol=True, - ) - order_repository = self.app.container.order_repository() - order = order_repository.find({"target_symbol": "BTC/EUR"}) - self.assertIsNotNone(order) + with self.assertRaises(OperationalException): + self.app.context.create_limit_order( + target_symbol="eur", + amount=1, + price=10, + order_side=OrderSide.BUY, + validate_symbol=True, + ) - def test_validate_symbol_true_multiple_data_sources(self): - """validate_symbol=True checks across all registered data sources.""" + def test_validate_symbol_true_accepts_valid_target(self): + """validate_symbol=True passes for a target that differs + from trading_symbol.""" self.app.add_strategy(StrategyOne) - self._register_data_source("BTC/EUR") - self._register_data_source("ETH/EUR") - # ETH/EUR should pass self.app.context.create_limit_order( - target_symbol="ETH/EUR", + target_symbol="BTC", amount=1, price=10, order_side=OrderSide.BUY, validate_symbol=True, ) order_repository = self.app.container.order_repository() - order = order_repository.find({"target_symbol": "ETH/EUR"}) + order = order_repository.find({"target_symbol": "BTC"}) self.assertIsNotNone(order) - # SOL/EUR should fail - with self.assertRaises(OperationalException): - self.app.context.create_limit_order( - target_symbol="SOL/EUR", - amount=1, - price=10, - order_side=OrderSide.BUY, - validate_symbol=True, - ) - - def test_validate_symbol_error_message_contains_known_symbols(self): - """Error message lists known symbols for user reference.""" + def test_validate_symbol_error_message(self): + """Error message explains the EUR/EUR problem and how to skip.""" self.app.add_strategy(StrategyOne) - self._register_data_source("BTC/EUR") - self._register_data_source("ETH/EUR") with self.assertRaises(OperationalException) as cm: self.app.context.create_limit_order( - target_symbol="TYPO", + target_symbol="EUR", amount=1, price=10, order_side=OrderSide.BUY, @@ -142,6 +117,5 @@ def test_validate_symbol_error_message_contains_known_symbols(self): ) error_msg = str(cm.exception) - self.assertIn("BTC/EUR", error_msg) - self.assertIn("ETH/EUR", error_msg) + self.assertIn("EUR/EUR", error_msg) self.assertIn("validate_symbol=False", error_msg) From 6249c23d7e67d6948abc71ce2aa7218511ce70f5 Mon Sep 17 00:00:00 2001 From: marcvanduyn Date: Wed, 22 Apr 2026 08:55:52 +0100 Subject: [PATCH 4/5] feat: validate data source exists for target/trading symbol pair (#247) validate_symbol now checks two things: 1. target_symbol != trading_symbol (prevents EUR/EUR) 2. A data source is registered for the target_symbol/trading_symbol combination (e.g. BTC/EUR) to ensure price history tracking --- investing_algorithm_framework/app/context.py | 41 +++++++-- tests/app/algorithm/test_validate_symbol.py | 94 +++++++++++++++----- 2 files changed, 105 insertions(+), 30 deletions(-) diff --git a/investing_algorithm_framework/app/context.py b/investing_algorithm_framework/app/context.py index 9d3c5b51..df52707e 100644 --- a/investing_algorithm_framework/app/context.py +++ b/investing_algorithm_framework/app/context.py @@ -53,32 +53,55 @@ def __init__( def _validate_target_symbol(self, target_symbol, market=None): """ - Validate that the target_symbol combined with the portfolio's - trading_symbol does not result in trading_symbol/trading_symbol - (e.g., EUR/EUR), which is a nonsensical order. + Validate the target_symbol for order creation: + 1. Prevents orders where target_symbol equals trading_symbol + (e.g. EUR/EUR). + 2. Checks that a data source exists for the + target_symbol/trading_symbol combination (e.g. BTC/EUR). Args: target_symbol: The symbol of the asset to trade market: The market to check against Raises: - OperationalException: If target_symbol equals - the trading_symbol + OperationalException: If validation fails """ portfolio = self.portfolio_service.find({"market": market}) - trading_symbol = portfolio.trading_symbol.upper() + trading_symbol = portfolio.trading_symbol - if target_symbol.upper() == trading_symbol: + # Check target_symbol != trading_symbol + if target_symbol.upper() == trading_symbol.upper(): raise OperationalException( f"target_symbol '{target_symbol}' is the same as " - f"the trading_symbol '{portfolio.trading_symbol}'. " + f"the trading_symbol '{trading_symbol}'. " f"This would result in a " - f"'{portfolio.trading_symbol}/{portfolio.trading_symbol}' " + f"'{trading_symbol}/{trading_symbol}' " f"order which is not valid. " f"To skip this check, set validate_symbol=False " f"or omit the parameter." ) + # Check that a data source is registered for this pair + expected_symbol = f"{target_symbol}/{trading_symbol}".upper() + known_symbols = set() + + if self.data_provider_service.data_provider_index is not None: + for data_source, _ in \ + self.data_provider_service \ + .data_provider_index.get_all(): + if data_source.symbol is not None: + known_symbols.add(data_source.symbol.upper()) + + if expected_symbol not in known_symbols: + sorted_symbols = sorted(known_symbols) + raise OperationalException( + f"No data source registered for '{expected_symbol}'. " + f"A data source is required to track price history. " + f"Registered data source symbols: {sorted_symbols}. " + f"To skip this check, set validate_symbol=False " + f"or omit the parameter." + ) + @property def config(self): """ diff --git a/tests/app/algorithm/test_validate_symbol.py b/tests/app/algorithm/test_validate_symbol.py index f6be2b47..a6ea798e 100644 --- a/tests/app/algorithm/test_validate_symbol.py +++ b/tests/app/algorithm/test_validate_symbol.py @@ -1,5 +1,5 @@ from investing_algorithm_framework import PortfolioConfiguration, \ - MarketCredential, OrderSide + MarketCredential, OrderSide, DataSource from investing_algorithm_framework.domain import OperationalException from tests.resources import TestBase from tests.resources.strategies_for_testing import StrategyOne @@ -8,8 +8,9 @@ class TestValidateSymbol(TestBase): """Tests for opt-in symbol validation on order creation (issue #247). - Validates that target_symbol is not the same as trading_symbol, - which would result in a nonsensical order (e.g. EUR/EUR). + Validates: + 1. target_symbol is not the same as trading_symbol (e.g. EUR/EUR) + 2. A data source exists for the target_symbol/trading_symbol pair """ portfolio_configurations = [ PortfolioConfiguration( @@ -28,8 +29,21 @@ class TestValidateSymbol(TestBase): "EUR": 1000 } - def test_default_allows_trading_symbol_as_target(self): - """Default behavior: no validation, even trading_symbol is accepted.""" + def _register_data_source(self, symbol): + """Register a data source symbol in the data provider index.""" + data_source = DataSource( + identifier=f"{symbol}_ohlcv", + symbol=symbol, + data_type="OHLCV", + time_frame="1d", + market="BITVAVO", + ) + data_provider_service = self.app.container.data_provider_service() + data_provider_service.data_provider_index\ + .data_providers_lookup[data_source] = None + + def test_default_allows_any_symbol(self): + """Default behavior: no validation, any symbol is accepted.""" self.app.add_strategy(StrategyOne) self.app.context.create_limit_order( target_symbol="EUR", @@ -41,8 +55,8 @@ def test_default_allows_trading_symbol_as_target(self): order = order_repository.find({"target_symbol": "EUR"}) self.assertIsNotNone(order) - def test_validate_symbol_false_allows_trading_symbol_as_target(self): - """Explicit validate_symbol=False allows trading_symbol as target.""" + def test_validate_symbol_false_allows_any_symbol(self): + """Explicit validate_symbol=False skips all validation.""" self.app.add_strategy(StrategyOne) self.app.context.create_limit_order( target_symbol="EUR", @@ -55,9 +69,8 @@ def test_validate_symbol_false_allows_trading_symbol_as_target(self): order = order_repository.find({"target_symbol": "EUR"}) self.assertIsNotNone(order) - def test_validate_symbol_true_rejects_trading_symbol_as_target(self): - """validate_symbol=True raises when target_symbol equals - trading_symbol (e.g. EUR/EUR).""" + def test_rejects_target_equals_trading_symbol(self): + """Rejects orders where target_symbol == trading_symbol (EUR/EUR).""" self.app.add_strategy(StrategyOne) with self.assertRaises(OperationalException) as cm: @@ -70,12 +83,11 @@ def test_validate_symbol_true_rejects_trading_symbol_as_target(self): ) error_msg = str(cm.exception) - self.assertIn("EUR", error_msg) - self.assertIn("trading_symbol", error_msg) self.assertIn("EUR/EUR", error_msg) + self.assertIn("trading_symbol", error_msg) - def test_validate_symbol_true_case_insensitive(self): - """validate_symbol=True catches trading_symbol regardless of case.""" + def test_rejects_target_equals_trading_symbol_case_insensitive(self): + """Case-insensitive check for target == trading_symbol.""" self.app.add_strategy(StrategyOne) with self.assertRaises(OperationalException): @@ -87,10 +99,27 @@ def test_validate_symbol_true_case_insensitive(self): validate_symbol=True, ) - def test_validate_symbol_true_accepts_valid_target(self): - """validate_symbol=True passes for a target that differs - from trading_symbol.""" + def test_rejects_missing_data_source(self): + """Rejects when no data source is registered for the pair.""" + self.app.add_strategy(StrategyOne) + + with self.assertRaises(OperationalException) as cm: + self.app.context.create_limit_order( + target_symbol="BTC", + amount=1, + price=10, + order_side=OrderSide.BUY, + validate_symbol=True, + ) + + error_msg = str(cm.exception) + self.assertIn("BTC/EUR", error_msg) + self.assertIn("No data source registered", error_msg) + + def test_accepts_with_matching_data_source(self): + """Passes when a data source exists for target/trading pair.""" self.app.add_strategy(StrategyOne) + self._register_data_source("BTC/EUR") self.app.context.create_limit_order( target_symbol="BTC", @@ -103,13 +132,14 @@ def test_validate_symbol_true_accepts_valid_target(self): order = order_repository.find({"target_symbol": "BTC"}) self.assertIsNotNone(order) - def test_validate_symbol_error_message(self): - """Error message explains the EUR/EUR problem and how to skip.""" + def test_rejects_wrong_data_source(self): + """Rejects when only a different pair is registered.""" self.app.add_strategy(StrategyOne) + self._register_data_source("ETH/EUR") with self.assertRaises(OperationalException) as cm: self.app.context.create_limit_order( - target_symbol="EUR", + target_symbol="BTC", amount=1, price=10, order_side=OrderSide.BUY, @@ -117,5 +147,27 @@ def test_validate_symbol_error_message(self): ) error_msg = str(cm.exception) - self.assertIn("EUR/EUR", error_msg) + self.assertIn("BTC/EUR", error_msg) + self.assertIn("ETH/EUR", error_msg) + + def test_error_message_lists_registered_symbols(self): + """Error message includes registered data source symbols.""" + self.app.add_strategy(StrategyOne) + self._register_data_source("BTC/EUR") + self._register_data_source("ETH/EUR") + + with self.assertRaises(OperationalException) as cm: + self.app.context.create_limit_order( + target_symbol="SOL", + amount=1, + price=10, + order_side=OrderSide.BUY, + validate_symbol=True, + ) + + error_msg = str(cm.exception) + self.assertIn("SOL/EUR", error_msg) + self.assertIn("BTC/EUR", error_msg) + self.assertIn("ETH/EUR", error_msg) + self.assertIn("validate_symbol=False", error_msg) self.assertIn("validate_symbol=False", error_msg) From 65c20c6aaea68525aa6654b5c214d499f39a62a1 Mon Sep 17 00:00:00 2001 From: marcvanduyn Date: Wed, 22 Apr 2026 09:30:47 +0100 Subject: [PATCH 5/5] test: add create_order tests for validate_symbol data source check (#247) Add 3 tests covering create_order with validate_symbol: - rejects when no data source registered for the pair - accepts when matching data source exists - default behavior skips validation --- tests/app/algorithm/test_validate_symbol.py | 57 ++++++++++++++++++- .../test_algorithm_backtest/algorithm_id.json | 2 +- .../backtest_EUR_20231201_20231202/run.json | 2 +- .../backtest_EUR_20231202_20231203/run.json | 2 +- 4 files changed, 58 insertions(+), 5 deletions(-) diff --git a/tests/app/algorithm/test_validate_symbol.py b/tests/app/algorithm/test_validate_symbol.py index a6ea798e..e71af130 100644 --- a/tests/app/algorithm/test_validate_symbol.py +++ b/tests/app/algorithm/test_validate_symbol.py @@ -1,5 +1,5 @@ from investing_algorithm_framework import PortfolioConfiguration, \ - MarketCredential, OrderSide, DataSource + MarketCredential, OrderSide, OrderType, DataSource from investing_algorithm_framework.domain import OperationalException from tests.resources import TestBase from tests.resources.strategies_for_testing import StrategyOne @@ -170,4 +170,57 @@ def test_error_message_lists_registered_symbols(self): self.assertIn("BTC/EUR", error_msg) self.assertIn("ETH/EUR", error_msg) self.assertIn("validate_symbol=False", error_msg) - self.assertIn("validate_symbol=False", error_msg) + + # ── create_order tests ────────────────────────────────────────── + + def test_create_order_rejects_missing_data_source(self): + """create_order with validate_symbol=True rejects when no + data source is registered for the pair.""" + self.app.add_strategy(StrategyOne) + + with self.assertRaises(OperationalException) as cm: + self.app.context.create_order( + target_symbol="BTC", + amount=1, + price=10, + order_type=OrderType.LIMIT, + order_side=OrderSide.BUY, + validate_symbol=True, + ) + + error_msg = str(cm.exception) + self.assertIn("BTC/EUR", error_msg) + self.assertIn("No data source registered", error_msg) + + def test_create_order_accepts_with_matching_data_source(self): + """create_order with validate_symbol=True passes when a + data source exists for the pair.""" + self.app.add_strategy(StrategyOne) + self._register_data_source("BTC/EUR") + + self.app.context.create_order( + target_symbol="BTC", + amount=1, + price=10, + order_type=OrderType.LIMIT, + order_side=OrderSide.BUY, + validate_symbol=True, + ) + order_repository = self.app.container.order_repository() + order = order_repository.find({"target_symbol": "BTC"}) + self.assertIsNotNone(order) + + def test_create_order_default_skips_validation(self): + """create_order without validate_symbol allows any symbol.""" + self.app.add_strategy(StrategyOne) + + self.app.context.create_order( + target_symbol="UNKNOWN", + amount=1, + price=10, + order_type=OrderType.LIMIT, + order_side=OrderSide.BUY, + ) + order_repository = self.app.container.order_repository() + order = order_repository.find({"target_symbol": "UNKNOWN"}) + self.assertIsNotNone(order) diff --git a/tests/resources/backtest_reports_for_testing/test_algorithm_backtest/algorithm_id.json b/tests/resources/backtest_reports_for_testing/test_algorithm_backtest/algorithm_id.json index 9ab210ec..fdb062a0 100644 --- a/tests/resources/backtest_reports_for_testing/test_algorithm_backtest/algorithm_id.json +++ b/tests/resources/backtest_reports_for_testing/test_algorithm_backtest/algorithm_id.json @@ -1,3 +1,3 @@ { - "algorithm_id": "TestStrategy" + "algorithm_id": "BacktestTestStrategy" } \ No newline at end of file diff --git a/tests/resources/backtest_reports_for_testing/test_algorithm_backtest/runs/backtest_EUR_20231201_20231202/run.json b/tests/resources/backtest_reports_for_testing/test_algorithm_backtest/runs/backtest_EUR_20231201_20231202/run.json index 1a29cedc..61a45db3 100644 --- a/tests/resources/backtest_reports_for_testing/test_algorithm_backtest/runs/backtest_EUR_20231201_20231202/run.json +++ b/tests/resources/backtest_reports_for_testing/test_algorithm_backtest/runs/backtest_EUR_20231201_20231202/run.json @@ -1 +1 @@ -{"backtest_start_date": "2023-12-01 00:00:00", "backtest_date_range_name": null, "backtest_end_date": "2023-12-02 00:00:00", "trading_symbol": "EUR", "initial_unallocated": 1000.0, "number_of_runs": 1441, "portfolio_snapshots": [{"metadata": "MetaData()", "portfolio_id": "1", "trading_symbol": "EUR", "pending_value": 0.0, "unallocated": 1000.0, "total_net_gain": 0.0, "total_revenue": 0.0, "total_cost": 0.0, "cash_flow": 0.0, "net_size": 1000.0, "created_at": "2023-12-01T00:00:00+00:00", "total_value": 1000.0}, {"metadata": "MetaData()", "portfolio_id": "1", "trading_symbol": "EUR", "pending_value": 0.0, "unallocated": 1000.0, "total_net_gain": 0.0, "total_revenue": 0.0, "total_cost": 0.0, "cash_flow": 0.0, "net_size": 1000.0, "created_at": "2023-12-02T00:00:00+00:00", "total_value": 1000.0}], "trades": [], "orders": [], "positions": [{"symbol": "EUR", "amount": 1000.0, "cost": 1000.0, "portfolio_id": 1}], "created_at": "2026-04-20 19:53:20", "symbols": [], "number_of_days": 0, "number_of_trades": 0, "number_of_trades_closed": 0, "number_of_trades_open": 0, "number_of_orders": 0, "number_of_positions": 0, "metadata": {}, "signals": {}, "signal_events": []} \ No newline at end of file +{"backtest_start_date": "2023-12-01 00:00:00", "backtest_date_range_name": null, "backtest_end_date": "2023-12-02 00:00:00", "trading_symbol": "EUR", "initial_unallocated": 1000.0, "number_of_runs": 1441, "portfolio_snapshots": [{"metadata": "MetaData()", "portfolio_id": "1", "trading_symbol": "EUR", "pending_value": 0.0, "unallocated": 1000.0, "total_net_gain": 0.0, "total_revenue": 0.0, "total_cost": 0.0, "cash_flow": 0.0, "net_size": 1000.0, "created_at": "2023-12-01T00:00:00+00:00", "total_value": 1000.0}, {"metadata": "MetaData()", "portfolio_id": "1", "trading_symbol": "EUR", "pending_value": 0.0, "unallocated": 1000.0, "total_net_gain": 0.0, "total_revenue": 0.0, "total_cost": 0.0, "cash_flow": 0.0, "net_size": 1000.0, "created_at": "2023-12-02T00:00:00+00:00", "total_value": 1000.0}], "trades": [], "orders": [], "positions": [{"symbol": "EUR", "amount": 1000.0, "cost": 1000.0, "portfolio_id": 1}], "created_at": "2026-04-22 08:04:47", "symbols": [], "number_of_days": 0, "number_of_trades": 0, "number_of_trades_closed": 0, "number_of_trades_open": 0, "number_of_orders": 0, "number_of_positions": 0, "metadata": {}, "signals": {}, "signal_events": []} \ No newline at end of file diff --git a/tests/resources/backtest_reports_for_testing/test_algorithm_backtest/runs/backtest_EUR_20231202_20231203/run.json b/tests/resources/backtest_reports_for_testing/test_algorithm_backtest/runs/backtest_EUR_20231202_20231203/run.json index bbac661a..8be5d1f8 100644 --- a/tests/resources/backtest_reports_for_testing/test_algorithm_backtest/runs/backtest_EUR_20231202_20231203/run.json +++ b/tests/resources/backtest_reports_for_testing/test_algorithm_backtest/runs/backtest_EUR_20231202_20231203/run.json @@ -1 +1 @@ -{"backtest_start_date": "2023-12-02 00:00:00", "backtest_date_range_name": null, "backtest_end_date": "2023-12-03 00:00:00", "trading_symbol": "EUR", "initial_unallocated": 1000.0, "number_of_runs": 1441, "portfolio_snapshots": [{"metadata": "MetaData()", "portfolio_id": "1", "trading_symbol": "EUR", "pending_value": 0.0, "unallocated": 1000.0, "total_net_gain": 0.0, "total_revenue": 0.0, "total_cost": 0.0, "cash_flow": 0.0, "net_size": 1000.0, "created_at": "2023-12-02T00:00:00+00:00", "total_value": 1000.0}, {"metadata": "MetaData()", "portfolio_id": "1", "trading_symbol": "EUR", "pending_value": 0.0, "unallocated": 1000.0, "total_net_gain": 0.0, "total_revenue": 0.0, "total_cost": 0.0, "cash_flow": 0.0, "net_size": 1000.0, "created_at": "2023-12-03T00:00:00+00:00", "total_value": 1000.0}], "trades": [], "orders": [], "positions": [{"symbol": "EUR", "amount": 1000.0, "cost": 1000.0, "portfolio_id": 1}], "created_at": "2026-04-20 19:56:42", "symbols": [], "number_of_days": 0, "number_of_trades": 0, "number_of_trades_closed": 0, "number_of_trades_open": 0, "number_of_orders": 0, "number_of_positions": 0, "metadata": {}, "signals": {}, "signal_events": []} \ No newline at end of file +{"backtest_start_date": "2023-12-02 00:00:00", "backtest_date_range_name": null, "backtest_end_date": "2023-12-03 00:00:00", "trading_symbol": "EUR", "initial_unallocated": 1000.0, "number_of_runs": 1441, "portfolio_snapshots": [{"metadata": "MetaData()", "portfolio_id": "1", "trading_symbol": "EUR", "pending_value": 0.0, "unallocated": 1000.0, "total_net_gain": 0.0, "total_revenue": 0.0, "total_cost": 0.0, "cash_flow": 0.0, "net_size": 1000.0, "created_at": "2023-12-02T00:00:00+00:00", "total_value": 1000.0}, {"metadata": "MetaData()", "portfolio_id": "1", "trading_symbol": "EUR", "pending_value": 0.0, "unallocated": 1000.0, "total_net_gain": 0.0, "total_revenue": 0.0, "total_cost": 0.0, "cash_flow": 0.0, "net_size": 1000.0, "created_at": "2023-12-03T00:00:00+00:00", "total_value": 1000.0}], "trades": [], "orders": [], "positions": [{"symbol": "EUR", "amount": 1000.0, "cost": 1000.0, "portfolio_id": 1}], "created_at": "2026-04-22 08:04:18", "symbols": [], "number_of_days": 0, "number_of_trades": 0, "number_of_trades_closed": 0, "number_of_trades_open": 0, "number_of_orders": 0, "number_of_positions": 0, "metadata": {}, "signals": {}, "signal_events": []} \ No newline at end of file