use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use pine_broker::{
BarBroker, Commission, Direction, Exit, OcaType, Order, OrderKind, PineFills, Sizing,
};
use pine_builtin_macro::BuiltinFunction;
use pine_core::PineVersion;
use pine_interpreter::{BuiltinFn, Interpreter, PineOutput, RuntimeError, Value};
const DEFAULT_INITIAL_CAPITAL: f64 = 1_000_000.0;
#[derive(BuiltinFunction)]
#[builtin(name = "strategy")]
struct StrategyFn {
#[allow(dead_code)]
title: String,
#[arg(default = "")]
shorttitle: String,
#[arg(default = false)]
overlay: bool,
#[arg(default = "")]
format: String,
#[arg(default = None)]
precision: Option<f64>,
#[arg(default = "")]
scale: String,
#[arg(default = None)]
pyramiding: Option<f64>,
#[arg(default = "fixed")]
default_qty_type: String,
#[arg(default = 1.0)]
default_qty_value: f64,
#[arg(default = None)]
initial_capital: Option<f64>,
#[arg(default = "")]
currency: String,
#[arg(default = 0.0)]
slippage: f64,
#[arg(default = "percent")]
commission_type: String,
#[arg(default = 0.0)]
commission_value: f64,
}
impl StrategyFn {
fn execute<O: PineOutput>(&self, ctx: &mut Interpreter<O>) -> Result<Value<O>, RuntimeError> {
let _ = (
&self.shorttitle,
self.overlay,
&self.format,
self.precision,
&self.scale,
&self.currency,
);
if ctx.broker.is_none() {
let initial_capital = self.initial_capital.unwrap_or(DEFAULT_INITIAL_CAPITAL);
let mintick = mintick_of(ctx);
let fills = PineFills {
slippage: self.slippage,
mintick,
};
let mut broker = BarBroker::new(fills, initial_capital)
.with_mintick(mintick)
.with_sizing(self.sizing())
.with_pyramiding(self.pyramiding.unwrap_or(0.0) as usize);
if self.commission_value != 0.0 {
let commission = match self.commission_type.as_str() {
"cash_per_contract" => Commission::CashPerContract(self.commission_value),
"cash_per_order" => Commission::CashPerOrder(self.commission_value),
_ => Commission::Percent(self.commission_value),
};
broker = broker.with_commission(commission);
}
ctx.broker = Some(Box::new(broker));
ctx.set_object_field(
"strategy",
"initial_capital",
Value::Number(initial_capital),
);
ctx.set_object_field("strategy", "equity", Value::Number(initial_capital));
}
Ok(Value::Na)
}
fn sizing(&self) -> Sizing {
match self.default_qty_type.as_str() {
"cash" => Sizing::Cash(self.default_qty_value),
"percent_of_equity" => Sizing::PercentOfEquity(self.default_qty_value),
_ => Sizing::Contracts(self.default_qty_value),
}
}
}
fn mintick_of<O: PineOutput>(ctx: &Interpreter<O>) -> f64 {
if let Some(Value::Object { fields, .. }) = ctx.get_variable("syminfo") {
if let Some(Value::Number(mintick)) = fields.borrow().get("mintick") {
return *mintick;
}
}
0.0
}
fn close_of<O: PineOutput>(ctx: &Interpreter<O>) -> f64 {
match ctx.get_variable("close") {
Some(Value::Series(series)) => match series.current.as_ref() {
Value::Number(n) => *n,
_ => f64::NAN,
},
Some(Value::Number(n)) => *n,
_ => f64::NAN,
}
}
fn non_empty(name: &str) -> Option<String> {
if name.is_empty() {
None
} else {
Some(name.to_string())
}
}
fn order_kind(limit: Option<f64>, stop: Option<f64>) -> OrderKind {
match (limit, stop) {
(Some(limit), Some(stop)) => OrderKind::StopLimit { stop, limit },
(Some(limit), None) => OrderKind::Limit(limit),
(None, Some(stop)) => OrderKind::Stop(stop),
(None, None) => OrderKind::Market,
}
}
#[derive(BuiltinFunction)]
#[builtin(name = "strategy.entry")]
struct StrategyEntry {
id: String,
direction: String,
#[arg(default = None)]
qty: Option<f64>,
#[arg(default = None)]
limit: Option<f64>,
#[arg(default = None)]
stop: Option<f64>,
#[arg(default = "")]
oca_name: String,
#[arg(default = "")]
oca_type: String,
#[arg(default = "")]
comment: String,
}
impl StrategyEntry {
fn execute<O: PineOutput>(&self, ctx: &mut Interpreter<O>) -> Result<Value<O>, RuntimeError> {
let sizing_price = close_of(ctx);
if let Some(broker) = ctx.broker.as_mut() {
broker.submit(Order {
id: self.id.clone(),
direction: Direction::from(self.direction.as_str()),
qty: self.qty,
qty_percent: None,
sizing_price: Some(sizing_price),
kind: order_kind(self.limit, self.stop),
reduce_only: false,
reverses: true,
close_target: None,
oca_name: non_empty(&self.oca_name),
oca_type: OcaType::from(self.oca_type.as_str()),
comment: self.comment.clone(),
});
}
Ok(Value::Na)
}
}
#[derive(BuiltinFunction)]
#[builtin(name = "strategy.order")]
struct StrategyOrder {
id: String,
direction: String,
#[arg(default = None)]
qty: Option<f64>,
#[arg(default = None)]
limit: Option<f64>,
#[arg(default = None)]
stop: Option<f64>,
#[arg(default = "")]
oca_name: String,
#[arg(default = "")]
oca_type: String,
#[arg(default = "")]
comment: String,
}
impl StrategyOrder {
fn execute<O: PineOutput>(&self, ctx: &mut Interpreter<O>) -> Result<Value<O>, RuntimeError> {
let sizing_price = close_of(ctx);
if let Some(broker) = ctx.broker.as_mut() {
broker.submit(Order {
id: self.id.clone(),
direction: Direction::from(self.direction.as_str()),
qty: self.qty,
qty_percent: None,
sizing_price: Some(sizing_price),
kind: order_kind(self.limit, self.stop),
reduce_only: false,
reverses: false,
close_target: None,
oca_name: non_empty(&self.oca_name),
oca_type: OcaType::from(self.oca_type.as_str()),
comment: self.comment.clone(),
});
}
Ok(Value::Na)
}
}
#[derive(BuiltinFunction)]
#[builtin(name = "strategy.close")]
struct StrategyClose {
id: String,
#[arg(default = "")]
comment: String,
#[arg(default = None)]
qty: Option<f64>,
#[arg(default = None)]
qty_percent: Option<f64>,
}
impl StrategyClose {
fn execute<O: PineOutput>(&self, ctx: &mut Interpreter<O>) -> Result<Value<O>, RuntimeError> {
if let Some(broker) = ctx.broker.as_mut() {
broker.submit(Order {
id: self.id.clone(),
direction: Direction::Long,
qty: self.qty,
qty_percent: self.qty_percent,
sizing_price: None,
kind: OrderKind::Market,
reduce_only: true,
reverses: false,
close_target: Some(self.id.clone()),
oca_name: None,
oca_type: OcaType::None,
comment: self.comment.clone(),
});
}
Ok(Value::Na)
}
}
#[derive(BuiltinFunction)]
#[builtin(name = "strategy.close_all")]
struct StrategyCloseAll {
#[arg(default = "")]
comment: String,
}
impl StrategyCloseAll {
fn execute<O: PineOutput>(&self, ctx: &mut Interpreter<O>) -> Result<Value<O>, RuntimeError> {
if let Some(broker) = ctx.broker.as_mut() {
broker.submit(Order {
id: "Close all".to_string(),
direction: Direction::Long,
qty: None,
qty_percent: None,
sizing_price: None,
kind: OrderKind::Market,
reduce_only: true,
reverses: false,
close_target: None,
oca_name: None,
oca_type: OcaType::None,
comment: self.comment.clone(),
});
}
Ok(Value::Na)
}
}
#[derive(BuiltinFunction)]
#[builtin(name = "strategy.cancel")]
struct StrategyCancel {
id: String,
}
impl StrategyCancel {
fn execute<O: PineOutput>(&self, ctx: &mut Interpreter<O>) -> Result<Value<O>, RuntimeError> {
if let Some(broker) = ctx.broker.as_mut() {
broker.cancel(&self.id);
}
Ok(Value::Na)
}
}
#[derive(BuiltinFunction)]
#[builtin(name = "strategy.cancel_all")]
struct StrategyCancelAll {}
impl StrategyCancelAll {
fn execute<O: PineOutput>(&self, ctx: &mut Interpreter<O>) -> Result<Value<O>, RuntimeError> {
if let Some(broker) = ctx.broker.as_mut() {
broker.cancel_all();
}
Ok(Value::Na)
}
}
#[derive(BuiltinFunction)]
#[builtin(name = "strategy.exit")]
struct StrategyExit {
id: String,
#[arg(default = "")]
from_entry: String,
#[arg(default = None)]
qty: Option<f64>,
#[arg(default = None)]
qty_percent: Option<f64>,
#[arg(default = None)]
profit: Option<f64>,
#[arg(default = None)]
limit: Option<f64>,
#[arg(default = None)]
loss: Option<f64>,
#[arg(default = None)]
stop: Option<f64>,
#[arg(default = None)]
trail_price: Option<f64>,
#[arg(default = None)]
trail_points: Option<f64>,
#[arg(default = None)]
trail_offset: Option<f64>,
#[arg(default = "")]
comment: String,
}
impl StrategyExit {
fn execute<O: PineOutput>(&self, ctx: &mut Interpreter<O>) -> Result<Value<O>, RuntimeError> {
let _ = &self.comment;
if let Some(broker) = ctx.broker.as_mut() {
broker.submit_exit(Exit {
limit: self.limit,
profit_ticks: self.profit,
stop: self.stop,
loss_ticks: self.loss,
trail_price: self.trail_price,
trail_points: self.trail_points,
trail_offset: self.trail_offset,
..Exit::resting(
self.id.clone(),
non_empty(&self.from_entry),
self.qty,
self.qty_percent,
)
});
}
Ok(Value::Na)
}
}
pub fn register<O: PineOutput>(_version: PineVersion) -> Value<O> {
let mut fields: HashMap<String, Value<O>> = HashMap::new();
fields.insert("entry".to_string(), StrategyEntry::builtin_value::<O>());
fields.insert("order".to_string(), StrategyOrder::builtin_value::<O>());
fields.insert("close".to_string(), StrategyClose::builtin_value::<O>());
fields.insert(
"close_all".to_string(),
StrategyCloseAll::builtin_value::<O>(),
);
fields.insert("exit".to_string(), StrategyExit::builtin_value::<O>());
fields.insert("cancel".to_string(), StrategyCancel::builtin_value::<O>());
fields.insert(
"cancel_all".to_string(),
StrategyCancelAll::builtin_value::<O>(),
);
fields.insert("long".to_string(), Value::String("long".to_string()));
fields.insert("short".to_string(), Value::String("short".to_string()));
fields.insert("fixed".to_string(), Value::String("fixed".to_string()));
fields.insert("cash".to_string(), Value::String("cash".to_string()));
fields.insert(
"percent_of_equity".to_string(),
Value::String("percent_of_equity".to_string()),
);
let mut commission: HashMap<String, Value<O>> = HashMap::new();
commission.insert("percent".to_string(), Value::String("percent".to_string()));
commission.insert(
"cash_per_contract".to_string(),
Value::String("cash_per_contract".to_string()),
);
commission.insert(
"cash_per_order".to_string(),
Value::String("cash_per_order".to_string()),
);
fields.insert(
"commission".to_string(),
Value::Object {
type_name: "strategy.commission".to_string(),
fields: Rc::new(RefCell::new(commission)),
call: None,
},
);
let mut oca: HashMap<String, Value<O>> = HashMap::new();
oca.insert("cancel".to_string(), Value::String("cancel".to_string()));
oca.insert("reduce".to_string(), Value::String("reduce".to_string()));
oca.insert("none".to_string(), Value::String("none".to_string()));
fields.insert(
"oca".to_string(),
Value::Object {
type_name: "strategy.oca".to_string(),
fields: Rc::new(RefCell::new(oca)),
call: None,
},
);
for name in [
"position_size",
"equity",
"initial_capital",
"netprofit",
"openprofit",
"grossprofit",
"grossloss",
"max_drawdown",
"max_runup",
] {
fields.insert(name.to_string(), Value::Number(0.0));
}
fields.insert("position_avg_price".to_string(), Value::Na);
for name in [
"opentrades",
"closedtrades",
"wintrades",
"losstrades",
"eventrades",
] {
fields.insert(name.to_string(), Value::Int(0));
}
Value::Object {
type_name: "strategy".to_string(),
fields: Rc::new(RefCell::new(fields)),
call: Some(Rc::new(StrategyFn::builtin_fn) as BuiltinFn<O>),
}
}