1010
1111import os
1212from collections .abc import Callable , Mapping
13+ from datetime import datetime , timezone
1314from typing import Any
1415
1516import pandas as pd
2425 mask_account_id ,
2526)
2627from application .runtime_broker_adapters import build_runtime_broker_adapters
28+ from application .state_persistence import GcsStateStore , build_gcs_state_store_from_env
29+ from application .strategy_run_persistence import (
30+ build_strategy_run_state ,
31+ is_duplicate_live_run ,
32+ persist_strategy_run_state ,
33+ read_latest_strategy_run_state ,
34+ resolve_strategy_run_period ,
35+ )
2736from decision_mapper import map_strategy_decision_to_plan
2837from notifications .telegram import build_sender , build_translator , render_cycle_summary
2938from quant_platform_kit .common .runtime_inputs import (
3847LIMIT_BUY_PREMIUM = 1.005
3948
4049
50+ def _utcnow () -> datetime :
51+ return datetime .now (timezone .utc )
52+
53+
4154def get_project_id () -> str | None :
4255 return os .getenv ("GOOGLE_CLOUD_PROJECT" )
4356
@@ -137,11 +150,15 @@ def run_strategy_cycle(
137150 runtime_settings : PlatformRuntimeSettings | None = None ,
138151 credentials : FirstradeCredentials | None = None ,
139152 client_factory : Callable [..., FirstradeBrokerClient ] = FirstradeBrokerClient ,
153+ state_store : GcsStateStore | None = None ,
140154 notification_sender : Callable [[str ], None ] | None = None ,
141155 env_reader : Callable [[str , str | None ], str | None ] = os .getenv ,
142156) -> dict [str , Any ]:
157+ now = _utcnow ()
143158 settings = runtime_settings or load_platform_runtime_settings (project_id_resolver = get_project_id )
144159 resolved_credentials = credentials or FirstradeCredentials .from_env (env_reader )
160+ store = state_store or build_gcs_state_store_from_env (env_reader )
161+ persist_strategy_runs = bool (settings .persist_strategy_runs and store is not None )
145162 client = _connect_client (
146163 credentials = resolved_credentials ,
147164 live_trading_enabled = settings .live_trading_enabled ,
@@ -193,6 +210,70 @@ def run_strategy_cycle(
193210 plan ,
194211 threshold_usd = settings .safe_haven_cash_substitute_threshold_usd ,
195212 )
213+ run_period = resolve_strategy_run_period (
214+ now = now ,
215+ plan = plan ,
216+ evaluation_metadata = getattr (evaluation , "metadata" , None ),
217+ )
218+ masked_account = mask_account_id (account )
219+ existing_run = None
220+ if persist_strategy_runs and not settings .dry_run_only :
221+ existing_run = read_latest_strategy_run_state (
222+ store = store ,
223+ account = masked_account ,
224+ strategy_profile = strategy_runtime .profile ,
225+ run_period = run_period ,
226+ )
227+ if is_duplicate_live_run (existing_run ):
228+ return {
229+ "ok" : True ,
230+ "api_kind" : "unofficial-reverse-engineered" ,
231+ "account" : masked_account ,
232+ "strategy_profile" : strategy_runtime .profile ,
233+ "strategy_display_name" : strategy_runtime .display_name ,
234+ "dry_run_only" : settings .dry_run_only ,
235+ "live_trading_enabled" : settings .live_trading_enabled ,
236+ "session_reused" : bool (getattr (client , "session_reused" , False )),
237+ "strategy_run_period" : run_period ,
238+ "strategy_run_persisted" : False ,
239+ "idempotency_skipped" : True ,
240+ "existing_strategy_run_stage" : existing_run .get ("stage" ),
241+ "existing_strategy_run_as_of" : existing_run .get ("as_of" ),
242+ "submitted_orders" : [],
243+ "skipped_orders" : [
244+ {
245+ "reason" : "duplicate_live_strategy_run" ,
246+ "run_period" : run_period ,
247+ }
248+ ],
249+ "action_done" : False ,
250+ }
251+ strategy_run_persisted = False
252+ strategy_run_persistence_error = None
253+ if persist_strategy_runs :
254+ planned_state = build_strategy_run_state (
255+ stage = "ORDERS_PLANNED" ,
256+ account = masked_account ,
257+ strategy_profile = strategy_runtime .profile ,
258+ strategy_display_name = strategy_runtime .display_name ,
259+ run_period = run_period ,
260+ dry_run_only = settings .dry_run_only ,
261+ live_trading_enabled = settings .live_trading_enabled ,
262+ session_reused = bool (getattr (client , "session_reused" , False )),
263+ portfolio_snapshot = plan .get ("portfolio" , {}),
264+ evaluation_metadata = getattr (evaluation , "metadata" , None ),
265+ plan = plan ,
266+ now = now ,
267+ )
268+ try :
269+ strategy_run_persisted = persist_strategy_run_state (
270+ store = store ,
271+ state = planned_state ,
272+ now = now ,
273+ )
274+ except Exception as exc :
275+ strategy_run_persisted = False
276+ strategy_run_persistence_error = f"{ type (exc ).__name__ } : { exc } "
196277 execution_result = execute_value_target_plan (
197278 plan = plan ,
198279 market_data_port = market_data_port ,
@@ -212,13 +293,44 @@ def run_strategy_cycle(
212293 "dry_run_only" : settings .dry_run_only ,
213294 "live_trading_enabled" : settings .live_trading_enabled ,
214295 "session_reused" : bool (getattr (client , "session_reused" , False )),
296+ "strategy_run_period" : run_period ,
297+ "strategy_run_persisted" : strategy_run_persisted ,
215298 "portfolio" : plan .get ("portfolio" , {}),
216299 "allocation" : plan .get ("allocation" , {}),
217300 "execution" : plan .get ("execution" , {}),
218301 "submitted_orders" : list (execution_result .submitted_orders ),
219302 "skipped_orders" : list (execution_result .skipped_orders ),
220303 "action_done" : execution_result .action_done ,
221304 }
305+ if strategy_run_persistence_error :
306+ result ["strategy_run_persistence_error" ] = strategy_run_persistence_error
307+ if persist_strategy_runs :
308+ completed_state = build_strategy_run_state (
309+ stage = "DRY_RUN_COMPLETED" if settings .dry_run_only else "SUBMITTED" ,
310+ account = masked_account ,
311+ strategy_profile = strategy_runtime .profile ,
312+ strategy_display_name = strategy_runtime .display_name ,
313+ run_period = run_period ,
314+ dry_run_only = settings .dry_run_only ,
315+ live_trading_enabled = settings .live_trading_enabled ,
316+ session_reused = bool (getattr (client , "session_reused" , False )),
317+ portfolio_snapshot = plan .get ("portfolio" , {}),
318+ evaluation_metadata = getattr (evaluation , "metadata" , None ),
319+ plan = plan ,
320+ submitted_orders = list (execution_result .submitted_orders ),
321+ skipped_orders = list (execution_result .skipped_orders ),
322+ action_done = execution_result .action_done ,
323+ now = now ,
324+ )
325+ try :
326+ result ["strategy_run_persisted" ] = persist_strategy_run_state (
327+ store = store ,
328+ state = completed_state ,
329+ now = now ,
330+ )
331+ except Exception as exc :
332+ result ["strategy_run_persisted" ] = False
333+ result ["strategy_run_persistence_error" ] = f"{ type (exc ).__name__ } : { exc } "
222334 try :
223335 result ["notification_sent" ] = _publish_cycle_notification (
224336 result ,
0 commit comments