Implementation of phase 4.1

This commit is contained in:
Nitzan Pomerantz
2025-10-26 10:58:46 +02:00
parent 4cb70c547b
commit 0ac9d136cd
13 changed files with 2115 additions and 373 deletions
+44 -62
View File
@@ -8,8 +8,9 @@ Focused on providing data metrics; the LLM interprets them for investment advice
import logging
from collections import defaultdict
from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional, Tuple
from typing import Dict, List, Optional, Tuple
from .models import Deal, MarketActivityScore, InvestmentAnalysis, LiquidityMetrics
from .statistics import calculate_std_dev
logger = logging.getLogger(__name__)
@@ -34,7 +35,7 @@ LIQUIDITY_LOW_THRESHOLD = 0.5
def parse_deal_dates(
deals: List[Dict[str, Any]], time_period_months: Optional[int] = None
deals: List[Deal], time_period_months: Optional[int] = None
) -> Tuple[List[str], Dict[str, int], Dict[str, int]]:
"""
Parse and filter deal dates from a list of deals.
@@ -44,7 +45,7 @@ def parse_deal_dates(
time period if specified, and groups deals by month and quarter.
Args:
deals: List of deal dictionaries with 'dealDate' field
deals: List of Deal model instances
time_period_months: Optional time period to filter (from today backwards)
Returns:
@@ -67,7 +68,7 @@ def parse_deal_dates(
deal_dates = []
for deal in deals:
date_str = deal.get("dealDate", "")
date_str = deal.deal_date
if not date_str:
continue
@@ -99,8 +100,8 @@ def parse_deal_dates(
def calculate_market_activity_score(
deals: List[Dict[str, Any]], time_period_months: int = 12
) -> Dict[str, Any]:
deals: List[Deal], time_period_months: int = 12
) -> MarketActivityScore:
"""
Calculate market activity and liquidity metrics.
@@ -108,17 +109,16 @@ def calculate_market_activity_score(
to provide a comprehensive view of market liquidity.
Args:
deals: List of deal dictionaries
deals: List of Deal model instances
time_period_months: Time period to analyze in months (default: 12)
Returns:
Dictionary containing:
MarketActivityScore model with:
- total_deals: Total number of deals
- deals_per_month: Average deals per month
- activity_score: Market activity score (0-100)
- trend: Activity trend ('increasing', 'stable', 'decreasing')
- monthly_distribution: Deals per month breakdown
- activity_level: Description ('very_high', 'high', 'moderate', 'low', 'very_low')
Raises:
ValueError: If deals list is empty or invalid
@@ -138,19 +138,14 @@ def calculate_market_activity_score(
# Based on deals per month using defined thresholds
if deals_per_month >= ACTIVITY_VERY_HIGH_THRESHOLD:
activity_score = 100
activity_level = "very_high"
elif deals_per_month >= ACTIVITY_HIGH_THRESHOLD:
activity_score = 75 + ((deals_per_month - ACTIVITY_HIGH_THRESHOLD) / ACTIVITY_HIGH_THRESHOLD) * 25
activity_level = "high"
elif deals_per_month >= ACTIVITY_MODERATE_THRESHOLD:
activity_score = 50 + ((deals_per_month - ACTIVITY_MODERATE_THRESHOLD) / (ACTIVITY_HIGH_THRESHOLD - ACTIVITY_MODERATE_THRESHOLD)) * 25
activity_level = "moderate"
elif deals_per_month >= ACTIVITY_LOW_THRESHOLD:
activity_score = 25 + ((deals_per_month - ACTIVITY_LOW_THRESHOLD) / (ACTIVITY_MODERATE_THRESHOLD - ACTIVITY_LOW_THRESHOLD)) * 25
activity_level = "low"
else:
activity_score = deals_per_month * 25
activity_level = "very_low"
# Calculate trend (compare first half vs second half)
sorted_months = sorted(monthly_deals.keys())
@@ -172,18 +167,17 @@ def calculate_market_activity_score(
else:
trend = "insufficient_data"
return {
"total_deals": total_deals,
"unique_months": unique_months,
"deals_per_month": round(deals_per_month, 2),
"activity_score": round(activity_score, 1),
"activity_level": activity_level,
"trend": trend,
"monthly_distribution": dict(sorted(monthly_deals.items())),
}
return MarketActivityScore(
activity_score=round(activity_score, 1),
total_deals=total_deals,
deals_per_month=round(deals_per_month, 2),
trend=trend,
time_period_months=time_period_months,
monthly_distribution=dict(sorted(monthly_deals.items())),
)
def analyze_investment_potential(deals: List[Dict[str, Any]]) -> Dict[str, Any]:
def analyze_investment_potential(deals: List[Deal]) -> InvestmentAnalysis:
"""
Analyze investment potential based on price trends and market stability.
@@ -192,10 +186,10 @@ def analyze_investment_potential(deals: List[Dict[str, Any]]) -> Dict[str, Any]:
data metrics; the LLM interprets them for investment advice.
Args:
deals: List of deal dictionaries with price and date information
deals: List of Deal model instances with price and date information
Returns:
Dictionary containing:
InvestmentAnalysis model containing:
- price_appreciation_rate: Annual price growth rate (%)
- price_volatility: Price volatility score (0-100, lower is more stable)
- market_stability: Stability rating ('very_stable', 'stable', 'moderate', 'volatile', 'very_volatile')
@@ -214,10 +208,10 @@ def analyze_investment_potential(deals: List[Dict[str, Any]]) -> Dict[str, Any]:
# Extract price per sqm and dates
price_data = []
for deal in deals:
price_per_sqm = deal.get("price_per_sqm")
date_str = deal.get("dealDate", "")
price_per_sqm = deal.price_per_sqm # Use computed field from Deal model
date_str = deal.deal_date
if isinstance(price_per_sqm, (int, float)) and price_per_sqm > 0 and date_str:
if price_per_sqm and price_per_sqm > 0 and date_str:
try:
# Parse date for sorting
year = int(date_str[:4])
@@ -311,22 +305,22 @@ def analyze_investment_potential(deals: List[Dict[str, Any]]) -> Dict[str, Any]:
else:
data_quality = "limited"
return {
"price_appreciation_rate": round(price_appreciation_rate, 2),
"price_volatility": round(volatility_score, 1),
"market_stability": market_stability,
"price_trend": price_trend,
"avg_price_per_sqm": round(avg_price_per_sqm, 0),
"price_change_pct": round(price_change_pct, 2),
"investment_score": round(investment_score, 1),
"data_quality": data_quality,
"sample_size": n,
}
return InvestmentAnalysis(
investment_score=round(investment_score, 1),
price_trend=price_trend,
price_appreciation_rate=round(price_appreciation_rate, 2),
price_volatility=round(volatility_score, 1),
market_stability=market_stability,
avg_price_per_sqm=round(avg_price_per_sqm, 0),
price_change_pct=round(price_change_pct, 2),
total_deals=n,
data_quality=data_quality,
)
def get_market_liquidity(
deals: List[Dict[str, Any]], time_period_months: int = 12
) -> Dict[str, Any]:
deals: List[Deal], time_period_months: int = 12
) -> LiquidityMetrics:
"""
Get detailed market liquidity and turnover metrics.
@@ -400,23 +394,11 @@ def get_market_liquidity(
else:
trend_direction = "insufficient_data"
# Find most active period
if quarterly_deals:
most_active_quarter = max(quarterly_deals.items(), key=lambda x: x[1])
most_active_period = f"{most_active_quarter[0]} ({most_active_quarter[1]} deals)"
else:
most_active_period = "N/A"
return {
"total_deals": total_deals,
"unique_months": unique_months,
"unique_quarters": unique_quarters,
"deals_per_month": round(deals_per_month, 2),
"deals_per_quarter": round(deals_per_quarter, 2),
"quarterly_breakdown": dict(sorted(quarterly_deals.items())),
"monthly_breakdown": dict(sorted(monthly_deals.items())),
"velocity_score": round(velocity_score, 1),
"liquidity_rating": liquidity_rating,
"trend_direction": trend_direction,
"most_active_period": most_active_period,
}
return LiquidityMetrics(
liquidity_score=round(velocity_score, 1),
total_deals=total_deals,
time_period_months=time_period_months,
avg_deals_per_month=round(deals_per_month, 2),
deal_velocity=round(deals_per_month, 2),
market_activity_level=liquidity_rating,
)