Hasta ahora los programas guardaban precios, contadores y ajustes en variables separadas. Con eso puedes crear muchos indicadores y estrategias. En un programa grande puede convenir tratar varios datos relacionados como una unidad: por ejemplo, los límites de una banda de precios o el precio y el momento de una señal.

Para eso sirve un tipo definido por el usuario, en inglés user-defined type, abreviado UDT. Un método es una operación asociada a un tipo concreto. Ambos forman parte de Pyne code, pero no los necesitas para los ejemplos siguientes. Si prefieres llegar primero a las estrategias, puedes pasar a depuración y volver aquí más adelante.

Un rango de precios se describe con dos números: sus límites inferior y superior. Un tipo propio permite dar nombre a ese par de datos:

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

class define un nuevo tipo llamado PriceRange. bottom y top son sus campos. Ambos admiten parte fraccionaria y su valor predeterminado es cero.

@udt lo convierte en un tipo de usuario de Pyne. Importamos udt desde pynecore.core.pine_udt. En Edge escribimos los campos dentro del tipo y definimos sus métodos por separado. No utilizamos herencia en este subconjunto del lenguaje.

El tipo y el objeto creado a partir de él

El tipo describe qué datos vamos a guardar juntos. Un par concreto se almacena en un objeto, también llamado instancia, creado a partir del tipo:

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

PriceRange() crea una instancia con los valores predeterminados. Las siguientes líneas asignan el mínimo actual al límite inferior y el máximo al superior. Puedes leer sus campos con current_range.bottom y current_range.top. El punto indica aquí acceso a un campo.

Puedes crear varios objetos del mismo tipo. Otro PriceRange puede almacenar otro rango; los nombres de los campos son iguales, pero los valores son independientes.

Método: una operación sobre un tipo concreto

El centro del rango es la media de sus límites. Podemos escribirlo como un método asociado al tipo:

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

@method marca la función como método. El tipo del primer parámetro indica los datos con los que trabaja: aquí, un objeto PriceRange. self es el nombre del objeto dentro del método. Es un nombre de parámetro; la anotación PriceRange establece su tipo.

La definición está fuera de class, en el nivel exterior del archivo. Importamos method desde pynecore.core.pine_method.

La llamada:

center = midpoint(current_range)

Llamamos al método conocido por su nombre y pasamos el objeto como primer argumento. Así, self recibe current_range. El número devuelto se guarda en center.

También podrías escribir este cálculo con una función normal. Un método ayuda a organizar el programa cuando tienes varias operaciones asociadas a un tipo y quieres dejar claro sobre qué datos trabajan.

El programa completo

Crea udt_ranges.py en 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)

Ejecútalo con los datos de demostración. La línea azul muestra mínimos; la naranja, máximos; la verde, el punto medio. En cada vela el punto verde queda entre los otros valores.

PyneIDE muestra el tipo PriceRange y el método midpoint; en el gráfico aparecen las líneas de límite inferior, superior y punto medio.
Los campos de PriceRange guardan los límites de la vela y midpoint calcula su punto medio.

El programa crea un nuevo PriceRange en cada vela. Aquí los campos no conservan los valores anteriores; siempre guardan los dos límites de la vela actual.

Asignación y copia

Asignar un objeto a otra variable no equivale a copiarlo:

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

Ambas variables se refieren al mismo objeto. La segunda línea también cambia current_range.top.

Si necesitas una instancia separada con los mismos campos, crea otro objeto:

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

En este tipo de dos números, modificar el límite superior de la nueva instancia no afecta al original. Guardamos los números en objetos separados. Si más adelante un campo contiene otro objeto o un array, pasarlo a una nueva instancia también transmite una referencia y no copia aparte su contenido.

Conserva un objeto entre velas

Para seguir usando el mismo objeto puedes aplicar Persistent, que ya conoces. Importa Persistent desde pynecore.

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

El objeto se crea una vez y sus campos se actualizan en cada vela. Puedes sustituir la creación y las dos asignaciones al inicio de main() por estas líneas. El cálculo del punto medio y el dibujo no cambian.

Aquí el gráfico no cambia porque damos los mismos valores a los campos. En otra tarea podrías modificarlos solo cuando ocurra un evento y conservar, por ejemplo, un rango elegido antes. El tipo por sí solo no hace persistente al objeto.

Pruébalo por tu cuenta

Crea un método width que devuelva la anchura del rango. Siguiendo midpoint, utiliza self: PriceRange y calcula con los campos.

Solución del ejercicio
@method
def width(self: PriceRange) -> float:
    return self.top - self.bottom

La llamada dentro de main():

range_width = width(current_range)

Si el máximo es 108 y el mínimo 101, la anchura es 7. El método solo lee los campos; no modifica ningún límite.