#![allow(unused)]
use anyhow::{Context, anyhow};
use chrono::{DateTime, FixedOffset, NaiveDateTime};
use log::warn;
use polars::{df, prelude::*};
use serde::{Deserialize, Serialize};
use std::cell::{Ref, RefCell};
use std::collections::{HashMap, HashSet};
use std::fmt;
use std::fs;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::str::FromStr;
use std::time::Instant;
use super::event::Event;
use super::operate::Operate;
use super::signal::{ANY, Signal};
#[cfg(feature = "python")]
use super::event::PyEvent;
#[cfg(feature = "python")]
use super::signal::PySignal;
#[cfg(feature = "python")]
use pyo3::exceptions::PyValueError;
#[cfg(feature = "python")]
use pyo3::prelude::*;
#[cfg(feature = "python")]
use pyo3::types::PyBytes;
fn parse_operate(s: &str) -> Result<Operate, String> {
if let Ok(op) = Operate::from_str(s) {
return Ok(op);
}
match s {
"持多" => Ok(Operate::HL),
"持空" => Ok(Operate::HS),
"持币" => Ok(Operate::HO),
"开多" => Ok(Operate::LO),
"平多" => Ok(Operate::LE),
"开空" => Ok(Operate::SO),
"平空" => Ok(Operate::SE),
_ => Err(format!("未知的operate值: {s}")),
}
}
#[cfg(feature = "python")]
use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods};
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum Pos {
Short,
#[default]
Flat,
Long,
}
impl fmt::Display for Pos {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Pos::Short => "空",
Pos::Flat => "空仓",
Pos::Long => "多",
};
write!(f, "{s}")
}
}
impl Pos {
pub fn to_f64(self) -> f64 {
match self {
Pos::Short => -1.0,
Pos::Flat => 0.0,
Pos::Long => 1.0,
}
}
pub fn from_f64(value: f64) -> Self {
if value > 0.5 {
Pos::Long
} else if value < -0.5 {
Pos::Short
} else {
Pos::Flat
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct LiteBar {
pub id: i32,
pub dt: DateTime<FixedOffset>,
pub price: f64,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct PositionUpdateProfile {
pub event_match_ns: u128,
pub fsm_ns: u128,
pub risk_ns: u128,
pub holds_ns: u128,
}
const ANY_CODE: i32 = -1;
const UNKNOWN_CODE: i32 = 0;
#[derive(Debug, Clone, Copy)]
struct EncodedSignalValue {
v1_code: i32,
v2_code: i32,
v3_code: i32,
score: i32,
}
#[derive(Debug, Clone)]
struct CachedEncodedSignalValue {
raw: String,
encoded: EncodedSignalValue,
}
#[derive(Debug, Clone, Copy)]
struct EncodedSignalClause {
key_id: usize,
v1_code: i32,
v2_code: i32,
v3_code: i32,
min_score: i32,
}
#[derive(Debug, Clone)]
struct CompiledEventMatcher {
signals_all: Vec<EncodedSignalClause>,
signals_any: Vec<EncodedSignalClause>,
signals_not: Vec<EncodedSignalClause>,
}
#[derive(Debug, Clone, Default)]
struct PositionEventMatcher {
keys: Vec<String>,
key_to_id: HashMap<String, usize>,
value_to_code: HashMap<String, i32>,
events: Vec<CompiledEventMatcher>,
}
impl PositionEventMatcher {
fn key_id(&mut self, key: String) -> usize {
if let Some(id) = self.key_to_id.get(key.as_str()) {
return *id;
}
let id = self.keys.len();
self.keys.push(key.clone());
self.key_to_id.insert(key, id);
id
}
fn value_code(&mut self, v: &str) -> i32 {
if v == ANY {
return ANY_CODE;
}
if let Some(code) = self.value_to_code.get(v) {
return *code;
}
let code = self.value_to_code.len() as i32 + 1;
self.value_to_code.insert(v.to_string(), code);
code
}
fn parse_v123_score(v: &str) -> Option<(&str, &str, &str, i32)> {
let mut it = v.splitn(4, '_');
let v1 = it.next()?;
let v2 = it.next()?;
let v3 = it.next()?;
let score = it.next()?.parse::<i32>().ok()?;
Some((v1, v2, v3, score))
}
fn compile_signal_clause(&mut self, signal: &Signal) -> Option<EncodedSignalClause> {
let key_id = self.key_id(signal.key());
let value = signal.value();
let (v1, v2, v3, score) = Self::parse_v123_score(value.as_str())?;
Some(EncodedSignalClause {
key_id,
v1_code: self.value_code(v1),
v2_code: self.value_code(v2),
v3_code: self.value_code(v3),
min_score: score,
})
}
fn compile_from_position(position: &Position) -> Self {
let mut matcher = Self::default();
matcher
.events
.reserve(position.opens.len() + position.exits.len());
for e in position.opens.iter().chain(position.exits.iter()) {
let mut all = Vec::with_capacity(e.signals_all.len());
let mut any = Vec::with_capacity(e.signals_any.len());
let mut not = Vec::with_capacity(e.signals_not.len());
for s in &e.signals_all {
if let Some(c) = matcher.compile_signal_clause(s) {
all.push(c);
}
}
for s in &e.signals_any {
if let Some(c) = matcher.compile_signal_clause(s) {
any.push(c);
}
}
for s in &e.signals_not {
if let Some(c) = matcher.compile_signal_clause(s) {
not.push(c);
}
}
matcher.events.push(CompiledEventMatcher {
signals_all: all,
signals_any: any,
signals_not: not,
});
}
matcher
}
#[inline]
fn encode_runtime_values_inplace(
&self,
signal_map: &HashMap<String, String>,
values: &mut [Option<EncodedSignalValue>],
cache: &mut [Option<CachedEncodedSignalValue>],
) {
for v in values.iter_mut() {
*v = None;
}
for (key_id, key) in self.keys.iter().enumerate() {
if let Some(raw) = signal_map.get(key.as_str()) {
if let Some(Some(cached)) = cache.get(key_id)
&& cached.raw == *raw
{
values[key_id] = Some(cached.encoded);
continue;
}
if let Some((v1, v2, v3, score)) = Self::parse_v123_score(raw) {
let v1_code = self.value_to_code.get(v1).copied().unwrap_or(UNKNOWN_CODE);
let v2_code = self.value_to_code.get(v2).copied().unwrap_or(UNKNOWN_CODE);
let v3_code = self.value_to_code.get(v3).copied().unwrap_or(UNKNOWN_CODE);
let encoded = EncodedSignalValue {
v1_code,
v2_code,
v3_code,
score,
};
values[key_id] = Some(encoded);
if let Some(slot) = cache.get_mut(key_id) {
*slot = Some(CachedEncodedSignalValue {
raw: raw.clone(),
encoded,
});
}
} else if let Some(slot) = cache.get_mut(key_id) {
*slot = None;
}
}
}
}
#[inline]
fn clause_match(values: &[Option<EncodedSignalValue>], c: EncodedSignalClause) -> bool {
if let Some(value) = values.get(c.key_id).and_then(|v| *v) {
value.score >= c.min_score
&& (c.v1_code == ANY_CODE || c.v1_code == value.v1_code)
&& (c.v2_code == ANY_CODE || c.v2_code == value.v2_code)
&& (c.v3_code == ANY_CODE || c.v3_code == value.v3_code)
} else {
false
}
}
fn find_first_match(
&self,
signal_map: &HashMap<String, String>,
values: &mut [Option<EncodedSignalValue>],
cache: &mut [Option<CachedEncodedSignalValue>],
) -> Option<usize> {
self.encode_runtime_values_inplace(signal_map, values, cache);
for (idx, evt) in self.events.iter().enumerate() {
if evt
.signals_not
.iter()
.any(|c| Self::clause_match(values, *c))
{
continue;
}
if evt
.signals_all
.iter()
.any(|c| !Self::clause_match(values, *c))
{
continue;
}
if !evt.signals_any.is_empty()
&& !evt
.signals_any
.iter()
.any(|c| Self::clause_match(values, *c))
{
continue;
}
return Some(idx);
}
None
}
}
#[derive(Debug, Clone)]
pub struct TempState {
pub end_dt: DateTime<FixedOffset>,
pub last_lo_dt: Option<DateTime<FixedOffset>>,
pub last_so_dt: Option<DateTime<FixedOffset>>,
}
#[allow(unused)]
#[derive(Debug, Clone)]
pub struct OperateRecord {
pub symbol: String,
pub dt: DateTime<FixedOffset>,
pub bar_id: i32,
pub price: f64,
pub op: Operate,
pub op_desc: Option<String>,
pub pos: Pos,
}
#[derive(Debug, Clone)]
pub struct HoldRecord {
pub dt: DateTime<FixedOffset>,
pub pos: Pos,
pub price: f64,
pub n1b: Option<f64>, }
#[derive(Default)]
pub struct HoldColumns {
pub dt: Vec<NaiveDateTime>, pub pos: Vec<i32>,
pub price: Vec<f64>,
pub n1b: Vec<Option<f64>>, }
impl HoldColumns {
pub fn from_records(records: Vec<HoldRecord>) -> Self {
let mut cols = HoldColumns::default();
cols.dt.reserve(records.len());
cols.pos.reserve(records.len());
cols.price.reserve(records.len());
cols.n1b.reserve(records.len());
for r in records {
cols.dt.push(r.dt.naive_local());
cols.pos.push(r.pos.to_f64() as i32);
cols.price.push(r.price);
cols.n1b.push(r.n1b);
}
cols
}
pub fn into_df(self) -> anyhow::Result<DataFrame> {
let df = df![
"dt" => self.dt,
"pos" => self.pos,
"price" => self.price,
"n1b" => self.n1b,
]
.context("创建 Hold DataFrame 失败")?;
Ok(df)
}
}
#[allow(unused)]
#[derive(Debug, Clone)]
pub struct LastEvent {
pub dt: DateTime<FixedOffset>,
pub bar_id: i32,
pub price: f64,
pub op: Operate,
pub op_desc: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct TradePairsColumns<'a> {
pub symbol: Vec<&'a str>,
pub strategy_mark: Vec<&'a str>,
pub direction: Vec<&'a str>,
pub open_dt: Vec<NaiveDateTime>,
pub close_dt: Vec<NaiveDateTime>,
pub open_price: Vec<f64>,
pub close_price: Vec<f64>,
pub holding_bar: Vec<i32>,
pub event_sequence: Vec<String>,
pub holding_day: Vec<f64>,
pub yield_profit_ratio: Vec<Option<f64>>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Position {
pub opens: Vec<Event>,
pub exits: Vec<Event>,
pub interval: i64,
pub timeout: i32,
pub stop_loss: f64,
#[serde(rename = "T0")]
pub t0: bool,
pub name: String,
pub symbol: String,
#[serde(skip)]
temp_state: Option<TempState>,
#[serde(skip)]
pos_changed: bool,
#[serde(skip)]
pub operates: Vec<OperateRecord>,
#[serde(skip)]
holds: Vec<HoldRecord>,
#[serde(skip)]
pos: Pos,
#[serde(skip)]
last_event: Option<LastEvent>, #[serde(skip)]
event_matcher: Option<PositionEventMatcher>,
#[serde(skip)]
event_match_values: Vec<Option<EncodedSignalValue>>,
#[serde(skip)]
event_match_cache: Vec<Option<CachedEncodedSignalValue>>,
}
pub fn load_position(path: &Path) -> anyhow::Result<Position> {
let content = fs::read_to_string(path).with_context(|| format!("读取文件失败: {path:?}"))?;
let mut position: Position =
serde_json::from_str(&content).with_context(|| format!("解析 JSON 失败: {path:?}"))?;
position.init_runtime_fields();
Ok(position)
}
impl Position {
fn init_runtime_fields(&mut self) {
self.normalize_event_hash_names();
self.event_matcher = None;
self.event_match_values.clear();
self.event_match_cache.clear();
}
pub fn normalize_runtime_fields(&mut self) {
self.init_runtime_fields();
}
pub fn get_pos(&self) -> Pos {
self.pos
}
pub fn get_pos_changed(&self) -> bool {
self.pos_changed
}
fn normalize_event_hash_names(&mut self) {
for event in self.opens.iter_mut().chain(self.exits.iter_mut()) {
event.refresh_hash_name();
}
}
fn ensure_event_matcher(&mut self) {
if self.event_matcher.is_none() {
let matcher = PositionEventMatcher::compile_from_position(self);
self.event_match_values = vec![None; matcher.keys.len()];
self.event_match_cache = vec![None; matcher.keys.len()];
self.event_matcher = Some(matcher);
}
}
fn event_tag_from_desc(desc: &str) -> &'static str {
if desc.starts_with("开多#") || desc.starts_with("LO#") {
"LO"
} else if desc.starts_with("开空#") || desc.starts_with("SO#") {
"SO"
} else if desc.starts_with("平多#") || desc.starts_with("LE#") {
"LE"
} else if desc.starts_with("平空#") || desc.starts_with("SE#") {
"SE"
} else {
"is_match"
}
}
fn format_event_desc_for_python(desc: &str) -> String {
if desc.contains('@') {
desc.to_string()
} else {
format!("{desc}@{}", Self::event_tag_from_desc(desc))
}
}
pub fn save(&self, path: &Path) -> anyhow::Result<()> {
let content = serde_json::to_string_pretty(self)
.with_context(|| format!("序列化 Position 失败: {path:?}"))?;
fs::write(path, content).with_context(|| format!("写入文件失败: {path:?}"))?;
Ok(())
}
pub fn with_event_hash_name(mut self, mode: &str, event_hash: &str) -> Self {
if let Some(pos) = self.name.find('#') {
self.name.truncate(pos);
}
self.name.push('#');
self.name.push_str(mode);
self.name.push_str(event_hash);
self
}
pub fn compute_md5_name(mut self) -> Self {
let mut context = md5::Context::new();
context.consume(format!("{:?}", self.opens));
context.consume(format!("{:?}", self.exits));
context.consume(format!("{:?}", self.interval));
context.consume(format!("{:?}", self.timeout));
context.consume(format!("{:?}", self.stop_loss));
context.consume(format!("{:?}", self.t0));
context.consume(format!("{:?}", self.symbol));
let digest = context.finalize();
let digest = hex::encode(*digest)[..8].to_uppercase();
if let Some(pos) = self.name.find('#') {
self.name.truncate(pos);
}
self.name.push('#');
self.name.push_str(&digest);
self
}
pub fn all_events(&self) -> impl Iterator<Item = &Event> {
self.opens.iter().chain(self.exits.iter())
}
fn create_operate_record(
&mut self,
dt: DateTime<FixedOffset>,
bar_id: i32,
price: f64,
op: Operate,
op_desc: Option<String>,
) -> OperateRecord {
self.pos_changed = true;
OperateRecord {
symbol: self.symbol.to_string(),
dt,
bar_id,
price,
op,
op_desc,
pos: self.pos,
}
}
fn is_different_day(dt: DateTime<FixedOffset>, other: Option<DateTime<FixedOffset>>) -> bool {
match other {
Some(o) => dt.date_naive() != o.date_naive(),
None => true,
}
}
pub fn update(&mut self, last_bar: LiteBar, last_signals: Rc<RefCell<HashSet<Signal>>>) {
let _ = self.update_profiled(last_bar, last_signals);
}
pub fn update_profiled(
&mut self,
last_bar: LiteBar,
last_signals: Rc<RefCell<HashSet<Signal>>>,
) -> PositionUpdateProfile {
self.update_profiled_with_signal_map(last_bar, Some(last_signals), None)
}
pub fn update_profiled_with_signal_map(
&mut self,
last_bar: LiteBar,
last_signals: Option<Rc<RefCell<HashSet<Signal>>>>,
signal_map: Option<&HashMap<String, String>>,
) -> PositionUpdateProfile {
if let Some(ref temp_state) = self.temp_state {
if temp_state.end_dt >= last_bar.dt {
warn!(
"请检查信号传入:最新信号时间: {} 在上次信号时间 {} 之前",
last_bar.dt, temp_state.end_dt
);
return PositionUpdateProfile::default();
}
} else {
self.temp_state = Some(TempState {
end_dt: last_bar.dt,
last_lo_dt: None,
last_so_dt: None,
});
}
let dt = last_bar.dt.fixed_offset();
let price = last_bar.price;
let bar_id = last_bar.id;
self.pos_changed = false;
let mut op = Operate::HO;
let mut op_desc: Option<String> = None;
let t_event = Instant::now();
let owned_signal_map;
let signal_map = if let Some(m) = signal_map {
m
} else if let Some(last_signals) = last_signals {
owned_signal_map = {
let signals = last_signals.borrow();
let mut m = HashMap::with_capacity(signals.len());
for s in signals.iter() {
m.insert(s.key(), s.value());
}
m
};
&owned_signal_map
} else {
return PositionUpdateProfile::default();
};
self.ensure_event_matcher();
if let Some(matcher) = self.event_matcher.as_ref()
&& let Some(event_idx) = matcher.find_first_match(
signal_map,
&mut self.event_match_values,
&mut self.event_match_cache,
)
{
let e = if event_idx < self.opens.len() {
&self.opens[event_idx]
} else {
&self.exits[event_idx - self.opens.len()]
};
op = e.operate;
op_desc = Some(e.name.to_string());
}
let event_match_ns = t_event.elapsed().as_nanos();
let t_fsm = Instant::now();
if let Some(ref mut ts) = self.temp_state {
ts.end_dt = dt;
}
if op == Operate::LO || op == Operate::SO {
self.last_event = Some(LastEvent {
dt,
bar_id,
price,
op,
op_desc: op_desc.clone(),
});
}
if op == Operate::LO {
let allow_open_long = match self.temp_state.as_ref().and_then(|t| t.last_lo_dt) {
None => true,
Some(prev_dt) => {
let interval_secs = (dt - prev_dt).num_seconds();
interval_secs > self.interval
}
};
if self.pos != Pos::Long && allow_open_long {
self.pos = Pos::Long;
if let Some(ref mut ts) = self.temp_state {
ts.last_lo_dt = Some(dt);
}
let rec =
self.create_operate_record(dt, bar_id, price, Operate::LO, op_desc.clone());
self.operates.push(rec);
} else {
let can_close_short = self.pos == Pos::Short
&& (self.t0
|| Self::is_different_day(
dt,
self.temp_state.as_ref().and_then(|t| t.last_so_dt),
));
if can_close_short {
self.pos = Pos::Flat;
let rec =
self.create_operate_record(dt, bar_id, price, Operate::SE, op_desc.clone());
self.operates.push(rec);
}
}
}
if op == Operate::SO {
let allow_open_short = match self.temp_state.as_ref().and_then(|t| t.last_so_dt) {
None => true,
Some(prev_dt) => (dt - prev_dt).num_seconds() > self.interval,
};
if self.pos != Pos::Short && allow_open_short {
self.pos = Pos::Short;
if let Some(ref mut ts) = self.temp_state {
ts.last_so_dt = Some(dt);
}
let rec =
self.create_operate_record(dt, bar_id, price, Operate::SO, op_desc.clone());
self.operates.push(rec);
} else {
let can_close_long = self.pos == Pos::Long
&& (self.t0
|| Self::is_different_day(
dt,
self.temp_state.as_ref().and_then(|t| t.last_lo_dt),
));
if can_close_long {
self.pos = Pos::Flat;
let rec =
self.create_operate_record(dt, bar_id, price, Operate::LE, op_desc.clone());
self.operates.push(rec);
}
}
}
let fsm_ns = t_fsm.elapsed().as_nanos();
let t_risk = Instant::now();
if self.pos == Pos::Long {
let allowed_by_t0 = if let Some(ref ts) = self.temp_state {
self.t0 || Self::is_different_day(dt, ts.last_lo_dt)
} else {
true
};
let last_ev_snapshot = self.last_event.as_ref().map(|e| (e.price, e.bar_id));
if allowed_by_t0 && let Some((ev_price, ev_bar_id)) = last_ev_snapshot {
let exit_desc = if op == Operate::LE {
Some(op_desc.clone())
} else if price / ev_price - 1.0 < -self.stop_loss / 10000.0 {
Some(Some(format!("平多@{}BP止损", self.stop_loss)))
} else if bar_id - ev_bar_id > self.timeout {
Some(Some(format!("平多@{}K超时", self.timeout)))
} else {
None
};
if let Some(exit_desc) = exit_desc {
self.pos = Pos::Flat;
let rec = self.create_operate_record(dt, bar_id, price, Operate::LE, exit_desc);
self.operates.push(rec);
}
}
}
if self.pos == Pos::Short {
let allowed_by_t0 = if let Some(ref ts) = self.temp_state {
self.t0 || Self::is_different_day(dt, ts.last_so_dt)
} else {
true
};
let last_ev_snapshot = self.last_event.as_ref().map(|e| (e.price, e.bar_id));
if allowed_by_t0 && let Some((ev_price, ev_bar_id)) = last_ev_snapshot {
let exit_desc = if op == Operate::SE {
Some(op_desc.clone())
} else if 1.0 - price / ev_price < -self.stop_loss / 10000.0 {
Some(Some(format!("平空@{}BP止损", self.stop_loss)))
} else if bar_id - ev_bar_id > self.timeout {
Some(Some(format!("平空@{}K超时", self.timeout)))
} else {
None
};
if let Some(exit_desc) = exit_desc {
self.pos = Pos::Flat;
let rec = self.create_operate_record(dt, bar_id, price, Operate::SE, exit_desc);
self.operates.push(rec);
}
}
}
let risk_ns = t_risk.elapsed().as_nanos();
let t_holds = Instant::now();
if let Some(last_hold) = self.holds.last_mut()
&& last_hold.price > 0.0
{
let n1b_value = (price / last_hold.price - 1.0) * 10000.0;
last_hold.n1b = Some(n1b_value);
}
self.holds.push(HoldRecord {
dt,
pos: self.pos,
price,
n1b: None,
});
let holds_ns = t_holds.elapsed().as_nanos();
PositionUpdateProfile {
event_match_ns,
fsm_ns,
risk_ns,
holds_ns,
}
}
pub fn pairs(&self) -> anyhow::Result<DataFrame> {
let mut trade_pairs = TradePairsColumns::default();
for (op1, op2) in self.operates.iter().zip(self.operates.iter().skip(1)) {
if op1.op != Operate::LO && op1.op != Operate::SO {
continue;
}
let yield_profit_ratio = if op1.price == 0.0 {
None
} else if op1.op == Operate::LO {
Some(op2.price / op1.price - 1.0)
} else {
Some(1.0 - op2.price / op1.price)
};
trade_pairs.symbol.push(&self.symbol);
trade_pairs.strategy_mark.push(&self.name);
trade_pairs.direction.push(if op1.op == Operate::LO {
"多头"
} else {
"空头"
});
trade_pairs.open_dt.push(op1.dt.naive_local());
trade_pairs.close_dt.push(op2.dt.naive_local());
trade_pairs.open_price.push(op1.price);
trade_pairs.close_price.push(op2.price);
trade_pairs.holding_bar.push(op2.bar_id - op1.bar_id);
trade_pairs
.event_sequence
.push(match (&op1.op_desc, &op2.op_desc) {
(None, None) => OP_DESC_NONE.to_string(),
(None, Some(desc)) => Self::format_event_desc_for_python(desc),
(Some(desc), None) => Self::format_event_desc_for_python(desc),
(Some(desc1), Some(desc2)) => format!(
"{} -> {}",
Self::format_event_desc_for_python(desc1),
Self::format_event_desc_for_python(desc2)
),
});
let holding_seconds = (op2.dt - op1.dt).num_seconds() as f64;
let holding_days = holding_seconds / 86400.0; trade_pairs.holding_day.push(holding_days);
let yield_profit_ratio_bp =
yield_profit_ratio.map(|r| (r * 10000.0 * 100.0).round() / 100.0);
trade_pairs.yield_profit_ratio.push(yield_profit_ratio_bp);
}
let df = df![
"标的代码" => trade_pairs.symbol,
"策略标记" => trade_pairs.strategy_mark,
"交易方向" => trade_pairs.direction,
"开仓时间" => trade_pairs.open_dt,
"平仓时间" => trade_pairs.close_dt,
"开仓价格" => trade_pairs.open_price,
"平仓价格" => trade_pairs.close_price,
"持仓K线数" => trade_pairs.holding_bar,
"事件序列" => trade_pairs.event_sequence,
"持仓天数" => trade_pairs.holding_day,
"盈亏比例" => trade_pairs.yield_profit_ratio,
]
.context("创建 Polars df 失败")?;
Ok(df)
}
pub fn holds(&self) -> anyhow::Result<DataFrame> {
let holds = self.holds.clone();
let holds = HoldColumns::from_records(holds);
let df = holds
.into_df()?
.lazy()
.with_columns([
lit(self.symbol.to_string()).alias("symbol"),
])
.collect()
.context("新增列 symbol 失败")?;
Ok(df)
}
}
const OP_DESC_NONE: &str = "无";
#[cfg_attr(feature = "python", gen_stub_pyclass)]
#[cfg_attr(feature = "python", pyclass(name = "Pos", module = "czsc._native"))]
#[derive(Debug, Clone)]
pub struct PyPos {
pub inner: Pos,
}
#[cfg(feature = "python")]
#[cfg_attr(feature = "python", gen_stub_pymethods)]
#[cfg_attr(feature = "python", pymethods)]
impl PyPos {
#[classmethod]
fn short(_cls: &Bound<'_, pyo3::types::PyType>) -> Self {
Self { inner: Pos::Short }
}
#[classmethod]
fn flat(_cls: &Bound<'_, pyo3::types::PyType>) -> Self {
Self { inner: Pos::Flat }
}
#[classmethod]
fn long(_cls: &Bound<'_, pyo3::types::PyType>) -> Self {
Self { inner: Pos::Long }
}
fn __str__(&self) -> String {
self.inner.to_string()
}
fn __repr__(&self) -> String {
format!("PyPos::{:?}", self.inner)
}
fn __eq__(&self, other: &Self) -> bool {
self.inner == other.inner
}
fn __add__(&self, other: &Self) -> f64 {
self.inner.to_f64() + other.inner.to_f64()
}
fn __radd__(&self, other: f64) -> f64 {
other + self.inner.to_f64()
}
fn __float__(&self) -> f64 {
self.inner.to_f64()
}
fn __int__(&self) -> i32 {
self.inner.to_f64() as i32
}
fn __lt__(&self, other: &Self) -> bool {
self.inner.to_f64() < other.inner.to_f64()
}
fn __le__(&self, other: &Self) -> bool {
self.inner.to_f64() <= other.inner.to_f64()
}
fn __gt__(&self, other: &Self) -> bool {
self.inner.to_f64() > other.inner.to_f64()
}
fn __ge__(&self, other: &Self) -> bool {
self.inner.to_f64() >= other.inner.to_f64()
}
}
#[cfg_attr(feature = "python", gen_stub_pyclass)]
#[cfg_attr(feature = "python", pyclass(name = "LiteBar", module = "czsc._native"))]
#[derive(Debug, Clone)]
pub struct PyLiteBar {
pub inner: LiteBar,
}
#[cfg(feature = "python")]
#[cfg_attr(feature = "python", gen_stub_pymethods)]
#[cfg_attr(feature = "python", pymethods)]
impl PyLiteBar {
#[new]
fn new_py(id: i32, dt: f64, price: f64) -> PyResult<Self> {
use chrono::{DateTime, FixedOffset, TimeZone, Utc};
let dt_utc = DateTime::from_timestamp(dt as i64, 0)
.ok_or_else(|| PyValueError::new_err("无效的时间戳"))?;
let dt = dt_utc.with_timezone(&FixedOffset::east_opt(0).unwrap());
Ok(Self {
inner: LiteBar { id, dt, price },
})
}
#[getter]
fn id(&self) -> i32 {
self.inner.id
}
#[getter]
fn dt(&self) -> f64 {
self.inner.dt.timestamp() as f64
}
#[getter]
fn price(&self) -> f64 {
self.inner.price
}
fn __repr__(&self) -> String {
format!(
"PyLiteBar(id={}, dt={}, price={})",
self.inner.id,
self.inner.dt.timestamp(),
self.inner.price
)
}
}
#[cfg_attr(feature = "python", gen_stub_pyclass)]
#[cfg_attr(
feature = "python",
pyclass(name = "Position", module = "czsc._native")
)]
#[derive(Debug, Clone)]
pub struct PyPosition {
pub inner: Position,
}
#[cfg(feature = "python")]
#[cfg_attr(feature = "python", gen_stub_pymethods)]
#[cfg_attr(feature = "python", pymethods)]
impl PyPosition {
#[new]
#[pyo3(signature = (symbol, opens, exits = vec![], interval = 0, timeout = 1000, stop_loss = 1000.0, t0 = false, name = None))]
#[allow(clippy::too_many_arguments)]
fn new_py(
symbol: String,
opens: Vec<PyEvent>,
exits: Vec<PyEvent>,
interval: i64,
timeout: i32,
stop_loss: f64,
t0: bool,
name: Option<String>,
) -> Self {
let opens: Vec<Event> = opens.into_iter().map(|e| e.inner).collect();
let exits: Vec<Event> = exits.into_iter().map(|e| e.inner).collect();
let name = name.unwrap_or_else(|| {
if !opens.is_empty() {
opens[0].name.clone()
} else {
"DefaultPosition".to_string()
}
});
let inner = Position {
opens,
exits,
interval,
timeout,
stop_loss,
t0,
name,
symbol,
temp_state: None,
pos_changed: false,
operates: Vec::new(),
holds: Vec::new(),
pos: Pos::Flat,
last_event: None,
event_matcher: None,
event_match_values: Vec::new(),
event_match_cache: Vec::new(),
};
let mut inner = inner;
inner.init_runtime_fields();
Self { inner }
}
#[classmethod]
fn load_from_file(_cls: &Bound<'_, pyo3::types::PyType>, path: String) -> PyResult<Self> {
let position = load_position(Path::new(&path))
.map_err(|e| PyValueError::new_err(format!("加载Position失败: {e}")))?;
Ok(Self { inner: position })
}
#[classmethod]
fn from_json(_cls: &Bound<'_, pyo3::types::PyType>, json_str: String) -> PyResult<Self> {
let mut inner: Position = serde_json::from_str(&json_str)
.map_err(|e| PyValueError::new_err(format!("JSON解析失败: {e}")))?;
inner.init_runtime_fields();
Ok(Self { inner })
}
#[getter]
fn opens(&self) -> Vec<PyEvent> {
self.inner
.opens
.iter()
.map(|e| PyEvent { inner: e.clone() })
.collect()
}
#[getter]
fn exits(&self) -> Vec<PyEvent> {
self.inner
.exits
.iter()
.map(|e| PyEvent { inner: e.clone() })
.collect()
}
#[getter]
fn interval(&self) -> i64 {
self.inner.interval
}
#[getter]
fn timeout(&self) -> i32 {
self.inner.timeout
}
#[getter]
fn stop_loss(&self) -> f64 {
self.inner.stop_loss
}
#[getter]
fn t0(&self) -> bool {
self.inner.t0
}
#[getter]
fn name(&self) -> String {
self.inner.name.clone()
}
#[getter]
fn symbol(&self) -> String {
self.inner.symbol.clone()
}
#[getter]
fn pos(&self) -> f64 {
self.inner.pos.to_f64()
}
#[getter]
fn pos_changed(&self) -> bool {
self.inner.pos_changed
}
#[getter]
fn end_dt(&self) -> Option<f64> {
self.inner
.temp_state
.as_ref()
.map(|ts| ts.end_dt.timestamp() as f64)
}
#[getter]
fn operates(&self, py: Python) -> PyResult<Vec<PyObject>> {
let mut result = Vec::new();
for op_record in &self.inner.operates {
let dict = pyo3::types::PyDict::new(py);
let timestamp = op_record.dt.timestamp() as f64;
dict.set_item("dt", timestamp)?;
dict.set_item("symbol", &op_record.symbol)?;
dict.set_item("bar_id", op_record.bar_id)?;
dict.set_item("price", op_record.price)?;
dict.set_item("op", op_record.op.to_string())?;
dict.set_item("op_desc", &op_record.op_desc)?;
dict.set_item("pos", op_record.pos.to_string())?;
result.push(dict.into());
}
Ok(result)
}
fn save(&self, path: String) -> PyResult<()> {
self.inner
.save(Path::new(&path))
.map_err(|e| PyValueError::new_err(format!("保存Position失败: {e}")))
}
fn to_json(&self) -> PyResult<String> {
serde_json::to_string_pretty(&self.inner)
.map_err(|e| PyValueError::new_err(format!("JSON序列化失败: {e}")))
}
fn all_events(&self) -> Vec<PyEvent> {
self.inner
.all_events()
.map(|e| PyEvent { inner: e.clone() })
.collect()
}
#[pyo3(signature = (arg1, arg2 = None))]
fn update(&mut self, arg1: PyObject, arg2: Option<PyObject>) -> PyResult<()> {
use pyo3::types::{PyDict, PyMapping};
use std::collections::HashSet;
Python::with_gil(|py| {
if let Some(arg2_val) = arg2 {
let last_bar: PyLiteBar = arg1.extract(py)?;
let last_signals: Vec<PySignal> = arg2_val.extract(py)?;
let signals: HashSet<Signal> = last_signals.into_iter().map(|s| s.inner).collect();
let signals_ref = Rc::new(RefCell::new(signals));
self.inner.update(last_bar.inner, signals_ref);
} else {
if let Ok(signals_dict) = arg1.downcast_bound::<PyDict>(py) {
let dt_obj = signals_dict.get_item("dt")?.ok_or_else(|| {
PyValueError::new_err("Missing 'dt' field in signals dict")
})?;
let id_obj = signals_dict.get_item("id")?.ok_or_else(|| {
PyValueError::new_err("Missing 'id' field in signals dict")
})?;
let close_obj = signals_dict.get_item("close")?.ok_or_else(|| {
PyValueError::new_err("Missing 'close' field in signals dict")
})?;
use chrono::{DateTime, FixedOffset, Utc};
let dt: DateTime<FixedOffset> = if let Ok(timestamp) = dt_obj.extract::<f64>() {
DateTime::from_timestamp(timestamp as i64, 0)
.ok_or_else(|| PyValueError::new_err("Invalid timestamp"))?
.with_timezone(&FixedOffset::east_opt(0).unwrap())
} else {
if let Ok(timestamp_method) = dt_obj.getattr("timestamp") {
let timestamp: f64 = timestamp_method.call0()?.extract()?;
DateTime::from_timestamp(timestamp as i64, 0)
.ok_or_else(|| {
PyValueError::new_err("Invalid timestamp from pandas")
})?
.with_timezone(&FixedOffset::east_opt(0).unwrap())
} else {
return Err(PyValueError::new_err("Cannot convert 'dt' to timestamp"));
}
};
let bar_id: i32 = id_obj.extract()?;
let price: f64 = close_obj.extract()?;
let lite_bar = LiteBar {
id: bar_id,
dt,
price,
};
let mut signal_set = HashSet::new();
for (key, value) in signals_dict.iter() {
let key_str = if let Ok(s) = key.extract::<String>() {
s
} else {
key.str()?.extract::<String>()?
};
if key_str == "symbol"
|| key_str == "dt"
|| key_str == "id"
|| key_str == "close"
|| key_str == "open"
|| key_str == "high"
|| key_str == "low"
|| key_str == "vol"
|| key_str == "amount"
|| key_str == "freq"
{
continue;
}
let value_str = if let Ok(s) = value.extract::<String>() {
s
} else {
value.str()?.extract::<String>()?
};
let signal_str = if value_str.split('_').count() == 4 {
format!("{key_str}_{value_str}")
} else {
format!("{key_str}_{value_str}_任意_任意_0")
};
if let Ok(signal) = Signal::from_str(&signal_str) {
signal_set.insert(signal);
}
}
let signals_ref = Rc::new(RefCell::new(signal_set));
self.inner.update(lite_bar, signals_ref);
} else if let Ok(signals_vec) = arg1.extract::<Vec<PySignal>>(py) {
let signal_set: HashSet<Signal> =
signals_vec.into_iter().map(|s| s.inner).collect();
let signals_ref = Rc::new(RefCell::new(signal_set));
use chrono::{DateTime, FixedOffset, Utc};
let dummy_bar = LiteBar {
id: 0,
dt: Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap()),
price: 0.0,
};
self.inner.update(dummy_bar, signals_ref);
} else {
return Err(PyValueError::new_err(
"Expected dict with 'dt', 'id', 'close' fields, or Vec<Signal>",
));
}
}
Ok(())
})
}
#[getter]
fn pairs(&self) -> PyResult<Py<pyo3::types::PyList>> {
let df = self
.inner
.pairs()
.map_err(|e| PyValueError::new_err(format!("生成交易对数据失败: {e}")))?;
Python::with_gil(|py| {
let list = pyo3::types::PyList::empty(py);
let height = df.height();
for i in 0..height {
let record = pyo3::types::PyDict::new(py);
if let Ok(symbol_col) = df.column("标的代码")
&& let Ok(value) = symbol_col.get(i)
{
record.set_item("标的代码", value.to_string())?;
}
if let Ok(direction_col) = df.column("交易方向")
&& let Ok(value) = direction_col.get(i)
{
record.set_item("交易方向", value.to_string())?;
}
if let Ok(open_dt_col) = df.column("开仓时间")
&& let Ok(value) = open_dt_col.get(i)
{
record.set_item("开仓时间", value.to_string())?;
}
if let Ok(close_dt_col) = df.column("平仓时间")
&& let Ok(value) = close_dt_col.get(i)
{
record.set_item("平仓时间", value.to_string())?;
}
if let Ok(open_price_col) = df.column("开仓价格")
&& let Ok(value) = open_price_col.get(i)
{
record.set_item("开仓价格", value.try_extract::<f64>().unwrap_or(0.0))?;
}
if let Ok(close_price_col) = df.column("平仓价格")
&& let Ok(value) = close_price_col.get(i)
{
record.set_item("平仓价格", value.try_extract::<f64>().unwrap_or(0.0))?;
}
if let Ok(holding_bar_col) = df.column("持仓K线数")
&& let Ok(value) = holding_bar_col.get(i)
{
record.set_item("持仓K线数", value.try_extract::<i32>().unwrap_or(0))?;
}
if let Ok(event_sequence_col) = df.column("事件序列")
&& let Ok(value) = event_sequence_col.get(i)
{
record.set_item("事件序列", value.to_string())?;
}
if let Ok(holding_day_col) = df.column("持仓天数")
&& let Ok(value) = holding_day_col.get(i)
{
record.set_item("持仓天数", value.try_extract::<f64>().unwrap_or(0.0))?;
}
if let Ok(yield_profit_ratio_col) = df.column("盈亏比例")
&& let Ok(value) = yield_profit_ratio_col.get(i)
{
match value {
polars::prelude::AnyValue::Null => {
record.set_item("盈亏比例", py.None())?;
}
_ => {
if let Ok(ratio) = value.try_extract::<f64>() {
record.set_item("盈亏比例", ratio)?;
} else {
record.set_item("盈亏比例", py.None())?;
}
}
}
}
list.append(record)?;
}
Ok(list.into())
})
}
#[getter]
fn holds(&self) -> PyResult<Py<pyo3::types::PyList>> {
Python::with_gil(|py| {
let list = pyo3::types::PyList::empty(py);
for hold_record in &self.inner.holds {
let record = pyo3::types::PyDict::new(py);
let timestamp = hold_record.dt.timestamp() as f64;
record.set_item("dt", timestamp)?;
record.set_item("pos", hold_record.pos.to_f64() as i32)?;
record.set_item("price", hold_record.price)?;
if let Some(n1b_value) = hold_record.n1b {
record.set_item("n1b", n1b_value)?;
} else {
record.set_item("n1b", py.None())?;
}
list.append(record)?;
}
Ok(list.into())
})
}
#[getter]
fn unique_signals(&self) -> Vec<String> {
let mut signals = HashSet::new();
for event in &self.inner.opens {
for signal in &event.signals_all {
signals.insert(signal.to_string());
}
for signal in &event.signals_any {
signals.insert(signal.to_string());
}
for signal in &event.signals_not {
signals.insert(signal.to_string());
}
}
for event in &self.inner.exits {
for signal in &event.signals_all {
signals.insert(signal.to_string());
}
for signal in &event.signals_any {
signals.insert(signal.to_string());
}
for signal in &event.signals_not {
signals.insert(signal.to_string());
}
}
signals.into_iter().collect()
}
#[getter]
fn events(&self) -> Vec<PyEvent> {
self.all_events()
}
fn __reduce__(&self, py: Python) -> PyResult<PyObject> {
use pyo3::IntoPyObject;
let opens: Vec<PyEvent> = self
.inner
.opens
.iter()
.map(|e| PyEvent { inner: e.clone() })
.collect();
let exits: Vec<PyEvent> = self
.inner
.exits
.iter()
.map(|e| PyEvent { inner: e.clone() })
.collect();
let args = (
self.inner.symbol.clone(),
opens,
exits,
self.inner.interval,
self.inner.timeout,
self.inner.stop_loss,
self.inner.t0,
Some(self.inner.name.clone()),
)
.into_pyobject(py)?;
let constructor = py.get_type::<Self>();
let result = (constructor, args).into_pyobject(py)?;
Ok(result.into())
}
#[pyo3(signature = (with_data = true))]
fn dump(&self, with_data: bool) -> PyResult<PyObject> {
Python::with_gil(|py| {
let dict = pyo3::types::PyDict::new(py);
dict.set_item("symbol", &self.inner.symbol)?;
dict.set_item("name", &self.inner.name)?;
let opens_list = pyo3::types::PyList::empty(py);
for event in &self.inner.opens {
let event_dict = pyo3::types::PyDict::new(py);
event_dict.set_item("name", &event.name)?;
event_dict.set_item("operate", event.operate.to_chinese())?;
let signals_all_list = pyo3::types::PyList::empty(py);
for signal in &event.signals_all {
let signal_dict = pyo3::types::PyDict::new(py);
signal_dict.set_item("key", signal.key())?;
signal_dict.set_item("value", signal.value())?;
signals_all_list.append(signal_dict)?;
}
event_dict.set_item("signals_all", signals_all_list)?;
let signals_any_list = pyo3::types::PyList::empty(py);
for signal in &event.signals_any {
let signal_dict = pyo3::types::PyDict::new(py);
signal_dict.set_item("key", signal.key())?;
signal_dict.set_item("value", signal.value())?;
signals_any_list.append(signal_dict)?;
}
event_dict.set_item("signals_any", signals_any_list)?;
let signals_not_list = pyo3::types::PyList::empty(py);
for signal in &event.signals_not {
let signal_dict = pyo3::types::PyDict::new(py);
signal_dict.set_item("key", signal.key())?;
signal_dict.set_item("value", signal.value())?;
signals_not_list.append(signal_dict)?;
}
event_dict.set_item("signals_not", signals_not_list)?;
opens_list.append(event_dict)?;
}
dict.set_item("opens", opens_list)?;
let exits_list = pyo3::types::PyList::empty(py);
for event in &self.inner.exits {
let event_dict = pyo3::types::PyDict::new(py);
event_dict.set_item("name", &event.name)?;
event_dict.set_item("operate", event.operate.to_chinese())?;
let signals_all_list = pyo3::types::PyList::empty(py);
for signal in &event.signals_all {
let signal_dict = pyo3::types::PyDict::new(py);
signal_dict.set_item("key", signal.key())?;
signal_dict.set_item("value", signal.value())?;
signals_all_list.append(signal_dict)?;
}
event_dict.set_item("signals_all", signals_all_list)?;
let signals_any_list = pyo3::types::PyList::empty(py);
for signal in &event.signals_any {
let signal_dict = pyo3::types::PyDict::new(py);
signal_dict.set_item("key", signal.key())?;
signal_dict.set_item("value", signal.value())?;
signals_any_list.append(signal_dict)?;
}
event_dict.set_item("signals_any", signals_any_list)?;
let signals_not_list = pyo3::types::PyList::empty(py);
for signal in &event.signals_not {
let signal_dict = pyo3::types::PyDict::new(py);
signal_dict.set_item("key", signal.key())?;
signal_dict.set_item("value", signal.value())?;
signals_not_list.append(signal_dict)?;
}
event_dict.set_item("signals_not", signals_not_list)?;
exits_list.append(event_dict)?;
}
dict.set_item("exits", exits_list)?;
dict.set_item("interval", self.inner.interval)?;
dict.set_item("timeout", self.inner.timeout)?;
dict.set_item("stop_loss", self.inner.stop_loss)?;
dict.set_item("T0", self.inner.t0)?;
if with_data {
if let Ok(pairs_list) = self.pairs() {
dict.set_item("pairs", pairs_list)?;
}
if let Ok(holds_list) = self.holds() {
dict.set_item("holds", holds_list)?;
}
}
Ok(dict.into())
})
}
#[classmethod]
fn load(_cls: &Bound<'_, pyo3::types::PyType>, data: PyObject) -> PyResult<Self> {
Python::with_gil(|py| {
let dict = match data.downcast_bound::<pyo3::types::PyDict>(py) {
Ok(d) => d.clone(),
Err(_) => {
if let Ok(s) = data.downcast_bound::<pyo3::types::PyString>(py) {
let json_str: String = s.extract()?;
let json_module = py.import("json")?;
let parsed = json_module.call_method1("loads", (json_str,))?;
parsed.downcast::<pyo3::types::PyDict>()?.clone()
} else {
return Err(pyo3::exceptions::PyTypeError::new_err(
"Expected dict or JSON string",
));
}
}
};
let symbol: String = dict
.get_item("symbol")?
.map(|item| item.extract())
.transpose()?
.unwrap_or_else(|| "UNKNOWN".to_string());
let name: String = dict
.get_item("name")?
.ok_or_else(|| pyo3::exceptions::PyKeyError::new_err("Missing 'name' field"))?
.extract()?;
let interval: i64 = dict
.get_item("interval")?
.ok_or_else(|| pyo3::exceptions::PyKeyError::new_err("Missing 'interval' field"))?
.extract()?;
let timeout: i32 = dict
.get_item("timeout")?
.ok_or_else(|| pyo3::exceptions::PyKeyError::new_err("Missing 'timeout' field"))?
.extract()?;
let stop_loss: f64 = dict
.get_item("stop_loss")?
.ok_or_else(|| pyo3::exceptions::PyKeyError::new_err("Missing 'stop_loss' field"))?
.extract()?;
let t0: bool = dict
.get_item("T0")?
.ok_or_else(|| pyo3::exceptions::PyKeyError::new_err("Missing 'T0' field"))?
.extract()?;
let opens_data = dict
.get_item("opens")?
.ok_or_else(|| pyo3::exceptions::PyKeyError::new_err("Missing 'opens' field"))?;
let opens_list = opens_data.downcast::<pyo3::types::PyList>()?;
let mut opens = Vec::new();
for item in opens_list.iter() {
let event_dict = item.downcast::<pyo3::types::PyDict>()?;
let event_name: String = event_dict
.get_item("name")?
.ok_or_else(|| {
pyo3::exceptions::PyKeyError::new_err("Missing 'name' in event")
})?
.extract()?;
let operate_str: String = event_dict
.get_item("operate")?
.ok_or_else(|| {
pyo3::exceptions::PyKeyError::new_err("Missing 'operate' in event")
})?
.extract()?;
let operate = parse_operate(&operate_str)
.map_err(|e| PyValueError::new_err(format!("无法解析operate: {e}")))?;
let signals_all_data = event_dict.get_item("signals_all")?.ok_or_else(|| {
pyo3::exceptions::PyKeyError::new_err("Missing 'signals_all' in event")
})?;
let signals_all_list = signals_all_data.downcast::<pyo3::types::PyList>()?;
let mut signals_all = Vec::new();
for signal_item in signals_all_list.iter() {
let signal_dict = signal_item.downcast::<pyo3::types::PyDict>()?;
let key: String = signal_dict
.get_item("key")?
.ok_or_else(|| {
pyo3::exceptions::PyKeyError::new_err("Missing 'key' in signal")
})?
.extract()?;
let value: String = signal_dict
.get_item("value")?
.ok_or_else(|| {
pyo3::exceptions::PyKeyError::new_err("Missing 'value' in signal")
})?
.extract()?;
let signal_str = format!("{key}_{value}");
if let Ok(signal) = Signal::from_str(&signal_str) {
signals_all.push(signal);
}
}
let mut signals_any = Vec::new();
if let Some(signals_any_data) = event_dict.get_item("signals_any")? {
let signals_any_list = signals_any_data.downcast::<pyo3::types::PyList>()?;
for signal_item in signals_any_list.iter() {
let signal_dict = signal_item.downcast::<pyo3::types::PyDict>()?;
let key: String = signal_dict
.get_item("key")?
.ok_or_else(|| {
pyo3::exceptions::PyKeyError::new_err("Missing 'key' in signal")
})?
.extract()?;
let value: String = signal_dict
.get_item("value")?
.ok_or_else(|| {
pyo3::exceptions::PyKeyError::new_err("Missing 'value' in signal")
})?
.extract()?;
let signal_str = format!("{key}_{value}");
if let Ok(signal) = Signal::from_str(&signal_str) {
signals_any.push(signal);
}
}
}
let mut signals_not = Vec::new();
if let Some(signals_not_data) = event_dict.get_item("signals_not")? {
let signals_not_list = signals_not_data.downcast::<pyo3::types::PyList>()?;
for signal_item in signals_not_list.iter() {
let signal_dict = signal_item.downcast::<pyo3::types::PyDict>()?;
let key: String = signal_dict
.get_item("key")?
.ok_or_else(|| {
pyo3::exceptions::PyKeyError::new_err("Missing 'key' in signal")
})?
.extract()?;
let value: String = signal_dict
.get_item("value")?
.ok_or_else(|| {
pyo3::exceptions::PyKeyError::new_err("Missing 'value' in signal")
})?
.extract()?;
let signal_str = format!("{key}_{value}");
if let Ok(signal) = Signal::from_str(&signal_str) {
signals_not.push(signal);
}
}
}
let event = Event {
name: event_name,
operate,
signals_all,
signals_any,
signals_not,
sha256: String::new(),
};
opens.push(event);
}
let exits_data = dict
.get_item("exits")?
.ok_or_else(|| pyo3::exceptions::PyKeyError::new_err("Missing 'exits' field"))?;
let exits_list = exits_data.downcast::<pyo3::types::PyList>()?;
let mut exits = Vec::new();
for item in exits_list.iter() {
let event_dict = item.downcast::<pyo3::types::PyDict>()?;
let event_name: String = event_dict
.get_item("name")?
.ok_or_else(|| {
pyo3::exceptions::PyKeyError::new_err("Missing 'name' in event")
})?
.extract()?;
let operate_str: String = event_dict
.get_item("operate")?
.ok_or_else(|| {
pyo3::exceptions::PyKeyError::new_err("Missing 'operate' in event")
})?
.extract()?;
let operate = parse_operate(&operate_str)
.map_err(|e| PyValueError::new_err(format!("无法解析operate: {e}")))?;
let signals_all_data = event_dict.get_item("signals_all")?.ok_or_else(|| {
pyo3::exceptions::PyKeyError::new_err("Missing 'signals_all' in event")
})?;
let signals_all_list = signals_all_data.downcast::<pyo3::types::PyList>()?;
let mut signals_all = Vec::new();
for signal_item in signals_all_list.iter() {
let signal_dict = signal_item.downcast::<pyo3::types::PyDict>()?;
let key: String = signal_dict
.get_item("key")?
.ok_or_else(|| {
pyo3::exceptions::PyKeyError::new_err("Missing 'key' in signal")
})?
.extract()?;
let value: String = signal_dict
.get_item("value")?
.ok_or_else(|| {
pyo3::exceptions::PyKeyError::new_err("Missing 'value' in signal")
})?
.extract()?;
let signal_str = format!("{key}_{value}");
if let Ok(signal) = Signal::from_str(&signal_str) {
signals_all.push(signal);
}
}
let mut signals_any = Vec::new();
if let Some(signals_any_data) = event_dict.get_item("signals_any")? {
let signals_any_list = signals_any_data.downcast::<pyo3::types::PyList>()?;
for signal_item in signals_any_list.iter() {
let signal_dict = signal_item.downcast::<pyo3::types::PyDict>()?;
let key: String = signal_dict
.get_item("key")?
.ok_or_else(|| {
pyo3::exceptions::PyKeyError::new_err("Missing 'key' in signal")
})?
.extract()?;
let value: String = signal_dict
.get_item("value")?
.ok_or_else(|| {
pyo3::exceptions::PyKeyError::new_err("Missing 'value' in signal")
})?
.extract()?;
let signal_str = format!("{key}_{value}");
if let Ok(signal) = Signal::from_str(&signal_str) {
signals_any.push(signal);
}
}
}
let mut signals_not = Vec::new();
if let Some(signals_not_data) = event_dict.get_item("signals_not")? {
let signals_not_list = signals_not_data.downcast::<pyo3::types::PyList>()?;
for signal_item in signals_not_list.iter() {
let signal_dict = signal_item.downcast::<pyo3::types::PyDict>()?;
let key: String = signal_dict
.get_item("key")?
.ok_or_else(|| {
pyo3::exceptions::PyKeyError::new_err("Missing 'key' in signal")
})?
.extract()?;
let value: String = signal_dict
.get_item("value")?
.ok_or_else(|| {
pyo3::exceptions::PyKeyError::new_err("Missing 'value' in signal")
})?
.extract()?;
let signal_str = format!("{key}_{value}");
if let Ok(signal) = Signal::from_str(&signal_str) {
signals_not.push(signal);
}
}
}
let event = Event {
name: event_name,
operate,
signals_all,
signals_any,
signals_not,
sha256: String::new(),
};
exits.push(event);
}
let inner = Position {
opens,
exits,
interval,
timeout,
stop_loss,
t0,
name,
symbol,
temp_state: None,
pos_changed: false,
operates: Vec::new(),
holds: Vec::new(),
pos: Pos::Flat,
last_event: None,
event_matcher: None,
event_match_values: Vec::new(),
event_match_cache: Vec::new(),
};
let mut inner = inner;
inner.init_runtime_fields();
Ok(Self { inner })
})
}
fn __repr__(&self) -> String {
format!(
"PyPosition(name='{}', symbol='{}', opens={}, exits={}, interval={})",
self.inner.name,
self.inner.symbol,
self.inner.opens.len(),
self.inner.exits.len(),
self.inner.interval
)
}
}