0Pricing
AI Agents · Lekcja

Testowanie wsteczne decyzji agenta

Symulowanie strategii agenta na danych historycznych w celu sprawdzenia wyników.

Testowanie wsteczne decyzji agenta to bezpłatna lekcja AI Agents na CoddyKit. To lekcja 4 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej AI Agents, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs AI Agents zawiera 4 lekcji w sumie.

Dlaczego przeprowadzać backtest decyzji agenta?

Nie można stwierdzić, czy agent finansowy generuje trafne rekomendacje, bez przetestowania go na danych historycznych. Backtestowanie odtwarza wcześniejsze warunki rynkowe za pomocą logiki decyzyjnej agenta i mierzy, jak te decyzje wpłynęłyby na wyniki.

Jest to podstawowe narzędzie do sprawdzania jakości strategii przed wdrożeniem na rynku rzeczywistym.

Pułapka obciążenia wyprzedzającego

Obciążenie wyprzedzające to najczęstszy i najbardziej niebezpieczny błąd w backtestowaniu: wykorzystanie przyszłych danych do podjęcia decyzji dotyczącej „przeszłości”. Przykładem jest użycie ceny zamknięcia dnia do podjęcia decyzji o kupnie na otwarciu tego samego dnia.

Każda decyzja historyczna musi wykorzystywać wyłącznie dane dostępne w symulowanym momencie jej podejmowania.

import pandas as pd
import yfinance as yf

# WRONG — uses today's close to decide today's trade
def bad_signal(history: pd.DataFrame, date: str) -> str:
    today_close = history.loc[date, 'Close']  # look-ahead!
    if today_close > history['Close'].mean():
        return 'BUY'
    return 'HOLD'

# CORRECT — uses only data available BEFORE the decision date
def good_signal(history: pd.DataFrame, date: str) -> str:
    past_data = history.loc[:date].iloc[:-1]  # exclude today
    if past_data.empty:
        return 'HOLD'
    today_open = history.loc[date, 'Open']    # open price known at market open
    if today_open > past_data['Close'].mean():
        return 'BUY'
    return 'HOLD'

Budowanie pętli odtwarzania danych historycznych

Należy przejść przez każdą historyczną datę. Na każdym kroku agent widzi wyłącznie dane dostępne do tego momentu. Należy zarejestrować decyzję, a następnie przejść do kolejnej daty, aby zaobserwować wynik.

import yfinance as yf
import pandas as pd

def backtest_simple(ticker: str, start: str, end: str) -> list[dict]:
    hist = yf.Ticker(ticker).history(start=start, end=end)
    results = []

    for i in range(20, len(hist)):  # need 20 days of history for signals
        window   = hist.iloc[:i]           # data available on day i
        today    = hist.iloc[i]
        decision = good_signal(window, str(hist.index[i].date()))

        results.append({
            'date':     str(hist.index[i].date()),
            'decision': decision,
            'open':     float(today['Open']),
            'close':    float(today['Close']),
            'next_day_return': None  # filled in next iteration
        })
    return results

Symulowanie logiki agenta na każdym kroku

Aby uzyskać bardziej realistyczny backtest, należy symulować pełne wywołanie narzędzia agenta dla każdej daty — w tym konstruowanie promptu, wywołanie LLM i parsowanie decyzji. W ten sposób testowany jest cały system, a nie tylko logika sygnałów.

import openai, json
import yfinance as yf

client = openai.OpenAI(api_key='YOUR_OPENAI_KEY')

def agent_decision_at_date(ticker: str, hist_up_to: pd.DataFrame) -> str:
    recent = hist_up_to.tail(20)
    closes = recent['Close'].tolist()
    returns = [round((closes[i] - closes[i-1]) / closes[i-1], 4)
               for i in range(1, len(closes))]
    prompt = (
        f'Ticker: {ticker}\n'
        f'Last 20 closing prices: {closes}\n'
        f'Daily returns: {returns}\n'
        f'Based on this data, recommend: BUY, SELL, or HOLD.\n'
        f'Return JSON: {{"action": "BUY/SELL/HOLD", "reason": "..."}}'
    )
    resp = client.chat.completions.create(
        model='gpt-4o-mini',
        messages=[{'role': 'user', 'content': prompt}],
        response_format={'type': 'json_object'}
    )
    return json.loads(resp.choices[0].message.content).get('action', 'HOLD')

Obliczanie współczynnika trafień

Współczynnik trafień mierzy, jaki odsetek decyzji BUY został poprzedzony dodatnią stopą zwrotu następnego dnia. Współczynnik trafień powyżej 50% oznacza, że agent prawidłowo przewiduje kierunek częściej niż wynikałoby to z przypadku.

def compute_hit_rate(results: list[dict]) -> dict:
    buy_decisions = [r for r in results if r['decision'] == 'BUY'
                     and r.get('next_day_return') is not None]

    if not buy_decisions:
        return {'hit_rate': None, 'sample_size': 0}

    correct = sum(1 for r in buy_decisions if r['next_day_return'] > 0)
    return {
        'hit_rate':     round(correct / len(buy_decisions), 4),
        'sample_size':  len(buy_decisions),
        'correct':      correct,
        'interpretation': 'above_chance' if correct / len(buy_decisions) > 0.52 else 'at_or_below_chance'
    }

if __name__ == '__main__':
    results = [
        {'decision': 'BUY', 'next_day_return': 0.012},
        {'decision': 'BUY', 'next_day_return': -0.004},
        {'decision': 'HOLD', 'next_day_return': None},
        {'decision': 'BUY', 'next_day_return': 0.007},
    ]
    print('Hit rate:', compute_hit_rate(results))

Obliczanie stopy zwrotu względem benchmarku

Należy porównać stopę zwrotu strategii z benchmarkiem buy-and-hold. Jeśli agent osiąga gorsze wyniki niż samo utrzymywanie indeksu, jego złożoność nie jest uzasadniona.

def compare_to_benchmark(results: list[dict], ticker: str, period: str = '1y') -> dict:
    import yfinance as yf

    # Strategy return: only invested on BUY days
    buy_days = [r for r in results if r['decision'] == 'BUY'
                and r.get('next_day_return') is not None]
    strategy_return = sum(r['next_day_return'] for r in buy_days)

    # Benchmark: buy-and-hold
    hist = yf.Ticker(ticker).history(period=period)['Close']
    bh_return = float((hist.iloc[-1] - hist.iloc[0]) / hist.iloc[0])

    return {
        'strategy_return_pct':  round(strategy_return * 100, 2),
        'benchmark_return_pct': round(bh_return * 100, 2),
        'outperformed':         strategy_return > bh_return,
        'alpha_pct':            round((strategy_return - bh_return) * 100, 2)
    }

Wpływ kosztów transakcyjnych

Bez uwzględnienia kosztów transakcyjnych (prowizji, spreadu i poślizgu) strategia może wydawać się zyskowna, choć w praktyce przynosi straty. Nawet 0,1% na transakcję może znacząco się skumulować przy dużej liczbie transakcji.

COMMISSION_PCT = 0.001   # 0.1% per trade (buy + sell)
SLIPPAGE_PCT   = 0.0005  # 0.05% per trade

def returns_after_costs(decisions: list[dict]) -> dict:
    total_return = 0.0
    total_cost   = 0.0

    for d in decisions:
        if d['decision'] == 'BUY' and d.get('next_day_return') is not None:
            gross = d['next_day_return']
            cost  = COMMISSION_PCT + SLIPPAGE_PCT
            net   = gross - cost
            total_return += net
            total_cost   += cost

    return {
        'gross_return_pct': round((total_return + total_cost) * 100, 2),
        'net_return_pct':   round(total_return * 100, 2),
        'total_costs_pct':  round(total_cost * 100, 2),
        'num_trades':       sum(1 for d in decisions if d['decision'] == 'BUY')
    }

if __name__ == '__main__':
    decisions = [
        {'decision': 'BUY', 'next_day_return': 0.012},
        {'decision': 'BUY', 'next_day_return': -0.004},
        {'decision': 'HOLD', 'next_day_return': None},
    ]
    print('Returns after trading costs:', returns_after_costs(decisions))

Walidacja walk-forward

Optymalizacja na próbie (trenowanie na tym samym okresie, na którym przeprowadza się test) prowadzi do przeuczenia. Walidacja walk-forward wykorzystuje kroczące okno treningowe, po którym następuje nowy okres testowy — tak jak w rzeczywistym wdrożeniu.

def walk_forward_test(ticker: str, total_years: int = 3,
                      train_months: int = 12, test_months: int = 3) -> list[dict]:
    import yfinance as yf
    from dateutil.relativedelta import relativedelta
    from datetime import date

    results = []
    start = date(date.today().year - total_years, 1, 1)
    end   = date.today()

    window_start = start
    while True:
        train_end = window_start + relativedelta(months=train_months)
        test_end  = train_end + relativedelta(months=test_months)
        if test_end > end:
            break

        # In practice: train your signal on window_start->train_end
        # then evaluate on train_end->test_end
        results.append({
            'train_period': f'{window_start} to {train_end}',
            'test_period':  f'{train_end} to {test_end}'
        })
        window_start += relativedelta(months=test_months)

    return results

Raport z backtestu

Wyniki backtestu należy podsumować w ustrukturyzowanym raporcie obejmującym: współczynnik trafień, łączną stopę zwrotu względem benchmarku, alfę, współczynnik Sharpe'a, maksymalne obsunięcie, liczbę transakcji oraz stopę zwrotu netto po kosztach.

def backtest_report(ticker: str, results: list[dict]) -> dict:
    hit = compute_hit_rate(results)
    benchmark = compare_to_benchmark(results, ticker)
    costs = returns_after_costs(results)

    return {
        'ticker':                  ticker,
        'total_decisions':         len(results),
        'buy_signals':             costs['num_trades'],
        'hit_rate':                hit.get('hit_rate'),
        'strategy_return_gross':   f'{costs["gross_return_pct"]}%',
        'strategy_return_net':     f'{costs["net_return_pct"]}%',
        'benchmark_return':        f'{benchmark["benchmark_return_pct"]}%',
        'alpha':                   f'{benchmark["alpha_pct"]}%',
        'outperformed_benchmark':  benchmark['outperformed'],
        'transaction_costs':       f'{costs["total_costs_pct"]}%'
    }

Interpretowanie wyników backtestu

Dobre wyniki backtestu nie gwarantują przyszłych wyników, ale złe wyniki wskazują strategie, których nie należy wdrażać. Najważniejsze sygnały ostrzegawcze to: współczynnik trafień poniżej 50%, ujemna alfa po uwzględnieniu kosztów, współczynnik Sharpe'a poniżej 0,5 lub maksymalne obsunięcie przekraczające tolerancję ryzyka.

def interpret_backtest(report: dict) -> list[str]:
    warnings = []

    hit_rate = report.get('hit_rate')
    if hit_rate and hit_rate < 0.50:
        warnings.append(f'Hit rate {hit_rate:.1%} is below chance — strategy predicts direction poorly')

    alpha_str = report.get('alpha', '0%')
    alpha = float(alpha_str.replace('%', '')) / 100
    if alpha < 0:
        warnings.append(f'Negative alpha {alpha_str}: strategy underperforms buy-and-hold')

    if not warnings:
        return ['Backtest results look acceptable — proceed with paper trading before live deployment']
    return warnings

if __name__ == '__main__':
    report = {'hit_rate': 0.45, 'alpha': '-1.2%'}
    for warning in interpret_backtest(report):
        print('-', warning)

Symulacja Monte Carlo na potrzeby oceny odporności

Pojedynczy backtest jest wrażliwy na wybrany okres. Symulacja Monte Carlo losowo zmienia kolejność historycznych stóp zwrotu, aby sprawdzić, czy wyniki utrzymują się przy różnych sekwencjach. Dzięki temu można ustalić, czy strategia jest odporna, czy tylko miała szczęście.

import numpy as np

def monte_carlo_backtest(daily_returns: list[float],
                         simulations: int = 1000) -> dict:
    returns_arr = np.array(daily_returns)
    final_returns = []

    for _ in range(simulations):
        shuffled = np.random.choice(returns_arr, size=len(returns_arr), replace=True)
        cumulative = float((1 + shuffled).prod() - 1)
        final_returns.append(cumulative)

    final_returns = sorted(final_returns)
    return {
        'median_return':      round(float(np.median(final_returns)) * 100, 2),
        'percentile_5':       round(float(np.percentile(final_returns, 5)) * 100, 2),
        'percentile_95':      round(float(np.percentile(final_returns, 95)) * 100, 2),
        'prob_positive':      round(sum(1 for r in final_returns if r > 0) / simulations, 3)
    }

Czym jest obciążenie wyprzedzające w backtestowaniu?

Obciążenie wyprzedzające to najczęstszy sposób przypadkowego zawyżenia wyników backtestu. Zrozumienie jego dokładnej definicji pozwala uniknąć budowania wadliwych backtestów.

Podsumowanie backtestowania decyzji agenta

Rzetelne backtestowanie wymaga: unikania obciążenia wyprzedzającego (decyzje wykorzystują wyłącznie dane z przeszłości), realistycznego symulowania logiki agenta na każdym kroku historycznym, mierzenia współczynnika trafień i alfy względem benchmarku, uwzględniania kosztów transakcyjnych oraz walidacji za pomocą podziałów walk-forward w celu wykrywania przeuczenia.

Często zadawane pytania

Czy lekcja „Testowanie wsteczne decyzji agenta” jest bezpłatna?

Tak — pełny tekst „Testowanie wsteczne decyzji agenta” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu AI Agents, przejdź na CoddyKit PRO. Kurs AI Agents zawiera 4 lekcji w sumie.

Co nauczysz się w „Testowanie wsteczne decyzji agenta”?

Symulowanie strategii agenta na danych historycznych w celu sprawdzenia wyników. Ćwiczysz AI Agents z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.

Czy potrzebuję doświadczenia, aby zacząć AI Agents?

Nie wymagamy żadnego doświadczenia. AI Agents w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 4 z 4.

Ile czasu zajmuje lekcja „Testowanie wsteczne decyzji agenta”?

Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.

Czy mogę pisać i uruchamiać kod w tej lekcji AI Agents?

Tak. Każda lekcja AI Agents zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.

Wszystkie lekcje w tym kursie

  1. Integracja z interfejsami API danych rynkowych
  2. Narzędzia agenta do analizy portfela
  3. Mechanizmy ochronne dotyczące ryzyka i zgodności
  4. Testowanie wsteczne decyzji agenta
← Powrót do AI Agents