-
Notifications
You must be signed in to change notification settings - Fork 1
Creating Custom Indicators
- Indicator Lifecycle and Registration
- Configuration System
- Signal and Drawable Objects
- Calculate Method Interface
- Signal Generation Patterns
- Drawing Elements and Shapes
- Event Handling and Chart Context
- Complete Example: False Breakout Indicator
- Performance Optimization and Debugging
The custom indicator development process in PyTradingView begins with the indicator lifecycle, which is managed through a well-defined class hierarchy and registration system. All custom indicators must inherit from the TVIndicator base class located in indicator_base.py, which defines the core interface and lifecycle methods for indicators.
Indicator registration is accomplished through the @register_indicator decorator, which registers the indicator class with the global IndicatorRegistry. This decorator accepts parameters such as the indicator name and enabled status, making the indicator available for use in the TradingView environment. When an indicator is registered, it becomes accessible for instantiation and execution within chart contexts.
The indicator lifecycle includes several key phases: initialization, configuration, calculation, drawing, and destruction. During initialization, the on_init method is called with references to the widget and chart objects, allowing the indicator to set up its internal state. The get_config method is then called to retrieve the indicator's configuration, which defines its parameters and appearance options.
Throughout the lifecycle, various callback methods are invoked to notify the indicator of important events. These include on_data_loaded when market data is loaded, on_calculate_start and on_calculate_end around calculation phases, and on_draw_start and on_draw_end around drawing operations. These lifecycle hooks enable indicators to perform setup, cleanup, and logging operations at appropriate times.
The configuration system in PyTradingView is built around the IndicatorConfig class, which provides a comprehensive framework for defining and managing indicator parameters. This system supports complex input types, style configuration, runtime modification, automatic UI generation, and validation.
Configuration begins with defining input parameters using the InputDefinition class, which specifies various characteristics of each parameter including its identifier, display name, type, default value, and validation constraints. The supported input types include integers, floats, booleans, strings, colors, dropdown options, and data sources. Each input can have additional constraints such as minimum and maximum values for numeric types, or a list of valid options for dropdowns.
classDiagram
class IndicatorConfig {
+str name
+str version
+str description
+str author
+bool enabled
+bool debug
+List[InputDefinition] inputs
+List[StyleDefinition] styles
+Callable[[Dict[str, Any]], None] on_config_changed
+get_input_value(input_id) Any
+set_input_value(input_id, value) Tuple[bool, Optional[str]]
+set_input_values(values) Tuple[bool, List[str]]
+get_style(style_id) Optional[StyleDefinition]
+update_style(style_id, **kwargs) Tuple[bool, Optional[str]]
+validate_all() Tuple[bool, List[str]]
+to_dict() Dict[str, Any]
+to_json(indent) str
+from_dict(data) IndicatorConfig
+from_json(json_str) IndicatorConfig
+reset_to_defaults() None
+get_inputs_by_group() Dict[str, List[InputDefinition]]
+get_styles_by_group() Dict[str, List[StyleDefinition]]
}
class InputDefinition {
+str id
+str display_name
+InputType type
+Any default_value
+Any value
+str tooltip
+List[InputOption] options
+Optional[Union[int, float]] min_value
+Optional[Union[int, float]] max_value
+Optional[Union[int, float]] step
+bool visible
+str group
+validate() Tuple[bool, Optional[str]]
+to_dict() Dict[str, Any]
}
class StyleDefinition {
+str id
+str display_name
+str color
+int line_width
+int line_style
+int transparency
+bool visible
+str group
+validate() Tuple[bool, Optional[str]]
+to_dict() Dict[str, Any]
}
class InputOption {
+str label
+Any value
+to_dict() Dict[str, Any]
}
enum InputType {
INTEGER
FLOAT
BOOLEAN
STRING
COLOR
OPTIONS
SOURCE
}
IndicatorConfig "1" *-- "0..*" InputDefinition : contains
IndicatorConfig "1" *-- "0..*" StyleDefinition : contains
InputDefinition "1" *-- "0..*" InputOption : has options
**Diagram sources **
Configuration also includes style definitions through the StyleDefinition class, which allows indicators to define visual properties such as colors, line widths, line styles, and transparency levels. These styles can be grouped and organized for better UI presentation. The configuration system supports grouping inputs and styles into categories, which are displayed as separate sections in the indicator settings UI.
The configuration object provides methods for getting and setting parameter values, both individually and in batches. The set_input_value method validates the new value against the parameter's constraints before applying it, ensuring data integrity. Similarly, the update_style method validates style changes and triggers appropriate callbacks when modifications occur.
Configuration changes are handled through the on_config_changed callback, which is invoked whenever a parameter or style is modified. This allows indicators to respond to configuration changes by triggering recalculation or updating their internal state. The configuration system also supports serialization through to_dict and to_json methods, enabling configuration persistence and sharing.
PyTradingView uses two primary data structures to represent indicator outputs: TVSignal for trading signals and TVDrawable for visual elements on the chart. These classes provide a standardized interface for communicating calculation results from indicators to the rendering engine.
The TVSignal class represents trading signals generated by indicators, such as buy, sell, or neutral signals. Each signal contains a signal type, a timestamp in UNIX seconds, a price level, and optional metadata. The use of timestamps instead of bar indices ensures data independence and cross-chart compatibility, as timestamps are universal across different chart configurations and timeframes.
classDiagram
class TVSignal {
+str signal_type
+int timestamp
+float price
+Dict[str, Any] metadata
}
class TVDrawable {
+List[Tuple[int, float]] points
+Any shape
+Dict[str, Any] metadata
}
TVDrawable "1" --> "1" TVSingleShape : uses
TVDrawable "1" --> "1" TVMultipleShape : uses
**Diagram sources **
The TVDrawable class represents visual elements that can be drawn on the chart, such as lines, arrows, shapes, and annotations. It contains a list of coordinate points in (time, price) format and a reference to a shape object that defines the visual appearance. The design decision to use timestamps instead of bar indices provides several advantages: alignment with the TradingView API, 40% performance improvement in drawing operations, data independence from DataFrames, and easier serialization.
Drawable objects can represent both single-point graphics (like arrows or markers) and multi-point graphics (like trend lines or channels). The shape property contains a TVSingleShape or TVMultipleShape instance that already includes style information, eliminating the need for additional configuration during rendering. This design simplifies the drawing process and ensures consistent styling across different chart elements.
Both signal and drawable objects support metadata, which can contain additional information such as styling overrides, labels, or custom data that influences how the elements are rendered. This metadata system provides flexibility for indicators to pass additional context to the rendering engine without modifying the core data structures.
The calculate method serves as the core processing function for custom indicators, responsible for analyzing market data and generating signals and drawing elements. This method receives a pandas DataFrame containing OHLC (open, high, low, close) data and returns a tuple of TVSignal and TVDrawable lists.
The input DataFrame must contain specific columns: 'time' (UNIX timestamp), 'open', 'high', 'low', and 'close', with optional columns like 'volume' or custom indicators. The method processes this data to identify patterns, calculate technical indicators, and generate trading signals based on the configured parameters.
flowchart TD
Start([calculate method entry]) --> ValidateInput["Validate input DataFrame"]
ValidateInput --> InputValid{"Has sufficient data?"}
InputValid --> |No| ReturnEmpty["Return empty signals and drawables"]
InputValid --> |Yes| GetConfig["Retrieve configuration parameters"]
GetConfig --> GetData["Extract OHLC data arrays"]
GetData --> ApplyLogic["Apply indicator logic and calculations"]
ApplyLogic --> GenerateSignals["Generate TVSignal objects"]
GenerateSignals --> GenerateDrawables["Generate TVDrawable objects"]
GenerateDrawables --> ReturnResults["Return (signals, drawables)"]
ReturnResults --> End([method exit])
**Diagram sources **
The calculation process typically follows these steps: first, the method validates that sufficient data is available for meaningful analysis. Then, it retrieves the current configuration values using the get_config method and extracts the relevant market data into numpy arrays for efficient processing.
The core logic of the indicator is then applied to detect patterns or calculate values. This may involve rolling window calculations, statistical analysis, pattern recognition, or other algorithmic approaches. During this phase, the indicator may maintain state across multiple calls to track trends, count occurrences, or remember previous conditions.
As signals and drawing elements are identified, they are created as TVSignal and TVDrawable objects with appropriate timestamps, prices, and metadata. The timestamps are derived from the 'time' column in the DataFrame, ensuring precise alignment with the corresponding price bars. The method returns both signals and drawables as separate lists, allowing the rendering engine to process them independently.
The calculate method is designed to be stateless in its interface, receiving all necessary data through parameters and returning results without side effects. However, indicators can maintain internal state between calls through instance variables, enabling the detection of multi-bar patterns and trend developments.
Signal generation in PyTradingView indicators follows specific patterns for creating buy/sell entries, stop-loss levels, and take-profit targets. These patterns are implemented through the creation of TVSignal objects with appropriate signal types and metadata that guides their visual representation and potential trading actions.
Buy and sell signals are represented by setting the signal_type field to 'buy' or 'sell' respectively. These signals typically correspond to entry points for trades and are often displayed as upward or downward arrows on the chart. The signal's timestamp indicates when the condition was met, while the price field specifies the execution level. Metadata can include styling information such as arrow color, size, and label text.
sequenceDiagram
participant Indicator as "Custom Indicator"
participant Engine as "Indicator Engine"
participant Chart as "TradingView Chart"
Indicator->>Indicator : Detect buy condition
Indicator->>Indicator : Create TVSignal(signal_type='buy', timestamp=t, price=p)
Indicator->>Engine : Return signal in calculate() result
Engine->>Engine : Process signal list
Engine->>Chart : Draw buy arrow at (t,p)
Chart-->>Chart : Display visual signal
Indicator->>Indicator : Detect sell condition
Indicator->>Indicator : Create TVSignal(signal_type='sell', timestamp=t, price=p)
Indicator->>Engine : Return signal in calculate() result
Engine->>Engine : Process signal list
Engine->>Chart : Draw sell arrow at (t,p)
Chart-->>Chart : Display visual signal
**Diagram sources **
Stop-loss and take-profit levels are typically implemented as TVDrawable objects rather than signals, using horizontal lines or other visual elements to mark price levels. These can be created as TVTrendLine shapes with specific styling to distinguish them from regular signals. The metadata field can indicate the type of level (stop-loss or take-profit) and include additional information like risk-reward ratios.
Advanced signal patterns may include conditional signals that depend on multiple criteria being met, filtered signals that require confirmation from other indicators, or timed signals that are only valid for a specific number of bars. These patterns are implemented by maintaining state within the indicator instance and applying logical conditions before generating signals.
Signal filtering based on market context is another common pattern, where signals are only generated during specific market conditions such as trending markets, high volatility periods, or particular times of day. This helps reduce false positives and improves the overall quality of trading signals.
The drawing system in PyTradingView provides extensive capabilities for creating visual elements on charts through the TVDrawable class and a comprehensive library of shape types. This system allows indicators to annotate charts with lines, arrows, geometric shapes, and other graphical elements that enhance technical analysis.
Drawing elements are created by instantiating TVDrawable objects that contain coordinate points and a shape specification. The points are specified as tuples of (time, price) using UNIX timestamps, which ensures precise positioning on the chart regardless of the current zoom level or timeframe. The shape property contains an instance of a specific shape class that defines the visual appearance and behavior of the element.
classDiagram
class TVDrawable {
+List[Tuple[int, float]] points
+Any shape
+Dict[str, Any] metadata
}
class TVSingleShape {
+Dict[str, Any] overrides
}
class TVMultipleShape {
+Dict[str, Any] overrides
}
class TVTrendLine {
+Dict[str, Any] overrides
}
class TVArrowUp {
+Dict[str, Any] overrides
}
class TVArrowDown {
+Dict[str, Any] overrides
}
class TVCircle {
+Dict[str, Any] overrides
}
TVDrawable "1" --> "1" TVSingleShape : uses
TVDrawable "1" --> "1" TVMultipleShape : uses
TVSingleShape <|-- TVArrowUp
TVSingleShape <|-- TVArrowDown
TVMultipleShape <|-- TVTrendLine
TVMultipleShape <|-- TVCircle
**Diagram sources **
The shapes library includes a wide variety of visual elements categorized by their functionality. Single-point shapes like TVArrowUp, TVArrowDown, and TVCircle are used for marking specific price points, while multi-point shapes like TVTrendLine, TVFibRetracement, and TVRectangle connect multiple points to form lines, channels, or geometric patterns.
Each shape can be customized through its overrides property, which accepts styling parameters such as line color, width, style (solid, dashed, dotted), transparency, and visibility. These styling options are applied when the shape is rendered on the chart, allowing indicators to create visually distinct elements for different types of signals or conditions.
The drawing system is integrated with the indicator lifecycle, with the engine automatically handling the creation and management of chart elements. When an indicator generates TVDrawable objects, the engine converts them to native TradingView graphics using the appropriate API calls (createShape for single-point elements and createMultipointShape for multi-point elements). The engine also manages the lifecycle of these elements, automatically clearing previous drawings before rendering new ones.
Event handling in PyTradingView indicators is facilitated through the chart context system, which manages indicator instances and state for individual charts. This system provides data isolation in multi-chart scenarios, ensuring that each chart maintains independent collections of indicator instances, drawing object references, and chart state cache.
The ChartContext class serves as the central component of this system, with each chart instance corresponding to a unique ChartContext. This context contains references to the chart object, a dictionary of active indicators, and information about the current trading symbol and timeframe. The ChartContextManager oversees all chart contexts, providing methods to create, retrieve, and remove contexts as charts are added or removed from the application.
classDiagram
class ChartContextManager {
+Dict[str, ChartContext] _contexts
+create_context(chart_id, chart) ChartContext
+get_context(chart_id) Optional[ChartContext]
+remove_context(chart_id) Optional[ChartContext]
+get_all_contexts() Dict[str, ChartContext]
+get_chart_ids() list[str]
+clear_all() None
+has_context(chart_id) bool
+get_charts_with_indicator(indicator_name) list[str]
+count_total_indicators() int
}
class ChartContext {
+str chart_id
+TVChart chart
+Dict[str, TVIndicator] active_indicators
+Optional[str] symbol
+Optional[str] interval
+add_indicator(name, indicator) None
+remove_indicator(name) Optional[TVIndicator]
+get_indicator(name) Optional[TVIndicator]
+has_indicator(name) bool
+clear_all_indicators() None
+get_indicator_names() list[str]
+update_symbol_interval(symbol, interval) None
}
ChartContextManager "1" *-- "0..*" ChartContext : manages
ChartContext "1" --> "1" TVChart : references
ChartContext "1" *-- "0..*" TVIndicator : contains active
**Diagram sources **
Indicators interact with the chart context through various lifecycle methods that provide access to the chart and widget objects. The on_init method receives these objects when the indicator is initialized, allowing it to store references for later use. The get_chart and get_widget helper methods provide convenient access to these objects throughout the indicator's lifetime.
Event propagation occurs through the indicator engine, which coordinates the execution of indicators and the handling of user interactions. When configuration changes occur, the engine notifies the appropriate indicators, which can then respond by recalculating their outputs and updating their visual elements. This event-driven architecture ensures that indicators remain synchronized with user preferences and market data changes.
The context system also supports cross-chart operations, allowing indicators to query the status of other charts or coordinate behavior across multiple views of the market. This enables advanced use cases such as multi-timeframe analysis, where an indicator on one chart can incorporate signals from indicators on other charts with different timeframes.
The false breakout indicator provides a comprehensive example of custom indicator development in PyTradingView, demonstrating real-world implementation patterns for configuration, calculation, and drawing strategies. This indicator detects false breakouts, which occur when price moves beyond a recent high or low but quickly reverses direction, potentially signaling a reversal opportunity.
The indicator is defined as a class that inherits from TVIndicator and is registered using the @register_indicator decorator. Its configuration is defined in the get_config method, which returns an IndicatorConfig object specifying various parameters grouped into logical categories. The main settings include the false breakout period, minimum period for new breakouts, and signal validity period, while advanced smoothing options include selection of smoothing type, length, and aggressive mode.
flowchart TD
Start([False Breakout Indicator]) --> DefineConfig["Define configuration parameters"]
DefineConfig --> MainSettings["Main Settings: period, min_period, max_period"]
DefineConfig --> AdvancedSmoothing["Advanced Smoothing: ma_type, ma_length, aggressive"]
DefineConfig --> DefineStyles["Define signal styles"]
DefineStyles --> UpStyle["False Breakout Up: red color"]
DefineStyles --> DownStyle["False Breakout Down: green color"]
DefineStyles --> ReturnConfig["Return IndicatorConfig"]
ReturnConfig --> CalculateMethod["Implement calculate method"]
CalculateMethod --> GetConfig["Retrieve configuration values"]
CalculateMethod --> PrepareData["Extract OHLC data arrays"]
CalculateMethod --> CalculateExtremes["Calculate highest/lowest values"]
CalculateExtremes --> ApplySmoothing["Apply selected smoothing filter"]
ApplySmoothing --> DetectConditions["Detect new high/low conditions"]
DetectConditions --> TrackState["Track breakout state"]
TrackState --> CheckBreakout["Check false breakout conditions"]
CheckBreakout --> GenerateSignals["Generate buy/sell signals"]
GenerateSignals --> CreateDrawables["Create horizontal lines"]
CreateDrawables --> ReturnResults["Return signals and drawables"]
**Diagram sources **
The calculation logic identifies false breakouts by tracking sequences of new highs and new lows. When price creates a new high, the indicator begins monitoring for a subsequent move below the trigger price, which would confirm a false breakout to the downside (a buy signal). Conversely, when price creates a new low, it monitors for a move above the trigger price, confirming a false breakout to the upside (a sell signal).
The indicator supports multiple smoothing filters, including a diamond filter (no smoothing), weighted moving average (WMA), and Hull moving average (HMA), which can be selected through the configuration UI. These filters help reduce noise and improve signal quality by smoothing the underlying price extremes before detecting breakouts.
For visualization, the indicator creates both signal arrows and horizontal lines at the breakout levels. The arrows are styled according to the configured colors for up and down signals, while the horizontal lines extend from the breakout point to the confirmation bar, providing a clear visual representation of the false breakout pattern. The drawing strategy uses the TVDrawable system with TVTrendLine shapes and appropriate styling overrides to match the configured appearance.
Developing high-performance indicators in PyTradingView requires attention to both computational efficiency and effective debugging practices. The framework provides several mechanisms for optimizing indicator calculations and diagnosing issues during development.
Performance optimization begins with efficient data processing in the calculate method. Since this method may be called frequently as new market data arrives, it should minimize unnecessary computations and leverage vectorized operations with numpy arrays whenever possible. The false breakout indicator demonstrates this principle by converting pandas Series to numpy arrays at the beginning of calculation and using numpy functions for rolling window operations.
Caching is another important optimization technique, where intermediate results are stored between calculation calls to avoid redundant computations. The TVIndicator base class provides built-in caching fields like _cached_df, _last_signals, and _last_drawables that can be used to store data and results across invocations. Indicators should also minimize the creation of temporary objects within the calculate method to reduce memory allocation overhead.
flowchart TD
Start([Performance Optimization]) --> EfficientData["Use numpy arrays instead of pandas"]
EfficientData --> VectorizedOps["Leverage vectorized operations"]
VectorizedOps --> MinimizeObjects["Minimize object creation"]
MinimizeObjects --> CacheResults["Cache intermediate results"]
CacheResults --> AvoidRedundant["Avoid redundant calculations"]
AvoidRedundant --> OptimizeLoops["Optimize loop structures"]
OptimizeLoops --> ProfileCode["Profile code to identify bottlenecks"]
Start --> DebuggingApproach["Debugging Approaches"]
DebuggingApproach --> EnableDebug["Enable debug mode in configuration"]
EnableDebug --> UseLogging["Use print statements or logging"]
UseLogging --> CheckLifecycle["Verify lifecycle method calls"]
CheckLifecycle --> ValidateConfig["Validate configuration parameters"]
ValidateConfig --> TestEdgeCases["Test edge cases and error conditions"]
TestEdgeCases --> UseExample["Use example indicators as reference"]
**Diagram sources **
The configuration system includes a debug flag that, when enabled, causes the indicator to output diagnostic information through print statements during its lifecycle. This can help identify issues with calculation logic, timing problems, or unexpected behavior. Developers should strategically place debug output in key methods like on_calculate_start, on_calculate_end, and within the calculate method itself to trace the execution flow.
Common debugging approaches include verifying that configuration parameters are being retrieved correctly, ensuring that timestamps are properly aligned with price data, and confirming that drawing elements appear in the expected locations on the chart. Testing with different market conditions and timeframe settings can reveal edge cases that need to be handled in the indicator logic.
The example indicators provided in the repository serve as valuable references for debugging, demonstrating proper implementation patterns and providing a baseline for expected behavior. By comparing a custom indicator's output with these examples, developers can identify discrepancies and refine their implementation for better accuracy and reliability.