Skip to content

Commit 920df19

Browse files
feat: add market_schedule module (#112)
* feat: add holiday_hours module * feat: integrate holiday schedule * feat: update gitignore to ignore .DS_Store * refactor: imports * fix: pre-commit * feat: wip market schedule using winnow parser * feat: wip add holiday day schedule parser * fix: format * feat: wip implement market_schedule parser * fix: format * feat: add market schedule can publish at test * feat: use new market hours in pyth agent * fix: format * refactor: rollback module restructure * rename market hours to legacy schedule * chore: add comment * feat: add support for 24:00 * fix: avoid parsing twice for verification * refactor: use match instead of if * refactor: use seq for time range parser * refactor: improve parser * refactor: improve parser * refactor: implement from trait * feat: add proptest * fix: day kind regex and add comments * refactor: improve comment * chore: increase pyth agent minor version
1 parent ce9ab72 commit 920df19

File tree

11 files changed

+775
-51
lines changed

11 files changed

+775
-51
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,6 @@ result
1010
**/*.rs.bk
1111
__pycache__
1212
keystore
13+
14+
# Mac OS
15+
.DS_Store

Cargo.lock

Lines changed: 73 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "pyth-agent"
3-
version = "2.5.2"
3+
version = "2.6.0"
44
edition = "2021"
55

66
[[bin]]
@@ -52,6 +52,8 @@ prometheus-client = "0.22.2"
5252
lazy_static = "1.4.0"
5353
toml_edit = "0.22.9"
5454
slog-bunyan = "2.5.0"
55+
winnow = "0.6.5"
56+
proptest = "1.4.0"
5557

5658
[dev-dependencies]
5759
tokio-util = { version = "0.7.10", features = ["full"] }

integration-tests/tests/test_integration.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import asyncio
2+
from datetime import datetime
23
import json
34
import os
45
import requests
@@ -63,9 +64,23 @@
6364
"quote_currency": "USD",
6465
"generic_symbol": "BTCUSD",
6566
"description": "BTC/USD",
67+
"schedule": f"America/New_York;O,O,O,O,O,O,O;{datetime.now().strftime('%m%d')}/O"
6668
},
6769
"metadata": {"jump_id": "78876709", "jump_symbol": "BTCUSD", "price_exp": -8, "min_publishers": 1},
6870
}
71+
SOL_USD = {
72+
"account": "",
73+
"attr_dict": {
74+
"symbol": "Crypto.SOL/USD",
75+
"asset_type": "Crypto",
76+
"base": "SOL",
77+
"quote_currency": "USD",
78+
"generic_symbol": "SOLUSD",
79+
"description": "SOL/USD",
80+
"schedule": f"America/New_York;O,O,O,O,O,O,O;{datetime.now().strftime('%m%d')}/C"
81+
},
82+
"metadata": {"jump_id": "78876711", "jump_symbol": "SOLUSD", "price_exp": -8, "min_publishers": 1},
83+
}
6984
AAPL_USD = {
7085
"account": "",
7186
"attr_dict": {
@@ -95,7 +110,7 @@
95110
},
96111
"metadata": {"jump_id": "78876710", "jump_symbol": "ETHUSD", "price_exp": -8, "min_publishers": 1},
97112
}
98-
ALL_PRODUCTS=[BTC_USD, AAPL_USD, ETH_USD]
113+
ALL_PRODUCTS=[BTC_USD, AAPL_USD, ETH_USD, SOL_USD]
99114

100115
asyncio.set_event_loop(asyncio.new_event_loop())
101116

@@ -277,6 +292,7 @@ def refdata_permissions(self, refdata_path):
277292
"AAPL": {"price": ["some_publisher_a"]},
278293
"BTCUSD": {"price": ["some_publisher_b", "some_publisher_a"]}, # Reversed order helps ensure permission discovery works correctly for publisher A
279294
"ETHUSD": {"price": ["some_publisher_b"]},
295+
"SOLUSD": {"price": ["some_publisher_a"]},
280296
}))
281297
f.flush()
282298
yield f.name
@@ -769,3 +785,38 @@ async def test_agent_respects_market_hours(self, client: PythAgentClient):
769785
assert final_price_account["price"] == 0
770786
assert final_price_account["conf"] == 0
771787
assert final_price_account["status"] == "unknown"
788+
789+
@pytest.mark.asyncio
790+
async def test_agent_respects_holiday_hours(self, client: PythAgentClient):
791+
'''
792+
Similar to test_agent_respects_market_hours, but using SOL_USD and
793+
asserting that nothing is published due to the symbol's all-closed holiday.
794+
'''
795+
796+
# Fetch all products
797+
products = {product["attr_dict"]["symbol"]: product for product in await client.get_all_products()}
798+
799+
# Find the product account ID corresponding to the AAPL/USD symbol
800+
product = products[SOL_USD["attr_dict"]["symbol"]]
801+
product_account = product["account"]
802+
803+
# Get the price account with which to send updates
804+
price_account = product["price_accounts"][0]["account"]
805+
806+
# Send an "update_price" request
807+
await client.update_price(price_account, 42, 2, "trading")
808+
time.sleep(2)
809+
810+
# Send another update_price request to "trigger" aggregation
811+
# (aggregation would happen if market hours were to fail, but
812+
# we want to catch that happening if there's a problem)
813+
await client.update_price(price_account, 81, 1, "trading")
814+
time.sleep(2)
815+
816+
# Confirm that the price account has not been updated
817+
final_product_state = await client.get_product(product_account)
818+
819+
final_price_account = final_product_state["price_accounts"][0]
820+
assert final_price_account["price"] == 0
821+
assert final_price_account["conf"] == 0
822+
assert final_price_account["status"] == "unknown"
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Seeds for failure cases proptest has generated in the past. It is
2+
# automatically read and these particular cases re-run before any
3+
# novel cases are generated.
4+
#
5+
# It is recommended to check this file in to source control so that
6+
# everyone who runs the test benefits from these saved cases.
7+
cc 173b9a862e3ad1149b0fdef292a11164ecab5b67b395857178f63294c3c9c0b7 # shrinks to s = "0000-0060"
8+
cc 6cf32e18287cb6de4b40f4326d1e9fd3be409086af3ccf75eac6f980c1f67052 # shrinks to s = TimeRange(00:00:00, 00:00:01)

src/agent.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ Note that there is an Oracle and Exporter for each network, but only one Local S
6363
################################################################################################################################## */
6464

6565
pub mod dashboard;
66-
pub mod market_hours;
66+
pub mod legacy_schedule;
67+
pub mod market_schedule;
6768
pub mod metrics;
6869
pub mod pythd;
6970
pub mod remote_keypair_loader;

src/agent/market_hours.rs renamed to src/agent/legacy_schedule.rs

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@ lazy_static! {
2828
}
2929

3030
/// Weekly market hours schedule
31+
/// TODO: Remove after all publishers have upgraded to support the new schedule format
3132
#[derive(Clone, Default, Debug, Eq, PartialEq)]
32-
pub struct WeeklySchedule {
33+
#[deprecated(note = "This struct is deprecated, use MarketSchedule instead.")]
34+
pub struct LegacySchedule {
3335
pub timezone: Tz,
3436
pub mon: MHKind,
3537
pub tue: MHKind,
@@ -40,7 +42,7 @@ pub struct WeeklySchedule {
4042
pub sun: MHKind,
4143
}
4244

43-
impl WeeklySchedule {
45+
impl LegacySchedule {
4446
pub fn all_closed() -> Self {
4547
Self {
4648
timezone: Default::default(),
@@ -76,7 +78,7 @@ impl WeeklySchedule {
7678
}
7779
}
7880

79-
impl FromStr for WeeklySchedule {
81+
impl FromStr for LegacySchedule {
8082
type Err = anyhow::Error;
8183
fn from_str(s: &str) -> Result<Self> {
8284
let mut split_by_commas = s.split(",");
@@ -235,9 +237,9 @@ mod tests {
235237
// Mon-Fri 9-5, inconsistent leading space on Tuesday, leading 0 on Friday (expected to be fine)
236238
let s = "Europe/Warsaw,9:00-17:00, 9:00-17:00,9:00-17:00,9:00-17:00,09:00-17:00,C,C";
237239

238-
let parsed: WeeklySchedule = s.parse()?;
240+
let parsed: LegacySchedule = s.parse()?;
239241

240-
let expected = WeeklySchedule {
242+
let expected = LegacySchedule {
241243
timezone: Tz::Europe__Warsaw,
242244
mon: MHKind::TimeRange(
243245
NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
@@ -273,7 +275,7 @@ mod tests {
273275
// Valid but missing a timezone
274276
let s = "O,C,O,C,O,C,O";
275277

276-
let parsing_result: Result<WeeklySchedule> = s.parse();
278+
let parsing_result: Result<LegacySchedule> = s.parse();
277279

278280
dbg!(&parsing_result);
279281
assert!(parsing_result.is_err());
@@ -284,7 +286,7 @@ mod tests {
284286
// One day short
285287
let s = "Asia/Hong_Kong,C,O,C,O,C,O";
286288

287-
let parsing_result: Result<WeeklySchedule> = s.parse();
289+
let parsing_result: Result<LegacySchedule> = s.parse();
288290

289291
dbg!(&parsing_result);
290292
assert!(parsing_result.is_err());
@@ -294,7 +296,7 @@ mod tests {
294296
fn test_parsing_gibberish_timezone_is_error() {
295297
// Pretty sure that one's extinct
296298
let s = "Pangea/New_Dino_City,O,O,O,O,O,O,O";
297-
let parsing_result: Result<WeeklySchedule> = s.parse();
299+
let parsing_result: Result<LegacySchedule> = s.parse();
298300

299301
dbg!(&parsing_result);
300302
assert!(parsing_result.is_err());
@@ -303,7 +305,7 @@ mod tests {
303305
#[test]
304306
fn test_parsing_gibberish_day_schedule_is_error() {
305307
let s = "Europe/Amsterdam,mondays are alright I guess,O,O,O,O,O,O";
306-
let parsing_result: Result<WeeklySchedule> = s.parse();
308+
let parsing_result: Result<LegacySchedule> = s.parse();
307309

308310
dbg!(&parsing_result);
309311
assert!(parsing_result.is_err());
@@ -313,7 +315,7 @@ mod tests {
313315
fn test_parsing_too_many_days_is_error() {
314316
// One day too many
315317
let s = "Europe/Lisbon,O,O,O,O,O,O,O,O,C";
316-
let parsing_result: Result<WeeklySchedule> = s.parse();
318+
let parsing_result: Result<LegacySchedule> = s.parse();
317319

318320
dbg!(&parsing_result);
319321
assert!(parsing_result.is_err());
@@ -322,7 +324,7 @@ mod tests {
322324
#[test]
323325
fn test_market_hours_happy_path() -> Result<()> {
324326
// Prepare a schedule of narrow ranges
325-
let wsched: WeeklySchedule = "America/New_York,00:00-1:00,1:00-2:00,2:00-3:00,3:00-4:00,4:00-5:00,5:00-6:00,6:00-7:00".parse()?;
327+
let wsched: LegacySchedule = "America/New_York,00:00-1:00,1:00-2:00,2:00-3:00,3:00-4:00,4:00-5:00,5:00-6:00,6:00-7:00".parse()?;
326328

327329
// Prepare UTC datetimes that fall before, within and after market hours
328330
let format = "%Y-%m-%d %H:%M";
@@ -379,7 +381,7 @@ mod tests {
379381
#[test]
380382
fn test_market_hours_midnight_00_24() -> Result<()> {
381383
// Prepare a schedule of midnight-neighboring ranges
382-
let wsched: WeeklySchedule =
384+
let wsched: LegacySchedule =
383385
"Europe/Amsterdam,23:00-24:00,00:00-01:00,O,C,C,C,C".parse()?;
384386

385387
let format = "%Y-%m-%d %H:%M";
@@ -433,8 +435,8 @@ mod tests {
433435
// CDT/CET 6h offset in use for 2 weeks, CDT/CEST 7h offset after)
434436
// * Autumn 2023: Oct29(EU)-Nov5(US) (clocks go back 1h,
435437
// CDT/CET 6h offset in use 1 week, CST/CET 7h offset after)
436-
let wsched_eu: WeeklySchedule = "Europe/Amsterdam,9:00-17:00,O,O,O,O,O,O".parse()?;
437-
let wsched_us: WeeklySchedule = "America/Chicago,2:00-10:00,O,O,O,O,O,O".parse()?;
438+
let wsched_eu: LegacySchedule = "Europe/Amsterdam,9:00-17:00,O,O,O,O,O,O".parse()?;
439+
let wsched_us: LegacySchedule = "America/Chicago,2:00-10:00,O,O,O,O,O,O".parse()?;
438440

439441
let format = "%Y-%m-%d %H:%M";
440442

0 commit comments

Comments
 (0)