EP12 advanced

Customizing Open-Source Trading Agents for Your Market

How to fork TradingAgents and adapt it for non-US markets — data sources, calendars, currencies, and regulatory constraints.

Most open-source trading agents assume you trade US equities on NYSE or NASDAQ. They hard-code Eastern Time, pull data from Yahoo Finance, and ignore the existence of T+1 settlement or short-selling bans. If you trade any other market, you hit a wall fast.

I’ve been adapting TradingAgents — a multi-agent trading system on GitHub — for non-US markets. The process taught me exactly where these projects break and how to fix them without a full rewrite.

Why Not Build From Scratch?

Because you’ll spend three months recreating what already exists. TradingAgents ships with agent coordination, signal aggregation, risk management, backtesting infrastructure, and broker API patterns. That’s six months of engineering someone already did.

The smart move: fork, adapt, test. Change only what’s market-specific. Keep the core architecture intact.

Here’s what actually needs changing.

Modification 1: Data Adapters

TradingAgents uses yfinance by default. It works — for US stocks. For other markets, you need local providers.

The adapter pattern is straightforward. TradingAgents defines a DataSource interface:

class DataSource:
    def get_historical(self, symbol: str, start: str, end: str) -> pd.DataFrame:
        """Returns OHLCV DataFrame with DatetimeIndex."""
        raise NotImplementedError

    def get_realtime(self, symbol: str) -> dict:
        """Returns latest quote: price, volume, bid, ask."""
        raise NotImplementedError

    def get_fundamentals(self, symbol: str) -> dict:
        """Returns PE, PB, market cap, etc."""
        raise NotImplementedError

Write a new class that implements this interface using your local data provider:

class LocalMarketDataSource(DataSource):
    def __init__(self, api_key: str):
        self.client = LocalProviderSDK(api_key)

    def get_historical(self, symbol: str, start: str, end: str) -> pd.DataFrame:
        raw = self.client.daily_bars(symbol, begin=start, end=end)
        # Normalize column names to match expected format
        df = pd.DataFrame(raw)
        df.columns = ["date", "open", "high", "low", "close", "volume"]
        df["date"] = pd.to_datetime(df["date"])
        df = df.set_index("date")
        return df

    def get_realtime(self, symbol: str) -> dict:
        quote = self.client.snapshot(symbol)
        return {
            "price": quote.last_price,
            "volume": quote.volume,
            "bid": quote.bid1,
            "ask": quote.ask1,
        }

The trap: column name mismatches. TradingAgents expects lowercase open, high, low, close, volume. Some providers return Open, Close, or turnover instead of volume. One mismatch and the entire pipeline silently produces garbage. Validate the output DataFrame shape before plugging it in.

Modification 2: Trading Calendar

US markets: Monday-Friday, 9:30am-4:00pm ET, about 10 holidays per year. Simple.

Other markets have quirks. Some close for lunch. Some have half-day sessions. Holiday calendars vary wildly — Lunar New Year, Golden Week, Diwali, and local bank holidays all create gaps.

TradingAgents has a TradingCalendar class. Replace it:

from exchange_calendars import get_calendar

class MarketCalendar:
    def __init__(self, exchange_code: str):
        # exchange_calendars supports 50+ exchanges
        # "XSHG" = Shanghai, "XLON" = London, "XTSE" = Toronto, etc.
        self.cal = get_calendar(exchange_code)

    def is_trading_day(self, date: str) -> bool:
        ts = pd.Timestamp(date)
        return self.cal.is_session(ts)

    def get_trading_hours(self, date: str) -> tuple:
        ts = pd.Timestamp(date)
        session = self.cal.session_open_close(ts)
        return (session.market_open, session.market_close)

    def next_trading_day(self, date: str) -> str:
        ts = pd.Timestamp(date)
        sessions = self.cal.sessions_in_range(ts + pd.Timedelta(days=1), ts + pd.Timedelta(days=10))
        return str(sessions[0].date())

The exchange_calendars library covers 50+ exchanges. Don’t hand-roll holiday lists — they change every year and you will miss one.

The lunch break issue is subtle. If your market closes 11:30-13:00 and your strategy sends an order at 12:15, what happens? In most brokers, the order queues until the afternoon session. But your backtester might not model this delay. Add explicit session-gap handling or your backtest results will be unrealistically good.

Modification 3: Currency Handling

If you trade instruments denominated in multiple currencies — or your account currency differs from the instrument currency — you need FX conversion.

class PortfolioTracker:
    def __init__(self, base_currency: str = "USD"):
        self.base_currency = base_currency

    def position_value(self, symbol: str, shares: int, local_price: float) -> float:
        instrument_currency = self.get_currency(symbol)
        if instrument_currency == self.base_currency:
            return shares * local_price

        fx_rate = self.get_fx_rate(instrument_currency, self.base_currency)
        return shares * local_price * fx_rate

    def get_fx_rate(self, from_ccy: str, to_ccy: str) -> float:
        # Cache FX rates, refresh every 5 minutes during market hours
        cache_key = f"{from_ccy}_{to_ccy}"
        if cache_key in self._fx_cache and not self._fx_stale(cache_key):
            return self._fx_cache[cache_key]
        rate = self.fx_provider.get_rate(from_ccy, to_ccy)
        self._fx_cache[cache_key] = rate
        return rate

FX adds complexity to P&L calculation, risk metrics, and position sizing. A 2% gain in local currency can become a 1% loss after FX conversion on a bad day. Your risk agent needs to see both local and base-currency exposures.

Modification 4: Regulatory Constraints

This is the one people forget until production. Every market has rules that directly affect trading logic.

ConstraintUSChina A-ShareIndiaEU
SettlementT+2T+1T+1T+2
Short sellingYesRestrictedRestrictedYes
Daily price limitNone±10%±20% circuitNone
Position limitVariesVariesVariesVaries
Tick size$0.01¥0.01VariableVariable

T+1 settlement means you can’t sell shares you bought today. Your strategy might generate a “buy then sell same day” signal that’s illegal. Add a settlement check:

class RegulationFilter:
    def __init__(self, settlement_days: int = 1, allow_short: bool = False,
                 price_limit_pct: float = None):
        self.settlement_days = settlement_days
        self.allow_short = allow_short
        self.price_limit_pct = price_limit_pct

    def validate_order(self, order, portfolio, calendar) -> tuple[bool, str]:
        # Check: can we sell this position?
        if order.side == "sell":
            position = portfolio.get_position(order.symbol)
            if position is None and not self.allow_short:
                return False, "Short selling not permitted"
            if position and position.buy_date:
                settle_date = calendar.add_trading_days(
                    position.buy_date, self.settlement_days
                )
                if order.date < settle_date:
                    return False, f"Settlement: cannot sell until {settle_date}"

        # Check: would this order hit price limit?
        if self.price_limit_pct:
            prev_close = self.get_prev_close(order.symbol)
            limit_up = prev_close * (1 + self.price_limit_pct)
            limit_down = prev_close * (1 - self.price_limit_pct)
            if not (limit_down <= order.price <= limit_up):
                return False, f"Price {order.price} outside limit [{limit_down}, {limit_up}]"

        return True, "ok"

Using Claude Code as the Modification Engine

Here’s where it gets efficient. Instead of manually refactoring every file, I use Claude Code to do the systematic changes.

The workflow:

  1. Fork the repository
  2. Ask Claude Code to map all files that import or reference yfinance — it finds every dependency
  3. Ask it to create a new adapter class conforming to the existing interface
  4. Ask it to replace all direct yfinance calls with the adapter abstraction
  5. Ask it to identify timezone-hardcoded strings (“US/Eastern”, “America/New_York”) and parameterize them

Claude Code is good at this kind of mechanical refactoring. It can hold the whole codebase in context, find every reference, and make consistent changes across 20+ files in one pass.

Testing Strategy: Parallel Backtests

The biggest risk when modifying a trading system: you break something subtle that only shows up in P&L, not in error logs.

Run parallel backtests:

# Original: US market, yfinance data, 2020-2024
python backtest.py --config original_us.yaml --output results_original.json

# Modified: same US market, same period, but using your new adapters
python backtest.py --config modified_us.yaml --output results_modified.json

# Compare
python compare_results.py results_original.json results_modified.json

If the results match on US data with your new adapter code, you haven’t broken anything. Only then switch to your target market data.

Common failure modes I’ve hit:

  • Off-by-one on dates: Your adapter returns data for trading days only, but the original included weekends as NaN rows. The model trained on a different row count.
  • Timezone mismatch: Daily bars timestamped at midnight UTC vs midnight local time. A one-day shift in training labels.
  • Volume units: Some providers report volume in lots (100 shares), others in individual shares. A 100x error in volume-based features.

The Rule: Fork, Adapt, Test Incrementally

Don’t rewrite the system. Don’t “improve” the architecture while you’re adapting it. Change one thing, verify it works, commit, move to the next change.

The order matters: data adapter first (everything depends on data), then calendar, then currency, then regulations. Each step is independently testable. If your calendar change breaks the backtest, you know exactly where to look.

I made the mistake of changing three things at once during my first adaptation. Spent two days debugging a P&L discrepancy that turned out to be a volume-unit mismatch hidden by a calendar bug. One change at a time. Always.