404 lines
14 KiB
Python
404 lines
14 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Test InsiderDataService with mock Finnhub client and real InsiderDataRepository.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
from datetime import datetime, timedelta
|
|
from typing import Any
|
|
|
|
# Add the project root to the path
|
|
sys.path.insert(0, os.path.abspath("."))
|
|
|
|
from tradingagents.clients.base import BaseClient
|
|
from tradingagents.models.context import DataQuality, InsiderContext, InsiderTransaction
|
|
from tradingagents.repositories.insider_repository import InsiderDataRepository
|
|
from tradingagents.services.insider_data_service import InsiderDataService
|
|
|
|
|
|
class MockFinnhubClient(BaseClient):
|
|
"""Mock Finnhub client that returns sample insider trading data."""
|
|
|
|
def __init__(self, **kwargs):
|
|
super().__init__(**kwargs)
|
|
self.connection_works = True
|
|
|
|
def test_connection(self) -> bool:
|
|
return self.connection_works
|
|
|
|
def get_data(self, *args, **kwargs) -> dict[str, Any]:
|
|
"""Not used directly by InsiderDataService."""
|
|
return {}
|
|
|
|
def get_insider_trading(
|
|
self, ticker: str, start_date: str, end_date: str
|
|
) -> dict[str, Any]:
|
|
"""Return mock insider trading data."""
|
|
# Use fixed dates within test range for predictable filtering
|
|
base_date = datetime(2024, 6, 15) # Within our test range
|
|
|
|
return {
|
|
"ticker": ticker,
|
|
"data_type": "insider_trading",
|
|
"transactions": [
|
|
{
|
|
"filingDate": (base_date - timedelta(days=30)).strftime("%Y-%m-%d"),
|
|
"name": "John Smith",
|
|
"change": -50000,
|
|
"sharesTotal": 150000,
|
|
"transactionPrice": 180.50,
|
|
"transactionCode": "S", # Sale
|
|
},
|
|
{
|
|
"filingDate": (base_date - timedelta(days=20)).strftime("%Y-%m-%d"),
|
|
"name": "Jane Doe",
|
|
"change": 25000,
|
|
"sharesTotal": 75000,
|
|
"transactionPrice": 185.25,
|
|
"transactionCode": "P", # Purchase
|
|
},
|
|
{
|
|
"filingDate": (base_date - timedelta(days=10)).strftime("%Y-%m-%d"),
|
|
"name": "Robert Johnson",
|
|
"change": -10000,
|
|
"sharesTotal": 40000,
|
|
"transactionPrice": 178.75,
|
|
"transactionCode": "S", # Sale
|
|
},
|
|
{
|
|
"filingDate": (base_date - timedelta(days=5)).strftime("%Y-%m-%d"),
|
|
"name": "Mary Wilson",
|
|
"change": 15000,
|
|
"sharesTotal": 65000,
|
|
"transactionPrice": 182.00,
|
|
"transactionCode": "P", # Purchase
|
|
},
|
|
],
|
|
"metadata": {
|
|
"source": "mock_finnhub",
|
|
"retrieved_at": datetime(2024, 1, 2).isoformat(),
|
|
"symbol": ticker,
|
|
},
|
|
}
|
|
|
|
|
|
def test_online_mode_with_mock_finnhub():
|
|
"""Test InsiderDataService in online mode with mock Finnhub client."""
|
|
# Create mock client and real repository
|
|
mock_finnhub = MockFinnhubClient()
|
|
real_repo = InsiderDataRepository("test_data")
|
|
|
|
# Create service in online mode
|
|
service = InsiderDataService(
|
|
finnhub_client=mock_finnhub,
|
|
repository=real_repo,
|
|
online_mode=True,
|
|
data_dir="test_data",
|
|
)
|
|
|
|
# Test getting insider context
|
|
context = service.get_insider_context(
|
|
symbol="AAPL",
|
|
start_date="2024-01-01",
|
|
end_date="2024-12-31",
|
|
force_refresh=True,
|
|
)
|
|
|
|
# Validate context structure
|
|
assert isinstance(context, InsiderContext)
|
|
assert context.symbol == "AAPL"
|
|
assert context.period["start"] == "2024-01-01"
|
|
assert context.period["end"] == "2024-12-31"
|
|
|
|
# Validate transactions
|
|
assert len(context.transactions) == 4
|
|
assert all(isinstance(tx, InsiderTransaction) for tx in context.transactions)
|
|
|
|
# Check transaction details
|
|
first_tx = context.transactions[0]
|
|
assert first_tx.name == "John Smith"
|
|
assert first_tx.change == -50000 # Sale
|
|
assert first_tx.transaction_code == "S"
|
|
assert first_tx.transaction_price == 180.50
|
|
|
|
# Validate sentiment data and net activity
|
|
assert "buy_sell_ratio" in context.sentiment_data
|
|
assert "insider_sentiment_score" in context.sentiment_data
|
|
|
|
assert "net_shares_change" in context.net_activity
|
|
assert "net_transaction_value" in context.net_activity
|
|
assert "buy_transactions" in context.net_activity
|
|
assert "sell_transactions" in context.net_activity
|
|
|
|
# Validate metadata
|
|
assert context.transaction_count == 4
|
|
assert "data_quality" in context.metadata
|
|
assert context.metadata["service"] == "insider_data"
|
|
|
|
# Test JSON serialization
|
|
json_output = context.model_dump_json(indent=2)
|
|
assert len(json_output) > 0
|
|
|
|
|
|
def test_insider_sentiment_analysis():
|
|
"""Test insider sentiment calculation based on transactions."""
|
|
mock_finnhub = MockFinnhubClient()
|
|
service = InsiderDataService(
|
|
finnhub_client=mock_finnhub, repository=None, online_mode=True
|
|
)
|
|
|
|
context = service.get_insider_context("TSLA", "2024-01-01", "2024-12-31")
|
|
|
|
# Check sentiment calculations
|
|
sentiment = context.sentiment_data
|
|
|
|
# Should have buy/sell ratio
|
|
assert "buy_sell_ratio" in sentiment
|
|
assert sentiment["buy_sell_ratio"] > 0
|
|
|
|
# Should have insider sentiment score (-1 to 1)
|
|
assert "insider_sentiment_score" in sentiment
|
|
assert -1.0 <= sentiment["insider_sentiment_score"] <= 1.0
|
|
|
|
# Check net activity calculations
|
|
net_activity = context.net_activity
|
|
|
|
# Net shares change: sum of all changes
|
|
expected_net_change = -50000 + 25000 + (-10000) + 15000 # -20000
|
|
assert net_activity["net_shares_change"] == expected_net_change
|
|
|
|
# Should have buy/sell transaction counts
|
|
assert net_activity["buy_transactions"] == 2 # Jane Doe and Mary Wilson
|
|
assert net_activity["sell_transactions"] == 2 # John Smith and Robert Johnson
|
|
|
|
|
|
def test_offline_mode():
|
|
"""Test InsiderDataService in offline mode."""
|
|
real_repo = InsiderDataRepository("test_data")
|
|
|
|
service = InsiderDataService(
|
|
finnhub_client=None, repository=real_repo, online_mode=False
|
|
)
|
|
|
|
# Should handle offline gracefully
|
|
context = service.get_insider_context("AAPL", "2024-01-01", "2024-12-31")
|
|
|
|
assert context.symbol == "AAPL"
|
|
assert len(context.transactions) == 0 # No data available offline
|
|
assert context.transaction_count == 0
|
|
# Sentiment data should have default values even with no transactions
|
|
assert context.sentiment_data.get("insider_sentiment_score", 0) == 0
|
|
assert context.sentiment_data.get("buy_sell_ratio", 0) == 0
|
|
# Net activity should have default values
|
|
assert context.net_activity.get("net_shares_change", 0) == 0
|
|
assert context.net_activity.get("net_transaction_value", 0) == 0
|
|
assert context.metadata.get("data_quality") == DataQuality.LOW
|
|
|
|
|
|
def test_empty_data_handling():
|
|
"""Test handling when no insider transactions are available."""
|
|
|
|
class EmptyDataClient(MockFinnhubClient):
|
|
def get_insider_trading(self, ticker, start_date, end_date):
|
|
return {
|
|
"ticker": ticker,
|
|
"data_type": "insider_trading",
|
|
"transactions": [],
|
|
"metadata": {"source": "mock_finnhub", "empty": True},
|
|
}
|
|
|
|
empty_client = EmptyDataClient()
|
|
service = InsiderDataService(
|
|
finnhub_client=empty_client, repository=None, online_mode=True
|
|
)
|
|
|
|
context = service.get_insider_context("XYZ", "2024-01-01", "2024-12-31")
|
|
|
|
# Should handle empty data gracefully
|
|
assert context.symbol == "XYZ"
|
|
assert len(context.transactions) == 0
|
|
assert context.transaction_count == 0
|
|
|
|
# Sentiment should be neutral with no data
|
|
assert context.sentiment_data.get("insider_sentiment_score", 0) == 0
|
|
assert context.net_activity.get("net_shares_change", 0) == 0
|
|
assert context.metadata.get("data_quality") == DataQuality.LOW
|
|
|
|
|
|
def test_error_handling():
|
|
"""Test error handling with broken client."""
|
|
|
|
class BrokenFinnhubClient(BaseClient):
|
|
def test_connection(self):
|
|
return False
|
|
|
|
def get_data(self, *args, **kwargs):
|
|
raise Exception("Finnhub API error")
|
|
|
|
def get_insider_trading(self, *args, **kwargs):
|
|
raise Exception("Finnhub API error")
|
|
|
|
broken_client = BrokenFinnhubClient()
|
|
service = InsiderDataService(
|
|
finnhub_client=broken_client, repository=None, online_mode=True
|
|
)
|
|
|
|
# Should handle errors gracefully
|
|
context = service.get_insider_context(
|
|
"FAIL", "2024-01-01", "2024-12-31", force_refresh=True
|
|
)
|
|
|
|
assert context.symbol == "FAIL"
|
|
assert len(context.transactions) == 0
|
|
assert context.transaction_count == 0
|
|
assert context.metadata.get("data_quality") == DataQuality.LOW
|
|
# Service logs errors but doesn't include them in metadata
|
|
|
|
|
|
def test_transaction_filtering():
|
|
"""Test filtering transactions by date range."""
|
|
|
|
# Create a client that returns transactions outside the date range
|
|
class DateFilterTestClient(MockFinnhubClient):
|
|
def get_insider_trading(self, ticker, start_date, end_date):
|
|
return {
|
|
"ticker": ticker,
|
|
"data_type": "insider_trading",
|
|
"transactions": [
|
|
{
|
|
"filingDate": "2023-12-15", # Before start date
|
|
"name": "Old Transaction",
|
|
"change": -1000,
|
|
"sharesTotal": 10000,
|
|
"transactionPrice": 100.0,
|
|
"transactionCode": "S",
|
|
},
|
|
{
|
|
"filingDate": "2024-06-15", # Within range
|
|
"name": "Valid Transaction",
|
|
"change": 5000,
|
|
"sharesTotal": 15000,
|
|
"transactionPrice": 110.0,
|
|
"transactionCode": "P",
|
|
},
|
|
{
|
|
"filingDate": "2025-01-15", # After end date
|
|
"name": "Future Transaction",
|
|
"change": -2000,
|
|
"sharesTotal": 8000,
|
|
"transactionPrice": 120.0,
|
|
"transactionCode": "S",
|
|
},
|
|
],
|
|
"metadata": {"source": "mock_finnhub"},
|
|
}
|
|
|
|
filter_client = DateFilterTestClient()
|
|
service = InsiderDataService(
|
|
finnhub_client=filter_client, repository=None, online_mode=True
|
|
)
|
|
|
|
context = service.get_insider_context("TEST", "2024-01-01", "2024-12-31")
|
|
|
|
# Should only include the transaction within the date range
|
|
assert len(context.transactions) == 1
|
|
assert context.transactions[0].name == "Valid Transaction"
|
|
assert context.transaction_count == 1
|
|
|
|
|
|
def test_json_structure():
|
|
"""Test JSON structure of insider context."""
|
|
mock_finnhub = MockFinnhubClient()
|
|
service = InsiderDataService(
|
|
finnhub_client=mock_finnhub, repository=None, online_mode=True
|
|
)
|
|
|
|
context = service.get_insider_context("NVDA", "2024-01-01", "2024-12-31")
|
|
json_data = context.model_dump()
|
|
|
|
# Validate required fields
|
|
required_fields = [
|
|
"symbol",
|
|
"period",
|
|
"transactions",
|
|
"sentiment_data",
|
|
"transaction_count",
|
|
"net_activity",
|
|
"metadata",
|
|
]
|
|
for field in required_fields:
|
|
assert field in json_data
|
|
|
|
# Validate transaction structure
|
|
if json_data["transactions"]:
|
|
transaction = json_data["transactions"][0]
|
|
required_tx_fields = [
|
|
"filing_date",
|
|
"name",
|
|
"change",
|
|
"shares",
|
|
"transaction_price",
|
|
"transaction_code",
|
|
]
|
|
for field in required_tx_fields:
|
|
assert field in transaction
|
|
|
|
# Validate sentiment data structure
|
|
sentiment = json_data["sentiment_data"]
|
|
assert "buy_sell_ratio" in sentiment
|
|
assert "insider_sentiment_score" in sentiment
|
|
|
|
# Validate net activity structure
|
|
net_activity = json_data["net_activity"]
|
|
expected_net_fields = [
|
|
"net_shares_change",
|
|
"net_transaction_value",
|
|
"buy_transactions",
|
|
"sell_transactions",
|
|
]
|
|
for field in expected_net_fields:
|
|
assert field in net_activity
|
|
|
|
# Validate metadata
|
|
metadata = json_data["metadata"]
|
|
assert "data_quality" in metadata
|
|
assert "service" in metadata
|
|
|
|
|
|
def test_comprehensive_sentiment_calculation():
|
|
"""Test comprehensive insider sentiment calculation."""
|
|
mock_finnhub = MockFinnhubClient()
|
|
service = InsiderDataService(
|
|
finnhub_client=mock_finnhub, repository=None, online_mode=True
|
|
)
|
|
|
|
context = service.get_insider_context("COMP", "2024-01-01", "2024-12-31")
|
|
|
|
# Validate sentiment calculations are reasonable
|
|
sentiment = context.sentiment_data
|
|
net_activity = context.net_activity
|
|
|
|
# Buy/sell ratio should be positive (we have both buys and sells)
|
|
assert sentiment["buy_sell_ratio"] >= 0
|
|
|
|
# Insider sentiment score should be between -1 and 1
|
|
assert -1.0 <= sentiment["insider_sentiment_score"] <= 1.0
|
|
|
|
# Net transaction value should be calculated correctly
|
|
expected_value = (
|
|
(-50000 * 180.50) # John Smith sale
|
|
+ (25000 * 185.25) # Jane Doe purchase
|
|
+ (-10000 * 178.75) # Robert Johnson sale
|
|
+ (15000 * 182.00) # Mary Wilson purchase
|
|
)
|
|
assert abs(net_activity["net_transaction_value"] - expected_value) < 0.01
|
|
|
|
# Transaction counts should match
|
|
assert net_activity["buy_transactions"] == 2
|
|
assert net_activity["sell_transactions"] == 2
|
|
|
|
# Net shares change
|
|
expected_net_shares = -50000 + 25000 + (-10000) + 15000 # -20000
|
|
assert net_activity["net_shares_change"] == expected_net_shares
|