-
Notifications
You must be signed in to change notification settings - Fork 1
Core Concepts
- Bidirectional Python-JavaScript Bridge Architecture
- Event-Driven System with EventBus
- Singleton Pattern Implementation
- Asynchronous Programming Model
- Data Isolation in Multi-Chart Scenarios
- Practical Examples and Best Practices
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/pyendpoint 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_servermethod 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
TVWidgetConfigto 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
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
EventTypeenum, 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
publishmethod handling async callbacks andpublish_syncproviding a bridge to synchronous contexts -
Error Resilience: Event publication uses
asyncio.gatherwithreturn_exceptions=Trueto 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
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_instanceclass method -
Initialization Guard: A flag
_initializedtracks whether an instance has been properly set up, preventing reinitialization -
Testing Support: The
resetclass 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
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.Taskobjects, 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
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
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)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}")Best practices for handling asynchronous operations:
- Always use await: When calling async methods, always use the await keyword
- Handle exceptions: Wrap async calls in try-except blocks to handle potential errors
- Avoid blocking calls: Never use time.sleep() in async code; use asyncio.sleep() instead
- Proper task management: When creating tasks, consider their lifecycle and clean up when appropriate
- 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
- 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