Você já conhece o cálculo da amplitude de um candle: high - low. Se precisar dele em vários cálculos, pode colocá-lo em uma função própria. A chamada pode deixar claro o objetivo do cálculo, e a fórmula fica em um único lugar.

Entradas e resultado de uma função

def price_range(top: float, bottom: float) -> float:
    return top - bottom

def define uma função. Seu nome é price_range. Ela espera duas entradas, chamadas top e bottom. Ambas são números que podem conter uma parte fracionária.

-> float indica o tipo do resultado. return calcula e devolve o resultado, encerrando aquela chamada de função.

O resultado de price_range(105, 100) é 5. A função não sabe se os números são preços e não escolhe um candle. Quem faz a chamada fornece os dois valores.

O corpo da função começa com quatro espaços de indentação. Neste exemplo, a definição fica ao lado de main(), no nível mais externo do arquivo. Por isso, passamos como parâmetros os dois preços necessários ao cálculo. Você também pode criar uma função dentro de main(); vamos experimentar isso adiante.

O escopo de um nome

top e bottom são parâmetros disponíveis apenas dentro de price_range. main() não pode acessá-los diretamente. Nela, você usa o resultado devolvido.

O escopo de um nome indica em qual parte do código ele está disponível. Duas funções diferentes podem usar os mesmos nomes de parâmetros sem que eles se tornem uma única variável compartilhada.

Definida no nível mais externo do arquivo, price_range não acessa automaticamente as variáveis locais de main(). Porém, pode usar nomes importados no início do arquivo, como close. Portanto, o local da definição também influencia quais nomes uma função consegue acessar.

A definição e a chamada de uma função são coisas diferentes. A definição estabelece a regra; a chamada a executa com valores concretos.

O programa completo

Crie o arquivo price_functions.py. Nele, a mesma função calcula a amplitude total do candle e a variação com sinal entre abertura e fechamento:

"""
@pyne edge
"""
from pynecore.lib import close, color, high, low, open, plot, script


def price_range(top: float, bottom: float) -> float:
    return top - bottom


@script.indicator("Price Functions", overlay=False)
def main():
    full_range = price_range(high, low)
    body_change = price_range(close, open)
    plot(full_range, "Full range", color=color.blue)
    plot(body_change, "Body change", color=color.orange)

Execute com os dados de demonstração. A amplitude total não é negativa, pois a máxima do candle é pelo menos igual à mínima. Já a variação entre abertura e fechamento pode ser negativa. A função não modifica os preços recebidos; apenas devolve a diferença entre os dois números.

As duas chamadas de função de price_functions.py desenham a amplitude total do candle e a variação com sinal do corpo.
A mesma função calcula dois resultados diferentes a partir de entradas distintas.

Neste exemplo, a função é curta e fácil de conferir à mão. Em programas maiores, você também pode dar nomes a tarefas mais complexas, mas não é obrigatório criar uma função para cada soma.

A ordem dos parâmetros

A chamada price_range(high, low) passa high para o parâmetro top e low para bottom. Se você os inverter, o sinal da subtração também será invertido.

Também é possível informar o nome do parâmetro:

full_range = price_range(top=high, bottom=low)

Essa forma é mais longa, mas pode facilitar a leitura de uma chamada mais complexa. O nome fornecido precisa corresponder exatamente ao nome do parâmetro na função.

Uma função dentro de main()

Uma função definida dentro de outra também pode acessar as variáveis da função que a envolve. Isso é útil quando vários cálculos usam a mesma configuração fornecida como input.

Vamos suavizar as duas linhas do programa anterior com uma média móvel. Definiremos o comprimento da média em um único input, usado pelos dois cálculos. A função interna recebe apenas os dois preços; ela encontra o comprimento entre os parâmetros de main().

No arquivo price_functions.py, mantenha a função externa price_range e o decorador do indicador. Acrescente input e ta à importação de pynecore.lib e substitua a definição de main() por:

def main(
    length: int = input.int(5, title="Average length", minval=1, maxval=200),
):
    def average_difference(top: float, bottom: float) -> float:
        return ta.sma(price_range(top, bottom), length)

    average_range = average_difference(high, low)
    average_body_change = average_difference(close, open)
    plot(average_range, "Average range", color=color.blue)
    plot(average_body_change, "Average body change", color=color.orange)

length é o input inteiro que você já conhece. A função interna average_difference recebe dois parâmetros: a chamada fornece os valores de top e bottom. Ela lê length na função main() que a envolve; por isso, você não precisa informar esse valor novamente nas chamadas.

A definição da função interna começa com quatro espaços, pois faz parte de main(). Seu próprio corpo, a linha de return, começa com oito espaços. O nome average_difference está disponível dentro de main(); ele não fica disponível no nível mais externo do arquivo.

price_range calcula a diferença entre os preços; ta.sma devolve a média móvel dessa diferença. Assim, a linha azul mostra a média da amplitude total dos candles; a laranja, a média da variação com sinal entre abertura e fechamento. As duas chamadas são executadas em cada candle, mantendo o cálculo das duas médias contínuo.

Execute o programa modificado e abra a tela Inputs pela engrenagem. Altere Average length de 5 para 20 e clique em Save. O gráfico aberto é recalculado automaticamente, e as duas linhas passam a usar médias de vinte candles. Você não precisou alterar as duas chamadas da função interna. Ao definir o comprimento como 1, obtém novamente as duas linhas do programa original.

Esse acesso às variáveis externas é chamado de closure. Ler o valor não significa que uma atribuição interna possa sobrescrever automaticamente a variável externa. Neste exemplo, apenas lemos o valor de length.

Na versão com parâmetros, todos os valores passados ficam visíveis na chamada, e é fácil usar a mesma função com outro par de preços. Uma função interna pode ser conveniente quando vários cálculos usam as mesmas variáveis criadas em main().

Uma função própria também pode ter estado

price_range calcula apenas com os valores recebidos. Ela não tem variáveis preservadas entre candles nem usa histórico. Por isso, você pode chamá-la a qualquer momento com dois valores.

Já average_difference, acima, usa estado: ta.sma, chamada dentro dela, calcula com valores anteriores. Você também pode criar variáveis Persistent ou Series diretamente em uma função própria.

Uma função contadora de eventos

Vamos contar separadamente os candles de alta e de baixa. Para isso, escreveremos uma função que recebe um valor lógico e guarda quantas vezes recebeu verdadeiro até agora.

No início do arquivo, importe Persistent:

from pynecore import Persistent

Escreva a função abaixo antes de main(), no nível mais externo do arquivo:

def count_events(event: bool) -> int:
    count: Persistent[int] = 0
    if event:
        count += 1
    return count

O valor inicial de count é zero. Quando event é verdadeiro, ele aumenta em um; quando é falso, preserva o valor. Nos dois casos, return devolve o total acumulado. Depois que a chamada termina, o valor da variável Persistent permanece para a próxima chamada.

Mantenha a configuração overlay=False. Substitua a definição e o corpo anteriores de main() por:

def main():
    bullish_count = count_events(close > open)
    bearish_count = count_events(close < open)
    plot(bullish_count, "Bullish count", color=color.green)
    plot(bearish_count, "Bearish count", color=color.red)

Chamamos a mesma função em dois lugares diferentes do código. Cada local de chamada tem um estado count separado: o primeiro conta candles de alta; o segundo, candles de baixa. A primeira chamada não aumenta o contador da segunda.

Por exemplo, se os quatro primeiros candles tiverem a sequência abaixo, os valores serão:

CandleRelação entre abertura e fechamentobullish_countbearish_count
PrimeiroFechamento mais alto10
SegundoFechamento mais baixo11
TerceiroOs dois preços iguais11
QuartoFechamento mais alto21

Execute com os dados de demonstração. A linha verde avança um passo apenas nos candles de alta; a vermelha, apenas nos de baixa. Uma nova execução reconstrói os dois contadores a partir de zero.

As duas chamadas são executadas em cada candle; a condição de incremento fica dentro da função. Em funções que usam estado, a ordem e a frequência das chamadas fazem parte do funcionamento. Se chamar uma função apenas em alguns candles, o histórico mantido dentro dela também avançará somente nessas chamadas.

Se precisar de um único contador compartilhado, chame a função uma vez e use a variável com o resultado em vários lugares. Um segundo local de chamada significa outro contador, separado.

Várias instruções em uma função

Se precisar de um resultado intermediário, você pode calculá-lo antes de return:

def midpoint(top: float, bottom: float) -> float:
    total = top + bottom
    return total / 2

total é uma variável local. Ao terminar a chamada, ela não se torna uma variável de main(). Devolvemos da função apenas o resultado.

Tente por conta própria

Adicione uma função midpoint ao programa e crie um indicador sobreposto aos candles que desenhe seus pontos médios. Use os parâmetros top e bottom da função para receber a máxima e a mínima.

Solução do exercício

Escreva a definição de midpoint acima de main(). No decorador do indicador, use overlay=True; o corpo deve ser:

center = midpoint(high, low)
plot(center, "Midpoint", color=color.blue, linewidth=2)

Você pode excluir desta versão os cálculos e desenhos das duas linhas anteriores. Remova também as importações não utilizadas.

Explique a chamada

Na linha price_range(close, open), explique qual valor vai para cada parâmetro, o que acontece na linha de return e onde main() recebe o resultado. Você também pode acompanhar esses três passos em uma chamada de função mais longa.