Skip to content

Commit

Permalink
#10 addressed
Browse files Browse the repository at this point in the history
credit call spread
  • Loading branch information
dragthor committed Apr 30, 2023
1 parent 2ebcc37 commit 9571bbc
Show file tree
Hide file tree
Showing 5 changed files with 231 additions and 55 deletions.
17 changes: 16 additions & 1 deletion TastyBot.Library/Strategy/BaseStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,5 +79,20 @@ public async Task<bool> ExistingPositions()

return existingPositions > 0;
}

protected TastySpread CreatCreditSpread(Leg shortLeg, Leg longLeg, decimal desiredCredit)
{
var creditSpread = new TastySpread()
{
source = StrategyOrderSource.Name,
ordertype = StrategyOrderType.Limit,
timeinforce = StrategyOrderInForce.Day,
price = desiredCredit.ToString(),
priceeffect = StrategyOrderResultType.Credit,
legs = new Leg[] { shortLeg, longLeg }
};

return creditSpread;
}
}
}
}
149 changes: 149 additions & 0 deletions TastyBot.Library/Strategy/CallSpread.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
using TastyBot.Library;
using TastyBot.Models;

namespace TastyBot.Strategy
{
public class CallSpread : BaseStrategy, ITastyBotStrategy
{
private readonly int _spreadWidth;
private readonly int _daysToExpiration;

public CallSpread(ITastyBot bot, IQuoteMachine quoteMachine, TastyAccount account, string ticker, int spreadWidth, int daysToExpiration) : base(bot, quoteMachine, account, ticker)
{
_spreadWidth = spreadWidth;
_daysToExpiration = daysToExpiration;
}

public static ITastyBotStrategy CreateInstance(ITastyBot bot, IQuoteMachine quoteMachine, TastyAccount account, string ticker, int spreadWidth, int daysToExpiration)
{
return new CallSpread(bot, quoteMachine, account, ticker, spreadWidth, daysToExpiration);
}

public async Task<StrategyAttemptResult> MakeAttempt()
{
const int dteRange = 5; // plus/minus days around desired DTE.
const int qty = 1;
const decimal otm = .10m; // 10% OTM.
const decimal priceIncrease = 2m; // Price increased 2%.
const decimal desiredCredit = 1.95m; // Something ridiculous so that we do NOT get filled.

// Keep at least $1,000K in cash. If this trade reduces our buying power below $1K, then do not submit the order.
const decimal maintainAtLeastThisMuchBuyingPower = 1000m;

var minDte = _daysToExpiration - dteRange;
var maxDte = _daysToExpiration + dteRange;

if (string.IsNullOrWhiteSpace(_ticker)) return StrategyAttemptResult.InvalidSetup;
if (_spreadWidth < 1) return StrategyAttemptResult.InvalidSetup; // Yeah, I know some tickers have more narrow strikes.
if (_daysToExpiration < 0) return StrategyAttemptResult.InvalidSetup;

if (minDte < 0) minDte = _daysToExpiration;

var anyOpenOrders = await OpenOrders();

// Bail if we already have an open order.
if (anyOpenOrders) return StrategyAttemptResult.NothingToDo;

var anyExistingPositions = await ExistingPositions();

// Bail if we already have a position.
if (anyExistingPositions) return StrategyAttemptResult.NothingToDo;

var quote = await _quoteMachine.getQuote(_ticker);

var increaseChange = Convert.ToDouble(Math.Ceiling(Convert.ToDecimal(quote.price) * otm));

var desiredStrike = Convert.ToDecimal(Math.Ceiling(quote.price + increaseChange));

if (Convert.ToDecimal(quote.dayChange) <= priceIncrease)
{
return StrategyAttemptResult.NothingToDo;
}

var optionChain = await _bot.getOptionChain(_ticker);

Strike? sellStrike = null;
Strike? buyStrike = null;

// Just look at the monthlies (due to SPX vs SPXW).
foreach (var chain in optionChain.items.ToList().Where(x => x.rootsymbol == _ticker))
{
if (sellStrike != null || buyStrike != null) break;

var expirations = chain.expirations.Where(x => x.daystoexpiration >= minDte && x.daystoexpiration <= maxDte).ToList().OrderByDescending(x => x.daystoexpiration);

foreach (var expiration in expirations)
{
var strikes = expiration.strikes.ToList();

sellStrike = strikes.Where(x => Convert.ToDecimal(x.strikeprice) == desiredStrike).FirstOrDefault();
buyStrike = strikes.Where(x => Convert.ToDecimal(x.strikeprice) == desiredStrike + _spreadWidth).FirstOrDefault();

if (sellStrike != null && buyStrike != null)
{
// Bingo.
break;
}
}
}

// Double check.
if (sellStrike == null || buyStrike == null)
{
return StrategyAttemptResult.StrikeNotFound;
}

var shortLeg = new Leg()
{
instrumenttype = StrategyInstrumentType.EquityOption,
symbol = sellStrike.call,
action = StrategyLegAction.SellToOpen,
quantity = qty.ToString()
};

var longLeg = new Leg()
{
instrumenttype = StrategyInstrumentType.EquityOption,
symbol = buyStrike.call,
action = StrategyLegAction.BuyToOpen,
quantity = qty.ToString()
};

var creditSpread = CreatCreditSpread(shortLeg, longLeg, desiredCredit);

var preview = await _bot.doDryRun(_account.account.accountnumber, creditSpread);

if (preview.order.status.ToLower() == "received")
{
var newBuyingPower = Convert.ToDecimal(preview.buyingpowereffect.newbuyingpower);

if (newBuyingPower > maintainAtLeastThisMuchBuyingPower)
{
// Warning: Actually places an order when _liveOrdersEnabled is true (default is false).
var order = await _bot.placeOrder(_account.account.accountnumber, creditSpread);

// If outside normal hours, the order will be received.
if (order.order.status.ToLower() == "routed" || order.order.status.ToLower() == "received")
{
return StrategyAttemptResult.OrderEntered;
}
else
{
return StrategyAttemptResult.OrderRoutingError;
}
}
}
else
{
if (preview.warnings.Length > 0)
{
return StrategyAttemptResult.OrderWarnings;
}

return StrategyAttemptResult.OrderNotReceived;
}

return StrategyAttemptResult.NothingToDo;
}
}
}
15 changes: 4 additions & 11 deletions TastyBot.Library/Strategy/PutSpread.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public async Task<StrategyAttemptResult> MakeAttempt()
const int dteRange = 5; // plus/minus days around desired DTE.
const int qty = 1;
const decimal otm = .90m; // 10% OTM.
const decimal priceDrop = -2m; // Price drops 2%.
const decimal priceDrop = -2m; // Price decreased 2%.
const decimal desiredCredit = 1.95m; // Something ridiculous so that we do NOT get filled.

// Keep at least $1,000K in cash. If this trade reduces our buying power below $1K, then do not submit the order.
Expand Down Expand Up @@ -107,15 +107,7 @@ public async Task<StrategyAttemptResult> MakeAttempt()
quantity = qty.ToString()
};

var creditSpread = new TastySpread()
{
source = StrategyOrderSource.Name,
ordertype = StrategyOrderType.Limit,
timeinforce = StrategyOrderInForce.Day,
price = desiredCredit.ToString(),
priceeffect = StrategyOrderResultType.Credit,
legs = new Leg[] { shortLeg, longLeg }
};
var creditSpread = CreatCreditSpread(shortLeg, longLeg, desiredCredit);

var preview = await _bot.doDryRun(_account.account.accountnumber, creditSpread);

Expand All @@ -128,7 +120,8 @@ public async Task<StrategyAttemptResult> MakeAttempt()
// Warning: Actually places an order when _liveOrdersEnabled is true (default is false).
var order = await _bot.placeOrder(_account.account.accountnumber, creditSpread);

if (order.order.status.ToLower() == "routed")
// If outside normal hours, the order will be received.
if (order.order.status.ToLower() == "routed" || order.order.status.ToLower() == "received")
{
return StrategyAttemptResult.OrderEntered;
} else
Expand Down
52 changes: 52 additions & 0 deletions TastyBot/ConsoleLogger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System;
using TastyBot.Library;
using TastyBot.Strategy;

namespace TastyBot
{
public class ConsoleLogger : ILogger
{
public void Info(string message)
{
if (string.IsNullOrWhiteSpace(message)) return;

Console.WriteLine(message);
}

public void Error(string message)
{
if (string.IsNullOrWhiteSpace(message)) return;

Console.WriteLine(message);
}

public static void ProcessResult(ILogger logger, StrategyAttemptResult result)
{
switch (result)
{
case StrategyAttemptResult.OrderEntered:
logger.Info("Order entered.");
break;
case StrategyAttemptResult.StrikeNotFound:
logger.Info("Unable to find desired strike(s).");
break;
case StrategyAttemptResult.InvalidSetup:
logger.Info("Invalid strategy or order setup.");
break;
case StrategyAttemptResult.OrderRoutingError:
logger.Info("Order routing issue.");
break;
case StrategyAttemptResult.OrderWarnings:
logger.Info("Order has warnings.");
break;
case StrategyAttemptResult.OrderNotReceived:
logger.Info("Order was not received.");
break;
case StrategyAttemptResult.NothingToDo:
default:
logger.Info("Nothing to do at this time.");
break;
}
}
}
}
53 changes: 10 additions & 43 deletions TastyBot/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,34 +35,18 @@ public static async Task Main(string[] args)

var primaryAccount = accounts.items.First();

// This is not an iron condor. Based on the current strategy, only one will be filled.
ITastyBotStrategy p = PutSpread.CreateInstance(tastyBot, quoteMachine, primaryAccount, "SPY", 5, 50);

var result = await p.MakeAttempt();

switch (result) {
case StrategyAttemptResult.OrderEntered:
logger.Info("Order entered.");
break;
case StrategyAttemptResult.StrikeNotFound:
logger.Info("Unable to find desired strike(s).");
break;
case StrategyAttemptResult.InvalidSetup:
logger.Info("Invalid strategy or order setup.");
break;
case StrategyAttemptResult.OrderRoutingError:
logger.Info("Order routing issue.");
break;
case StrategyAttemptResult.OrderWarnings:
logger.Info("Order has warnings.");
break;
case StrategyAttemptResult.OrderNotReceived:
logger.Info("Order was not received.");
break;
case StrategyAttemptResult.NothingToDo:
default:
logger.Info("Nothing to do at this time.");
break;
}
var putSpreadResult = await p.MakeAttempt();

ConsoleLogger.ProcessResult(logger, putSpreadResult);

ITastyBotStrategy c = CallSpread.CreateInstance(tastyBot, quoteMachine, primaryAccount, "SPY", 5, 50);

var callSpreadResult = await c.MakeAttempt();

ConsoleLogger.ProcessResult(logger, callSpreadResult);
}
catch (Exception ex)
{
Expand All @@ -74,22 +58,5 @@ public static async Task Main(string[] args)
await quoteMachine.Terminate();
}
}

public class ConsoleLogger : ILogger
{
public void Info(string message)
{
if (string.IsNullOrWhiteSpace(message)) return;

Console.WriteLine(message);
}

public void Error(string message)
{
if (string.IsNullOrWhiteSpace(message)) return;

Console.WriteLine(message);
}
}
}
}

0 comments on commit 9571bbc

Please sign in to comment.