Chamada de funções
Transforma pedidos de trading em linguagem natural em chamadas a funções tipadas comuns, mapeando nomes de função e argumentos de conjunto fechado para perguntas do TypeSafe com consciência de confiança.
Quando você pede um “latte grande com gelo e aveia, sem açúcar”, o barista não anota sua frase. Ele marca quatro opções em um copo. Este cookbook faz a mesma coisa com uma API de trading: entra uma frase, e sai um nome de função e seus argumentos como enums avaliados, cada um com uma confiança.
"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
Essas chamadas vão para dez funções comuns em um assistente de trading. Seus argumentos pegam valores de listas fixas, então já são 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,
): ...
Um argumento cujos valores vêm de uma lista fixa é um conjunto fechado. Quando ele pega um valor dessa lista, recebe uma pergunta Choice sobre exatamente esses valores, então o que chega à função é um valor que a função aceita. Você deixa as funções em paz. O que você acrescenta é uma spec que diz em palavras simples o que cada argumento significa. No final você tem um Dispatcher que pode apontar para suas próprias funções.
Configuração
pip install ipython polars matplotlib numpy 'cooksafe>=0.2.0,<0.3.0'
Defina TYPESAFE_API_KEY. Dois módulos ficam ao lado deste arquivo. trader.py contém as dez funções, mais um cliente TypeSafe que lê respostas de um cache, então re-renderizar reproduz os números abaixo sem chamar a API. dispatch.py contém o código que lê uma assinatura e uma spec e faz a chamada.
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
Encontre os conjuntos fechados nas assinaturas
Os type hints já dizem quais argumentos vêm de uma lista fixa e o que há em cada lista. closed_sets lê uma assinatura e classifica esses argumentos em três formas: uma choice (um Literal, ou seja, um valor da lista), um set (um list[Literal[...]], ou seja, qualquer número deles), ou uma flag (um bool, ou seja, ligado ou desligado). As dez funções estão definidas em 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 mostra o que fica de fora. Dos seus três argumentos, dois são conjuntos fechados. O terceiro, limit, é um int, então nunca recebe uma pergunta e mantém seu valor padrão de 3. Texto livre, números e datas funcionam do mesmo jeito: sem pergunta, e o valor padrão da função prevalece.
Escreva a spec
O Literal te dá as strings "1mo" e "3mo". Ele não diz que um usuário que digita “este trimestre” se refere à segunda. A spec diz isso. Ela contém uma pergunta por argumento, uma linha por opção, uma descrição por função e mais uma pergunta que escolhe entre as funções. Ela fica em spec.json, e um LLM pode escrevê-la para você a partir das assinaturas.
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"
}
}
}
As chaves das opções são as strings que a função recebe, então depois nada precisa mapear um rótulo de volta a um argumento. stated torna um argumento opcional. É uma segunda pergunta de sim/não que pergunta se o comando diz algo sobre esse argumento. Quando a resposta é não, a chamada deixa esse argumento de fora e o valor padrão da própria função se aplica.
Um argumento do tipo set recebe sua pergunta uma vez por membro, com {} no lugar do nome do membro. "Does the user want {} in the comparison?" vira uma pergunta por ticker.
Escreva cada pergunta sobre a ideia, e não sobre as palavras que um usuário poderia escolher, porque a correspondência é por significado: “is amd tracking nvidia lately” chega a rolling_correlation mesmo que nem tracking nem lately apareçam em lugar nenhum de spec.json. Evite nomear uma pergunta igual ao seu parâmetro - "Which resolution?" não dá ao comando nada com que corresponder.
Transforme a spec em perguntas
Dispatcher constrói as perguntas a partir da spec uma vez. Cada comando é então uma requisição que carrega a escolha de função e os argumentos de todas as funções, e o dispatcher lê só as respostas da função escolhida.
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?
Execute catorze comandos
Cada requisição ocupa uma linha, e sua confidence é o julgamento menos certo por trás dessa chamada.
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
Os dois comandos longos saíram como pedido. “plot rolling correlation between nvda and spy for the past month” preencheu quatro argumentos a partir de uma frase. Dois deles, symbol e benchmark, saem dos mesmos seis tickers, e cada ticker caiu no argumento certo porque as perguntas deixam os papéis claros: o que está sendo medido, nomeado primeiro contra o segundo nomeado, a referência. “compare nvda amd and msft over the past three months” colocou três tickers no conjunto e deixou os outros três de fora.
Rodando três deles:
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')
E os que respondem em 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
Leia a confiança
confidence informa o julgamento menos certo da chamada, em vez do produto de todos eles, já que um único argumento errado basta para estragar o resultado. Um produto responde a uma pergunta diferente (“toda parte está certa”), e cai à medida que uma função recebe mais argumentos, quer algum julgamento seja frágil ou não.
De onde veio esse 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 e resolution são ambos omitidos aqui, porque “lately” não diz há quanto tempo nem com quais barras, então rolling_correlation roda com seus próprios padrões de um mês e barras horárias. É para isso que serve a pergunta stated. Sem ela, a choice teria que nomear alguma janela, e teria nomeado uma com confiança.
Abra no playground
O link abaixo contém um comando e as perguntas da função que ele escolheu: a choice sobre as dez descrições de função e os quatro argumentos de rolling_correlation. Edite o comando lá e os argumentos mudam junto.
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})"
)
)
Abra o comando e suas perguntas no playground do TypeSafe →