diff --git a/investing_algorithm_framework/app/context.py b/investing_algorithm_framework/app/context.py index ef4ebe9b..df52707e 100644 --- a/investing_algorithm_framework/app/context.py +++ b/investing_algorithm_framework/app/context.py @@ -51,6 +51,57 @@ def __init__( self.trade_take_profit_service: TradeTakeProfitService = \ trade_take_profit_service + def _validate_target_symbol(self, target_symbol, market=None): + """ + 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 validation fails + """ + portfolio = self.portfolio_service.find({"market": market}) + trading_symbol = portfolio.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 '{trading_symbol}'. " + f"This would result in a " + 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): """ @@ -78,7 +129,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 +148,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, + validates that target_symbol is not the trading_symbol. 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 +219,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 +251,15 @@ 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, + validates that target_symbol is not the trading_symbol. 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/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 new file mode 100644 index 00000000..e71af130 --- /dev/null +++ b/tests/app/algorithm/test_validate_symbol.py @@ -0,0 +1,226 @@ +from investing_algorithm_framework import PortfolioConfiguration, \ + MarketCredential, OrderSide, OrderType, 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). + + 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( + 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_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", + amount=1, + price=10, + order_side=OrderSide.BUY, + ) + order_repository = self.app.container.order_repository() + order = order_repository.find({"target_symbol": "EUR"}) + self.assertIsNotNone(order) + + 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", + amount=1, + price=10, + order_side=OrderSide.BUY, + validate_symbol=False, + ) + order_repository = self.app.container.order_repository() + order = order_repository.find({"target_symbol": "EUR"}) + self.assertIsNotNone(order) + + 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: + self.app.context.create_limit_order( + target_symbol="EUR", + amount=1, + price=10, + order_side=OrderSide.BUY, + validate_symbol=True, + ) + + error_msg = str(cm.exception) + self.assertIn("EUR/EUR", error_msg) + self.assertIn("trading_symbol", error_msg) + + 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): + self.app.context.create_limit_order( + target_symbol="eur", + amount=1, + price=10, + order_side=OrderSide.BUY, + validate_symbol=True, + ) + + 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", + 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_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="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("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) + + # ── 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