문서

함수 호출

함수 이름과 닫힌 집합 인수를 신뢰도를 인식하는 TypeSafe 질문에 매핑하여, 자연어 트레이딩 요청을 평범한 타입 지정 함수 호출로 바꿉니다.

“large iced oat latte, no sweetener”를 주문하면, 바리스타는 당신의 문장을 적지 않습니다. 컵에 네 개의 옵션을 표시할 뿐입니다. 이 cookbook은 트레이딩 API에 대해 같은 일을 합니다. 문장이 들어가면, 평가된 enum으로서의 함수 이름과 그 인수들이 각각 신뢰도와 함께 나옵니다.

"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,
): ...

고정 목록에서 값을 취하는 인수는 닫힌 집합(closed set)입니다. 그 목록에서 값 하나를 취할 때는 정확히 그 값들에 대한 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"라는 문자열을 줍니다. 사용자가 “this quarter”라고 입력한 것이 두 번째를 뜻한다고는 말해 주지 않습니다. spec이 그것을 말합니다. 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는 인수를 선택 사항으로 만듭니다. 그것은 명령이 그 인수에 대해 전혀 말하는지 묻는 두 번째 예/아니오 질문입니다. 답이 아니오면, 호출은 그 인수를 빼고 함수 자체의 기본값이 적용됩니다.

set 인수는 멤버마다 질문을 하나씩 받으며, {}가 멤버 이름을 대신합니다. "Does the user want {} in the comparison?"은 티커마다 질문 하나가 됩니다.

사용자가 고를 법한 단어가 아니라 아이디어에 대해 각 질문을 작성하십시오. 매칭이 의미에 기반하기 때문입니다. “is amd tracking nvidia lately”는 spec.json 어디에도 tracking도 lately도 없는데도 rolling_correlation에 도달합니다. 질문에 그 매개변수 이름을 붙이는 것은 피하십시오. "Which resolution?"은 명령이 맞춰 볼 것을 아무것도 주지 않습니다.

spec을 질문으로 바꾸기

Dispatcher는 spec으로부터 질문을 한 번만 만듭니다. 그러면 각 명령은 함수 선택과 모든 함수의 인수를 담은 하나의 요청이 되고, dispatcher는 선택된 함수의 답만 읽습니다.

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”는 티커 세 개를 set에 넣고 나머지 세 개는 뺐습니다.

그중 세 개를 실행하면:

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')
output output output

그리고 텍스트로 답하는 것들:

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는 어떤 window를 반드시 지목해야 하고, 자신 있게 하나를 지목했을 것입니다.

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})"
    )
)
TypeSafe playground에서 이 명령과 그 질문들 열기 →