refactor: remove mock trading functionality from backend and frontend

Removes all mock price simulation features:
- Delete MockPriceManager from backend/data/
- Remove mock_mode, enable_mock, is_mock_mode flags from services
- Remove mock CLI options and config
- Remove mock mode UI components and state from frontend
- Update tests to remove mock references

Now system supports only live and backtest modes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-26 13:38:51 +08:00
parent fecf8a9466
commit 9bcc4221a4
21 changed files with 56 additions and 709 deletions

View File

@@ -181,7 +181,6 @@ class LaunchConfig(BaseModel):
start_date: Optional[str] = Field(default=None, description="回测开始日期 YYYY-MM-DD")
end_date: Optional[str] = Field(default=None, description="回测结束日期 YYYY-MM-DD")
poll_interval: int = Field(default=10, ge=1, le=300, description="市场数据轮询间隔(秒)")
enable_mock: bool = Field(default=False, description="是否启用模拟模式(使用模拟价格数据)")
class LaunchResponse(BaseModel):
@@ -756,7 +755,6 @@ async def start_runtime(
"start_date": config.start_date,
"end_date": config.end_date,
"poll_interval": config.poll_interval,
"enable_mock": config.enable_mock,
}
retention_keep = max(1, int(os.getenv("RUNS_RETENTION_COUNT", "20") or "20"))

View File

@@ -1019,11 +1019,6 @@ def backtest(
@app.command()
def live(
mock: bool = typer.Option(
False,
"--mock",
help="Use mock mode with simulated prices (for testing)",
),
config_name: str = typer.Option(
"live",
"--config-name",
@@ -1078,7 +1073,6 @@ def live(
Example:
evotraders live # Run immediately (default)
evotraders live --mock # Mock mode
evotraders live -t 22:30 # Run at 22:30 local time daily
evotraders live --schedule-mode intraday --interval-minutes 60
evotraders live --trigger-time now # Run immediately
@@ -1086,16 +1080,14 @@ def live(
"""
schedule_mode = str(_normalize_typer_value(schedule_mode, "daily"))
interval_minutes = int(_normalize_typer_value(interval_minutes, 60))
mode_name = "MOCK" if mock else "LIVE"
console.print(
Panel.fit(
f"[bold cyan]EvoTraders {mode_name} Mode[/bold cyan]",
"[bold cyan]EvoTraders LIVE Mode[/bold cyan]",
border_style="cyan",
),
)
# Check for required API key in live mode
if not mock:
env_file = get_project_root() / ".env"
if not env_file.exists():
console.print("\n[yellow]Warning: .env file not found[/yellow]")
@@ -1168,9 +1160,6 @@ def live(
# Display configuration
console.print("\n[bold]Configuration:[/bold]")
if mock:
console.print(" Mode: [yellow]MOCK[/yellow] (Simulated prices)")
else:
console.print(
" Mode: [green]LIVE[/green] (Real-time prices via Finnhub)",
)
@@ -1188,8 +1177,7 @@ def live(
project_root = get_project_root()
os.chdir(project_root)
# Data update (if not mock mode)
if not mock:
# Data update
run_data_updater(project_root)
auto_update_market_store(
config_name,
@@ -1200,10 +1188,6 @@ def live(
end_date=nyse_now.date().isoformat(),
force=False,
)
else:
console.print(
"\n[dim]Mock mode enabled - skipping data update[/dim]\n",
)
# Build command using backend.main
cmd = [
@@ -1229,8 +1213,6 @@ def live(
str(interval_minutes),
]
if mock:
cmd.append("--mock")
if enable_memory:
cmd.append("--enable-memory")

View File

@@ -244,10 +244,8 @@ async def run_pipeline(
start_date = bootstrap.get("start_date")
end_date = bootstrap.get("end_date")
enable_memory = bootstrap.get("enable_memory", False)
enable_mock = bootstrap.get("enable_mock", False)
is_backtest = mode == "backtest"
is_mock = enable_mock or mode == "mock" or (not is_backtest and os.getenv("MOCK_MODE", "false").lower() == "true")
# ======================================================================
# PHASE 0: Initialize runtime manager
@@ -288,9 +286,8 @@ async def run_pipeline(
market_service = MarketService(
tickers=tickers,
poll_interval=10,
mock_mode=is_mock and not is_backtest,
backtest_mode=is_backtest,
api_key=os.getenv("FINNHUB_API_KEY") if not is_mock and not is_backtest else None,
api_key=os.getenv("FINNHUB_API_KEY") if not is_backtest else None,
backtest_start_date=start_date if is_backtest else None,
backtest_end_date=end_date if is_backtest else None,
)
@@ -387,7 +384,6 @@ async def run_pipeline(
scheduler_callback=scheduler_callback,
config={
"mode": mode,
"mock_mode": is_mock,
"backtest_mode": is_backtest,
"tickers": tickers,
"config_name": run_id,

View File

@@ -465,7 +465,6 @@ class StateSync:
payload = {
"server_mode": self._state.get("server_mode", "live"),
"is_mock_mode": self._state.get("is_mock_mode", False),
"is_backtest": self._state.get("is_backtest", False),
"tickers": self._state.get("tickers"),
"runtime_config": self._state.get("runtime_config"),

View File

@@ -1,6 +1,5 @@
# -*- coding: utf-8 -*-
from backend.data.historical_price_manager import HistoricalPriceManager
from backend.data.mock_price_manager import MockPriceManager
from backend.data.polling_price_manager import PollingPriceManager
__all__ = ["MockPriceManager", "PollingPriceManager", "HistoricalPriceManager"]
__all__ = ["PollingPriceManager", "HistoricalPriceManager"]

View File

@@ -1,244 +0,0 @@
# -*- coding: utf-8 -*-
"""
Mock Price Manager - For testing during non-trading hours
Generates virtual real-time price data
"""
import logging
import os
import random
import threading
import time
from typing import Callable, Dict, List, Optional
from backend.data.provider_utils import normalize_symbol
logger = logging.getLogger(__name__)
class MockPriceManager:
"""Mock Price Manager - Generates virtual prices for testing"""
def __init__(self, poll_interval: int = 10, volatility: float = 0.5):
"""
Args:
poll_interval: Price update interval in seconds
volatility: Price volatility percentage
"""
if poll_interval is None:
poll_interval = int(os.getenv("MOCK_POLL_INTERVAL", "5"))
if volatility is None:
volatility = float(os.getenv("MOCK_VOLATILITY", "0.5"))
self.poll_interval = poll_interval
self.volatility = volatility
self.subscribed_symbols: List[str] = []
self.base_prices: Dict[str, float] = {}
self.open_prices: Dict[str, float] = {}
self.latest_prices: Dict[str, float] = {}
self.price_callbacks: List[Callable] = []
self.running = False
self._thread: Optional[threading.Thread] = None
self.default_base_prices = {
"AAPL": 237.50,
"MSFT": 425.30,
"GOOGL": 161.50,
"AMZN": 218.45,
"NVDA": 950.00,
"META": 573.22,
"TSLA": 342.15,
"AMD": 168.90,
"NFLX": 688.25,
"INTC": 42.18,
"COIN": 285.50,
"PLTR": 45.80,
"BABA": 88.30,
"DIS": 112.50,
"BKNG": 4850.00,
}
logger.info(
f"MockPriceManager initialized (interval: {self.poll_interval}s, "
f"volatility: {self.volatility}%)",
)
def subscribe(
self,
symbols: List[str],
base_prices: Dict[str, float] = None,
):
"""Subscribe to stock symbols"""
for symbol in symbols:
symbol = normalize_symbol(symbol)
if symbol not in self.subscribed_symbols:
self.subscribed_symbols.append(symbol)
if base_prices and symbol in base_prices:
base_price = base_prices[symbol]
elif symbol in self.default_base_prices:
base_price = self.default_base_prices[symbol]
else:
base_price = random.uniform(50, 500)
self.base_prices[symbol] = base_price
self.open_prices[symbol] = base_price
self.latest_prices[symbol] = base_price
logger.info(
f"Subscribed to mock price: {symbol} (base: ${base_price:.2f})", # noqa: E501
)
def unsubscribe(self, symbols: List[str]):
"""Unsubscribe from symbols"""
for symbol in symbols:
symbol = normalize_symbol(symbol)
if symbol in self.subscribed_symbols:
self.subscribed_symbols.remove(symbol)
self.base_prices.pop(symbol, None)
self.open_prices.pop(symbol, None)
self.latest_prices.pop(symbol, None)
logger.info(f"Unsubscribed: {symbol}")
def add_price_callback(self, callback: Callable):
"""Add price update callback"""
self.price_callbacks.append(callback)
def _generate_price_update(self, symbol: str) -> float:
"""Generate price update based on random walk"""
current_price = self.latest_prices.get(
symbol,
self.base_prices[symbol],
)
change_percent = random.uniform(-self.volatility, self.volatility)
new_price = current_price * (1 + change_percent / 100)
# 10% chance of larger movement
if random.random() < 0.1:
trend_factor = random.uniform(-2, 2)
new_price = new_price * (1 + trend_factor / 100)
# Limit intraday movement to +/-10%
open_price = self.open_prices[symbol]
max_price = open_price * 1.10
min_price = open_price * 0.90
new_price = max(min_price, min(max_price, new_price))
return new_price
def _update_prices(self):
"""Update prices for all subscribed stocks"""
timestamp = int(time.time() * 1000)
for symbol in self.subscribed_symbols:
try:
new_price = self._generate_price_update(symbol)
self.latest_prices[symbol] = new_price
open_price = self.open_prices[symbol]
ret = ((new_price - open_price) / open_price) * 100
price_data = {
"symbol": symbol,
"price": new_price,
"timestamp": timestamp,
"volume": random.randint(1000000, 10000000),
"open": open_price,
"high": max(new_price, open_price),
"low": min(new_price, open_price),
"previous_close": open_price,
"ret": ret,
}
for callback in self.price_callbacks:
try:
callback(price_data)
except Exception as e:
logger.error(
f"Mock price callback error ({symbol}): {e}",
)
logger.debug(
f"Mock {symbol}: ${new_price:.2f} [ret: {ret:+.2f}%]",
)
except Exception as e:
logger.error(f"Failed to generate mock price ({symbol}): {e}")
def _polling_loop(self):
"""Main polling loop"""
logger.info(
f"Mock price generation started (interval: {self.poll_interval}s)",
)
while self.running:
try:
start_time = time.time()
self._update_prices()
elapsed = time.time() - start_time
sleep_time = max(0, self.poll_interval - elapsed)
if sleep_time > 0:
time.sleep(sleep_time)
except Exception as e:
logger.error(f"Mock polling loop error: {e}")
time.sleep(5)
def start(self):
"""Start mock price generation"""
if self.running:
logger.warning("Mock price manager already running")
return
if not self.subscribed_symbols:
logger.warning("No stocks subscribed")
return
self.running = True
self._thread = threading.Thread(target=self._polling_loop, daemon=True)
self._thread.start()
logger.info(
f"Mock price manager started: {', '.join(self.subscribed_symbols)}", # noqa: E501
)
def stop(self):
"""Stop mock price generation"""
self.running = False
if self._thread:
self._thread.join(timeout=5)
logger.info("Mock price manager stopped")
def get_latest_price(self, symbol: str) -> Optional[float]:
"""Get latest price for symbol"""
return self.latest_prices.get(symbol)
def get_all_latest_prices(self) -> Dict[str, float]:
"""Get all latest prices"""
return self.latest_prices.copy()
def get_open_price(self, symbol: str) -> Optional[float]:
"""Get open price for symbol"""
return self.open_prices.get(symbol)
def reset_open_prices(self):
"""Reset open prices for new trading day"""
for symbol in self.subscribed_symbols:
last_close = self.latest_prices[symbol]
gap_percent = random.uniform(-1, 1)
new_open = last_close * (1 + gap_percent / 100)
self.open_prices[symbol] = new_open
self.latest_prices[symbol] = new_open
logger.info("Open prices reset")
def set_base_price(self, symbol: str, price: float):
"""Manually set base price for testing"""
if symbol in self.subscribed_symbols:
self.base_prices[symbol] = price
self.open_prices[symbol] = price
self.latest_prices[symbol] = price
logger.info(f"{symbol} base price set to: ${price:.2f}")
else:
logger.warning(f"{symbol} not subscribed")

View File

@@ -130,10 +130,8 @@ async def run_gateway(
end_date = bootstrap.get("end_date")
enable_memory = bootstrap.get("enable_memory", False)
poll_interval = int(bootstrap.get("poll_interval", 10))
enable_mock = bootstrap.get("enable_mock", False)
is_backtest = mode == "backtest"
is_mock = enable_mock or mode == "mock" or (not is_backtest and os.getenv("MOCK_MODE", "false").lower() == "true")
logger.info(f"[Gateway Server] Starting run {run_id} on port {port}")
@@ -152,9 +150,8 @@ async def run_gateway(
market_service = MarketService(
tickers=tickers,
poll_interval=poll_interval,
mock_mode=is_mock and not is_backtest,
backtest_mode=is_backtest,
api_key=os.getenv("FINNHUB_API_KEY") if not is_mock and not is_backtest else None,
api_key=os.getenv("FINNHUB_API_KEY") if not is_backtest else None,
backtest_start_date=start_date if is_backtest else None,
backtest_end_date=end_date if is_backtest else None,
)
@@ -247,7 +244,6 @@ async def run_gateway(
scheduler_callback=scheduler_callback,
config={
"mode": mode,
"mock_mode": is_mock,
"backtest_mode": is_backtest,
"tickers": tickers,
"config_name": run_id,

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""
Main Entry Point
Supports: backtest, live, mock modes
Supports: backtest, live modes
"""
import argparse
import asyncio
@@ -231,11 +231,8 @@ async def run_with_gateway(args):
market_service = MarketService(
tickers=tickers,
poll_interval=args.poll_interval,
mock_mode=args.mock and not is_backtest,
backtest_mode=is_backtest,
api_key=os.getenv("FINNHUB_API_KEY")
if not args.mock and not is_backtest
else None,
api_key=os.getenv("FINNHUB_API_KEY") if not is_backtest else None,
backtest_start_date=args.start_date if is_backtest else None,
backtest_end_date=args.end_date if is_backtest else None,
)
@@ -320,7 +317,6 @@ async def run_with_gateway(args):
scheduler_callback=scheduler_callback,
config={
"mode": args.mode,
"mock_mode": args.mock,
"backtest_mode": is_backtest,
"tickers": tickers,
"config_name": config_name,
@@ -353,8 +349,7 @@ def main():
"""Main entry point"""
parser = argparse.ArgumentParser(description="Trading System")
parser.add_argument("--mode", choices=["live", "backtest"], default="live")
parser.add_argument("--mock", action="store_true")
parser.add_argument("--config-name", default="mock")
parser.add_argument("--config-name", default="live")
parser.add_argument("--host", default="0.0.0.0")
parser.add_argument("--port", type=int, default=8765)
parser.add_argument(

View File

@@ -111,7 +111,6 @@ class Gateway:
host=host,
port=port,
poll_interval=self.config.get("poll_interval", 10),
mock=self.config.get("mock_mode", False),
tickers=self.config.get("tickers", []),
initial_cash=self.storage.initial_cash,
start_date=self._backtest_start_date or "",
@@ -125,10 +124,6 @@ class Gateway:
self.state_sync.update_state("status", "initializing")
self.state_sync.update_state("server_mode", self.mode)
self.state_sync.update_state("is_backtest", self.is_backtest)
self.state_sync.update_state(
"is_mock_mode",
self.config.get("mock_mode", False),
)
self.state_sync.update_state("tickers", self.config.get("tickers", []))
self.state_sync.update_state(
"runtime_config",
@@ -545,13 +540,13 @@ class Gateway:
websocket: ServerConnection,
data: Dict[str, Any],
) -> None:
"""Run one live/mock trading cycle on demand."""
"""Run one live trading cycle on demand."""
if self.is_backtest:
await websocket.send(
json.dumps(
{
"type": "error",
"message": "Manual trigger is only available in live/mock mode.",
"message": "Manual trigger is only available in live mode.",
},
ensure_ascii=False,
),

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""
Market Data Service
Supports live, mock, and backtest modes
Supports live and backtest modes
"""
import asyncio
import logging
@@ -36,7 +36,6 @@ class MarketService:
self,
tickers: List[str],
poll_interval: int = 10,
mock_mode: bool = False,
backtest_mode: bool = False,
api_key: Optional[str] = None,
backtest_start_date: Optional[str] = None,
@@ -44,7 +43,6 @@ class MarketService:
):
self.tickers = [normalize_symbol(ticker) for ticker in tickers]
self.poll_interval = poll_interval
self.mock_mode = mock_mode
self.backtest_mode = backtest_mode
self.api_key = api_key
self.backtest_start_date = backtest_start_date
@@ -69,8 +67,6 @@ class MarketService:
"""Return the active live quote provider for UI/debugging."""
if self.backtest_mode:
return "backtest"
if self.mock_mode:
return "mock"
if self._price_manager and hasattr(self._price_manager, "provider"):
provider = getattr(self._price_manager, "provider", None)
if isinstance(provider, str) and provider.strip():
@@ -81,8 +77,6 @@ class MarketService:
def mode_name(self) -> str:
if self.backtest_mode:
return "BACKTEST"
elif self.mock_mode:
return "MOCK"
return "LIVE"
async def start(self, broadcast_func: Callable):
@@ -96,8 +90,6 @@ class MarketService:
if self.backtest_mode:
self._start_backtest_mode()
elif self.mock_mode:
self._start_mock_mode()
else:
self._start_real_mode()
@@ -125,20 +117,6 @@ class MarketService:
return callback
def _start_mock_mode(self):
from backend.data.mock_price_manager import MockPriceManager
self._price_manager = MockPriceManager(
poll_interval=self.poll_interval,
volatility=0.5,
)
self._price_manager.add_price_callback(self._make_price_callback())
self._price_manager.subscribe(
self.tickers,
base_prices={t: 100.0 for t in self.tickers},
)
self._price_manager.start()
def _start_real_mode(self):
from backend.data.polling_price_manager import PollingPriceManager
@@ -262,12 +240,6 @@ class MarketService:
if removed:
self._price_manager.unsubscribe(removed)
if added:
if self.mock_mode:
self._price_manager.subscribe(
added,
base_prices={ticker: 100.0 for ticker in added},
)
else:
self._price_manager.subscribe(added)
if self.backtest_mode and self._current_date:

View File

@@ -34,7 +34,6 @@ def test_live_runs_incremental_market_store_update_before_start(monkeypatch, tmp
monkeypatch.setattr(cli.subprocess, "run", fake_run)
cli.live(
mock=False,
config_name="smoke_fullstack",
host="0.0.0.0",
port=8765,

View File

@@ -6,153 +6,10 @@ import logging
from unittest.mock import MagicMock, AsyncMock, patch
import pytest
from backend.services.market import MarketService
from backend.data.mock_price_manager import MockPriceManager
from backend.data.polling_price_manager import PollingPriceManager
from backend.llm.models import RetryChatModel
class TestMockPriceManager:
def test_init_default(self):
manager = MockPriceManager()
assert manager.poll_interval == 10
assert manager.volatility == 0.5
assert manager.running is False
assert len(manager.subscribed_symbols) == 0
def test_init_custom(self):
manager = MockPriceManager(poll_interval=5, volatility=1.0)
assert manager.poll_interval == 5
assert manager.volatility == 1.0
def test_subscribe(self):
manager = MockPriceManager()
manager.subscribe(["AAPL", "MSFT"])
assert "AAPL" in manager.subscribed_symbols
assert "MSFT" in manager.subscribed_symbols
assert manager.base_prices["AAPL"] == 237.50 # default price
assert manager.base_prices["MSFT"] == 425.30 # default price
def test_subscribe_with_base_prices(self):
manager = MockPriceManager()
manager.subscribe(["AAPL"], base_prices={"AAPL": 100.0})
assert manager.base_prices["AAPL"] == 100.0
assert manager.open_prices["AAPL"] == 100.0
assert manager.latest_prices["AAPL"] == 100.0
def test_subscribe_unknown_symbol(self):
manager = MockPriceManager()
manager.subscribe(["UNKNOWN"])
assert "UNKNOWN" in manager.subscribed_symbols
assert manager.base_prices["UNKNOWN"] > 0 # random price generated
def test_unsubscribe(self):
manager = MockPriceManager()
manager.subscribe(["AAPL", "MSFT"])
manager.unsubscribe(["AAPL"])
assert "AAPL" not in manager.subscribed_symbols
assert "MSFT" in manager.subscribed_symbols
def test_add_price_callback(self):
manager = MockPriceManager()
callback = MagicMock()
manager.add_price_callback(callback)
assert callback in manager.price_callbacks
def test_generate_price_update_within_bounds(self):
manager = MockPriceManager(volatility=0.5)
manager.subscribe(["AAPL"], base_prices={"AAPL": 100.0})
for _ in range(100):
new_price = manager._generate_price_update("AAPL")
# Should be within +/-10% of open
assert 90.0 <= new_price <= 110.0
def test_update_prices_triggers_callback(self):
manager = MockPriceManager()
manager.subscribe(["AAPL"], base_prices={"AAPL": 100.0})
callback = MagicMock()
manager.add_price_callback(callback)
manager._update_prices()
callback.assert_called_once()
call_args = callback.call_args[0][0]
assert call_args["symbol"] == "AAPL"
assert "price" in call_args
assert "timestamp" in call_args
def test_start_stop(self):
manager = MockPriceManager(poll_interval=1)
manager.subscribe(["AAPL"], base_prices={"AAPL": 100.0})
manager.start()
assert manager.running is True
time.sleep(0.1) # let thread start
manager.stop()
assert manager.running is False
def test_start_without_subscription(self):
manager = MockPriceManager()
manager.start()
assert (
manager.running is False
) # should not start without subscriptions
def test_get_latest_price(self):
manager = MockPriceManager()
manager.subscribe(["AAPL"], base_prices={"AAPL": 100.0})
price = manager.get_latest_price("AAPL")
assert price == 100.0
def test_get_latest_price_unknown(self):
manager = MockPriceManager()
price = manager.get_latest_price("UNKNOWN")
assert price is None
def test_get_all_latest_prices(self):
manager = MockPriceManager()
manager.subscribe(
["AAPL", "MSFT"],
base_prices={"AAPL": 100.0, "MSFT": 200.0},
)
prices = manager.get_all_latest_prices()
assert prices["AAPL"] == 100.0
assert prices["MSFT"] == 200.0
def test_reset_open_prices(self):
manager = MockPriceManager()
manager.subscribe(["AAPL"], base_prices={"AAPL": 100.0})
manager.latest_prices["AAPL"] = 105.0
manager.reset_open_prices()
# Open price should change (based on latest with small gap)
assert manager.open_prices["AAPL"] != 100.0
def test_set_base_price(self):
manager = MockPriceManager()
manager.subscribe(["AAPL"], base_prices={"AAPL": 100.0})
manager.set_base_price("AAPL", 150.0)
assert manager.base_prices["AAPL"] == 150.0
assert manager.open_prices["AAPL"] == 150.0
assert manager.latest_prices["AAPL"] == 150.0
class TestPollingPriceManager:
def test_init(self):
manager = PollingPriceManager(api_key="test_key", poll_interval=30)
@@ -288,35 +145,12 @@ class TestRetryChatModel:
class TestMarketService:
def test_init_mock_mode(self):
service = MarketService(
tickers=["AAPL", "MSFT"],
poll_interval=10,
mock_mode=True,
)
assert service.tickers == ["AAPL", "MSFT"]
assert service.poll_interval == 10
assert service.mock_mode is True
assert service.running is False
def test_init_real_mode(self):
service = MarketService(
tickers=["AAPL"],
mock_mode=False,
api_key="test_key",
)
assert service.mock_mode is False
assert service.api_key == "test_key"
@patch("backend.services.market.get_data_sources", return_value=["yfinance", "local_csv"])
@patch.object(PollingPriceManager, "start")
def test_start_real_mode_with_yfinance(self, _mock_start, _mock_sources):
service = MarketService(
tickers=["AAPL"],
poll_interval=10,
mock_mode=False,
)
service._start_real_mode()
@@ -330,7 +164,6 @@ class TestMarketService:
service = MarketService(
tickers=["AAPL"],
poll_interval=10,
mock_mode=False,
)
service._start_real_mode()
@@ -338,30 +171,11 @@ class TestMarketService:
assert isinstance(service._price_manager, PollingPriceManager)
assert service._price_manager.provider == "yfinance"
@pytest.mark.asyncio
async def test_start_mock_mode(self):
service = MarketService(
tickers=["AAPL"],
poll_interval=10,
mock_mode=True,
)
broadcast_func = AsyncMock()
await service.start(broadcast_func)
assert service.running is True
assert service._price_manager is not None
assert isinstance(service._price_manager, MockPriceManager)
service.stop()
@patch("backend.services.market.get_data_sources", return_value=["finnhub", "yfinance"])
@pytest.mark.asyncio
async def test_start_real_mode_without_api_key(self, _mock_sources):
service = MarketService(
tickers=["AAPL"],
mock_mode=False,
api_key=None,
)
@@ -376,11 +190,12 @@ class TestMarketService:
async def test_start_already_running(self):
service = MarketService(
tickers=["AAPL"],
mock_mode=True,
backtest_mode=True,
)
broadcast_func = AsyncMock()
# First start with backtest mode
await service.start(broadcast_func)
assert service.running is True
@@ -392,7 +207,7 @@ class TestMarketService:
def test_stop(self):
service = MarketService(
tickers=["AAPL"],
mock_mode=True,
backtest_mode=True,
)
service.running = True
service._price_manager = MagicMock()
@@ -405,7 +220,7 @@ class TestMarketService:
def test_stop_when_not_running(self):
service = MarketService(
tickers=["AAPL"],
mock_mode=True,
backtest_mode=True,
)
# Should not raise
@@ -413,20 +228,20 @@ class TestMarketService:
assert service.running is False
def test_get_price_sync(self):
service = MarketService(tickers=["AAPL"], mock_mode=True)
service = MarketService(tickers=["AAPL"], backtest_mode=True)
service.cache["AAPL"] = {"price": 150.0, "open": 148.0}
price = service.get_price_sync("AAPL")
assert price == 150.0
def test_get_price_sync_not_found(self):
service = MarketService(tickers=["AAPL"], mock_mode=True)
service = MarketService(tickers=["AAPL"], backtest_mode=True)
price = service.get_price_sync("MSFT")
assert price is None
def test_get_all_prices(self):
service = MarketService(tickers=["AAPL", "MSFT"], mock_mode=True)
service = MarketService(tickers=["AAPL", "MSFT"], backtest_mode=True)
service.cache["AAPL"] = {"price": 150.0}
service.cache["MSFT"] = {"price": 400.0}
@@ -437,7 +252,7 @@ class TestMarketService:
@pytest.mark.asyncio
async def test_broadcast_price_update(self):
service = MarketService(tickers=["AAPL"], mock_mode=True)
service = MarketService(tickers=["AAPL"], backtest_mode=True)
service._broadcast_func = AsyncMock()
price_data = {
@@ -457,7 +272,7 @@ class TestMarketService:
@pytest.mark.asyncio
async def test_broadcast_price_update_no_func(self):
service = MarketService(tickers=["AAPL"], mock_mode=True)
service = MarketService(tickers=["AAPL"], backtest_mode=True)
service._broadcast_func = None
price_data = {"symbol": "AAPL", "price": 150.0, "open": 148.0}
@@ -465,67 +280,6 @@ class TestMarketService:
# Should not raise
await service._broadcast_price_update(price_data)
@pytest.mark.asyncio
async def test_price_callback_thread_safety(self):
service = MarketService(
tickers=["AAPL"],
poll_interval=1,
mock_mode=True,
)
received_prices = []
async def capture_broadcast(msg):
received_prices.append(msg)
await service.start(capture_broadcast)
# Wait for at least one price update
await asyncio.sleep(1.5)
service.stop()
# Should have received at least one price update
assert len(received_prices) >= 1
assert received_prices[0]["type"] == "price_update"
class TestMarketServiceIntegration:
@pytest.mark.asyncio
async def test_full_mock_cycle(self):
service = MarketService(
tickers=["AAPL", "MSFT"],
poll_interval=1,
mock_mode=True,
)
messages = []
async def collect_messages(msg):
messages.append(msg)
await service.start(collect_messages)
# Wait for price updates
await asyncio.sleep(2.5)
service.stop()
# Should have received multiple price updates
assert len(messages) >= 2
# Check message structure
symbols_seen = set()
for msg in messages:
assert msg["type"] == "price_update"
assert "symbol" in msg
assert "price" in msg
assert "ret" in msg
symbols_seen.add(msg["symbol"])
# Should have prices for both tickers
assert "AAPL" in symbols_seen or "MSFT" in symbols_seen
if __name__ == "__main__":
pytest.main([__file__, "-v"])

View File

@@ -310,7 +310,6 @@ def test_start_runtime_restore_reuses_historical_run_id(monkeypatch, tmp_path):
"enable_memory": False,
"mode": "live",
"poll_interval": 10,
"enable_mock": False,
},
}
}

View File

@@ -30,7 +30,6 @@ class TerminalDashboard:
self.port = 8765
self.poll_interval = 10
self.trigger_time = "now"
self.mock = False
self.enable_memory = False
self.local_time = ""
self.nyse_time = ""
@@ -65,7 +64,6 @@ class TerminalDashboard:
port: int,
poll_interval: int,
trigger_time: str = "now",
mock: bool = False,
enable_memory: bool = False,
local_time: str = "",
nyse_time: str = "",
@@ -82,7 +80,6 @@ class TerminalDashboard:
self.port = port
self.poll_interval = poll_interval
self.trigger_time = trigger_time
self.mock = mock
self.enable_memory = enable_memory
self.local_time = local_time
self.nyse_time = nyse_time
@@ -109,8 +106,6 @@ class TerminalDashboard:
# Mode line
if self.mode == "backtest":
mode_str = "[cyan]Backtest[/cyan]"
elif self.mock:
mode_str = "[yellow]MOCK[/yellow]"
else:
mode_str = "[green]LIVE[/green]"
@@ -216,8 +211,6 @@ class TerminalDashboard:
title = "[bold cyan]EvoTraders[/bold cyan]"
if self.mode == "backtest":
title += " [dim]Backtest[/dim]"
elif self.mock:
title += " [dim]Mock[/dim]"
else:
title += " [dim]Live[/dim]"

View File

@@ -291,7 +291,6 @@ export default function LiveTradingApp() {
financial_datasets: 'FINANCIAL DATASETS',
local_csv: 'CSV',
polygon: 'POLYGON',
mock: 'MOCK',
backtest: 'BACKTEST',
}), []);
@@ -415,7 +414,6 @@ export default function LiveTradingApp() {
pollIntervalDraft={runtimeControls.pollIntervalDraft}
startDateDraft={runtimeControls.startDateDraft}
endDateDraft={runtimeControls.endDateDraft}
enableMockDraft={runtimeControls.enableMockDraft}
watchlistDraftSymbols={runtimeControls.watchlistDraftSymbols}
watchlistInputValue={runtimeControls.watchlistInputValue}
watchlistSuggestions={runtimeControls.watchlistSuggestions}
@@ -432,7 +430,6 @@ export default function LiveTradingApp() {
onPollIntervalChange={runtimeControls.setPollIntervalDraft}
onStartDateChange={runtimeControls.setStartDateDraft}
onEndDateChange={runtimeControls.setEndDateDraft}
onEnableMockChange={runtimeControls.setEnableMockDraft}
onWatchlistInputChange={runtimeControls.handleWatchlistInputChange}
onWatchlistInputKeyDown={runtimeControls.handleWatchlistInputKeyDown}
onWatchlistAdd={runtimeControls.handleWatchlistAdd}

View File

@@ -72,7 +72,6 @@ export default function AppShell({
pollIntervalDraft,
startDateDraft,
endDateDraft,
enableMockDraft,
watchlistDraftSymbols,
watchlistInputValue,
watchlistSuggestions,
@@ -89,7 +88,6 @@ export default function AppShell({
onPollIntervalChange,
onStartDateChange,
onEndDateChange,
onEnableMockChange,
onWatchlistInputChange,
onWatchlistInputKeyDown,
onWatchlistAdd,
@@ -186,44 +184,6 @@ export default function AppShell({
<Header />
<div className="header-right" style={{ display: 'flex', alignItems: 'center', gap: 24, marginLeft: 'auto', flexWrap: 'wrap', minWidth: 0 }}>
{/* Mock Mode Indicator */}
{virtualTime && (
<div style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '4px 10px', borderRadius: 4, background: '#FF9800', border: '1px solid #FFB74D' }}>
<span style={{ fontSize: '14px' }}></span>
<span style={{ fontSize: '11px', fontWeight: 600, color: '#FFFFFF', fontFamily: '"Courier New", monospace', letterSpacing: '0.5px' }}>
模拟模式
</span>
</div>
)}
{/* Clock Display (only in Mock mode) */}
{virtualTime && (
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 2, padding: '4px 12px', borderRadius: 4, background: '#1A237E', border: '1px solid #3F51B5' }}>
<span style={{ fontSize: '11px', color: '#999', fontFamily: '"Courier New", monospace', textTransform: 'uppercase', letterSpacing: '0.5px' }}>虚拟时间</span>
<span style={{ fontSize: '14px', fontWeight: 700, color: '#FFFFFF', fontFamily: '"Courier New", monospace', letterSpacing: '1px' }}>
{now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false })}
</span>
<span style={{ fontSize: '10px', color: '#999', fontFamily: '"Courier New", monospace' }}>
{now.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}
</span>
</div>
{/* Fast Forward Button (only in Mock mode) */}
<button
onClick={() => {
if (agentRequests?.clientRef?.current) {
agentRequests.clientRef.current.send({ type: 'fast_forward_time', minutes: 30 });
}
}}
style={{ padding: '6px 12px', borderRadius: 4, background: '#3F51B5', border: '1px solid #5C6BC0', color: '#FFFFFF', fontSize: '12px', fontFamily: '"Courier New", monospace', fontWeight: 600, cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 4, textTransform: 'uppercase', letterSpacing: '0.5px' }}
title="快进30分钟 (Mock模式)"
>
+30min
</button>
</div>
)}
{/* Unified Status Indicator */}
<div className="header-status-inline">
<span className={`status-dot ${isConnected ? (isUpdating ? 'updating' : 'live') : 'offline'}`} />
@@ -320,7 +280,6 @@ export default function AppShell({
pollInterval={pollIntervalDraft}
startDate={startDateDraft}
endDate={endDateDraft}
enableMock={enableMockDraft}
watchlistSymbols={watchlistDraftSymbols}
watchlistInputValue={watchlistInputValue}
watchlistSuggestions={watchlistSuggestions}
@@ -339,7 +298,6 @@ export default function AppShell({
onPollIntervalChange={onPollIntervalChange}
onStartDateChange={onStartDateChange}
onEndDateChange={onEndDateChange}
onEnableMockChange={onEnableMockChange}
onWatchlistInputChange={onWatchlistInputChange}
onWatchlistInputKeyDown={onWatchlistInputKeyDown}
onWatchlistAdd={onWatchlistAdd}

View File

@@ -5,10 +5,10 @@ import { formatNumber, formatFullNumber } from '../utils/formatters';
/**
* Helper function to get the start time of the most recent trading session
* Trading session: 22:30 - next day 05:00
* @param {Date|null} virtualTime - Virtual time from server (for mock mode), or null to use real time
* @param {Date|null} virtualTime - Virtual time from server, or null to use real time
*/
function getRecentTradingSessionStart(virtualTime = null) {
// Use virtual time if provided (for mock mode), otherwise use real time
// Use virtual time if provided, otherwise use real time
let now;
if (virtualTime) {
// Ensure virtualTime is a valid Date object

View File

@@ -30,7 +30,6 @@ export default function RuntimeSettingsPanel({
pollInterval,
startDate,
endDate,
enableMock,
watchlistSymbols,
watchlistInputValue,
watchlistSuggestions,
@@ -49,7 +48,6 @@ export default function RuntimeSettingsPanel({
onPollIntervalChange,
onStartDateChange,
onEndDateChange,
onEnableMockChange,
onWatchlistInputChange,
onWatchlistInputKeyDown,
onWatchlistAdd,
@@ -580,21 +578,6 @@ export default function RuntimeSettingsPanel({
}}
/>
</label>
<label style={{ display: 'flex', alignItems: 'center', gap: 10, marginTop: 2 }}>
<input
type="checkbox"
checked={enableMock}
onChange={(e) => onEnableMockChange(e.target.checked)}
style={{
width: 16,
height: 16,
accentColor: '#0D47A1',
cursor: 'pointer'
}}
/>
<span style={{ fontSize: '11px', color: '#111111', fontWeight: 700 }}>启用模拟数据 (Mock)</span>
</label>
</div>
)}

View File

@@ -63,8 +63,6 @@ export function useRuntimeControls({ clientRef, currentTickers, addSystemMessage
setStartDateDraft,
endDateDraft,
setEndDateDraft,
enableMockDraft,
setEnableMockDraft,
runtimeConfigFeedback,
setRuntimeConfigFeedback,
isRuntimeConfigSaving,
@@ -400,7 +398,6 @@ export function useRuntimeControls({ clientRef, currentTickers, addSystemMessage
poll_interval: Number(pollIntervalDraft) || Number(DEFAULT_POLL_INTERVAL),
start_date: startDateDraft || null,
end_date: endDateDraft || null,
enable_mock: Boolean(enableMockDraft)
});
setIsRuntimeConfigSaving(false);
@@ -424,7 +421,6 @@ export function useRuntimeControls({ clientRef, currentTickers, addSystemMessage
addSystemMessage,
clientRef,
enableMemoryDraft,
enableMockDraft,
endDateDraft,
initialCashDraft,
intervalMinutesDraft,
@@ -463,11 +459,9 @@ export function useRuntimeControls({ clientRef, currentTickers, addSystemMessage
setPollIntervalDraft(DEFAULT_POLL_INTERVAL);
setStartDateDraft("");
setEndDateDraft("");
setEnableMockDraft(false);
setRuntimeConfigFeedback(null);
}, [
setEnableMemoryDraft,
setEnableMockDraft,
setEndDateDraft,
setInitialCashDraft,
setIntervalMinutesDraft,
@@ -542,7 +536,6 @@ export function useRuntimeControls({ clientRef, currentTickers, addSystemMessage
pollIntervalDraft,
startDateDraft,
endDateDraft,
enableMockDraft,
runtimeConfigFeedback,
isRuntimeConfigSaving,
isWatchlistSavingRef,
@@ -582,7 +575,6 @@ export function useRuntimeControls({ clientRef, currentTickers, addSystemMessage
setPollIntervalDraft,
setStartDateDraft,
setEndDateDraft,
setEnableMockDraft,
setIsWatchlistSaving,
setIsRuntimeConfigSaving
};

View File

@@ -225,19 +225,10 @@ export function useWebSocketConnection({
setTickers((prevTickers) => buildTickersFromSymbols(state.tickers, prevTickers));
}
const isMockMode = state.is_mock_mode === true;
if (state.market_status) {
setMarketStatus(state.market_status);
if (isMockMode && state.market_status.current_time) {
try {
setVirtualTime(new Date(state.market_status.current_time));
} catch (error) {
console.error('Error parsing virtual time from market_status:', error);
}
} else {
setVirtualTime(null);
}
}
if (state.trading_days_total) {
setProgress({
@@ -783,19 +774,14 @@ export function useWebSocketConnection({
if (e.beijing_time_str) {
const statusEmoji = { market_open: '📊', off_market: '⏸️', non_trading_day: '📅', trade_execution: '💼' };
const emoji = statusEmoji[e.status] || '⏰';
const isMockMode = e.is_mock_mode === true;
let logMessage = `${emoji} ${isMockMode ? '虚拟时间' : '时间'}: ${e.beijing_time_str} | 状态: ${e.status}`;
let logMessage = `${emoji} 时间: ${e.beijing_time_str} | 状态: ${e.status}`;
if (e.hours_to_open !== undefined) logMessage += ` | 距离开盘: ${e.hours_to_open}小时`;
if (e.hours_to_trade !== undefined) logMessage += ` | 距离交易: ${e.hours_to_trade}小时`;
if (e.trading_date) logMessage += ` | 交易日: ${e.trading_date}`;
console.log(logMessage);
if (isMockMode && e.beijing_time) {
try { setVirtualTime(new Date(e.beijing_time)); } catch (error) { console.error('Error parsing virtual time:', error); }
} else {
setVirtualTime(null);
}
}
if (e.market_status) setMarketStatus(e.market_status);
},

View File

@@ -75,7 +75,6 @@ export const useRuntimeStore = create((set) => ({
pollIntervalDraft: '10',
startDateDraft: '',
endDateDraft: '',
enableMockDraft: false,
setLaunchModeDraft: (launchModeDraft) => set((state) => ({ launchModeDraft: resolveValue(launchModeDraft, state.launchModeDraft) })),
setRestoreRunIdDraft: (restoreRunIdDraft) => set((state) => ({ restoreRunIdDraft: resolveValue(restoreRunIdDraft, state.restoreRunIdDraft) })),
setRuntimeHistoryRuns: (runtimeHistoryRuns) => set((state) => ({ runtimeHistoryRuns: resolveValue(runtimeHistoryRuns, state.runtimeHistoryRuns) })),
@@ -90,7 +89,6 @@ export const useRuntimeStore = create((set) => ({
setPollIntervalDraft: (pollIntervalDraft) => set((state) => ({ pollIntervalDraft: resolveValue(pollIntervalDraft, state.pollIntervalDraft) })),
setStartDateDraft: (startDateDraft) => set((state) => ({ startDateDraft: resolveValue(startDateDraft, state.startDateDraft) })),
setEndDateDraft: (endDateDraft) => set((state) => ({ endDateDraft: resolveValue(endDateDraft, state.endDateDraft) })),
setEnableMockDraft: (enableMockDraft) => set((state) => ({ enableMockDraft: resolveValue(enableMockDraft, state.enableMockDraft) })),
// Runtime config feedback
runtimeConfigFeedback: null,