Llamada a funciones
Convierte solicitudes de trading en lenguaje natural en llamadas a funciones tipadas normales asignando nombres de función y argumentos de conjunto cerrado a preguntas de TypeSafe con conciencia de confianza.
Cuando pides un «latte grande con hielo y avena, sin endulzar», el barista no apunta tu frase. Marca cuatro opciones en un vaso. Este cookbook hace lo mismo con una API de trading: entra una frase y sale un nombre de función y sus argumentos como enums evaluados, cada uno con una confianza.
"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
Esas llamadas van a diez funciones normales de un asistente de trading. Sus argumentos toman valores de listas fijas, así que ya son Literal:
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,
): ...
Un argumento cuyos valores vienen de una lista fija es un conjunto cerrado. Cuando toma un valor de esa lista, recibe una pregunta Choice sobre exactamente esos valores, así que lo que llega a la función es un valor que la función acepta. Dejas las funciones intactas. Lo que añades es una spec que dice con palabras llanas qué significa cada argumento. Al final tienes un Dispatcher que puedes apuntar a tus propias funciones.
Configuración
pip install ipython polars matplotlib numpy 'cooksafe>=0.2.0,<0.3.0'
Define TYPESAFE_API_KEY. Dos módulos están junto a este archivo. trader.py contiene las diez funciones, más un cliente de TypeSafe que lee las respuestas de una caché, así que volver a renderizar reproduce los números de abajo sin llamar a la API. dispatch.py contiene el código que lee una firma y una spec y hace la llamada.
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
Encuentra los conjuntos cerrados en las firmas
Los type hints ya dicen qué argumentos vienen de una lista fija y qué hay en cada lista. closed_sets lee una firma y clasifica esos argumentos en tres formas: una choice (un Literal, es decir, un valor de la lista), un set (un list[Literal[...]], es decir, cualquier número de ellos) o un flag (un bool, es decir, activado o desactivado). Las diez funciones están definidas en trader.py.
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 muestra lo que se queda fuera. De sus tres argumentos, dos son conjuntos cerrados. El tercero, limit, es un int, así que nunca recibe una pregunta y conserva su valor por defecto de 3. El texto libre, los números y las fechas funcionan igual: sin pregunta, y el valor por defecto de la función se mantiene.
Escribe la spec
El Literal te da las cadenas "1mo" y "3mo". No dice que un usuario que escribe «este trimestre» se refiere a la segunda. Eso lo dice la spec. Contiene una pregunta por argumento, una línea por opción, una descripción por función y una pregunta más que elige entre las funciones. Vive en spec.json, y un LLM puede escribirla por ti a partir de las firmas.
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"
}
}
}
Las claves de las opciones son las cadenas que toma la función, así que después nada tiene que volver a asignar una etiqueta a un argumento. stated hace que un argumento sea opcional. Es una segunda pregunta de sí o no que pregunta si el comando dice algo sobre ese argumento en absoluto. Cuando la respuesta es no, la llamada omite ese argumento y se aplica el valor por defecto de la propia función.
Un argumento de tipo set recibe su pregunta una vez por miembro, con {} en lugar del nombre del miembro. "Does the user want {} in the comparison?" se convierte en una pregunta por ticker.
Escribe cada pregunta sobre la idea en lugar de sobre las palabras que un usuario podría elegir, porque la coincidencia es por significado: «is amd tracking nvidia lately» llega a rolling_correlation aunque ni tracking ni lately aparezcan en ningún lugar de spec.json. Evita nombrar una pregunta igual que su parámetro: "Which resolution?" no le da nada al comando con lo que coincidir.
Convierte la spec en preguntas
Dispatcher construye las preguntas a partir de la spec una sola vez. Cada comando es entonces una solicitud que lleva la elección de función y los argumentos de todas las funciones, y el dispatcher lee solo las respuestas de la función elegida.
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?
Ejecuta catorce comandos
Una solicitud ocupa una línea, y su confidence es el juicio menos seguro que hay detrás de esa llamada.
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
Ambos comandos largos salieron como se pedía. «plot rolling correlation between nvda and spy for the past month» rellenó cuatro argumentos a partir de una frase. Dos de ellos, symbol y benchmark, salen de los mismos seis tickers, y cada ticker fue a parar al argumento correcto porque las preguntas dejan claros los roles: el que se mide, nombrado primero frente a el segundo nombrado, la referencia. «compare nvda amd and msft over the past three months» puso tres tickers en el conjunto y dejó fuera los otros tres.
Ejecutando tres de ellos:
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')
Y los que responden en texto:
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
Lee la confianza
confidence informa del juicio menos seguro de la llamada, en lugar del producto de todos ellos, ya que un solo argumento equivocado basta para estropear el resultado. Un producto responde a otra pregunta («¿está bien cada parte?»), y baja a medida que una función toma más argumentos, tanto si algún juicio es flojo como si no.
De dónde salió ese número, argumento por argumento:
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 y resolution se omiten ambos aquí, porque «lately» no dice cuánto tiempo atrás ni con qué barras, así que rolling_correlation se ejecuta con sus propios valores por defecto de un mes y barras horarias. Para eso está la pregunta stated. Sin ella, la choice tendría que nombrar alguna ventana, y habría nombrado una con seguridad.
Ábrelo en el playground
El enlace de abajo contiene un comando y las preguntas de la función que eligió: la choice sobre las diez descripciones de función y los cuatro argumentos de rolling_correlation. Edita el comando allí y los argumentos cambian con él.
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})"
)
)
Abre el comando y sus preguntas en el playground de TypeSafe →