263 lines
8.8 KiB
Python
263 lines
8.8 KiB
Python
"""
|
|
Lemontropia Suite - Market Price Tracker
|
|
Fetch and track market prices from EntropiaWiki and other sources.
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
import requests
|
|
from decimal import Decimal
|
|
from pathlib import Path
|
|
from dataclasses import dataclass
|
|
from typing import Dict, Optional, List
|
|
from datetime import datetime, timedelta
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass
|
|
class MarketPrice:
|
|
"""Market price data for an item."""
|
|
item_name: str
|
|
markup: Decimal # Percentage (e.g., 120.5 means 120.5%)
|
|
tt_value: Decimal # Trade Terminal value
|
|
mu_value: Decimal # Markup value
|
|
last_updated: datetime
|
|
source: str # Where the price came from
|
|
|
|
|
|
class EntropiaWikiPrices:
|
|
"""
|
|
Fetch market prices from EntropiaWiki.
|
|
|
|
Note: This requires scraping or API access.
|
|
EntropiaWiki may have rate limits.
|
|
"""
|
|
|
|
BASE_URL = "https://www.entropiawiki.com"
|
|
|
|
def __init__(self, cache_duration: int = 3600):
|
|
self.cache_duration = cache_duration # Seconds
|
|
self.price_cache: Dict[str, MarketPrice] = {}
|
|
self.cache_timestamps: Dict[str, datetime] = {}
|
|
|
|
self.session = requests.Session()
|
|
self.session.headers.update({
|
|
'User-Agent': 'Lemontropia-Suite/1.0 (Personal Use)'
|
|
})
|
|
|
|
def _is_cache_valid(self, item_name: str) -> bool:
|
|
"""Check if cached price is still valid."""
|
|
if item_name not in self.cache_timestamps:
|
|
return False
|
|
|
|
age = datetime.now() - self.cache_timestamps[item_name]
|
|
return age.seconds < self.cache_duration
|
|
|
|
def get_item_price(self, item_name: str) -> Optional[MarketPrice]:
|
|
"""
|
|
Get market price for item.
|
|
|
|
Note: This is a placeholder. Actual implementation would need
|
|
to scrape EntropiaWiki or use their API if available.
|
|
"""
|
|
# Check cache first
|
|
if self._is_cache_valid(item_name):
|
|
return self.price_cache[item_name]
|
|
|
|
# Placeholder: In reality, this would fetch from wiki
|
|
# For now, return None to indicate we need real implementation
|
|
logger.debug(f"Price lookup not implemented for: {item_name}")
|
|
return None
|
|
|
|
def clear_cache(self):
|
|
"""Clear price cache."""
|
|
self.price_cache.clear()
|
|
self.cache_timestamps.clear()
|
|
|
|
|
|
class ManualPriceTracker:
|
|
"""
|
|
Manual price tracking for items you frequently trade.
|
|
"""
|
|
|
|
def __init__(self, data_dir: Optional[Path] = None):
|
|
self.data_dir = data_dir or Path.home() / ".lemontropia" / "prices"
|
|
self.data_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
self.prices: Dict[str, MarketPrice] = {}
|
|
self._load_prices()
|
|
|
|
def _load_prices(self):
|
|
"""Load saved prices."""
|
|
price_file = self.data_dir / "manual_prices.json"
|
|
if price_file.exists():
|
|
try:
|
|
with open(price_file, 'r') as f:
|
|
data = json.load(f)
|
|
for name, price_data in data.items():
|
|
self.prices[name] = MarketPrice(
|
|
item_name=name,
|
|
markup=Decimal(str(price_data.get('markup', 100))),
|
|
tt_value=Decimal(str(price_data.get('tt', 0))),
|
|
mu_value=Decimal(str(price_data.get('mu', 0))),
|
|
last_updated=datetime.fromisoformat(price_data.get('updated', datetime.now().isoformat())),
|
|
source='manual'
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Failed to load prices: {e}")
|
|
|
|
def _save_prices(self):
|
|
"""Save prices to file."""
|
|
price_file = self.data_dir / "manual_prices.json"
|
|
try:
|
|
data = {}
|
|
for name, price in self.prices.items():
|
|
data[name] = {
|
|
'markup': str(price.markup),
|
|
'tt': str(price.tt_value),
|
|
'mu': str(price.mu_value),
|
|
'updated': price.last_updated.isoformat(),
|
|
}
|
|
with open(price_file, 'w') as f:
|
|
json.dump(data, f, indent=2)
|
|
except Exception as e:
|
|
logger.error(f"Failed to save prices: {e}")
|
|
|
|
def set_price(self, item_name: str, markup: Decimal, tt_value: Decimal):
|
|
"""Manually set price for an item."""
|
|
mu_value = tt_value * (markup / 100)
|
|
|
|
self.prices[item_name] = MarketPrice(
|
|
item_name=item_name,
|
|
markup=markup,
|
|
tt_value=tt_value,
|
|
mu_value=mu_value,
|
|
last_updated=datetime.now(),
|
|
source='manual'
|
|
)
|
|
|
|
self._save_prices()
|
|
logger.info(f"Set price for {item_name}: {markup}%")
|
|
|
|
def get_price(self, item_name: str) -> Optional[MarketPrice]:
|
|
"""Get price for item."""
|
|
return self.prices.get(item_name)
|
|
|
|
def calculate_loot_value(self, loot_items: List[Tuple[str, Decimal, int]]) -> Dict:
|
|
"""
|
|
Calculate total value of loot based on tracked prices.
|
|
|
|
Args:
|
|
loot_items: List of (item_name, tt_value, quantity)
|
|
|
|
Returns:
|
|
Dict with tt_value, mu_value, markup breakdown
|
|
"""
|
|
total_tt = Decimal("0")
|
|
total_mu = Decimal("0")
|
|
unknown_items = []
|
|
|
|
for item_name, tt_value, quantity in loot_items:
|
|
item_tt = tt_value * quantity
|
|
total_tt += item_tt
|
|
|
|
price = self.get_price(item_name)
|
|
if price:
|
|
item_mu = item_tt * (price.markup / 100)
|
|
total_mu += item_mu
|
|
else:
|
|
# Assume 100% (TT value) if no price
|
|
total_mu += item_tt
|
|
unknown_items.append(item_name)
|
|
|
|
return {
|
|
'total_tt': total_tt,
|
|
'total_mu': total_mu,
|
|
'total_markup': (total_mu / total_tt * 100) if total_tt > 0 else Decimal("0"),
|
|
'unknown_items': list(set(unknown_items)),
|
|
}
|
|
|
|
|
|
class ProfitCalculator:
|
|
"""
|
|
Calculate crafting/hunting profitability with current market prices.
|
|
"""
|
|
|
|
def __init__(self, price_tracker: ManualPriceTracker):
|
|
self.price_tracker = price_tracker
|
|
|
|
def calculate_crafting_profit(self, blueprint: str,
|
|
material_costs: Dict[str, Decimal],
|
|
output_items: List[Tuple[str, Decimal, int]]) -> Dict:
|
|
"""
|
|
Calculate crafting profitability.
|
|
|
|
Args:
|
|
blueprint: Blueprint name
|
|
material_costs: Dict of material_name -> cost
|
|
output_items: List of (item_name, tt_value, quantity)
|
|
|
|
Returns:
|
|
Profit analysis
|
|
"""
|
|
# Calculate input cost
|
|
input_cost = sum(material_costs.values())
|
|
|
|
# Calculate output value
|
|
loot_value = self.price_tracker.calculate_loot_value(output_items)
|
|
output_mu = loot_value['total_mu']
|
|
|
|
# Calculate profit
|
|
profit = output_mu - input_cost
|
|
profit_margin = (profit / input_cost * 100) if input_cost > 0 else Decimal("0")
|
|
|
|
return {
|
|
'input_cost': input_cost,
|
|
'output_tt': loot_value['total_tt'],
|
|
'output_mu': output_mu,
|
|
'profit': profit,
|
|
'profit_margin': profit_margin,
|
|
'is_profitable': profit > 0,
|
|
'unknown_prices': loot_value['unknown_items'],
|
|
}
|
|
|
|
def calculate_hunting_profit(self, ammo_cost: Decimal,
|
|
armor_decay: Decimal,
|
|
healing_cost: Decimal,
|
|
loot_items: List[Tuple[str, Decimal, int]]) -> Dict:
|
|
"""
|
|
Calculate hunting profitability.
|
|
|
|
Args:
|
|
ammo_cost: Total ammo spent
|
|
armor_decay: Armor decay cost
|
|
healing_cost: Healing cost
|
|
loot_items: List of (item_name, tt_value, quantity)
|
|
> Returns:
|
|
Profit analysis
|
|
"""
|
|
total_cost = ammo_cost + armor_decay + healing_cost
|
|
|
|
loot_value = self.price_tracker.calculate_loot_value(loot_items)
|
|
loot_mu = loot_value['total_mu']
|
|
|
|
profit = loot_mu - total_cost
|
|
return_pct = (loot_mu / total_cost * 100) if total_cost > 0 else Decimal("0")
|
|
|
|
return {
|
|
'total_cost': total_cost,
|
|
'ammo_cost': ammo_cost,
|
|
'armor_decay': armor_decay,
|
|
'healing_cost': healing_cost,
|
|
'loot_tt': loot_value['total_tt'],
|
|
'loot_mu': loot_mu,
|
|
'profit': profit,
|
|
'return_pct': return_pct,
|
|
'unknown_prices': loot_value['unknown_items'],
|
|
}
|
|
|
|
|
|
# Export main classes
|
|
__all__ = ['MarketPrice', 'ManualPriceTracker', 'ProfitCalculator', 'EntropiaWikiPrices']
|