Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions backend/app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,14 @@ def get_base_model_configs() -> List[Tuple[str, str]]:
"trading_cycle": "daily_before_close",
"realtime_prices": 600, # Stock prices: 10 minutes
"polymarket_prices": 1800, # Polymarket prices: 30 minutes by default
"forex_prices": 900, # Forex prices: 15 minutes
}

TRADING_CONFIG = {
"initial_cash_stock": 1000,
"initial_cash_polymarket": 500,
"initial_cash_bitmex": 1000,
"initial_cash_forex": 1000,
"max_consecutive_failures": 3,
"recovery_wait_time": 3600,
"error_retry_time": 600,
Expand Down Expand Up @@ -169,3 +171,4 @@ class MockMode(str, Enum):
STOCK_MOCK_MODE = MockMode.NONE
POLYMARKET_MOCK_MODE = MockMode.NONE
BITMEX_MOCK_MODE = MockMode.NONE
FOREX_MOCK_MODE = MockMode.NONE
38 changes: 36 additions & 2 deletions backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@
)
from live_trade_bench.systems import (
BitMEXPortfolioSystem,
ForexPortfolioSystem,
PolymarketPortfolioSystem,
StockPortfolioSystem,
)

from .config import (
ALLOWED_ORIGINS,
BITMEX_MOCK_MODE,
FOREX_MOCK_MODE,
MODELS_DATA_FILE,
MODELS_DATA_HIST_FILE,
MODELS_DATA_INIT_FILE,
Expand All @@ -46,6 +48,7 @@
from .price_data import (
get_next_price_update_time,
update_bitmex_prices_and_values,
update_forex_prices_and_values,
update_polymarket_prices_and_values,
update_stock_prices_and_values,
)
Expand All @@ -59,6 +62,7 @@
stock_system = None
polymarket_system = None
bitmex_system = None
forex_system = None
# Background scheduler instance; assigned during startup to keep reference alive
scheduler = None

Expand All @@ -81,10 +85,15 @@
MockMode.NONE: BitMEXPortfolioSystem,
}

FOREX_SYSTEMS = {
MockMode.NONE: ForexPortfolioSystem,
}

# Initialize systems immediately when module loads
stock_system = STOCK_SYSTEMS[STOCK_MOCK_MODE].get_instance()
polymarket_system = POLYMARKET_SYSTEMS[POLYMARKET_MOCK_MODE].get_instance()
bitmex_system = BITMEX_SYSTEMS[BITMEX_MOCK_MODE].get_instance()
forex_system = FOREX_SYSTEMS[FOREX_MOCK_MODE].get_instance()

# Add agents for real systems
if STOCK_MOCK_MODE == MockMode.NONE:
Expand All @@ -99,14 +108,21 @@
for display_name, model_id in get_base_model_configs():
bitmex_system.add_agent(display_name, 1000.0, model_id)

# Add Forex agents (paper trading with $1,000 each)
for display_name, model_id in get_base_model_configs():
forex_system.add_agent(display_name, 1000.0, model_id)

# 🆕 加载历史数据到Account内存中
print("🔄 Loading historical data to account memory...")
load_historical_data_to_accounts(stock_system, polymarket_system, bitmex_system)
load_historical_data_to_accounts(
stock_system, polymarket_system, bitmex_system, forex_system
)
print("✅ Historical data loading completed")

stock_system.initialize_for_live()
polymarket_system.initialize_for_live()
bitmex_system.initialize_for_live()
forex_system.initialize_for_live()


def get_stock_system():
Expand All @@ -127,6 +143,11 @@ def get_bitmex_system():
return bitmex_system


def get_forex_system():
"""Get the forex system instance."""
global forex_system
return forex_system

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -178,10 +199,12 @@ def get_next_price_update():
"""Expose the next scheduled realtime price update."""
stock_time = get_next_price_update_time("stock")
poly_time = get_next_price_update_time("polymarket")
forex_time = get_next_price_update_time("forex")

response = {
"stock": stock_time.isoformat() if stock_time else None,
"polymarket": poly_time.isoformat() if poly_time else None,
"forex": forex_time.isoformat() if forex_time else None,
}

# Backward compatibility for older clients expecting single field
Expand Down Expand Up @@ -222,7 +245,7 @@ def load_backtest_as_initial_data():
def safe_generate_models_data():
if should_run_trading_cycle():
logger.info("🕐 Running trading cycle at market close time...")
generate_models_data(stock_system, polymarket_system, bitmex_system)
generate_models_data(stock_system, polymarket_system, bitmex_system, forex_system)
else:
logger.info("⏰ Skipping trading cycle - not in market time window")

Expand Down Expand Up @@ -301,6 +324,17 @@ def schedule_background_tasks(scheduler: BackgroundScheduler):
id="update_bitmex_prices",
replace_existing=True,
)
forex_interval = UPDATE_FREQUENCY["forex_prices"]
logger.info(
f"💱 Scheduled forex price update job for every {forex_interval} seconds ({forex_interval//60} minutes)"
)
scheduler.add_job(
update_forex_prices_and_values,
"interval",
seconds=forex_interval,
id="update_forex_prices",
replace_existing=True,
)
scheduler.add_job(
update_news_data,
"interval",
Expand Down
12 changes: 10 additions & 2 deletions backend/app/models_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,9 @@ def _serialize_positions(model_data):
return model_data


def load_historical_data_to_accounts(stock_system, polymarket_system, bitmex_system=None):
def load_historical_data_to_accounts(
stock_system, polymarket_system, bitmex_system=None, forex_system=None
):
"""Load historical data to account memory on every startup.

This function ALWAYS loads data to restore account state, regardless of whether
Expand Down Expand Up @@ -165,6 +167,8 @@ def load_historical_data_to_accounts(stock_system, polymarket_system, bitmex_sys
system = polymarket_system
elif category == "bitmex" and bitmex_system is not None:
system = bitmex_system
elif category == "forex" and forex_system is not None:
system = forex_system
else:
continue

Expand Down Expand Up @@ -207,7 +211,9 @@ def restore_account_from_historical_data(account, historical_model_data):
account.total_fees = historical_model_data.get("total_fees", 0.0)


def generate_models_data(stock_system, polymarket_system, bitmex_system=None) -> None:
def generate_models_data(
stock_system, polymarket_system, bitmex_system=None, forex_system=None
) -> None:
"""Generate and save model data for all systems"""
try:
print("🚀 Starting data generation for all markets...")
Expand All @@ -218,6 +224,8 @@ def generate_models_data(stock_system, polymarket_system, bitmex_system=None) ->
systems = {"stock": stock_system, "polymarket": polymarket_system}
if bitmex_system is not None:
systems["bitmex"] = bitmex_system
if forex_system is not None:
systems["forex"] = forex_system

for market_type, system in systems.items():
print(f"--- Processing {market_type.upper()} market ---")
Expand Down
19 changes: 16 additions & 3 deletions backend/app/news_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,22 @@
def update_news_data() -> None:
print("📰 Updating news data...")

all_news_data = {"stock": [], "polymarket": [], "bitmex": []}
all_news_data = {"stock": [], "polymarket": [], "bitmex": [], "forex": []}

try:
from .main import get_bitmex_system, get_polymarket_system, get_stock_system
from .main import (
get_bitmex_system,
get_forex_system,
get_polymarket_system,
get_stock_system,
)

stock_system = get_stock_system()
polymarket_system = get_polymarket_system()
bitmex_system = get_bitmex_system()
forex_system = get_forex_system()

if not stock_system or not polymarket_system or not bitmex_system:
if not stock_system or not polymarket_system or not bitmex_system or not forex_system:
print("❌ Failed to get system instances")
return

Expand All @@ -26,18 +32,22 @@ def update_news_data() -> None:
polymarket_system.initialize_for_live()
if not bitmex_system.universe:
bitmex_system.initialize_for_live()
if not forex_system.universe:
forex_system.initialize_for_live()

# Fetch market data
stock_market_data = stock_system._fetch_market_data(for_date=None)
polymarket_market_data = polymarket_system._fetch_market_data(for_date=None)
bitmex_market_data = bitmex_system._fetch_market_data(for_date=None)
forex_market_data = forex_system._fetch_market_data(for_date=None)

# Fetch news data
stock_news = stock_system._fetch_news_data(stock_market_data, for_date=None)
polymarket_news = polymarket_system._fetch_news_data(
polymarket_market_data, for_date=None
)
bitmex_news = bitmex_system._fetch_news_data(bitmex_market_data, for_date=None)
forex_news = forex_system._fetch_news_data(forex_market_data, for_date=None)

all_news_data["stock"] = [
item for sublist in stock_news.values() for item in sublist
Expand All @@ -48,6 +58,9 @@ def update_news_data() -> None:
all_news_data["bitmex"] = [
item for sublist in bitmex_news.values() for item in sublist
]
all_news_data["forex"] = [
item for sublist in forex_news.values() for item in sublist
]

with open(NEWS_DATA_FILE, "w") as f:
json.dump(all_news_data, f, indent=4)
Expand Down
118 changes: 118 additions & 0 deletions backend/app/price_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
_NEXT_UPDATE_TIMES: Dict[str, Optional[datetime]] = {
"stock": None,
"polymarket": None,
"forex": None,
}


Expand All @@ -47,6 +48,9 @@ def _compute_next_price_update_time(market: str, now_utc: datetime) -> datetime:
if market == "polymarket":
interval = timedelta(seconds=UPDATE_FREQUENCY["polymarket_prices"])
return (now_utc + interval).astimezone(pytz.UTC)
if market == "forex":
interval = timedelta(seconds=UPDATE_FREQUENCY["forex_prices"])
return (now_utc + interval).astimezone(pytz.UTC)

# Default to stock market schedule
tz = pytz.timezone("US/Eastern")
Expand Down Expand Up @@ -521,6 +525,115 @@ def _update_single_model(
return False


class ForexPriceUpdater:
def __init__(self) -> None:
self.initial_cash = TRADING_CONFIG["initial_cash_forex"]

def update_realtime_prices_and_values(self) -> None:
try:
models_data = _load_models_data()
if not models_data:
logger.warning("⚠️ No models data found, skipping forex update")
return

price_cache = self._build_price_cache()
if not price_cache:
logger.warning("⚠️ No forex price data available, skipping update")
return

updated_count = 0
for model in models_data:
if model.get("category") != "forex":
continue
if self._update_single_model(model, price_cache):
updated_count += 1

_save_models_data(models_data)
logger.info(f"✅ Successfully updated {updated_count} forex models")
except Exception as exc:
logger.error(f"❌ Failed to update forex prices: {exc}")
raise
finally:
next_time = _compute_next_price_update_time("forex", datetime.now(pytz.UTC))
_set_next_price_update_time("forex", next_time)
logger.info(f"🕒 Next forex price update target: {next_time.isoformat()}")

def _build_price_cache(self) -> Dict[str, float]:
system = self._get_forex_system()
if system is None:
return {}

try:
market_data = system._fetch_market_data()
except Exception as exc:
logger.error(f"❌ Failed to fetch forex market data: {exc}")
return {}

price_cache: Dict[str, float] = {}
for pair, payload in market_data.items():
price = payload.get("current_price")
if price is None:
continue
try:
price_cache[pair] = float(price)
except (TypeError, ValueError):
continue
return price_cache

def _get_forex_system(self):
try:
from .main import get_forex_system

system = get_forex_system()
if system is not None:
return system
except Exception:
pass

try:
from live_trade_bench.systems import ForexPortfolioSystem

return ForexPortfolioSystem.get_instance()
except Exception as exc:
logger.error(f"❌ Unable to access forex system: {exc}")
return None

def _update_single_model(
self, model: Dict[str, Any], price_cache: Dict[str, float]
) -> bool:
try:
portfolio = model.get("portfolio", {})
positions = portfolio.get("positions", {}) or {}
cash = float(portfolio.get("cash", 0.0))
total_value = cash

for pair, position in positions.items():
price = price_cache.get(pair)
if price is not None:
position["current_price"] = price
else:
price = float(position.get("current_price", 0.0))

quantity = float(position.get("quantity", 0.0))
total_value += quantity * price

portfolio["total_value"] = total_value

profit = total_value - self.initial_cash
model["profit"] = profit
model["performance"] = (
(profit / self.initial_cash) * 100 if self.initial_cash else 0.0
)

_update_profit_history(model, total_value, profit)
return True
except Exception as exc:
logger.error(
f"❌ Failed to update forex model {model.get('name', 'Unknown')}: {exc}"
)
return False


class BitMEXPriceUpdater:
"""Price updater for BitMEX perpetual contracts."""

Expand Down Expand Up @@ -768,6 +881,7 @@ def _create_crypto_benchmark(
stock_price_updater = RealtimePriceUpdater()
polymarket_price_updater = PolymarketPriceUpdater()
bitmex_price_updater = BitMEXPriceUpdater()
forex_price_updater = ForexPriceUpdater()


def update_stock_prices_and_values() -> None:
Expand All @@ -783,6 +897,10 @@ def update_bitmex_prices_and_values() -> None:
bitmex_price_updater.update_realtime_prices_and_values()


def update_forex_prices_and_values() -> None:
forex_price_updater.update_realtime_prices_and_values()


def update_realtime_prices_and_values() -> None:
"""Backward-compatible alias for stock price updates."""
update_stock_prices_and_values()
Loading
Loading