Os programas anteriores armazenavam preços, contadores e configurações em variáveis separadas. Isso permite criar muitos indicadores e estratégias. Em um programa maior, porém, pode ser útil tratar vários dados relacionados como uma unidade: por exemplo, os dois limites de uma faixa de preços ou o preço e o instante de um sinal.

É para isso que serve um tipo definido pelo usuário, em inglês user-defined type, ou UDT. Um método é uma operação associada a um tipo específico. Ambos fazem parte de Pyne code, mas você não precisa usá-los nos próximos exemplos do livro. Se quiser chegar primeiro às estratégias, pode seguir para a depuração e voltar aqui depois.

Dois números descrevem uma faixa de preços: seus limites inferior e superior. Um tipo próprio permite dar um nome a esse par:

@udt
class PriceRange:
    bottom: float = 0.0
    top: float = 0.0

class define um novo tipo, chamado PriceRange. bottom e top são seus campos. Ambos podem armazenar números fracionários e têm zero como valor padrão.

A marcação @udt transforma isso em um tipo definido pelo usuário de Pyne. Importamos udt do módulo pynecore.core.pine_udt. Em Edge, escrevemos os campos na definição do tipo e definimos os métodos associados separadamente. Não usamos herança nesse subconjunto da linguagem.

O tipo e o objeto criado a partir dele

O tipo descreve quais dados serão armazenados juntos. Um par concreto fica em um objeto, também chamado de instância, criado a partir do tipo:

current_range = PriceRange()
current_range.bottom = low
current_range.top = high

PriceRange() cria uma instância com os valores padrão dos campos. As duas linhas seguintes definem seu limite inferior como a mínima do candle atual e o superior como a máxima. Você lê os campos com current_range.bottom e current_range.top. Aqui, o ponto indica o acesso a um campo.

Você pode criar vários objetos do mesmo tipo. Outro PriceRange, por exemplo, pode armazenar outra faixa de preços: os nomes dos campos são iguais, mas seus valores são independentes.

Método: uma operação sobre um tipo específico

O ponto médio da faixa é a média dos dois limites. Podemos escrever esse cálculo como um método associado ao tipo:

@method
def midpoint(self: PriceRange) -> float:
    return (self.bottom + self.top) / 2

@method marca a função como um método. O tipo do primeiro parâmetro informa sobre quais dados ele trabalha: aqui, um objeto PriceRange. Dentro do método, self é o nome do objeto cujo ponto médio queremos calcular. É um nome de parâmetro; a anotação PriceRange define seu tipo.

A definição fica fora do bloco class, no nível mais externo do arquivo. Importamos method do módulo pynecore.core.pine_method.

A chamada:

center = midpoint(current_range)

Chamamos o método conhecido pelo nome e passamos, como primeiro argumento, o objeto sobre o qual ele trabalha. Assim, o parâmetro self recebe o objeto current_range. O número devolvido é armazenado na variável center.

Você também poderia escrever esse cálculo como uma função simples. O método ajuda na organização quando há várias operações associadas a um tipo e você quer explicitar sobre quais dados elas trabalham.

O programa completo

Crie o arquivo udt_ranges.py no grupo Scripts:

"""
@pyne edge
"""
from pynecore.core.pine_method import method
from pynecore.core.pine_udt import udt
from pynecore.lib import color, high, low, plot, script


@udt
class PriceRange:
    bottom: float = 0.0
    top: float = 0.0


@method
def midpoint(self: PriceRange) -> float:
    return (self.bottom + self.top) / 2


@script.indicator("Price Range Objects", overlay=True)
def main():
    current_range = PriceRange()
    current_range.bottom = low
    current_range.top = high
    center = midpoint(current_range)
    plot(current_range.bottom, "Lower", color=color.blue)
    plot(current_range.top, "Upper", color=color.orange)
    plot(center, "Midpoint", color=color.green, linewidth=2)

Execute com os dados de demonstração. A linha azul mostra a mínima dos candles; a laranja, a máxima; a verde, o ponto médio. Em cada candle, o ponto verde fica entre os outros dois valores.

O PyneIDE mostra o código do tipo PriceRange e do método midpoint; o gráfico exibe três linhas para o limite inferior, o superior e o ponto médio.
Os dois campos de PriceRange armazenam os limites do candle; o método midpoint calcula o ponto médio entre eles.

O programa cria um novo objeto PriceRange em cada candle. Neste exemplo, os campos não preservam os valores do candle anterior; sempre armazenam os dois limites do candle atual.

Atribuição e cópia

Atribuir um objeto a outra variável não é o mesmo que copiá-lo:

other_range = current_range
other_range.top = other_range.top + 1.0

As duas variáveis apontam para o mesmo objeto. Por isso, a segunda linha também altera current_range.top.

Se quiser uma instância separada com os mesmos valores nos campos, crie um novo objeto:

other_range = PriceRange()
other_range.bottom = current_range.bottom
other_range.top = current_range.top
other_range.top = other_range.top + 1.0

Nesse tipo formado por dois números, alterar o limite superior da nova instância não altera o da original. Os dois números ficam em objetos separados. Se, mais adiante, um campo armazenar outro objeto ou um array, passá-lo a uma nova instância também passará uma referência, sem criar uma cópia separada do conteúdo.

Preserve um objeto entre candles

Para continuar usando o mesmo objeto, você também pode aplicar a anotação Persistent que já conhece. Para isso, importe Persistent do módulo pynecore.

current_range: Persistent[PriceRange] = PriceRange()
current_range.bottom = low
current_range.top = high

O objeto é criado uma vez, e seus dois campos são atualizados a cada candle. Você pode substituir a criação do objeto e as duas atribuições de campos no começo de main() por essas três linhas. O cálculo do ponto médio e os desenhos continuam iguais.

Neste exemplo, o gráfico não muda: atribuímos os mesmos valores aos campos. Em outra tarefa, você pode alterar os campos apenas quando ocorrer um evento e, assim, preservar uma faixa de preços selecionada anteriormente. O tipo, por si só, não torna o objeto persistente.

Tente por conta própria

Crie um método chamado width que devolva a largura da faixa de preços. Seguindo o modelo de midpoint, escreva um parâmetro self: PriceRange e calcule o resultado com os dois campos.

Solução do exercício
@method
def width(self: PriceRange) -> float:
    return self.top - self.bottom

A chamada dentro de main():

range_width = width(current_range)

Se a máxima de um candle for 108 e a mínima for 101, a largura será 7. O método apenas lê os campos; não altera nenhum dos limites.