Skip to content

Commit 5be64e7

Browse files
authored
docs: Documentation for event-based models (#117)
# Summary Adds documentation for event-based process models. # Changes * Completes the tutorial for event-based models. * Momentum trading signals on S&P500.
1 parent b61d61e commit 5be64e7

File tree

10 files changed

+980
-2
lines changed

10 files changed

+980
-2
lines changed

.github/instructions/models.instructions.md

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,4 +95,85 @@ import asyncio
9595

9696
async with process:
9797
await process.run()
98-
```
98+
```
99+
100+
## Event-driven models
101+
102+
You can help users to implement event-driven models using Plugboard's event system. Components can emit and handle events to communicate with each other.
103+
104+
Examples of where you might want to use events include:
105+
* A component that monitors a data stream and emits an event when a threshold is crossed;
106+
* A component that listens for events and triggers actions in response, e.g. sending an alert;
107+
* A trading algorithm that uses events to signal buy/sell decisions.
108+
109+
Events must be defined by inheriting from the `plugboard.events.Event` class. Each event class should define the data it carries using a Pydantic `BaseModel`. For example:
110+
111+
```python
112+
from pydantic import BaseModel
113+
from plugboard.events import Event
114+
115+
class MyEventData(BaseModel):
116+
some_value: int
117+
another_value: str
118+
119+
class MyEvent(Event):
120+
data: MyEventData
121+
```
122+
123+
Components can emit events using the `self.io.queue_event()` method or by returning them from an event handler. Event handlers are defined using methods decorated with `@EventClass.handler`. For example:
124+
125+
```python
126+
from plugboard.component import Component, IOController as IO
127+
128+
class MyEventPublisher(Component):
129+
io = IO(inputs=["some_input"], output_events=[MyEvent])
130+
131+
async def step(self) -> None:
132+
# Emit an event
133+
event_data = MyEventData(some_value=42, another_value=f"received {self.some_input}")
134+
self.io.queue_event(MyEvent(source=self.name, data=event_data))
135+
136+
class MyEventSubscriber(Component):
137+
io = IO(input_events=[MyEvent], output_events=[MyEvent])
138+
139+
@MyEvent.handler
140+
async def handle_my_event(self, event: MyEvent) -> MyEvent:
141+
# Handle the event
142+
print(f"Received event: {event.data}")
143+
output_event_data = MyEventData(some_value=event.data.some_value + 1, another_value="handled")
144+
return MyEvent(source=self.name, data=output_event_data)
145+
```
146+
147+
To assemble a process with event-driven components, you can use the same approach as for non-event-driven components. You will need to create connectors for event-driven components using `plugboard.events.event_connector_builder.EventConnectorBuilder`. For example:
148+
149+
```python
150+
from plugboard.connector import AsyncioConnector, ConnectorBuilder
151+
from plugboard.events.event_connector_builder import EventConnectorBuilder
152+
from plugboard.process import LocalProcess
153+
154+
# Define components....
155+
component_1 = ...
156+
component_2 = ...
157+
158+
# Define connectors for non-event components as before
159+
connect = lambda in_, out_: AsyncioConnector(spec=ConnectorSpec(source=in_, target=out_))
160+
connectors = [
161+
connect("component_1.output", "component_2.input"),
162+
...
163+
]
164+
165+
connector_builder = ConnectorBuilder(connector_cls=AsyncioConnector)
166+
event_connector_builder = EventConnectorBuilder(connector_builder=connector_builder)
167+
event_connectors = list(event_connector_builder.build(components).values())
168+
169+
process = LocalProcess(
170+
components=[
171+
component_1, component_2, ...
172+
],
173+
connectors=connectors + event_connectors,
174+
)
175+
```
176+
177+
## Exporting models
178+
179+
If the user wants to export their model you use in the CLI, you can do this by calling `process.dump("path/to/file.yaml")`.
Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,78 @@
1-
Tutorial coming soon.
1+
---
2+
tags:
3+
- events
4+
---
5+
So far everything we have built in Plugboard has been a **discrete-time model**. This means that the whole model advances step-wise, i.e. `step` gets called on each [`Component`][plugboard.component.Component], calculating all of their outputs before advancing the simulation on.
6+
7+
In this tutorial we're going to introduce an **event-driven model**, where data can be passed around between components based on triggers that you can define. Event-based models can be useful in a variety of scenarios, for example when modelling parts moving around a production line, or to trigger expensive computation only when certain conditions are met in the model.
8+
9+
## Event-based model
10+
11+
Here's the model that we're going to build. Given a stream of random numbers, we'll trigger `HighEvent` whenever the value is above `0.8` and `LowEvent` whenever the value is below `0.2`. This allows us to funnel data into different parts of the model: in this case we'll just save the latest high/low values to a file at each step. In the diagram the _dotted lines_ represent the flow of event data: `FindHighLowValues` will publish events, while `CollectHigh` and `CollectLow` will subscribe to receive high and low events respectively.
12+
13+
```mermaid
14+
flowchart LR
15+
collect-high@{ shape: rounded, label: CollectHigh<br>**collect-high** } --> save-high@{ shape: rounded, label: FileWriter<br>**save-high** }
16+
collect-low@{ shape: rounded, label: CollectLow<br>**collect-low** } --> save-low@{ shape: rounded, label: FileWriter<br>**save-low** }
17+
random-generator@{ shape: rounded, label: Random<br>**random-generator** } --> find-high-low@{ shape: rounded, label: FindHighLowValues<br>**find-high-low** }
18+
low_event@{ shape: hex, label: LowEvent } -.-> collect-low@{ shape: rounded, label: CollectLow<br>**collect-low** }
19+
high_event@{ shape: hex, label: HighEvent } -.-> collect-high@{ shape: rounded, label: CollectHigh<br>**collect-high** }
20+
find-high-low@{ shape: rounded, label: FindHighLowValues<br>**find-high-low** } -.-> high_event@{ shape: hex, label: HighEvent }
21+
find-high-low@{ shape: rounded, label: FindHighLowValues<br>**find-high-low** } -.-> low_event@{ shape: hex, label: LowEvent }
22+
```
23+
24+
## Defining events
25+
26+
First we need to define the events that are going to get used in the model. Each event needs a name, in this case `"high_event"` and `"low_event"` and a `data` type associated with it. Use a [Pydantic](https://docs.pydantic.dev/latest/) model to define the format of this `data` field.
27+
28+
```python
29+
--8<-- "examples/tutorials/005_events/hello_events.py:events"
30+
```
31+
32+
## Building components to create and consume events
33+
34+
So far all of our process models have run step-by-step until completion. When a model contains event-driven components, we need a way to tell them to stop at the end of the simulation, otherwise they will stay running and listening for events forever.
35+
36+
In this example, our `Random` component will drive the process by generating input random values. When it has completed `iters` iterations, we call `self.io.close()` to stop the model, causing other components in the model to shutdown.
37+
38+
```python
39+
--8<-- "examples/tutorials/005_events/hello_events.py:source-component"
40+
```
41+
42+
Next, we will define `FindHighLowValues` to identify high and low values in the stream of random numbers and publish `HighEvent` and `LowEvent` respectively.
43+
44+
```python
45+
--8<-- "examples/tutorials/005_events/hello_events.py:event-publisher"
46+
```
47+
48+
1. See how we use the [`IOController`][plugboard.component.IOController] to declare that this [`Component`][plugboard.component.Component] will publish events.
49+
2. Use `self.io.queue_event` to send an event from a [`Component`][plugboard.component.Component]. Here we are senging the `HighEvent` or `LowEvent` depending on the input value.
50+
51+
Finally, we need components to subscribe to these events and process them. Use the `Event.handler` decorator to identify the method on each [`Component`][plugboard.component.Component] that will do this processing.
52+
53+
```python
54+
--8<-- "examples/tutorials/005_events/hello_events.py:event-consumers"
55+
```
56+
57+
1. Specify the events that this [`Component`][plugboard.component.Component] will subscribe to.
58+
2. Use this decorator to indicate that we handle `HighEvent` here...
59+
3. ...and we handle `LowEvent` here.
60+
61+
!!! note
62+
In a real model you could define whatever logic you need inside your event handler, e.g. create a file, publish another event, etc. Here we just store the event on an attribute so that its value can be output via the `step()` method.
63+
64+
## Putting it all together
65+
66+
Now we can create a [`Process`][plugboard.process.Process] from all these components. The outputs from `CollectLow` and `CollectHigh` are connected to separate [`FileWriter`][plugboard.library.FileWriter] components so that we'll get a CSV file containing the latest high and low values at each step of the simulation.
67+
68+
!!! info
69+
We need a few extra lines of code to create connectors for the event-based parts of the model. If you define your process in YAML this will be done automatically for you, but if you are defining the process in code then you will need to use the [`EventConnectorBuilder`][plugboard.events.EventConnectorBuilder] to do this.
70+
71+
```python hl_lines="15-17"
72+
--8<-- "examples/tutorials/005_events/hello_events.py:main"
73+
```
74+
75+
1. These connectors are for the normal, non-event driven parts of the model and connect [`Component`][plugboard.component.Component]` inputs and outputs.
76+
2. These lines will set up connectors for the events in the model.
77+
78+
Take a look at the `high.csv` and `low.csv` files: the first few rows will usually be empty, and then as soon as high or low values are identified they will start to appear in the CSVs. As usual, you can run this model from the CLI using `plugboard process run model.yaml`.

examples/demos/finance/.meta.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
tags:
2+
- finance
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.csv
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
tags:
2+
- events

0 commit comments

Comments
 (0)