Skip to content

Core Concepts

levi edited this page Nov 6, 2025 · 2 revisions

Core Concepts

Table of Contents

  1. Bidirectional Python-JavaScript Bridge Architecture
  2. Event-Driven System with EventBus
  3. Singleton Pattern Implementation
  4. Asynchronous Programming Model
  5. Data Isolation in Multi-Chart Scenarios
  6. Practical Examples and Best Practices

Bidirectional Python-JavaScript Bridge Architecture

The PyTradingView framework implements a bidirectional communication bridge between the Python runtime and TradingView's JavaScript environment using HTTP-based RPC (Remote Procedure Call) mechanisms. The TVBridge class serves as the central component for this integration, establishing two-way communication channels through dedicated HTTP routes.

The bridge operates by exposing REST endpoints on the Python side to receive calls from JavaScript, while simultaneously maintaining the capability to initiate calls to the JavaScript environment. Key architectural components include:

  • Web-to-Python Communication: The /web/call/py endpoint receives method invocations from the JavaScript frontend, which are then routed to appropriate handlers based on class name and method name
  • Python-to-Web Communication: The call_node_server method enables Python to invoke methods in the JavaScript environment through HTTP POST requests to the Node server
  • Connection Management: The bridge automatically detects available ports and establishes connections with the Node server through retry logic with exponential backoff
  • Configuration Integration: The bridge integrates with TVWidgetConfig to provide configuration data to the frontend when requested

This architecture enables seamless interoperability between Python's computational capabilities and TradingView's visualization features, allowing complex calculations to be performed in Python while maintaining real-time updates in the TradingView interface.

sequenceDiagram
participant JS as "TradingView JavaScript"
participant Bridge as "TVBridge"
participant Python as "Python Runtime"
JS->>Bridge : POST /web/call/py
Bridge->>Python : Route to appropriate handler
Python->>Bridge : Process request
Bridge->>JS : Return response
Python->>Bridge : call_node_server()
Bridge->>JS : HTTP POST to Node server
JS->>Bridge : Return result
Bridge->>Python : Return to caller
Loading

Event-Driven System with EventBus

PyTradingView employs an event-driven architecture centered around the EventBus class, which implements a publish-subscribe pattern for decoupled component communication. This system enables various components to communicate without direct dependencies, promoting loose coupling and maintainability.

The EventBus implementation provides several key features:

  • Type-Safe Events: Events are strongly typed using the EventType enum, which defines categories such as widget lifecycle events, chart events, indicator events, and bridge events
  • Asynchronous Processing: The event system supports both synchronous and asynchronous event handling, with the publish method handling async callbacks and publish_sync providing a bridge to synchronous contexts
  • Error Resilience: Event publication uses asyncio.gather with return_exceptions=True to ensure that failures in one handler do not prevent other handlers from executing
  • Thread Safety: The singleton implementation includes proper initialization checks to prevent multiple instances

Components can subscribe to specific event types and receive notifications when those events are published. For example, when a chart is ready, the system publishes a CHART_READY event that any interested components can handle. This decoupled approach allows for flexible system composition and easy extension of functionality.

classDiagram
class EventBus {
+get_instance() EventBus
+subscribe(event_type, callback) void
+unsubscribe(event_type, callback) void
+publish(event_type, data, source) Awaitable~None~
+publish_sync(event_type, data, source) void
}
class EventType {
WIDGET_CREATED
WIDGET_READY
CHART_READY
INDICATOR_LOADED
BRIDGE_CONNECTED
}
class Event {
+type EventType
+data Dict[str, Any]
+source Optional[str]
}
EventBus --> Event : publishes
Event --> EventType : has type
Loading

Singleton Pattern Implementation

The framework extensively uses the singleton pattern to ensure global state consistency across the application. This pattern is implemented in several core components, most notably in the TVEngineSingleton base class and the EventBus class.

The singleton implementation follows a thread-safe approach using the __new__ method override and a class-level lock to prevent race conditions during instance creation. Key characteristics include:

  • Thread Safety: The implementation uses a threading lock to ensure that only one instance is created even in multi-threaded environments
  • Lazy Initialization: Instances are created only when first requested through the get_instance class method
  • Initialization Guard: A flag _initialized tracks whether an instance has been properly set up, preventing reinitialization
  • Testing Support: The reset class method allows for singleton reset during testing, though this is explicitly warned against for production use

This pattern ensures that components like the event bus, object pool, and engine maintain a single, consistent state throughout the application lifecycle. It prevents issues that could arise from multiple instances holding different states and provides a reliable mechanism for global access to shared resources.

classDiagram
class TVEngineSingleton {
_instance Optional[TVEngineSingleton]
_lock Optional[threading.Lock]
+get_instance(config) TVEngineSingleton
+reset() void
__new__(cls, config) object
}
TVEngineSingleton <|-- TVEngine : inherits
TVEngineSingleton <|-- EventBus : inherits
Loading

Asynchronous Programming Model

PyTradingView embraces asynchronous programming throughout its architecture, leveraging Python's async/await syntax for non-blocking operations. This model is essential for maintaining responsiveness while handling I/O operations such as HTTP requests and event processing.

Key aspects of the asynchronous model include:

  • Non-Blocking I/O: HTTP requests to the Node server are handled asynchronously using aiohttp, preventing the main thread from being blocked during network operations
  • Event Loop Integration: The event bus provides both async (publish) and sync (publish_sync) publishing methods, allowing integration with both async and synchronous code
  • Task Management: Asynchronous event handlers are managed as asyncio.Task objects, with proper cleanup to prevent resource leaks
  • Concurrency: Multiple event handlers can run concurrently when an event is published, improving performance and responsiveness

The framework handles the complexity of async programming by providing clear patterns for implementation. For example, the TVSubscribeManager uses asyncio.create_task to schedule async handlers and maintains a set of cleanup tasks to ensure proper resource management.

flowchart TD
Start([Event Occurs]) --> CheckAsync["Check if async handlers exist"]
CheckAsync --> |Yes| CreateTasks["Create asyncio tasks for all handlers"]
CreateTasks --> Schedule["Schedule tasks with asyncio"]
Schedule --> WaitForCompletion["Wait for all tasks to complete"]
WaitForCompletion --> End([Event Processing Complete])
CheckAsync --> |No| End
Loading

Data Isolation in Multi-Chart Scenarios

To support multiple charts with independent states, PyTradingView implements a data isolation mechanism through the ChartContext and ChartContextManager classes. This architecture ensures that each chart maintains its own state, including active indicators, symbol information, and interval settings.

The key components of this system are:

  • ChartContext: A data class that encapsulates all state specific to a single chart, including references to the chart object, active indicators, and market data context
  • ChartContextManager: A manager class that maintains a registry of all chart contexts, providing methods to create, retrieve, and destroy contexts
  • Per-Chart State: Each chart has its own collection of active indicators, preventing interference between charts
  • Lifecycle Management: Contexts are created when charts are initialized and destroyed when charts are removed, ensuring proper resource cleanup

This design enables users to work with multiple charts simultaneously, each potentially displaying different instruments with different sets of indicators, without state conflicts. The manager also provides utility methods for cross-chart operations, such as finding all charts that use a particular indicator.

classDiagram
class ChartContext {
+chart_id str
+chart TVChart
+active_indicators Dict[str, TVIndicator]
+symbol Optional[str]
+interval Optional[str]
+add_indicator(name, indicator) void
+remove_indicator(name) Optional[TVIndicator]
+get_indicator(name) Optional[TVIndicator]
}
class ChartContextManager {
_contexts Dict[str, ChartContext]
+create_context(chart_id, chart) ChartContext
+get_context(chart_id) Optional[ChartContext]
+remove_context(chart_id) Optional[ChartContext]
+get_all_contexts() Dict[str, ChartContext]
+clear_all() void
}
ChartContextManager --> ChartContext : manages
Loading

Practical Examples and Best Practices

Event Subscription Example

To subscribe to events using the EventBus system:

# Get the singleton instance
event_bus = EventBus.get_instance()

# Define an async event handler
async def on_chart_ready(event: Event):
    chart_data = event.data
    print(f"Chart ready with data: {chart_data}")

# Subscribe to the CHART_READY event
event_bus.subscribe(EventType.CHART_READY, on_chart_ready)

# Later, unsubscribe when no longer needed
event_bus.unsubscribe(EventType.CHART_READY, on_chart_ready)

Bridge Communication Pattern

When communicating between Python and JavaScript:

# Get the bridge instance
bridge = TVBridge.get_instance()

# Call a method in the JavaScript environment
params = TVMethodCall(
    class_name="TVWidget", 
    object_id="widget_123",
    method_name="updateLayout",
    kwargs={"layout": "advanced"}
)

response = await bridge.call_node_server(params)
if response.error:
    print(f"Call failed: {response.error}")
else:
    print(f"Call succeeded: {response.result}")

Asynchronous Operation Handling

Best practices for handling asynchronous operations:

  1. Always use await: When calling async methods, always use the await keyword
  2. Handle exceptions: Wrap async calls in try-except blocks to handle potential errors
  3. Avoid blocking calls: Never use time.sleep() in async code; use asyncio.sleep() instead
  4. Proper task management: When creating tasks, consider their lifecycle and clean up when appropriate

Performance Considerations

  • Minimize bridge calls: Batch operations when possible to reduce HTTP overhead
  • Efficient event handling: Keep event handlers lightweight and offload heavy processing to background tasks
  • Proper resource cleanup: Always unsubscribe from events and dispose of resources when components are destroyed
  • Connection resilience: The bridge automatically handles connection retries, but applications should monitor connection status

Lifecycle Management

  • Initialization: Use the singleton pattern's get_instance() method rather than direct instantiation
  • Cleanup: When shutting down, clear event subscribers and dispose of contexts to prevent memory leaks
  • Error handling: Implement proper error handling around bridge communications and event processing

Clone this wiki locally