Вызов функций
Превращает запросы к торговле на естественном языке в вызовы обычных типизированных функций, сопоставляя имена функций и аргументы из закрытых множеств с вопросами TypeSafe, учитывающими уверенность.
Когда вы заказываете «большой айс-латте на овсяном молоке, без сахара», бариста не записывает вашу фразу. Он отмечает четыре опции на стакане. Этот cookbook делает то же самое для торгового API: на вход идёт фраза, а на выходе получаются имя функции и её аргументы как оценённые перечисления, каждый с уверенностью.
"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
Эти вызовы идут к десяти обычным функциям в торговом ассистенте. Их аргументы берут
значения из фиксированных списков, так что они уже 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,
): ...
Аргумент, чьи значения берутся из фиксированного списка, — это закрытое множество. Когда
он берёт одно значение из этого списка, он получает вопрос Choice ровно по этим
значениям, так что до функции доходит значение, которое она принимает. Сами функции вы не
трогаете. Вы добавляете spec, которая простыми словами говорит, что означает каждый
аргумент. В итоге у вас будет Dispatcher, который можно нацелить на свои функции.
Установка
pip install ipython polars matplotlib numpy 'cooksafe>=0.2.0,<0.3.0'
Задайте TYPESAFE_API_KEY. Два модуля лежат рядом с этим файлом. trader.py содержит
десять функций плюс клиент TypeSafe, читающий ответы из кэша, поэтому повторный рендеринг
воспроизводит числа ниже без обращения к API. dispatch.py содержит код, который читает
сигнатуру и spec и делает вызов.
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
Поиск закрытых множеств в сигнатурах
Аннотации типов уже говорят, какие аргументы берутся из фиксированного списка и что в
каждом списке. closed_sets читает сигнатуру и разбирает эти аргументы на три формы:
choice (это Literal, то есть одно значение из списка), set (это
list[Literal[...]], то есть любое их число) или flag (это bool, то есть включено
или выключено). Все десять функций определены в 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 показывает, что остаётся в стороне. Из его трёх аргументов два — закрытые
множества. Третий, limit, — это int, поэтому он никогда не получает вопроса и
сохраняет своё значение по умолчанию 3. Свободный текст, числа и даты работают так же:
вопроса нет, и значение функции по умолчанию остаётся в силе.
Написание spec
Literal даёт вам строки "1mo" и "3mo". Он не говорит, что пользователь, набирающий
«этот квартал», имеет в виду вторую. Это говорит spec. Она содержит вопрос на аргумент,
строку на опцию, описание на функцию и ещё один вопрос, который выбирает между функциями.
Она лежит в spec.json, и LLM может написать её вам по сигнатурам.
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"
}
}
}
Ключи опций — это строки, которые принимает функция, поэтому потом ничего не нужно
отображать метку обратно в аргумент. stated делает аргумент необязательным. Это второй
вопрос «да/нет», спрашивающий, говорит ли команда вообще что-нибудь об этом аргументе.
Когда ответ «нет», вызов опускает этот аргумент и применяется собственное значение функции
по умолчанию.
Аргумент-множество получает свой вопрос по одному разу на элемент, где {} подставляет имя
элемента. "Does the user want {} in the comparison?" становится одним вопросом на каждый
тикер.
Пишите каждый вопрос об идее, а не о словах, которые может выбрать пользователь, потому
что сопоставление идёт по смыслу: «is amd tracking nvidia lately» доходит до
rolling_correlation, хотя ни tracking, ни lately нигде не встречаются в spec.json.
Избегайте называть вопрос по имени его параметра — "Which resolution?" не даёт команде
ничего, с чем сопоставляться.
Превращение spec в вопросы
Dispatcher строит вопросы из spec один раз. Затем каждая команда — это один запрос,
несущий выбор функции и аргументы каждой функции, а диспетчер читает ответы только
выбранной функции.
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?
Запуск четырнадцати команд
Запрос занимает одну строку, и его confidence — это наименее уверенное суждение, стоящее
за этим вызовом.
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
Обе длинные команды вышли так, как было запрошено. «plot rolling correlation between nvda
and spy for the past month» заполнила четыре аргумента из одного предложения. Два из них,
symbol и benchmark, берутся из одних и тех же шести тикеров, и каждый тикер попал в
нужный аргумент, потому что вопросы проговаривают роли: тот, что измеряется, названный
первым, против названного вторым, эталона. «compare nvda amd and msft over the past
three months» поместила три тикера во множество и оставила три других в стороне.
Запуск трёх из них:
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')
А те, что отвечают текстом:
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
Чтение уверенности
confidence сообщает наименее уверенное суждение в вызове, а не произведение их всех,
поскольку одного неверного аргумента достаточно, чтобы испортить результат. Произведение
отвечает на другой вопрос («все ли части верны») и падает по мере того, как функция
принимает больше аргументов, независимо от того, шатко ли хоть одно суждение.
Откуда взялось это число, аргумент за аргументом:
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 и resolution здесь оба опущены, потому что «lately» не говорит, как давно или на
каких барах, поэтому rolling_correlation работает на своих значениях по умолчанию — одном
месяце и часовых барах. Для этого и нужен вопрос stated. Без него choice пришлось бы
назвать какое-то окно, и оно уверенно назвало бы его.
Открыть в playground
Ссылка ниже содержит одну команду и вопросы для функции, которую она выбрала: choice по
десяти описаниям функций и четыре аргумента rolling_correlation. Отредактируйте команду
там, и аргументы изменятся вместе с ней.
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})"
)
)
Открыть команду и её вопросы в playground TypeSafe →