Dokumentation

Funktionsaufrufe

Übersetzt natürlichsprachliche Trading-Anfragen in Aufrufe gewöhnlicher typisierter Funktionen, indem Funktionsnamen und Argumente aus geschlossenen Mengen auf konfidenzbewusste TypeSafe-Fragen abgebildet werden.

Wenn du einen „großen eisgekühlten Hafer-Latte ohne Süßstoff“ bestellst, schreibt der Barista deinen Satz nicht auf. Er markiert vier Optionen auf einem Becher. Dieses Cookbook macht dasselbe für eine Trading-API: Ein Satz geht hinein, und heraus kommt ein Funktionsname mit seinen Argumenten als ausgewertete Enums, jedes mit einer Konfidenz.

"plot rolling correlation between nvda and spy for the past month"
    rolling_correlation(symbol='NVDA', benchmark='SPY', window='1mo')   confidence 0.91

"compare nvda amd and msft over the past three months"
    compare_returns(symbols=['NVDA', 'AMD', 'MSFT'], window='3mo')      confidence 0.94

"show me apple daily with volume"
    plot_price(symbol='AAPL', resolution='1d', include_volume=True)     confidence 0.75

"what tickers do you have"
    list_symbols()                                                     confidence 1.00

Diese Aufrufe gehen an zehn gewöhnliche Funktionen in einem Trading-Assistenten. Ihre Argumente beziehen Werte aus festen Listen, es sind also bereits Literals:

def plot_price(
    symbol: Literal["SPY", "NVDA", "AMD", "AAPL", "MSFT", "TSLA"],
    style: Literal["line", "candles"] = "line",
    resolution: Literal["1m", "5m", "15m", "1h", "1d"] = "15m",
    window: Literal["1d", "1w", "1mo", "3mo"] = "1w",
    include_volume: bool = False,
    moving_average: Literal["9", "20", "50"] | None = None,
    log_scale: bool = False,
): ...

Ein Argument, dessen Werte aus einer festen Liste stammen, ist eine geschlossene Menge. Wenn es einen Wert aus dieser Liste annimmt, bekommt es eine Choice-Frage über genau diese Werte, sodass alles, was die Funktion erreicht, ein von ihr akzeptierter Wert ist. Die Funktionen lässt du unangetastet. Was du hinzufügst, ist eine Spezifikation, die in klaren Worten sagt, was jedes Argument bedeutet. Am Ende hast du einen Dispatcher, den du auf deine eigenen Funktionen richten kannst.

Einrichtung

pip install ipython polars matplotlib numpy 'cooksafe>=0.2.0,<0.3.0'

Lege TYPESAFE_API_KEY fest. Zwei Module liegen neben dieser Datei. trader.py enthält die zehn Funktionen plus einen TypeSafe-Client, der Antworten aus einem Cache liest, sodass ein erneutes Rendern die Zahlen unten ohne API-Aufrufe wiedergibt. dispatch.py enthält den Code, der eine Signatur und eine Spezifikation liest und den Aufruf macht.

import json
from pathlib import Path

from cooksafe import make_playground_link
from dispatch import ROUTE, Dispatcher, closed_sets
from IPython.display import Markdown, display
from trader import TOOLS, client, load

TYPESAFE_MODEL = "jev-1.12"
print(f"{len(TOOLS)} functions over {load().height:,} one-minute bars")
10 functions over 156,780 one-minute bars

Die geschlossenen Mengen in den Signaturen finden

Die Typ-Hinweise sagen bereits, welche Argumente aus einer festen Liste stammen und was in jeder Liste steht. closed_sets liest eine Signatur und sortiert diese Argumente in drei Formen: eine choice (ein Literal, also ein Wert aus der Liste), eine set (ein list[Literal[...]], also beliebig viele davon) oder ein flag (ein bool, also an oder aus). Alle zehn Funktionen sind in trader.py definiert.

for name, fn in TOOLS.items():
    shapes = closed_sets(fn)
    print(
        f"  {name:<20}{len(shapes)}  "
        + ", ".join(f"{a}:{s}" for a, (s, _) in shapes.items())
    )
print(
    f"\n{sum(len(closed_sets(fn)) for fn in TOOLS.values())} fillable arguments in total"
)
  list_symbols        0
  market_summary      1  window:choice
  plot_price          7  symbol:choice, style:choice, resolution:choice, window:choice, include_volume:flag, moving_average:choice, log_scale:flag
  intraday_pattern    3  symbol:choice, window:choice, metric:choice
  compare_returns     3  symbols:set, window:choice, normalize:flag
  rolling_correlation 4  symbol:choice, benchmark:choice, window:choice, resolution:choice
  summary_stats       2  symbol:choice, window:choice
  volatility          3  symbol:choice, window:choice, annualized:flag
  top_movers          2  window:choice, direction:choice
  drawdown            3  symbol:choice, window:choice, plot:flag

28 fillable arguments in total

top_movers zeigt, was weggelassen wird. Von seinen drei Argumenten sind zwei geschlossene Mengen. Das dritte, limit, ist ein int, bekommt also nie eine Frage und behält seinen Standardwert 3. Freier Text, Zahlen und Datumsangaben funktionieren genauso: keine Frage, und der Standardwert der Funktion bleibt.

Die Spezifikation schreiben

Das Literal gibt dir die Zeichenketten "1mo" und "3mo". Es sagt nicht, dass eine Person, die „dieses Quartal“ tippt, die zweite meint. Das sagt die Spezifikation. Sie enthält eine Frage pro Argument, eine Zeile pro Option, eine Beschreibung pro Funktion und eine weitere Frage, die zwischen den Funktionen wählt. Sie liegt in spec.json, und ein LLM kann sie dir aus den Signaturen schreiben.

SPEC = json.loads(Path("spec.json").read_text())
for argument in ("style", "moving_average"):
    print(
        json.dumps(
            {argument: SPEC["functions"]["plot_price"]["arguments"][argument]}, indent=2
        )
    )
{
  "style": {
    "question": "Does the user want a plain line or candles?",
    "stated": "Does the user say how the chart should be drawn, such as a line, candles, or OHLC bars?",
    "options": {
      "line": "a simple line through the closing prices",
      "candles": "a candlestick or OHLC chart, showing each bar's open, high, low and close"
    }
  }
}
{
  "moving_average": {
    "question": "How many bars should the moving average cover - nine, twenty, or fifty?",
    "stated": "Does the user ask for a moving average or a smoothed line over the candles?",
    "options": {
      "9": "a nine-bar moving average, a fast one",
      "20": "a twenty-bar moving average",
      "50": "a fifty-bar moving average, a slow one"
    }
  }
}

Die Optionsschlüssel sind die Zeichenketten, die die Funktion annimmt, also muss nichts einen Namen nachträglich auf ein Argument zurückführen. stated macht ein Argument optional. Es ist eine zweite Ja/Nein-Frage, die fragt, ob der Befehl überhaupt etwas über dieses Argument sagt. Wenn die Antwort nein lautet, lässt der Aufruf das Argument weg, und der eigene Standardwert der Funktion gilt.

Ein set-Argument bekommt seine Frage einmal pro Mitglied, wobei {} für den Mitgliednamen steht. "Does the user want {} in the comparison?" wird zu einer Frage pro Ticker.

Schreibe jede Frage über den Gedanken statt über die Wörter, die eine Person wählen könnte, denn der Abgleich läuft über die Bedeutung: „is amd tracking nvidia lately“ erreicht rolling_correlation, obwohl weder tracking noch lately irgendwo in spec.json vorkommt. Benenne eine Frage nicht nach ihrem Parameter – "Which resolution?" gibt dem Befehl nichts zum Abgleichen.

Die Spezifikation in Fragen überführen

Dispatcher baut die Fragen einmal aus der Spezifikation. Jeder Befehl ist dann eine Anfrage, die die Wahl der Funktion und die Argumente jeder Funktion trägt, und der Dispatcher liest nur die Antworten der gewählten Funktion.

assistant = Dispatcher(SPEC, TOOLS, client)
print(f"{len(assistant.questions)} questions per command, among them:")
for qid in (
    "__tool__",
    "plot_price.style",
    "plot_price.style?",
    "compare_returns.symbols.NVDA",
):
    question = assistant.questions[qid]
    print(f"  {qid:<30}{question['type']:<8}{str(question['instructions'])[:64]}")
54 questions per command, among them:
  __tool__                      choice  What is the user asking the trading assistant to do?
  plot_price.style              choice  Does the user want a plain line or candles?
  plot_price.style?             noul    Does the user say how the chart should be drawn, such as a line,
  compare_returns.symbols.NVDA  noul    Does the user want NVDA in the comparison?

Vierzehn Befehle ausführen

Eine Anfrage belegt eine Zeile, und ihre confidence ist das unsicherste Urteil hinter diesem Aufruf.

COMMANDS = [
    "show nvda 1h",
    "plot rolling correlation between nvda and spy for the past month",
    "when during the day does nvda trade the most",
    "what moved today",
    "what tickers do you have",
    "how did the market do this week",
    "candles for tesla with a 20 period moving average",
    "compare nvda amd and msft over the past three months",
    "how volatile is tsla",
    "biggest losers today",
    "worst drawdown for nvda this quarter, and chart it please",
    "spy stats for the last month",
    "show me apple daily with volume",
    "is amd tracking nvidia lately",
]

CALLS = {command: assistant(command) for command in COMMANDS}
for command, call in CALLS.items():
    print(f'  "{command}"')
    print(
        f"      {str(call):<66}confidence {call.confidence:.2f}"
        f"   tool {call.tool.probability:.2f}"
    )
  "show nvda 1h"
      plot_price(symbol='NVDA', resolution='1h')                        confidence 0.78   tool 1.00
  "plot rolling correlation between nvda and spy for the past month"
      rolling_correlation(symbol='NVDA', benchmark='SPY', window='1mo') confidence 0.91   tool 1.00
  "when during the day does nvda trade the most"
      intraday_pattern(symbol='NVDA')                                   confidence 0.53   tool 1.00
  "what moved today"
      top_movers(window='1d', direction='gainers')                      confidence 0.90   tool 0.90
  "what tickers do you have"
      list_symbols()                                                    confidence 1.00   tool 1.00
  "how did the market do this week"
      market_summary(window='1w')                                       confidence 0.96   tool 0.99
  "candles for tesla with a 20 period moving average"
      plot_price(symbol='TSLA', style='candles', moving_average='20')   confidence 0.69   tool 0.97
  "compare nvda amd and msft over the past three months"
      compare_returns(symbols=['NVDA', 'AMD', 'MSFT'], window='3mo')    confidence 0.94   tool 1.00
  "how volatile is tsla"
      volatility(symbol='TSLA')                                         confidence 0.96   tool 1.00
  "biggest losers today"
      top_movers(window='1d', direction='losers')                       confidence 0.98   tool 0.98
  "worst drawdown for nvda this quarter, and chart it please"
      drawdown(symbol='NVDA', window='3mo', plot=True)                  confidence 0.84   tool 0.84
  "spy stats for the last month"
      summary_stats(symbol='SPY', window='1mo')                         confidence 0.88   tool 0.88
  "show me apple daily with volume"
      plot_price(symbol='AAPL', resolution='1d', include_volume=True)   confidence 0.75   tool 0.85
  "is amd tracking nvidia lately"
      rolling_correlation(symbol='AMD', benchmark='NVDA')               confidence 0.82   tool 0.82

Beide langen Befehle kamen wie gewünscht heraus. „plot rolling correlation between nvda and spy for the past month“ füllte vier Argumente aus einem Satz. Zwei davon, symbol und benchmark, schöpfen aus denselben sechs Tickern, und jeder Ticker landete im richtigen Argument, weil die Fragen die Rollen ausschreiben: das zu messende, zuerst genannte gegen das zweite genannte, den Maßstab. „compare nvda amd and msft over the past three months“ legte drei Ticker in das Set und ließ die anderen drei weg.

Drei davon ausgeführt:

for command in (
    "plot rolling correlation between nvda and spy for the past month",
    "compare nvda amd and msft over the past three months",
    "when during the day does nvda trade the most",
):
    print(f'"{command}"  ->  {CALLS[command]}')
    display(CALLS[command].run())
"plot rolling correlation between nvda and spy for the past month"  ->  rolling_correlation(symbol='NVDA', benchmark='SPY', window='1mo')
"compare nvda amd and msft over the past three months"  ->  compare_returns(symbols=['NVDA', 'AMD', 'MSFT'], window='3mo')
"when during the day does nvda trade the most"  ->  intraday_pattern(symbol='NVDA')
Ausgabe Ausgabe Ausgabe

Und die, die im Text antworten:

for command in ("how did the market do this week", "biggest losers today"):
    print(f'"{command}"  ->  {CALLS[command]}')
    print(CALLS[command].run(), "\n")
"how did the market do this week"  ->  market_summary(window='1w')
the board over 1w
  NVDA     254.12    9.62%    389,465,563
  AMD      184.20    1.51%    182,740,497
  AAPL     258.71    0.97%    223,818,998
  SPY      664.86    0.40%    138,617,365
  MSFT     451.35    0.26%    113,427,173
  TSLA     320.22   -0.97%    266,317,023

"biggest losers today"  ->  top_movers(window='1d', direction='losers')
top 3 losers over 1d
  AMD      -0.57%  ->  184.20
  MSFT      0.67%  ->  451.35
  AAPL      1.40%  ->  258.71

Die Konfidenz lesen

confidence meldet das unsicherste Urteil im Aufruf und nicht das Produkt aller, denn ein falsches Argument genügt, um das Ergebnis zu verderben. Ein Produkt beantwortet eine andere Frage („stimmt jeder Teil“), und es fällt, je mehr Argumente eine Funktion annimmt, ob ein einzelnes Urteil wackelig ist oder nicht.

Woher diese Zahl stammt, Argument für Argument:

call = CALLS["is amd tracking nvidia lately"]
print(f'"is amd tracking nvidia lately"  ->  {call}   confidence {call.confidence:.2f}')
for name, argument in call.arguments.items():
    top = sorted(argument.distribution.items(), key=lambda kv: -kv[1])[:3]
    shown = "omitted, default stands" if argument.omitted else repr(argument.value)
    print(
        f"  {name:<12}{shown:<26}p {argument.probability:.2f}   "
        + "  ".join(f"{k} {v:.2f}" for k, v in top)
    )
print(f"  weakest argument: {call.weakest().name}")
"is amd tracking nvidia lately"  ->  rolling_correlation(symbol='AMD', benchmark='NVDA')   confidence 0.82
  symbol      'AMD'                     p 0.87   AMD 0.87  NVDA 0.13  AAPL 0.00
  benchmark   'NVDA'                    p 0.78   NVDA 0.92  AMD 0.08  AAPL 0.00
  window      omitted, default stands   p 0.96
  resolution  omitted, default stands   p 0.99
  weakest argument: benchmark

window und resolution werden hier beide weggelassen, weil „lately“ nicht sagt, wie weit zurück oder auf welchen Bars, also läuft rolling_correlation mit seinen eigenen Standardwerten von einem Monat und stündlichen Bars. Dafür ist die stated-Frage da. Ohne sie müsste die choice irgendein Fenster benennen, und sie würde eines mit hoher Konfidenz benannt haben.

Im Playground öffnen

Der Link unten enthält einen Befehl und die Fragen für die Funktion, die er gewählt hat: die choice über die zehn Funktionsbeschreibungen und die vier Argumente von rolling_correlation. Bearbeite den Befehl dort, und die Argumente ändern sich mit ihm.

COMMAND = "plot rolling correlation between nvda and spy for the past month"
picked = CALLS[COMMAND]
playground_link = make_playground_link(
    COMMAND,
    {ROUTE: assistant.questions[ROUTE]}
    | {q: v for q, v in assistant.questions.items() if q.startswith(f"{picked.name}.")},
    models=[TYPESAFE_MODEL],
)
display(
    Markdown(
        f"🔗 [Open the command and its questions in the TypeSafe playground]({playground_link})"
    )
)
Öffne den Befehl und seine Fragen im TypeSafe-Playground →